Commit 91c68eaa authored by Nguyễn Hải Sơn's avatar Nguyễn Hải Sơn

Update

parent 22d732ce
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#using-environment-variables
# Prisma supports the native connection string format for PostgreSQL, MySQL and SQLite.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
DATABASE_URL="mysql://qldh:qldh@2021!wGqvF9DV4W@localhost:3308/qldh"
\ No newline at end of file
node_modules
dist
node_modules/
dist/
*.log
.env
\ No newline at end of file
# REST API Example
This example shows how to implement a **REST API with TypeScript** using [Express](https://expressjs.com/) and [Prisma Client](https://www.prisma.io/docs/concepts/components/prisma-client). It is based on a SQLite database, you can find the database file with some dummy data at [`./prisma/dev.db`](./prisma/dev.db).
## Getting started
### 1. Download example and install dependencies
Download this example:
```
curl https://codeload.github.com/prisma/prisma-examples/tar.gz/latest | tar -xz --strip=2 prisma-examples-latest/typescript/rest-express
```
Install npm dependencies:
```
cd rest-express
npm install
```
<details><summary><strong>Alternative:</strong> Clone the entire repo</summary>
Clone this repository:
```
git clone git@github.com:prisma/prisma-examples.git --depth=1
```
Install npm dependencies:
```
cd prisma-examples/typescript/rest-express
npm install
```
</details>
### 2. Create and seed the database
Run the following command to create your SQLite database file. This also creates the `User` and `Post` tables that are defined in [`prisma/schema.prisma`](./prisma/schema.prisma):
```
npx prisma migrate dev --name init
```
Now, seed the database with the sample data in [`prisma/seed.ts`](./prisma/seed.ts) by running the following command:
```
npx prisma db seed --preview-feature
```
### 3. Start the REST API server
```
npm run dev
```
The server is now running on `http://localhost:3000`. You can now the API requests, e.g. [`http://localhost:3000/feed`](http://localhost:3000/feed).
## Using the REST API
You can access the REST API of the server using the following endpoints:
### `GET`
- `/post/:id`: Fetch a single post by its `id`
- `/feed?searchString={searchString}&take={take}&skip={skip}&orderBy={orderBy}`: Fetch all _published_ posts
- Query Parameters
- `searchString` (optional): This filters posts by `title` or `content`
- `take` (optional): This specifies how many objects should be returned in the list
- `skip` (optional): This specifies how many of the returned objects in the list should be skipped
- `orderBy` (optional): The sort order for posts in either ascending or descending order. The value can either `asc` or `desc`
- `/user/:id/drafts`: Fetch user's drafts by their `id`
- `/users`: Fetch all users
### `POST`
- `/post`: Create a new post
- Body:
- `title: String` (required): The title of the post
- `content: String` (optional): The content of the post
- `authorEmail: String` (required): The email of the user that creates the post
- `/signup`: Create a new user
- Body:
- `email: String` (required): The email address of the user
- `name: String` (optional): The name of the user
- `postData: PostCreateInput[]` (optional): The posts of the user
### `PUT`
- `/publish/:id`: Toggle the publish value of a post by its `id`
- `/post/:id/views`: Increases the `viewCount` of a `Post` by one `id`
### `DELETE`
- `/post/:id`: Delete a post by its `id`
## Evolving the app
Evolving the application typically requires two steps:
1. Migrate your database using Prisma Migrate
1. Update your application code
For the following example scenario, assume you want to add a "profile" feature to the app where users can create a profile and write a short bio about themselves.
### 1. Migrate your database using Prisma Migrate
The first step is to add a new table, e.g. called `Profile`, to the database. You can do this by adding a new model to your [Prisma schema file](./prisma/schema.prisma) file and then running a migration afterwards:
```diff
// ./prisma/schema.prisma
model User {
id Int @default(autoincrement()) @id
name String?
email String @unique
posts Post[]
+ profile Profile?
}
model Post {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
title String
content String?
published Boolean @default(false)
viewCount Int @default(0)
author User? @relation(fields: [authorId], references: [id])
authorId Int?
}
+model Profile {
+ id Int @default(autoincrement()) @id
+ bio String?
+ user User @relation(fields: [userId], references: [id])
+ userId Int @unique
+}
```
Once you've updated your data model, you can execute the changes against your database with the following command:
```
npx prisma migrate dev --name add-profile
```
This adds another migration to the `prisma/migrations` directory and creates the new `Profile` table in the database.
### 2. Update your application code
You can now use your `PrismaClient` instance to perform operations against the new `Profile` table. Those operations can be used to implement API endpoints in the REST API.
#### 2.1 Add the API endpoint to your app
Update your `index.ts` file by adding a new endpoint to your API:
```ts
app.post('/user/:id/profile', async (req, res) => {
const { id } = req.params
const { bio } = req.body
const profile = await prisma.profile.create({
data: {
bio,
user: {
connect: {
id: Number(id)
}
}
}
})
res.json(profile)
})
```
#### 2.2 Testing out your new endpoint
Restart your application server and test out your new endpoint.
##### `POST`
- `/user/:id/profile`: Create a new profile based on the user id
- Body:
- `bio: String` : The bio of the user
<details><summary>Expand to view more sample Prisma Client queries on <code>Profile</code></summary>
Here are some more sample Prisma Client queries on the new <code>Profile</code> model:
##### Create a new profile for an existing user
```ts
const profile = await prisma.profile.create({
data: {
bio: 'Hello World',
user: {
connect: { email: 'alice@prisma.io' },
},
},
})
```
##### Create a new user with a new profile
```ts
const user = await prisma.user.create({
data: {
email: 'john@prisma.io',
name: 'John',
profile: {
create: {
bio: 'Hello World',
},
},
},
})
```
##### Update the profile of an existing user
```ts
const userWithUpdatedProfile = await prisma.user.update({
where: { email: 'alice@prisma.io' },
data: {
profile: {
update: {
bio: 'Hello Friends',
},
},
},
})
```
</details>
## Switch to another database (e.g. PostgreSQL, MySQL, SQL Server)
If you want to try this example with another database than SQLite, you can adjust the the database connection in [`prisma/schema.prisma`](./prisma/schema.prisma) by reconfiguring the `datasource` block.
Learn more about the different connection configurations in the [docs](https://www.prisma.io/docs/reference/database-reference/connection-urls).
<details><summary>Expand for an overview of example configurations with different databases</summary>
### PostgreSQL
For PostgreSQL, the connection URL has the following structure:
```prisma
datasource db {
provider = "postgresql"
url = "postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=SCHEMA"
}
```
Here is an example connection string with a local PostgreSQL database:
```prisma
datasource db {
provider = "postgresql"
url = "postgresql://janedoe:mypassword@localhost:5432/notesapi?schema=public"
}
```
### MySQL
For MySQL, the connection URL has the following structure:
```prisma
datasource db {
provider = "mysql"
url = "mysql://USER:PASSWORD@HOST:PORT/DATABASE"
}
```
Here is an example connection string with a local MySQL database:
```prisma
datasource db {
provider = "mysql"
url = "mysql://janedoe:mypassword@localhost:3306/notesapi"
}
```
### Microsoft SQL Server (Preview)
Here is an example connection string with a local Microsoft SQL Server database:
```prisma
datasource db {
provider = "sqlserver"
url = "sqlserver://localhost:1433;initial catalog=sample;user=sa;password=mypassword;"
}
```
Because SQL Server is currently in [Preview](https://www.prisma.io/docs/about/releases#preview), you need to specify the `previewFeatures` on your `generator` block:
```prisma
generator client {
provider = "prisma-client-js"
previewFeatures = ["microsoftSqlServer"]
}
```
</details>
## Next steps
- Check out the [Prisma docs](https://www.prisma.io/docs)
- Share your feedback in the [`prisma2`](https://prisma.slack.com/messages/CKQTGR6T0/) channel on the [Prisma Slack](https://slack.prisma.io/)
- Create issues and ask questions on [GitHub](https://github.com/prisma/prisma/)
- Watch our biweekly "What's new in Prisma" livestreams on [Youtube](https://www.youtube.com/channel/UCptAHlN1gdwD89tFM3ENb6w)
\ No newline at end of file
This diff is collapsed.
{
"name": "api",
"name": "rest-express",
"version": "1.0.0",
"description": "",
"main": "index.js",
"license": "MIT",
"scripts": {
"start": "node dist/app.js",
"dev": "nodemon src/app.ts",
"start": "node dist/index.js",
"dev": "nodemon src/index.ts",
"build": "tsc -p ."
},
"author": "",
"license": "ISC",
"dependencies": {
"@prisma/client": "2.20.1",
"body-parser": "^1.19.0",
"cors": "^2.8.5",
"crypto-js": "^4.0.0",
"express": "4.17.1",
"express-validator": "^6.10.0",
"http-status-codes": "^2.1.4",
"jsonwebtoken": "^8.5.1",
"log4js": "^6.3.0",
"mysql": "^2.18.1",
"uuid": "^8.3.2"
},
"devDependencies": {
"@types/cors": "^2.8.10",
"@types/crypto-js": "^4.0.1",
"@types/express": "^4.17.11",
"@types/jsonwebtoken": "^8.5.1",
"@types/mysql": "^2.15.18",
"@types/node": "^14.14.37",
"@types/uuid": "^8.3.0",
"@typescript-eslint/eslint-plugin": "^4.21.0",
"@typescript-eslint/parser": "^4.21.0",
"eslint": "^7.23.0",
......@@ -20,14 +35,11 @@
"eslint-plugin-prettier": "^3.3.1",
"nodemon": "^2.0.7",
"prettier": "^2.2.1",
"prisma": "2.20.1",
"ts-node": "^9.1.1",
"typescript": "^4.2.3"
},
"dependencies": {
"@types/mysql": "^2.15.18",
"express": "^4.17.1",
"express-validator": "^6.10.0",
"http-status-codes": "^2.1.4",
"mysql": "^2.18.1"
"engines": {
"node": ">=10.0.0"
}
}
This diff is collapsed.
export const AppConfig = {
PORT: 3000,
DB_CONNECTION_AMOUNT: 3,
DB_HOST: 'localhost',
DB_USER: 'qldh',
DB_PASSWORD: 'qldh@2021!wGqvF9DV4W',
DB_DATABASE: 'qldh',
DB_PORT: 3308,
JWT_SECRET:
'1f6f8049cb10c1302d1c9ed34ba7e6be7da6fa57a050e2af8174dac29a386f4a811092fc2a491366efd8adfa47692c1997ef1b37840439d3a3c650ca679b200d',
};
import log4js from 'log4js';
const isProduction = process.env.NODE_ENV === 'production';
console.log(isProduction);
log4js.configure({
appenders: {
log: {
type: 'file',
filename: 'log.log',
maxLogSize: 10485760,
backups: 10,
compress: true,
},
},
categories: {
default: {
appenders: ['log'],
level: isProduction ? 'info' : 'debug',
},
},
});
export const logger = log4js.getLogger('log');
export enum queryErrorCode {
P2000 = '',
P2001 = '',
P2002 = '',
P2003 = '',
P2004 = '',
P2005 = '',
P2006 = '',
P2007 = '',
P2008 = '',
P2009 = '',
P2010 = '',
P2011 = '',
P2012 = '',
P2013 = '',
P2014 = '',
P2015 = '',
P2016 = '',
P2017 = '',
P2018 = '',
P2019 = '',
P2020 = '',
P2021 = '',
P2022 = '',
P2023 = '',
P2024 = '',
P2025 = '',
}
import { StatusCodes } from 'http-status-codes';
import { AppConfig } from './../../configurations/app-config';
import { sign, verify } from 'jsonwebtoken';
import { NextFunction, Request, Response } from 'express';
import { errorBody } from '../../response/error-body';
import { ResponseMessages } from '../../response/response-messages';
import { account_cms } from '@prisma/client';
export const generateAccessToken = (user: account_cms) => {
return sign({ ...user }, AppConfig.JWT_SECRET, { expiresIn: '1d' });
};
export const authenticateToken = (req: Request, res: Response, next: NextFunction) => {
const authHeader = req.headers.authorization;
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res
.status(StatusCodes.UNAUTHORIZED)
.send(errorBody(ResponseMessages.UNAUTHORIZED, StatusCodes.UNAUTHORIZED, ResponseMessages.UNAUTHORIZED));
}
verify(token, AppConfig.JWT_SECRET, (error: any, user: any) => {
if (error) {
return res
.status(StatusCodes.FORBIDDEN)
.send(errorBody(error, StatusCodes.FORBIDDEN, ResponseMessages.FORBIDDEN));
}
// @ts-ignore
req.user = user;
next();
});
};
import { account_cms, Prisma, PrismaClient } from '@prisma/client';
import express from 'express';
import { AppConfig } from './configurations/app-config';
import { v4 as uuidv4 } from 'uuid';
import { loginRouter } from './routes/login';
export const prisma = new PrismaClient();
declare global {
namespace Express {
interface Request {
user?: account_cms;
requestId: string;
}
}
}
const app = express();
app.use((req, res, next) => {
req.requestId = uuidv4();
next();
});
app.use(express.json());
app.use('/login', loginRouter);
const server = app.listen(AppConfig.PORT, () => console.log(`🚀 Server ready at: http://localhost:${AppConfig.PORT}`));
import { Prisma } from '@prisma/client';
import { Request, Response } from 'express';
import { Result, ValidationError } from 'express-validator';
import { ReasonPhrases, StatusCodes } from 'http-status-codes';
import { logger } from '../../configurations/log4js.config';
import { ValidatorMessage } from '../../validators/validator-config';
export const errorBody = (data: unknown, code: number, requestId: string, message?: string) => {
return {
requestId,
error: {
code,
message,
},
data,
};
};
export const queryErrorResponse = (req: Request, res: Response, error: Prisma.PrismaClientKnownRequestError) => {
logger.error(`[${req.requestId}][QUERY ERROR ${error.code}]: `, error.stack);
logger.info(`[${req.requestId}][QUERY ERROR ${error.code}]: `, error.message);
res.status(StatusCodes.INTERNAL_SERVER_ERROR).send(
errorBody(error.message, StatusCodes.INTERNAL_SERVER_ERROR, req.requestId, ReasonPhrases.INTERNAL_SERVER_ERROR),
);
};
export const validationErrorResponse = (req: Request, res: Response, errors: Result<ValidationError>) => {
const errorArray = errors.array();
res.status(StatusCodes.UNPROCESSABLE_ENTITY).send(
errorBody(errorArray, StatusCodes.UNPROCESSABLE_ENTITY, errorArray[0].msg),
);
};
export enum ResponseMessages {
UNAUTHORIZED = 'Vui lòng đăng nhập',
FORBIDDEN = 'Không được phép truy cập',
}
import { Request, Response } from 'express';
import { ReasonPhrases, StatusCodes } from 'http-status-codes';
export const successBody = (data: unknown, code: number, requestId: string, message?: string) => {
return {
requestId,
error: {
code,
message,
},
data,
};
};
export const successResponse = <T>(req: Request, res: Response, data: T) => {
res.status(StatusCodes.OK).send(successBody(data, StatusCodes.OK, req.requestId, ReasonPhrases.OK));
};
import { account_cms, Prisma } from '@prisma/client';
import { MD5 } from 'crypto-js';
import { Request, Response, Router } from 'express';
import { validationResult } from 'express-validator';
import { StatusCodes } from 'http-status-codes';
import { prisma } from '../..';
import { logger } from '../../configurations/log4js.config';
import { generateAccessToken } from '../../functions/jwt-authentication';
import { errorBody, queryErrorResponse, validationErrorResponse } from '../../response/error-body';
import { successResponse } from '../../response/success-body';
import { loginValidator } from '../../validators/login';
import { ValidatorMessage } from '../../validators/validator-config';
const loginRouter = Router();
loginRouter.post('/', loginValidator, async (req: Request, res: Response) => {
try {
const validationErrors = validationResult(req);
if (validationErrors.isEmpty()) {
const reqBody = req.body as account_cms;
const user = await prisma.account_cms.findFirst({
where: {
user_name: {
equals: reqBody.user_name,
},
password: {
equals: MD5(reqBody.password || '').toString(),
},
},
});
if (user) {
const jwtToken = generateAccessToken(user);
const data = {
id: user.id,
user_name: user.user_name,
email: user.email,
accessToken: jwtToken,
};
return successResponse(req, res, data);
} else {
logger.warn(`[${req.requestId}]: `, req.body);
return res
.status(StatusCodes.UNPROCESSABLE_ENTITY)
.send(
errorBody(
ValidatorMessage.WRONG_USERNAME_OR_PASSWORD,
StatusCodes.UNPROCESSABLE_ENTITY,
req.requestId,
ValidatorMessage.WRONG_USERNAME_OR_PASSWORD,
),
);
}
} else {
return validationErrorResponse(req, res, validationErrors);
}
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
return queryErrorResponse(req, res, error);
}
}
});
export { loginRouter };
import { check } from 'express-validator';
import { passwordMaxLength, usernameMaxLength, ValidatorMessage } from '../validator-config';
export const loginValidator = [
check('user_name', ValidatorMessage.REQUIRE_USERNAME).notEmpty(),
check('user_name', ValidatorMessage.INVALID_USERNAME).isString(),
check('user_name', ValidatorMessage.INVALID_USERNAME_LENGTH).isLength({
max: usernameMaxLength,
}),
check('password', ValidatorMessage.REQUIRE_PASSWORD).notEmpty(),
check('password', ValidatorMessage.INVALID_PASSWORD).isString(),
check('password', ValidatorMessage.INVALID_PASSWORD_LENGTH).isLength({
max: passwordMaxLength,
}),
];
import { account_cms } from '@prisma/client';
export const usernameMaxLength = 30;
export const passwordMaxLength = 50;
export const ValidatorMessage = {
REQUIRE_USERNAME: 'Vui lòng nhập tên đăng nhập',
INVALID_USERNAME: 'Tên đăng nhập không hợp lệ',
INVALID_USERNAME_LENGTH: `Độ dài tối đa của tên đăng nhập là ${usernameMaxLength} ký tự`,
REQUIRE_PASSWORD: 'Vui lòng nhập mật khẩu',
INVALID_PASSWORD: 'Mật khẩu không hợp lệ',
INVALID_PASSWORD_LENGTH: `Độ dài tối đa của mật khẩu là ${passwordMaxLength} ký tự`,
WRONG_USERNAME_OR_PASSWORD: 'Tài khoản hoặc mật khẩu sai',
};
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./dist" /* Redirect output structure to the directory. */,
"rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
// "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
/* Module Resolution Options */
"moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"sourceMap": true,
"outDir": "dist",
"strict": true,
"lib": ["esnext"],
"esModuleInterop": true,
"moduleResolution": "node",
"skipLibCheck": true /* Skip type checking of declaration files. */,
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment