nestjs-prisma-cli 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/index.js +193 -0
- package/bin/template/.github/workflows/deploy-to-ecr.yml +68 -0
- package/bin/template/prisma/schema.prisma +20 -0
- package/bin/template/prisma/seed.ts +30 -0
- package/bin/template/src/app.controller.spec.ts +22 -0
- package/bin/template/src/app.controller.ts +14 -0
- package/bin/template/src/app.module.ts +42 -0
- package/bin/template/src/app.service.ts +8 -0
- package/bin/template/src/common/classes/base64.ts +22 -0
- package/bin/template/src/common/decorator/public.decorator.ts +4 -0
- package/bin/template/src/common/dto/index.ts +4 -0
- package/bin/template/src/common/dto/paginated-response.dto.ts +15 -0
- package/bin/template/src/common/dto/pagination.dto.ts +34 -0
- package/bin/template/src/common/enums/db-error-code.enum.ts +5 -0
- package/bin/template/src/common/enums/index.ts +4 -0
- package/bin/template/src/common/enums/message-code.enum.ts +20 -0
- package/bin/template/src/common/http-interceptor/http-error-type.ts +31 -0
- package/bin/template/src/common/http-interceptor/http-exception.filter.ts +94 -0
- package/bin/template/src/common/http-interceptor/index.ts +5 -0
- package/bin/template/src/common/http-interceptor/logging.interceptor.ts +45 -0
- package/bin/template/src/common/http-interceptor/response.interceptor.ts +41 -0
- package/bin/template/src/common/logger/winston.logger.ts +54 -0
- package/bin/template/src/common/s3/s3.module.ts +8 -0
- package/bin/template/src/common/s3/s3.service.spec.ts +18 -0
- package/bin/template/src/common/s3/s3.service.ts +118 -0
- package/bin/template/src/common/utils/pagination.util.ts +45 -0
- package/bin/template/src/main.ts +60 -0
- package/bin/template/src/modules/auth/auth.controller.spec.ts +18 -0
- package/bin/template/src/modules/auth/auth.controller.ts +98 -0
- package/bin/template/src/modules/auth/auth.module.ts +26 -0
- package/bin/template/src/modules/auth/auth.service.spec.ts +18 -0
- package/bin/template/src/modules/auth/auth.service.ts +64 -0
- package/bin/template/src/modules/auth/dto/login.dto.ts +12 -0
- package/bin/template/src/modules/auth/jwt/jwt.guard.spec.ts +7 -0
- package/bin/template/src/modules/auth/jwt/jwt.guard.ts +21 -0
- package/bin/template/src/modules/auth/jwt/jwt.strategy.ts +26 -0
- package/bin/template/src/modules/user/dto/create-user.dto.ts +21 -0
- package/bin/template/src/modules/user/dto/update-user.dto.ts +4 -0
- package/bin/template/src/modules/user/user.controller.spec.ts +20 -0
- package/bin/template/src/modules/user/user.controller.ts +54 -0
- package/bin/template/src/modules/user/user.module.ts +9 -0
- package/bin/template/src/modules/user/user.service.spec.ts +18 -0
- package/bin/template/src/modules/user/user.service.ts +132 -0
- package/bin/template/src/prisma/prisma.module.ts +9 -0
- package/bin/template/src/prisma/prisma.service.spec.ts +18 -0
- package/bin/template/src/prisma/prisma.service.ts +9 -0
- package/bin/template/test/app.e2e-spec.ts +25 -0
- package/bin/template/test/jest-e2e.json +9 -0
- package/package.json +38 -0
package/bin/index.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import inquirer from "inquirer";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import fs from "fs-extra";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { execa } from "execa";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
|
|
11
|
+
async function main() {
|
|
12
|
+
console.log(chalk.blue("🚀 Welcome to NestJS + Prisma Project Generator!"));
|
|
13
|
+
|
|
14
|
+
const { projectName } = await inquirer.prompt([
|
|
15
|
+
{ type: "input", name: "projectName", message: "Enter your project name:" }
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
const dbSafeName = projectName.replace(/-/g, "_") + "_db";
|
|
19
|
+
const projectPath = path.join(process.cwd(), projectName);
|
|
20
|
+
|
|
21
|
+
if (fs.existsSync(projectPath)) {
|
|
22
|
+
console.log(chalk.red(`❌ Folder "${projectName}" already exists!`));
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const { database } = await inquirer.prompt([
|
|
27
|
+
{
|
|
28
|
+
type: "list",
|
|
29
|
+
name: "database",
|
|
30
|
+
message: "Select your database:",
|
|
31
|
+
choices: ["PostgreSQL", "MySQL", "SQLite", "MongoDB", "CockroachDB", "SQLServer"]
|
|
32
|
+
}
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
console.log(chalk.green(`📦 Creating NestJS project "${projectName}"...`));
|
|
36
|
+
await execa("npx", ["@nestjs/cli", "new", projectName, "--skip-install"], { stdio: "inherit" });
|
|
37
|
+
|
|
38
|
+
console.log(chalk.green("📥 Installing dependencies..."));
|
|
39
|
+
await execa("npm", ["install"], { cwd: projectPath, stdio: "inherit" });
|
|
40
|
+
|
|
41
|
+
await execa("npm", [
|
|
42
|
+
"install",
|
|
43
|
+
"--save",
|
|
44
|
+
"@aws-sdk/client-s3",
|
|
45
|
+
"@aws-sdk/s3-request-presigner",
|
|
46
|
+
"@nestjs/config",
|
|
47
|
+
"@nestjs/jwt",
|
|
48
|
+
"@nestjs/passport",
|
|
49
|
+
"@nestjs/swagger",
|
|
50
|
+
"argon2",
|
|
51
|
+
"class-transformer",
|
|
52
|
+
"class-validator",
|
|
53
|
+
"moment",
|
|
54
|
+
"multer",
|
|
55
|
+
"passport",
|
|
56
|
+
"passport-jwt",
|
|
57
|
+
"prisma",
|
|
58
|
+
"@prisma/client",
|
|
59
|
+
"reflect-metadata",
|
|
60
|
+
"rxjs",
|
|
61
|
+
"swagger-ui-express",
|
|
62
|
+
"winston",
|
|
63
|
+
"winston-daily-rotate-file"
|
|
64
|
+
], { cwd: projectPath, stdio: "inherit" });
|
|
65
|
+
|
|
66
|
+
await execa("npm", [
|
|
67
|
+
"install",
|
|
68
|
+
"--save-dev",
|
|
69
|
+
"@eslint/eslintrc",
|
|
70
|
+
"@eslint/js",
|
|
71
|
+
"@nestjs/cli",
|
|
72
|
+
"@nestjs/schematics",
|
|
73
|
+
"@nestjs/testing",
|
|
74
|
+
"@swc/cli",
|
|
75
|
+
"@swc/core",
|
|
76
|
+
"@types/express",
|
|
77
|
+
"@types/jest",
|
|
78
|
+
"@types/multer",
|
|
79
|
+
"@types/node",
|
|
80
|
+
"@types/supertest",
|
|
81
|
+
"eslint",
|
|
82
|
+
"eslint-config-prettier",
|
|
83
|
+
"eslint-plugin-prettier",
|
|
84
|
+
"globals",
|
|
85
|
+
"jest",
|
|
86
|
+
"prettier",
|
|
87
|
+
"source-map-support",
|
|
88
|
+
"supertest",
|
|
89
|
+
"ts-jest",
|
|
90
|
+
"ts-loader",
|
|
91
|
+
"ts-node",
|
|
92
|
+
"tsconfig-paths",
|
|
93
|
+
"typescript",
|
|
94
|
+
"typescript-eslint"
|
|
95
|
+
], { cwd: projectPath, stdio: "inherit" });
|
|
96
|
+
|
|
97
|
+
console.log(chalk.green("✅ Dependencies installed!"));
|
|
98
|
+
|
|
99
|
+
const provider = database.toLowerCase() === "sqlserver" ? "sqlserver" : database.toLowerCase();
|
|
100
|
+
const prismaSchema = `generator client {
|
|
101
|
+
provider = "prisma-client-js"
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
datasource db {
|
|
105
|
+
provider = "${provider}"
|
|
106
|
+
url = env("DATABASE_URL")
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
model User {
|
|
110
|
+
id Int @id @default(autoincrement())
|
|
111
|
+
userId String @unique
|
|
112
|
+
name String?
|
|
113
|
+
email String
|
|
114
|
+
password String
|
|
115
|
+
isActive Boolean @default(true)
|
|
116
|
+
createdAt DateTime @default(now())
|
|
117
|
+
updatedAt DateTime @updatedAt
|
|
118
|
+
|
|
119
|
+
@@map("tbl_user")
|
|
120
|
+
}
|
|
121
|
+
`;
|
|
122
|
+
await fs.outputFile(path.join(projectPath, "prisma/schema.prisma"), prismaSchema);
|
|
123
|
+
|
|
124
|
+
const defaultUrlMap = {
|
|
125
|
+
postgresql: `postgresql://root:password@localhost:5432/${dbSafeName}?schema=public`,
|
|
126
|
+
mysql: `mysql://root:password@localhost:3306/${dbSafeName}?schema=public`,
|
|
127
|
+
sqlite: `file:./dev.db`,
|
|
128
|
+
mongodb: `mongodb://localhost:27017/${dbSafeName}`,
|
|
129
|
+
cockroachdb: `postgresql://root:password@localhost:26257/${dbSafeName}?sslmode=disable`,
|
|
130
|
+
sqlserver: `sqlserver://localhost:1433;database=${dbSafeName};user=sa;password=your_password;encrypt=false`
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const envContent = `DATABASE_URL="${defaultUrlMap[provider]}"
|
|
134
|
+
|
|
135
|
+
JWT_ACCESS_SECRET="JWT_ACCESS_SECRET"
|
|
136
|
+
JWT_REFRESH_SECRET="JWT_REFRESH_SECRET"
|
|
137
|
+
JWT_ACCESS_EXPIRATION_TIME="1d"
|
|
138
|
+
JWT_REFRESH_EXPIRATION_TIME="30d"
|
|
139
|
+
|
|
140
|
+
AWS_ACCESS_KEY_ID="AWS_ACCESS_KEY_ID"
|
|
141
|
+
AWS_SECRET_ACCESS_KEY="AWS_SECRET_ACCESS_KEY"
|
|
142
|
+
AWS_REGION=ap-southeast-1
|
|
143
|
+
S3_BUCKET="S3_BUCKET"
|
|
144
|
+
|
|
145
|
+
NODE_ENV=dev
|
|
146
|
+
PROJECT_NAME=${dbSafeName}
|
|
147
|
+
PORT=3000
|
|
148
|
+
`;
|
|
149
|
+
|
|
150
|
+
await fs.outputFile(path.join(projectPath, ".env"), envContent);
|
|
151
|
+
console.log(chalk.green("✅ Prisma schema + .env created!"));
|
|
152
|
+
|
|
153
|
+
await execa("npx", ["prisma", "generate"], { cwd: projectPath, stdio: "inherit" });
|
|
154
|
+
console.log(chalk.green("✅ Prisma client generated!"));
|
|
155
|
+
|
|
156
|
+
const templatePath = path.join(__dirname, "template");
|
|
157
|
+
if (!fs.existsSync(templatePath)) {
|
|
158
|
+
console.log(chalk.red("❌ Template folder not found!"));
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
await fs.copy(templatePath, projectPath, { overwrite: true });
|
|
162
|
+
console.log(chalk.green("✅ Template files copied successfully!"));
|
|
163
|
+
|
|
164
|
+
const packageJsonPath = path.join(projectPath, "package.json");
|
|
165
|
+
const packageJson = await fs.readJSON(packageJsonPath);
|
|
166
|
+
packageJson.scripts = {
|
|
167
|
+
...packageJson.scripts,
|
|
168
|
+
"prisma:migrate": "prisma migrate dev --name init",
|
|
169
|
+
"prisma:generate": "prisma generate",
|
|
170
|
+
"prisma:deploy": "prisma migrate deploy",
|
|
171
|
+
"prisma:studio": "prisma studio",
|
|
172
|
+
"prisma:reset": "prisma migrate reset --force",
|
|
173
|
+
"postinstall": "prisma generate",
|
|
174
|
+
"migrate:dev": "dotenv -e .env -- prisma migrate dev",
|
|
175
|
+
"migrate:staging": "dotenv -e .env -- prisma migrate deploy",
|
|
176
|
+
"migrate:prod": "dotenv -e .env -- prisma migrate deploy",
|
|
177
|
+
"seed": "ts-node prisma/seed.ts"
|
|
178
|
+
};
|
|
179
|
+
await fs.writeJSON(packageJsonPath, packageJson, { spaces: 2 });
|
|
180
|
+
console.log(chalk.green("✅ package.json scripts updated successfully!"));
|
|
181
|
+
|
|
182
|
+
console.log(chalk.yellow("🎉 Project is ready! Next steps:"));
|
|
183
|
+
console.log(chalk.cyan(`cd ${projectName}`));
|
|
184
|
+
console.log(chalk.cyan("Check .env"));
|
|
185
|
+
console.log(chalk.cyan("npm run prisma:migrate"));
|
|
186
|
+
console.log(chalk.cyan("npm run seed"));
|
|
187
|
+
console.log(chalk.cyan("npm run start:dev"));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
main().catch(err => {
|
|
191
|
+
console.error(chalk.red("❌ Error:"), err);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
name: Build, Push to ECR, and Deploy to App Runner
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- dev-server
|
|
7
|
+
- staging-server
|
|
8
|
+
- prod-server
|
|
9
|
+
|
|
10
|
+
env:
|
|
11
|
+
AWS_REGION: ap-southeast-1
|
|
12
|
+
ECR_REGISTRY: 339713129923.dkr.ecr.ap-southeast-1.amazonaws.com
|
|
13
|
+
ECR_REPOSITORY: freshmoe-hrms-api
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
deploy:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
|
|
19
|
+
steps:
|
|
20
|
+
- name: Checkout source code
|
|
21
|
+
uses: actions/checkout@v3
|
|
22
|
+
|
|
23
|
+
- name: Configure AWS credentials
|
|
24
|
+
uses: aws-actions/configure-aws-credentials@v2
|
|
25
|
+
with:
|
|
26
|
+
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
|
27
|
+
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
|
28
|
+
aws-region: ${{ env.AWS_REGION }}
|
|
29
|
+
|
|
30
|
+
- name: Login to Amazon ECR
|
|
31
|
+
id: login-ecr
|
|
32
|
+
uses: aws-actions/amazon-ecr-login@v1
|
|
33
|
+
|
|
34
|
+
- name: Determine image tag based on branch
|
|
35
|
+
id: vars
|
|
36
|
+
run: |
|
|
37
|
+
SHORT_SHA=${GITHUB_SHA::7}
|
|
38
|
+
|
|
39
|
+
case "${GITHUB_REF##*/}" in
|
|
40
|
+
dev-server)
|
|
41
|
+
BRANCH_TAG="dev"
|
|
42
|
+
;;
|
|
43
|
+
staging-server)
|
|
44
|
+
BRANCH_TAG="staging"
|
|
45
|
+
;;
|
|
46
|
+
prod-server)
|
|
47
|
+
BRANCH_TAG="prod"
|
|
48
|
+
;;
|
|
49
|
+
*)
|
|
50
|
+
echo "Unsupported branch: ${GITHUB_REF##*/}"
|
|
51
|
+
exit 1
|
|
52
|
+
;;
|
|
53
|
+
esac
|
|
54
|
+
|
|
55
|
+
echo "SHORT_SHA=${SHORT_SHA}" >> $GITHUB_ENV
|
|
56
|
+
echo "BRANCH_TAG=${BRANCH_TAG}" >> $GITHUB_ENV
|
|
57
|
+
|
|
58
|
+
- name: Build, tag, and push Docker image to ECR
|
|
59
|
+
run: |
|
|
60
|
+
docker build --no-cache -t $ECR_REGISTRY/$ECR_REPOSITORY:$BRANCH_TAG .
|
|
61
|
+
docker tag $ECR_REGISTRY/$ECR_REPOSITORY:$BRANCH_TAG $ECR_REGISTRY/$ECR_REPOSITORY:$SHORT_SHA
|
|
62
|
+
|
|
63
|
+
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$BRANCH_TAG
|
|
64
|
+
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$SHORT_SHA
|
|
65
|
+
|
|
66
|
+
echo "Docker images pushed:"
|
|
67
|
+
echo " - $ECR_REGISTRY/$ECR_REPOSITORY:$BRANCH_TAG"
|
|
68
|
+
echo " - $ECR_REGISTRY/$ECR_REPOSITORY:$SHORT_SHA"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
generator client {
|
|
2
|
+
provider = "prisma-client-js"
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
datasource db {
|
|
6
|
+
provider = "mysql"
|
|
7
|
+
url = env("DATABASE_URL")
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
model User {
|
|
11
|
+
id Int @id @default(autoincrement())
|
|
12
|
+
userId String @unique
|
|
13
|
+
name String?
|
|
14
|
+
email String
|
|
15
|
+
password String
|
|
16
|
+
createdAt DateTime @default(now())
|
|
17
|
+
updatedAt DateTime @updatedAt
|
|
18
|
+
@@map("tbl_user")
|
|
19
|
+
}
|
|
20
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { PrismaClient } from "@prisma/client";
|
|
2
|
+
import * as argon2 from "argon2";
|
|
3
|
+
|
|
4
|
+
const prisma = new PrismaClient();
|
|
5
|
+
|
|
6
|
+
async function main() {
|
|
7
|
+
const hashedPassword = await argon2.hash("Asdfasdf@123");
|
|
8
|
+
|
|
9
|
+
const user = await prisma.user.upsert({
|
|
10
|
+
where: { userId: "USER_20250815001" },
|
|
11
|
+
update: {},
|
|
12
|
+
create: {
|
|
13
|
+
userId: "USER_20250815001",
|
|
14
|
+
name: "Kyaw Soe",
|
|
15
|
+
email: "kyawsoe@gmail.com",
|
|
16
|
+
password: hashedPassword,
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
console.log("Seeded user successfully.");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
main()
|
|
24
|
+
.catch((e) => {
|
|
25
|
+
console.error(e);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
})
|
|
28
|
+
.finally(async () => {
|
|
29
|
+
await prisma.$disconnect();
|
|
30
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Test, TestingModule } from '@nestjs/testing';
|
|
2
|
+
import { AppController } from './app.controller';
|
|
3
|
+
import { AppService } from './app.service';
|
|
4
|
+
|
|
5
|
+
describe('AppController', () => {
|
|
6
|
+
let appController: AppController;
|
|
7
|
+
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
const app: TestingModule = await Test.createTestingModule({
|
|
10
|
+
controllers: [AppController],
|
|
11
|
+
providers: [AppService],
|
|
12
|
+
}).compile();
|
|
13
|
+
|
|
14
|
+
appController = app.get<AppController>(AppController);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('root', () => {
|
|
18
|
+
it('should return "Hello World!"', () => {
|
|
19
|
+
expect(appController.getHello()).toBe('Hello World!');
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Controller, Get } from '@nestjs/common';
|
|
2
|
+
import { AppService } from './app.service';
|
|
3
|
+
import { Public } from './common/decorator/public.decorator';
|
|
4
|
+
|
|
5
|
+
@Public()
|
|
6
|
+
@Controller()
|
|
7
|
+
export class AppController {
|
|
8
|
+
constructor(private readonly appService: AppService) {}
|
|
9
|
+
|
|
10
|
+
@Get()
|
|
11
|
+
getHello(): string {
|
|
12
|
+
return this.appService.getHello();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { APP_INTERCEPTOR } from '@nestjs/core';
|
|
3
|
+
import { APP_GUARD } from '@nestjs/core';
|
|
4
|
+
import { AppController } from './app.controller';
|
|
5
|
+
import { AppService } from './app.service';
|
|
6
|
+
import { PrismaModule } from './prisma/prisma.module';
|
|
7
|
+
import {
|
|
8
|
+
HttpResponseInterceptor,
|
|
9
|
+
LoggingInterceptor,
|
|
10
|
+
} from './common/http-interceptor/index';
|
|
11
|
+
import { JwtAuthGuard } from './modules/auth/jwt/jwt.guard';
|
|
12
|
+
import { ConfigModule } from '@nestjs/config';
|
|
13
|
+
import { UserModule } from './modules/user/user.module';
|
|
14
|
+
import { AuthModule } from './modules/auth/auth.module';
|
|
15
|
+
|
|
16
|
+
@Module({
|
|
17
|
+
imports: [
|
|
18
|
+
ConfigModule.forRoot({
|
|
19
|
+
isGlobal: true,
|
|
20
|
+
}),
|
|
21
|
+
PrismaModule,
|
|
22
|
+
UserModule,
|
|
23
|
+
AuthModule
|
|
24
|
+
],
|
|
25
|
+
controllers: [AppController],
|
|
26
|
+
providers: [
|
|
27
|
+
{
|
|
28
|
+
provide: APP_INTERCEPTOR,
|
|
29
|
+
useClass: LoggingInterceptor,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
provide: APP_INTERCEPTOR,
|
|
33
|
+
useClass: HttpResponseInterceptor,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
provide: APP_GUARD,
|
|
37
|
+
useClass: JwtAuthGuard,
|
|
38
|
+
},
|
|
39
|
+
AppService,
|
|
40
|
+
],
|
|
41
|
+
})
|
|
42
|
+
export class AppModule {}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { IsString, IsNotEmpty, Matches } from 'class-validator';
|
|
2
|
+
|
|
3
|
+
export default class Base64File {
|
|
4
|
+
@IsString()
|
|
5
|
+
@IsNotEmpty()
|
|
6
|
+
name: string;
|
|
7
|
+
|
|
8
|
+
@IsString()
|
|
9
|
+
@IsNotEmpty()
|
|
10
|
+
@Matches(
|
|
11
|
+
/^data:([A-Za-z0-9]+\/[A-Za-z0-9\-\.]+);base64,[A-Za-z0-9\-_+=/]+$/,
|
|
12
|
+
{
|
|
13
|
+
message:
|
|
14
|
+
'content must be a valid base64-encoded string with a valid data URI scheme',
|
|
15
|
+
},
|
|
16
|
+
)
|
|
17
|
+
content: string;
|
|
18
|
+
|
|
19
|
+
@IsString()
|
|
20
|
+
@IsNotEmpty()
|
|
21
|
+
mimeType: string;
|
|
22
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ApiProperty } from '@nestjs/swagger';
|
|
2
|
+
|
|
3
|
+
export class PaginatedResponseDto<T> {
|
|
4
|
+
@ApiProperty()
|
|
5
|
+
total: number;
|
|
6
|
+
|
|
7
|
+
@ApiProperty()
|
|
8
|
+
page: number;
|
|
9
|
+
|
|
10
|
+
@ApiProperty()
|
|
11
|
+
limit: number;
|
|
12
|
+
|
|
13
|
+
@ApiProperty({ isArray: true })
|
|
14
|
+
data: T[];
|
|
15
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ApiPropertyOptional } from '@nestjs/swagger';
|
|
2
|
+
import { Type } from 'class-transformer';
|
|
3
|
+
import {
|
|
4
|
+
IsInt,
|
|
5
|
+
IsOptional,
|
|
6
|
+
IsString,
|
|
7
|
+
Min,
|
|
8
|
+
IsBooleanString,
|
|
9
|
+
} from 'class-validator';
|
|
10
|
+
|
|
11
|
+
export class PaginationDto {
|
|
12
|
+
@ApiPropertyOptional({ example: 1, description: 'Page number' })
|
|
13
|
+
@IsOptional()
|
|
14
|
+
@Type(() => Number)
|
|
15
|
+
@IsInt()
|
|
16
|
+
@Min(1)
|
|
17
|
+
page?: number = 1;
|
|
18
|
+
|
|
19
|
+
@ApiPropertyOptional({ example: 20, description: 'Items per page' })
|
|
20
|
+
@IsOptional()
|
|
21
|
+
@Type(() => Number)
|
|
22
|
+
@IsInt()
|
|
23
|
+
@Min(1)
|
|
24
|
+
limit?: number = 20;
|
|
25
|
+
|
|
26
|
+
@ApiPropertyOptional({ example: '', description: 'Search term' })
|
|
27
|
+
@IsOptional()
|
|
28
|
+
@IsString()
|
|
29
|
+
search?: string;
|
|
30
|
+
|
|
31
|
+
@IsOptional()
|
|
32
|
+
@IsBooleanString()
|
|
33
|
+
all?: string;
|
|
34
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export enum MESSAGE_CODE {
|
|
2
|
+
SAVE_SUCCESS = 201,
|
|
3
|
+
SIGNIN_SUCCESS = 202,
|
|
4
|
+
SUCCESS = 203,
|
|
5
|
+
PASSWORD_SUCCESS = 204,
|
|
6
|
+
DATA_NOT_FOUND = 205,
|
|
7
|
+
USER_VERIFY = 206,
|
|
8
|
+
WRONG_USER_PW = 401,
|
|
9
|
+
INVALID_USER_ID = 402,
|
|
10
|
+
MISSED_TOKEN = 403,
|
|
11
|
+
UNAUTHORIZED = 404,
|
|
12
|
+
USER_EXIST = 406,
|
|
13
|
+
TOKEN_ERROR = 407,
|
|
14
|
+
INVALID = 408,
|
|
15
|
+
NOT_FOUND = 409,
|
|
16
|
+
ROUTE_NOT_FOUND = 410,
|
|
17
|
+
REQUEST_FIELD_REQUIRED = 411,
|
|
18
|
+
DELETE_USER = 412,
|
|
19
|
+
SERVER_ERROR = 501,
|
|
20
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const HttpErrorType = {
|
|
2
|
+
400: 'BAD_REQUEST',
|
|
3
|
+
401: 'UNAUTHORIZED',
|
|
4
|
+
402: 'PAYMENT_REQUIRED',
|
|
5
|
+
403: 'FORBIDDEN',
|
|
6
|
+
404: 'NOT_FOUND',
|
|
7
|
+
405: 'METHOD_NOT_ALLOWED',
|
|
8
|
+
406: 'NOT_ACCEPTABLE',
|
|
9
|
+
407: 'PROXY_AUTHENTICATION_REQUIRED',
|
|
10
|
+
408: 'REQUEST_TIMEOUT',
|
|
11
|
+
409: 'CONFLICT',
|
|
12
|
+
410: 'GONE',
|
|
13
|
+
411: 'LENGTH_REQUIRED',
|
|
14
|
+
412: 'PRECONDITION_FAILED',
|
|
15
|
+
413: 'PAYLOAD_TOO_LARGE',
|
|
16
|
+
414: 'URI_TOO_LONG',
|
|
17
|
+
415: 'UNSUPPORTED_MEDIA_TYPE',
|
|
18
|
+
416: 'REQUESTED_RANGE_NOT_SATISFIABLE',
|
|
19
|
+
417: 'EXPECTATION_FAILED',
|
|
20
|
+
418: 'I_AM_A_TEAPOT',
|
|
21
|
+
421: 'MISDIRECTED',
|
|
22
|
+
422: 'UNPROCESSABLE_ENTITY',
|
|
23
|
+
424: 'FAILED_DEPENDENCY',
|
|
24
|
+
429: 'TOO_MANY_REQUESTS',
|
|
25
|
+
500: 'INTERNAL_SERVER_ERROR',
|
|
26
|
+
501: 'NOT_IMPLEMENTED',
|
|
27
|
+
502: 'BAD_GATEWAY',
|
|
28
|
+
503: 'SERVICE_UNAVAILABLE',
|
|
29
|
+
504: 'GATEWAY_TIMEOUT',
|
|
30
|
+
505: 'HTTP_VERSION_NOT_SUPPORTED',
|
|
31
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ExceptionFilter,
|
|
3
|
+
Catch,
|
|
4
|
+
ArgumentsHost,
|
|
5
|
+
HttpException,
|
|
6
|
+
HttpStatus,
|
|
7
|
+
} from '@nestjs/common';
|
|
8
|
+
import { Request, Response } from 'express';
|
|
9
|
+
import { DBErrorCode, MESSAGE_CODE } from '../enums';
|
|
10
|
+
import { AppLogger } from '../logger/winston.logger';
|
|
11
|
+
|
|
12
|
+
@Catch()
|
|
13
|
+
export class HttpExceptionFilter implements ExceptionFilter {
|
|
14
|
+
catch(exception: unknown, host: ArgumentsHost) {
|
|
15
|
+
console.log(exception, 'exception');
|
|
16
|
+
const ctx = host.switchToHttp();
|
|
17
|
+
const request = ctx.getRequest<Request>();
|
|
18
|
+
const response = ctx.getResponse<Response>();
|
|
19
|
+
|
|
20
|
+
let status = HttpStatus.INTERNAL_SERVER_ERROR;
|
|
21
|
+
let message = 'An unexpected error occurred';
|
|
22
|
+
let messageCode: string | number = status;
|
|
23
|
+
|
|
24
|
+
if (exception instanceof HttpException) {
|
|
25
|
+
status = exception.getStatus();
|
|
26
|
+
const exceptionResponse = exception.getResponse();
|
|
27
|
+
|
|
28
|
+
if (typeof exceptionResponse === 'string') {
|
|
29
|
+
message = capitalizeFirst(exceptionResponse);
|
|
30
|
+
messageCode = MESSAGE_CODE.INVALID;
|
|
31
|
+
} else if (
|
|
32
|
+
typeof exceptionResponse === 'object' &&
|
|
33
|
+
exceptionResponse !== null
|
|
34
|
+
) {
|
|
35
|
+
const res: any = exceptionResponse;
|
|
36
|
+
const rawMessage = Array.isArray(res.message)
|
|
37
|
+
? res.message[0]
|
|
38
|
+
: (res.message ?? message);
|
|
39
|
+
|
|
40
|
+
message = capitalizeFirst(rawMessage);
|
|
41
|
+
messageCode =
|
|
42
|
+
res.messageCode ??
|
|
43
|
+
(status === HttpStatus.BAD_REQUEST ? MESSAGE_CODE.INVALID : status);
|
|
44
|
+
} else {
|
|
45
|
+
message = capitalizeFirst(exception.message);
|
|
46
|
+
messageCode = status;
|
|
47
|
+
}
|
|
48
|
+
} else if (typeof exception === 'object' && exception !== null) {
|
|
49
|
+
const ex: any = exception;
|
|
50
|
+
if (ex.code) {
|
|
51
|
+
switch (ex.code) {
|
|
52
|
+
case DBErrorCode.PgUniqueConstraintViolation:
|
|
53
|
+
status = HttpStatus.CONFLICT;
|
|
54
|
+
message = 'Unique constraint violated';
|
|
55
|
+
messageCode = MESSAGE_CODE.INVALID;
|
|
56
|
+
break;
|
|
57
|
+
case DBErrorCode.PgForeignKeyConstraintViolation:
|
|
58
|
+
status = HttpStatus.CONFLICT;
|
|
59
|
+
message = 'Foreign key constraint violated';
|
|
60
|
+
messageCode = MESSAGE_CODE.INVALID;
|
|
61
|
+
break;
|
|
62
|
+
case DBErrorCode.PgNotNullConstraintViolation:
|
|
63
|
+
status = HttpStatus.BAD_REQUEST;
|
|
64
|
+
message = 'Not null constraint violated';
|
|
65
|
+
messageCode = MESSAGE_CODE.INVALID;
|
|
66
|
+
break;
|
|
67
|
+
default:
|
|
68
|
+
message = capitalizeFirst(ex.message || 'Database exception');
|
|
69
|
+
messageCode = status;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const duration = Date.now() - (request as any).__startTime || 0;
|
|
75
|
+
const logMessage = `${request.method} ${request.originalUrl} ${status} - ${duration}ms | Message Code: ${messageCode} | Message: ${message} | Headers: ${JSON.stringify(request.headers)}`;
|
|
76
|
+
|
|
77
|
+
if (status >= 500) {
|
|
78
|
+
AppLogger.error(logMessage);
|
|
79
|
+
} else {
|
|
80
|
+
AppLogger.warn(logMessage);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
response.status(status).json({
|
|
84
|
+
statusCode: status,
|
|
85
|
+
messageCode,
|
|
86
|
+
message,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function capitalizeFirst(text: string): string {
|
|
92
|
+
if (!text || typeof text !== 'string') return text;
|
|
93
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
94
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { HttpExceptionFilter } from './http-exception.filter';
|
|
2
|
+
import { LoggingInterceptor } from './logging.interceptor';
|
|
3
|
+
import { HttpResponseInterceptor } from './response.interceptor';
|
|
4
|
+
|
|
5
|
+
export { HttpResponseInterceptor, LoggingInterceptor, HttpExceptionFilter };
|