base-lw-project-test 1.0.0
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 +139 -0
- package/package.json +27 -0
- package/template/.env +1 -0
- package/template/ecosystem.config.js +18 -0
- package/template/package.json +39 -0
- package/template/scripts/commit-smart.sh +29 -0
- package/template/src/config/cors.ts +24 -0
- package/template/src/config/database.ts +15 -0
- package/template/src/config/env.ts +54 -0
- package/template/src/config/index.ts +4 -0
- package/template/src/config/logger.ts +11 -0
- package/template/src/modules/example/example.controller.ts +62 -0
- package/template/src/modules/example/example.repository.ts +48 -0
- package/template/src/modules/example/example.routes.ts +15 -0
- package/template/src/modules/example/example.schema.ts +30 -0
- package/template/src/modules/example/example.service.ts +139 -0
- package/template/src/modules/example/example.validators.ts +19 -0
- package/template/src/modules/example/index.ts +6 -0
- package/template/src/modules/health/health.controller.ts +10 -0
- package/template/src/modules/health/health.routes.ts +6 -0
- package/template/src/modules/health/index.ts +2 -0
- package/template/src/routes/index.ts +8 -0
- package/template/src/server.ts +41 -0
- package/template/src/shared/constants/db.constant.ts +3 -0
- package/template/src/shared/constants/errors.constant.ts +8 -0
- package/template/src/shared/constants/index.ts +2 -0
- package/template/src/shared/errors/AlreadyExistsException.ts +14 -0
- package/template/src/shared/errors/BadRequestException.ts +11 -0
- package/template/src/shared/errors/BaseError.ts +12 -0
- package/template/src/shared/errors/ErrorResponse.ts +14 -0
- package/template/src/shared/errors/ForbiddenException.ts +12 -0
- package/template/src/shared/errors/InternalServerException.ts +11 -0
- package/template/src/shared/errors/InvalidParamsException.ts +12 -0
- package/template/src/shared/errors/NotFoundException.ts +12 -0
- package/template/src/shared/errors/UnauthorizedException.ts +12 -0
- package/template/src/shared/errors/error.handler.ts +23 -0
- package/template/src/shared/errors/index.ts +10 -0
- package/template/src/shared/handlers/health.handler.ts +9 -0
- package/template/src/shared/handlers/index.ts +1 -0
- package/template/src/shared/helpers/index.ts +1 -0
- package/template/src/shared/helpers/object.helper.ts +5 -0
- package/template/src/shared/middlewares/auth.middleware.ts +32 -0
- package/template/src/shared/middlewares/index.ts +1 -0
- package/template/src/shared/types/auth.context.ts +4 -0
- package/template/src/shared/types/fastify.d.ts +8 -0
- package/template/src/shared/types/index.ts +3 -0
- package/template/src/shared/types/mongo.ts +12 -0
- package/template/src/shared/types/route.ts +35 -0
- package/template/src/shared/utils/index.ts +2 -0
- package/template/src/shared/utils/jwt.ts +26 -0
- package/template/src/shared/utils/validate.ts +11 -0
- package/template/src/shared/validators/index.ts +3 -0
- package/template/src/shared/validators/number.utils.ts +15 -0
- package/template/src/shared/validators/objectId.ts +6 -0
- package/template/src/shared/validators/pageable.schema.ts +11 -0
- package/template/src/shared/validators/yup.ts +9 -0
- package/template/tsconfig.json +41 -0
package/bin/index.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "fs-extra";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import inquirer from "inquirer";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import ora from "ora";
|
|
8
|
+
import { execSync } from "child_process";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
|
|
14
|
+
// 🔥 pega autor do git
|
|
15
|
+
const getGitUser = () => {
|
|
16
|
+
try {
|
|
17
|
+
return execSync("git config user.name").toString().trim();
|
|
18
|
+
} catch {
|
|
19
|
+
return "";
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// 🔁 replace global
|
|
24
|
+
const replaceInFiles = async (dir, replacements) => {
|
|
25
|
+
const files = await fs.readdir(dir);
|
|
26
|
+
|
|
27
|
+
for (const file of files) {
|
|
28
|
+
const fullPath = path.join(dir, file);
|
|
29
|
+
const stat = await fs.stat(fullPath);
|
|
30
|
+
|
|
31
|
+
if (stat.isDirectory()) {
|
|
32
|
+
await replaceInFiles(fullPath, replacements);
|
|
33
|
+
} else {
|
|
34
|
+
let content = await fs.readFile(fullPath, "utf8");
|
|
35
|
+
|
|
36
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
37
|
+
const regex = new RegExp(key, "g");
|
|
38
|
+
content = content.replace(regex, value);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
await fs.writeFile(fullPath, content);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const run = async () => {
|
|
47
|
+
console.log(chalk.green("🚀 Criando projeto base LW...\n"));
|
|
48
|
+
|
|
49
|
+
const answers = await inquirer.prompt([
|
|
50
|
+
{
|
|
51
|
+
type: "input",
|
|
52
|
+
name: "projectName",
|
|
53
|
+
message: "Nome do projeto:",
|
|
54
|
+
default: "my-app",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
type: "input",
|
|
58
|
+
name: "port",
|
|
59
|
+
message: "Porta da aplicação:",
|
|
60
|
+
default: "3000",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
type: "input",
|
|
64
|
+
name: "description",
|
|
65
|
+
message: "Descrição:",
|
|
66
|
+
default: "API com Fastify + Mongoose",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
type: "input",
|
|
70
|
+
name: "author",
|
|
71
|
+
message: "Autor:",
|
|
72
|
+
default: getGitUser(),
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
type: "list",
|
|
76
|
+
name: "packageManager",
|
|
77
|
+
message: "Gerenciador de pacotes:",
|
|
78
|
+
choices: ["npm", "yarn", "pnpm"],
|
|
79
|
+
default: "npm",
|
|
80
|
+
},
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
const projectPath = path.resolve(process.cwd(), answers.projectName);
|
|
84
|
+
const templatePath = path.join(__dirname, "../template");
|
|
85
|
+
|
|
86
|
+
// ❌ evita sobrescrever
|
|
87
|
+
if (fs.existsSync(projectPath)) {
|
|
88
|
+
console.log(chalk.red("❌ Essa pasta já existe"));
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 🚀 copiar template
|
|
93
|
+
const copySpinner = ora("Copiando template...").start();
|
|
94
|
+
await fs.copy(templatePath, projectPath);
|
|
95
|
+
copySpinner.succeed("Template copiado!");
|
|
96
|
+
|
|
97
|
+
// 🔁 replace global
|
|
98
|
+
const replaceSpinner = ora("Configurando projeto...").start();
|
|
99
|
+
|
|
100
|
+
await replaceInFiles(projectPath, {
|
|
101
|
+
"__PROJECT_NAME__": answers.projectName,
|
|
102
|
+
"__PORT__": answers.port,
|
|
103
|
+
"__DESCRIPTION__": answers.description,
|
|
104
|
+
"__AUTHOR__": answers.author || "Unknown",
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
replaceSpinner.succeed("Projeto configurado!");
|
|
108
|
+
|
|
109
|
+
// 📦 instalar deps baseado no gerenciador
|
|
110
|
+
const installSpinner = ora("Instalando dependências...").start();
|
|
111
|
+
|
|
112
|
+
let installCommand = "";
|
|
113
|
+
let runDevCommand = "";
|
|
114
|
+
|
|
115
|
+
if (answers.packageManager === "yarn") {
|
|
116
|
+
installCommand = "yarn";
|
|
117
|
+
runDevCommand = "yarn dev";
|
|
118
|
+
} else if (answers.packageManager === "pnpm") {
|
|
119
|
+
installCommand = "pnpm install";
|
|
120
|
+
runDevCommand = "pnpm dev";
|
|
121
|
+
} else {
|
|
122
|
+
installCommand = "npm install";
|
|
123
|
+
runDevCommand = "npm run dev";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
execSync(installCommand, {
|
|
127
|
+
cwd: projectPath,
|
|
128
|
+
stdio: "ignore",
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
installSpinner.succeed("Dependências instaladas!");
|
|
132
|
+
|
|
133
|
+
console.log(chalk.green("\n✅ Projeto criado com sucesso!"));
|
|
134
|
+
console.log(chalk.yellow("\n👉 Próximos passos:"));
|
|
135
|
+
console.log(chalk.blue(`cd ${answers.projectName}`));
|
|
136
|
+
console.log(chalk.blue(runDevCommand + "\n"));
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
run();
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "base-lw-project-test",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI para gerar projetos Fastify + Mongoose",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"bin": {
|
|
7
|
+
"base-lw-project": "./bin/index.js"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"chalk": "^5.0.0",
|
|
12
|
+
"fs-extra": "^11.2.0",
|
|
13
|
+
"inquirer": "^9.0.0",
|
|
14
|
+
"ora": "^9.3.0"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"bin",
|
|
18
|
+
"template"
|
|
19
|
+
],
|
|
20
|
+
"keywords": [
|
|
21
|
+
"cli",
|
|
22
|
+
"fastify",
|
|
23
|
+
"mongoose",
|
|
24
|
+
"template",
|
|
25
|
+
"scaffold"
|
|
26
|
+
]
|
|
27
|
+
}
|
package/template/.env
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
PORT=__PORT__
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
apps: [
|
|
3
|
+
{
|
|
4
|
+
name: 'base-project-api',
|
|
5
|
+
script: './dist/server.js',
|
|
6
|
+
instances: 1,
|
|
7
|
+
autorestart: true,
|
|
8
|
+
watch: false,
|
|
9
|
+
max_memory_restart: '1G',
|
|
10
|
+
env: {
|
|
11
|
+
NODE_ENV: 'development',
|
|
12
|
+
},
|
|
13
|
+
env_production: {
|
|
14
|
+
NODE_ENV: 'production',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PROJECT_NAME__",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "index.js",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "__AUTHOR__",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "tsx watch src/server.ts",
|
|
9
|
+
"typecheck": "tsc --noEmit",
|
|
10
|
+
"lint": "eslint . --ext .ts",
|
|
11
|
+
"lint:fix": "eslint . --ext .ts --fix",
|
|
12
|
+
"format": "prettier . --write",
|
|
13
|
+
"format:check": "prettier . --check",
|
|
14
|
+
"commit:smart": "bash scripts/commit-smart.sh",
|
|
15
|
+
"build": "tsup src",
|
|
16
|
+
"start": "node ./dist/server.js"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@fastify/cors": "11.2.0",
|
|
20
|
+
"axios": "^1.14.0",
|
|
21
|
+
"dotenv": "^17.3.1",
|
|
22
|
+
"fastify": "^5.8.4",
|
|
23
|
+
"jsonwebtoken": "^9.0.3",
|
|
24
|
+
"mongoose": "^9.3.3",
|
|
25
|
+
"tsup": "8.5.1",
|
|
26
|
+
"yup": "^1.7.1"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/jsonwebtoken": "^9.0.10",
|
|
30
|
+
"@types/node": "^25.5.0",
|
|
31
|
+
"@typescript-eslint/eslint-plugin": "^8.57.2",
|
|
32
|
+
"@typescript-eslint/parser": "^8.57.2",
|
|
33
|
+
"eslint": "^8",
|
|
34
|
+
"eslint-config-prettier": "^10.1.8",
|
|
35
|
+
"prettier": "^3.8.1",
|
|
36
|
+
"tsx": "^4.21.0",
|
|
37
|
+
"typescript": "^5.9.3"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
echo "-> Formatando o projeto..."
|
|
5
|
+
yarn format
|
|
6
|
+
|
|
7
|
+
echo "-> Validando lint..."
|
|
8
|
+
yarn lint
|
|
9
|
+
|
|
10
|
+
echo
|
|
11
|
+
read -r -p "Mensagem do commit: " commit_message
|
|
12
|
+
|
|
13
|
+
if [[ -z "${commit_message}" ]]; then
|
|
14
|
+
echo "Mensagem de commit vazia. Operacao cancelada."
|
|
15
|
+
exit 1
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
echo "-> Adicionando arquivos..."
|
|
19
|
+
git add -A
|
|
20
|
+
|
|
21
|
+
if git diff --cached --quiet; then
|
|
22
|
+
echo "Nenhuma alteracao para commitar."
|
|
23
|
+
exit 1
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
echo "-> Criando commit..."
|
|
27
|
+
git commit -m "${commit_message}"
|
|
28
|
+
|
|
29
|
+
echo "Commit criado com sucesso."
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { FastifyCorsOptions, FastifyCorsOptionsDelegate } from '@fastify/cors'
|
|
2
|
+
import { FastifyRegisterOptions } from 'fastify'
|
|
3
|
+
import { env } from './env'
|
|
4
|
+
|
|
5
|
+
const allowedOrigins = env.CORS_ORIGINS.split(',')
|
|
6
|
+
.map((origin) => origin.trim())
|
|
7
|
+
.filter(Boolean)
|
|
8
|
+
|
|
9
|
+
export const corsConfig: FastifyRegisterOptions<FastifyCorsOptions | FastifyCorsOptionsDelegate> = {
|
|
10
|
+
origin: (origin, cb) => {
|
|
11
|
+
if (!origin) {
|
|
12
|
+
cb(null, true)
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (allowedOrigins.includes(origin)) {
|
|
17
|
+
cb(null, true)
|
|
18
|
+
} else {
|
|
19
|
+
cb(new Error(`Not allowed by CORS: ${origin}`), false)
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
|
24
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import mongoose from 'mongoose'
|
|
2
|
+
import { env } from '@config/env'
|
|
3
|
+
|
|
4
|
+
const MONGO_URI = `${env.DB_URI}/${env.DB_NAME}`
|
|
5
|
+
|
|
6
|
+
export const connectDatabase = async () => {
|
|
7
|
+
try {
|
|
8
|
+
await mongoose.connect(MONGO_URI)
|
|
9
|
+
|
|
10
|
+
console.log('✅ Mongo connected')
|
|
11
|
+
} catch (error) {
|
|
12
|
+
console.error('❌ Mongo connection error', error)
|
|
13
|
+
process.exit(1)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as yup from 'yup'
|
|
2
|
+
import 'dotenv/config'
|
|
3
|
+
|
|
4
|
+
const envSchema = yup.object({
|
|
5
|
+
NODE_ENV: yup
|
|
6
|
+
.mixed<'development' | 'test' | 'production'>()
|
|
7
|
+
.oneOf(['development', 'test', 'production'])
|
|
8
|
+
.default('development')
|
|
9
|
+
.required(),
|
|
10
|
+
|
|
11
|
+
PORT: yup
|
|
12
|
+
.number()
|
|
13
|
+
.transform((_, originalValue) => Number(originalValue))
|
|
14
|
+
.default(8080)
|
|
15
|
+
.required(),
|
|
16
|
+
|
|
17
|
+
BODY_LIMIT: yup
|
|
18
|
+
.number()
|
|
19
|
+
.transform((_, originalValue) => Number(originalValue))
|
|
20
|
+
.default(10485760)
|
|
21
|
+
.required(),
|
|
22
|
+
|
|
23
|
+
JWT_EXPIRES_IN: yup.string().default('7d').required(),
|
|
24
|
+
|
|
25
|
+
JWT_SECRET: yup.string().min(10).required(),
|
|
26
|
+
|
|
27
|
+
DB_NAME: yup.string().min(1).required(),
|
|
28
|
+
|
|
29
|
+
DB_URI: yup
|
|
30
|
+
.string()
|
|
31
|
+
.matches(/^mongodb(\+srv)?:\/\//, 'DB must be a valid Mongo URI')
|
|
32
|
+
.required(),
|
|
33
|
+
|
|
34
|
+
CORS_ORIGINS: yup.string().default('http://localhost:3000').required(),
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
let env: yup.InferType<typeof envSchema>
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
env = envSchema.validateSync(process.env, {
|
|
41
|
+
abortEarly: false,
|
|
42
|
+
stripUnknown: true,
|
|
43
|
+
})
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.error('❌ Invalid environment variables')
|
|
46
|
+
|
|
47
|
+
if (err instanceof yup.ValidationError) {
|
|
48
|
+
console.error(err.errors)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
process.exit(1)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { env }
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { AppRouteHandler, RHWithParamsId } from '@types'
|
|
2
|
+
import {
|
|
3
|
+
createExampleSchema,
|
|
4
|
+
listExamplesSchema,
|
|
5
|
+
paramsIdSchema,
|
|
6
|
+
updateExampleSchema,
|
|
7
|
+
} from './example.validators'
|
|
8
|
+
import { ExampleService } from './example.service'
|
|
9
|
+
import { validateSchema } from '@utils'
|
|
10
|
+
import { HttpStatusCode } from 'axios'
|
|
11
|
+
|
|
12
|
+
const createExample: AppRouteHandler = async (req, res) => {
|
|
13
|
+
const body = await validateSchema(createExampleSchema, req.body)
|
|
14
|
+
|
|
15
|
+
const result = await ExampleService.createExample(body)
|
|
16
|
+
|
|
17
|
+
return res.status(HttpStatusCode.Created).send(result)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const listExamples: AppRouteHandler = async (req, res) => {
|
|
21
|
+
const query = await validateSchema(listExamplesSchema, req.query)
|
|
22
|
+
|
|
23
|
+
const result = await ExampleService.listExamples(query)
|
|
24
|
+
|
|
25
|
+
return res.status(HttpStatusCode.Ok).send(result)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const getExampleById: RHWithParamsId = async (req, res) => {
|
|
29
|
+
const params = await validateSchema(paramsIdSchema, req.params)
|
|
30
|
+
|
|
31
|
+
const result = await ExampleService.getExampleById(params.id)
|
|
32
|
+
|
|
33
|
+
return res.status(HttpStatusCode.Ok).send(result)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const updateExample: RHWithParamsId = async (req, res) => {
|
|
37
|
+
const params = await validateSchema(paramsIdSchema, req.params)
|
|
38
|
+
const body = await validateSchema(updateExampleSchema, req.body)
|
|
39
|
+
|
|
40
|
+
const result = await ExampleService.updateExample({
|
|
41
|
+
id: params.id,
|
|
42
|
+
...body,
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
return res.status(HttpStatusCode.Ok).send(result)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const deleteExample: RHWithParamsId = async (req, res) => {
|
|
49
|
+
const params = await validateSchema(paramsIdSchema, req.params)
|
|
50
|
+
|
|
51
|
+
await ExampleService.deleteExample(params.id)
|
|
52
|
+
|
|
53
|
+
return res.status(HttpStatusCode.NoContent).send()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const ExampleController = {
|
|
57
|
+
createExample,
|
|
58
|
+
listExamples,
|
|
59
|
+
getExampleById,
|
|
60
|
+
updateExample,
|
|
61
|
+
deleteExample,
|
|
62
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ExampleModel } from './example.schema'
|
|
2
|
+
|
|
3
|
+
const findByName = async (name: string) => {
|
|
4
|
+
return ExampleModel.findOne({ name })
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const findById = async (id: string) => {
|
|
8
|
+
return ExampleModel.findById(id)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const list = async ({ page, limit }: { page: number; limit: number }) => {
|
|
12
|
+
const skip = (page - 1) * limit
|
|
13
|
+
|
|
14
|
+
const [items, total] = await Promise.all([
|
|
15
|
+
ExampleModel.find().sort({ createdAt: -1 }).skip(skip).limit(limit),
|
|
16
|
+
ExampleModel.countDocuments(),
|
|
17
|
+
])
|
|
18
|
+
|
|
19
|
+
return { items, total }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const create = async (data: { name: string; description?: string }) => {
|
|
23
|
+
return ExampleModel.create(data)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const update = async (
|
|
27
|
+
id: string,
|
|
28
|
+
data: {
|
|
29
|
+
name?: string
|
|
30
|
+
description?: string
|
|
31
|
+
isActive?: boolean
|
|
32
|
+
},
|
|
33
|
+
) => {
|
|
34
|
+
return ExampleModel.findByIdAndUpdate(id, data, { new: true })
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const remove = async (id: string) => {
|
|
38
|
+
return ExampleModel.findByIdAndDelete(id)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const ExampleRepository = {
|
|
42
|
+
findByName,
|
|
43
|
+
findById,
|
|
44
|
+
list,
|
|
45
|
+
create,
|
|
46
|
+
update,
|
|
47
|
+
remove,
|
|
48
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { FastifyPluginAsync } from 'fastify'
|
|
2
|
+
import { AuthMiddleware } from '@middlewares'
|
|
3
|
+
import { ExampleController } from './example.controller'
|
|
4
|
+
|
|
5
|
+
export const exampleRoutes: FastifyPluginAsync = async (fastify) => {
|
|
6
|
+
fastify.register(async (authenticated) => {
|
|
7
|
+
authenticated.addHook('preHandler', AuthMiddleware)
|
|
8
|
+
|
|
9
|
+
authenticated.post('/', ExampleController.createExample)
|
|
10
|
+
authenticated.get('/', ExampleController.listExamples)
|
|
11
|
+
authenticated.get('/:id', ExampleController.getExampleById)
|
|
12
|
+
authenticated.patch('/:id', ExampleController.updateExample)
|
|
13
|
+
authenticated.delete('/:id', ExampleController.deleteExample)
|
|
14
|
+
})
|
|
15
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Schema, model, InferSchemaType } from 'mongoose'
|
|
2
|
+
import { COLLECTIONS } from '@constants'
|
|
3
|
+
|
|
4
|
+
const ExampleSchema = new Schema(
|
|
5
|
+
{
|
|
6
|
+
name: {
|
|
7
|
+
type: String,
|
|
8
|
+
required: true,
|
|
9
|
+
unique: true,
|
|
10
|
+
trim: true,
|
|
11
|
+
},
|
|
12
|
+
description: {
|
|
13
|
+
type: String,
|
|
14
|
+
required: false,
|
|
15
|
+
trim: true,
|
|
16
|
+
},
|
|
17
|
+
isActive: {
|
|
18
|
+
type: Boolean,
|
|
19
|
+
default: true,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
timestamps: true,
|
|
24
|
+
versionKey: false,
|
|
25
|
+
},
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
export type Example = InferSchemaType<typeof ExampleSchema>
|
|
29
|
+
|
|
30
|
+
export const ExampleModel = model('Example', ExampleSchema, COLLECTIONS.EXAMPLES)
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AlreadyExistsException,
|
|
3
|
+
BadRequestException,
|
|
4
|
+
NotFoundException,
|
|
5
|
+
} from '@errors'
|
|
6
|
+
import { omitUndefined } from '@helpers'
|
|
7
|
+
import { ExampleRepository } from './example.repository'
|
|
8
|
+
|
|
9
|
+
type CreateExampleInput = {
|
|
10
|
+
name: string
|
|
11
|
+
description?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type UpdateExampleInput = {
|
|
15
|
+
id: string
|
|
16
|
+
name?: string
|
|
17
|
+
description?: string
|
|
18
|
+
isActive?: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type ListExamplesInput = {
|
|
22
|
+
page?: number
|
|
23
|
+
limit?: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type ExampleResponse = {
|
|
27
|
+
id: string
|
|
28
|
+
name: string
|
|
29
|
+
description?: string
|
|
30
|
+
isActive: boolean
|
|
31
|
+
createdAt: Date
|
|
32
|
+
updatedAt: Date
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const toExampleResponse = (example: {
|
|
36
|
+
_id: { toString(): string }
|
|
37
|
+
name: string
|
|
38
|
+
description?: string | null
|
|
39
|
+
isActive: boolean
|
|
40
|
+
createdAt: Date
|
|
41
|
+
updatedAt: Date
|
|
42
|
+
}): ExampleResponse => {
|
|
43
|
+
return {
|
|
44
|
+
id: example._id.toString(),
|
|
45
|
+
name: example.name,
|
|
46
|
+
description: example.description ?? undefined,
|
|
47
|
+
isActive: example.isActive,
|
|
48
|
+
createdAt: example.createdAt,
|
|
49
|
+
updatedAt: example.updatedAt,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const createExample = async ({ name, description }: CreateExampleInput) => {
|
|
54
|
+
const exists = await ExampleRepository.findByName(name)
|
|
55
|
+
|
|
56
|
+
if (exists) {
|
|
57
|
+
throw new AlreadyExistsException('Example with this name already exists')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const example = await ExampleRepository.create({
|
|
61
|
+
name,
|
|
62
|
+
description,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
data: toExampleResponse(example),
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const listExamples = async ({ page = 1, limit = 10 }: ListExamplesInput) => {
|
|
71
|
+
const result = await ExampleRepository.list({
|
|
72
|
+
page,
|
|
73
|
+
limit,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
data: result.items.map(toExampleResponse),
|
|
78
|
+
meta: {
|
|
79
|
+
page,
|
|
80
|
+
limit,
|
|
81
|
+
total: result.total,
|
|
82
|
+
totalPages: Math.ceil(result.total / limit),
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const getExampleById = async (id: string) => {
|
|
88
|
+
const example = await ExampleRepository.findById(id)
|
|
89
|
+
|
|
90
|
+
if (!example) {
|
|
91
|
+
throw new NotFoundException('Example not found')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
data: toExampleResponse(example),
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const updateExample = async ({ id, name, description, isActive }: UpdateExampleInput) => {
|
|
100
|
+
const payload = omitUndefined({ name, description, isActive })
|
|
101
|
+
|
|
102
|
+
if (Object.keys(payload).length === 0) {
|
|
103
|
+
throw new BadRequestException('No fields provided for update')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (name) {
|
|
107
|
+
const exists = await ExampleRepository.findByName(name)
|
|
108
|
+
|
|
109
|
+
if (exists && exists._id.toString() !== id) {
|
|
110
|
+
throw new AlreadyExistsException('Example with this name already exists')
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const updated = await ExampleRepository.update(id, payload)
|
|
115
|
+
|
|
116
|
+
if (!updated) {
|
|
117
|
+
throw new NotFoundException('Example not found')
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
data: toExampleResponse(updated),
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const deleteExample = async (id: string) => {
|
|
126
|
+
const deleted = await ExampleRepository.remove(id)
|
|
127
|
+
|
|
128
|
+
if (!deleted) {
|
|
129
|
+
throw new NotFoundException('Example not found')
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export const ExampleService = {
|
|
134
|
+
createExample,
|
|
135
|
+
listExamples,
|
|
136
|
+
getExampleById,
|
|
137
|
+
updateExample,
|
|
138
|
+
deleteExample,
|
|
139
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { pageableSchema } from '@validators'
|
|
2
|
+
import { boolean, object, string } from 'yup'
|
|
3
|
+
|
|
4
|
+
export const createExampleSchema = object({
|
|
5
|
+
name: string().min(2).required(),
|
|
6
|
+
description: string().max(1000).optional(),
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
export const listExamplesSchema = pageableSchema
|
|
10
|
+
|
|
11
|
+
export const paramsIdSchema = object({
|
|
12
|
+
id: string().required(),
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
export const updateExampleSchema = object({
|
|
16
|
+
name: string().min(2).optional(),
|
|
17
|
+
description: string().max(1000).optional(),
|
|
18
|
+
isActive: boolean().optional(),
|
|
19
|
+
})
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { exampleRoutes } from '../modules/example/example.routes'
|
|
2
|
+
import { healthRoutes } from '../modules/health/health.routes'
|
|
3
|
+
import { FastifyInstance } from 'fastify'
|
|
4
|
+
|
|
5
|
+
export const registerRoutes = async (app: FastifyInstance) => {
|
|
6
|
+
app.register(healthRoutes, { prefix: '/health' })
|
|
7
|
+
app.register(exampleRoutes, { prefix: '/examples' })
|
|
8
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import Fastify from "fastify";
|
|
2
|
+
import { connectDatabase, corsConfig, env, loggerConfig } from "@config";
|
|
3
|
+
import { errorHandler } from "@errors";
|
|
4
|
+
import { registerRoutes } from "@routes";
|
|
5
|
+
import cors from "@fastify/cors";
|
|
6
|
+
import "@validators/yup";
|
|
7
|
+
|
|
8
|
+
export const buildApp = () => {
|
|
9
|
+
const app = Fastify({
|
|
10
|
+
logger: loggerConfig,
|
|
11
|
+
bodyLimit: env.BODY_LIMIT,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
app.setErrorHandler(errorHandler);
|
|
15
|
+
|
|
16
|
+
app.register(cors, corsConfig);
|
|
17
|
+
|
|
18
|
+
app.register(registerRoutes);
|
|
19
|
+
|
|
20
|
+
return app;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const start = async () => {
|
|
24
|
+
const app = buildApp();
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
await connectDatabase();
|
|
28
|
+
|
|
29
|
+
await app.listen({
|
|
30
|
+
port: env.PORT,
|
|
31
|
+
host: "0.0.0.0",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
app.log.info(`Server __PROJECT_NAME__ listening on port ${env.PORT}`);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
app.log.error(err);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
void start();
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const ERRORS_MESSAGES = {
|
|
2
|
+
UNEXPECTED_ERROR: 'An unexpected error occurred.',
|
|
3
|
+
ALREADY_EXISTS: 'Resource already exists.',
|
|
4
|
+
INVALID_PARAMS: 'Invalid request payload.',
|
|
5
|
+
NOT_FOUND: 'Resource not found.',
|
|
6
|
+
UNAUTHORIZED: 'Unauthorized.',
|
|
7
|
+
FORBIDDEN: 'Forbidden.',
|
|
8
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { HttpStatusCode } from 'axios'
|
|
2
|
+
import { ERRORS_MESSAGES } from '@constants'
|
|
3
|
+
import { BaseError } from '@errors/BaseError'
|
|
4
|
+
|
|
5
|
+
const DEFAULT_MESSAGE = ERRORS_MESSAGES.ALREADY_EXISTS
|
|
6
|
+
|
|
7
|
+
export class AlreadyExistsException extends BaseError {
|
|
8
|
+
constructor(message?: string) {
|
|
9
|
+
super({
|
|
10
|
+
message: message || DEFAULT_MESSAGE,
|
|
11
|
+
statusCode: HttpStatusCode.BadRequest,
|
|
12
|
+
})
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { HttpStatusCode } from 'axios'
|
|
2
|
+
import { BaseError } from '@errors/BaseError'
|
|
3
|
+
|
|
4
|
+
export class BadRequestException extends BaseError {
|
|
5
|
+
constructor(message?: string) {
|
|
6
|
+
super({
|
|
7
|
+
message: message,
|
|
8
|
+
statusCode: HttpStatusCode.BadRequest,
|
|
9
|
+
})
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { HttpStatusCode } from 'axios'
|
|
2
|
+
import { ERRORS_MESSAGES } from '@constants'
|
|
3
|
+
|
|
4
|
+
export class BaseError {
|
|
5
|
+
message: string
|
|
6
|
+
statusCode: number
|
|
7
|
+
|
|
8
|
+
constructor({ message, statusCode }: { message?: string; statusCode?: number }) {
|
|
9
|
+
this.message = message || ERRORS_MESSAGES.UNEXPECTED_ERROR
|
|
10
|
+
this.statusCode = statusCode || HttpStatusCode.InternalServerError
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { HttpStatusCode } from 'axios'
|
|
2
|
+
import { ERRORS_MESSAGES } from '@constants'
|
|
3
|
+
import { BaseError } from '@errors/BaseError'
|
|
4
|
+
|
|
5
|
+
export class ForbiddenException extends BaseError {
|
|
6
|
+
constructor(message?: string) {
|
|
7
|
+
super({
|
|
8
|
+
message: message || ERRORS_MESSAGES.FORBIDDEN,
|
|
9
|
+
statusCode: HttpStatusCode.Forbidden,
|
|
10
|
+
})
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { HttpStatusCode } from 'axios'
|
|
2
|
+
import { BaseError } from '@errors/BaseError'
|
|
3
|
+
|
|
4
|
+
export class InternalServerException extends BaseError {
|
|
5
|
+
constructor(message?: string) {
|
|
6
|
+
super({
|
|
7
|
+
message: message,
|
|
8
|
+
statusCode: HttpStatusCode.InternalServerError,
|
|
9
|
+
})
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { HttpStatusCode } from 'axios'
|
|
2
|
+
import { ERRORS_MESSAGES } from '@constants'
|
|
3
|
+
import { BaseError } from '@errors/BaseError'
|
|
4
|
+
|
|
5
|
+
export class InvalidParamsException extends BaseError {
|
|
6
|
+
constructor(message?: string) {
|
|
7
|
+
super({
|
|
8
|
+
message: message || ERRORS_MESSAGES.INVALID_PARAMS,
|
|
9
|
+
statusCode: HttpStatusCode.BadRequest,
|
|
10
|
+
})
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { HttpStatusCode } from 'axios'
|
|
2
|
+
import { ERRORS_MESSAGES } from '@constants'
|
|
3
|
+
import { BaseError } from '@errors/BaseError'
|
|
4
|
+
|
|
5
|
+
export class NotFoundException extends BaseError {
|
|
6
|
+
constructor(message?: string) {
|
|
7
|
+
super({
|
|
8
|
+
message: message || ERRORS_MESSAGES.NOT_FOUND,
|
|
9
|
+
statusCode: HttpStatusCode.NotFound,
|
|
10
|
+
})
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { HttpStatusCode } from 'axios'
|
|
2
|
+
import { ERRORS_MESSAGES } from '@constants'
|
|
3
|
+
import { BaseError } from '@errors/BaseError'
|
|
4
|
+
|
|
5
|
+
export class UnauthorizedException extends BaseError {
|
|
6
|
+
constructor(message?: string) {
|
|
7
|
+
super({
|
|
8
|
+
message: message || ERRORS_MESSAGES.UNAUTHORIZED,
|
|
9
|
+
statusCode: HttpStatusCode.Unauthorized,
|
|
10
|
+
})
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { FastifyReply, FastifyRequest } from 'fastify'
|
|
2
|
+
import { BaseError, ErrorResponse } from '@errors'
|
|
3
|
+
import { ValidationError } from 'yup'
|
|
4
|
+
import { ERRORS_MESSAGES } from '@constants'
|
|
5
|
+
import { HttpStatusCode } from 'axios'
|
|
6
|
+
|
|
7
|
+
export const errorHandler = (error: unknown, _req: FastifyRequest, res: FastifyReply) => {
|
|
8
|
+
console.error(error)
|
|
9
|
+
|
|
10
|
+
if (error instanceof BaseError) {
|
|
11
|
+
return res.status(error.statusCode).send(new ErrorResponse(error.message))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (error instanceof ValidationError) {
|
|
15
|
+
return res
|
|
16
|
+
.status(HttpStatusCode.BadRequest)
|
|
17
|
+
.send(new ErrorResponse(ERRORS_MESSAGES.INVALID_PARAMS))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return res
|
|
21
|
+
.status(HttpStatusCode.InternalServerError)
|
|
22
|
+
.send(new ErrorResponse(ERRORS_MESSAGES.UNEXPECTED_ERROR))
|
|
23
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from './AlreadyExistsException'
|
|
2
|
+
export * from './BadRequestException'
|
|
3
|
+
export * from './BaseError'
|
|
4
|
+
export * from './ErrorResponse'
|
|
5
|
+
export * from './InternalServerException'
|
|
6
|
+
export * from './InvalidParamsException'
|
|
7
|
+
export * from './NotFoundException'
|
|
8
|
+
export * from './UnauthorizedException'
|
|
9
|
+
export * from './error.handler'
|
|
10
|
+
export * from './ForbiddenException'
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { HttpStatusCode } from 'axios'
|
|
2
|
+
import { FastifyReply, FastifyRequest } from 'fastify'
|
|
3
|
+
|
|
4
|
+
export const healthHandler = async (_req: FastifyRequest, reply: FastifyReply) => {
|
|
5
|
+
return reply.status(HttpStatusCode.Ok).send({
|
|
6
|
+
status: 'ok',
|
|
7
|
+
timestamp: new Date().toISOString(),
|
|
8
|
+
})
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './health.handler'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './object.helper'
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { UnauthorizedException } from '@errors'
|
|
2
|
+
import { verifyToken } from '@utils'
|
|
3
|
+
import { FastifyReply, FastifyRequest } from 'fastify'
|
|
4
|
+
|
|
5
|
+
export const AuthMiddleware = async (req: FastifyRequest, _res: FastifyReply) => {
|
|
6
|
+
const authHeader = req.headers.authorization
|
|
7
|
+
|
|
8
|
+
if (!authHeader) {
|
|
9
|
+
throw new UnauthorizedException('Missing authorization header')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const [scheme, token] = authHeader.split(' ')
|
|
13
|
+
|
|
14
|
+
if (scheme !== 'Bearer' || !token) {
|
|
15
|
+
throw new UnauthorizedException('Invalid token format')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const { userId, role } = verifyToken(token)
|
|
20
|
+
|
|
21
|
+
if (!userId) {
|
|
22
|
+
throw new UnauthorizedException('Invalid token payload')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
req.user = {
|
|
26
|
+
id: userId,
|
|
27
|
+
role,
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
throw new UnauthorizedException('Invalid or expired token')
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './auth.middleware'
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { FastifyReply, FastifyRequest, RouteGenericInterface } from 'fastify'
|
|
2
|
+
|
|
3
|
+
export interface AppRouteGeneric<
|
|
4
|
+
TBody = unknown,
|
|
5
|
+
TQuery = unknown,
|
|
6
|
+
TParams = unknown,
|
|
7
|
+
THeaders = unknown,
|
|
8
|
+
> extends RouteGenericInterface {
|
|
9
|
+
Body: TBody
|
|
10
|
+
Querystring: TQuery
|
|
11
|
+
Params: TParams
|
|
12
|
+
Headers: THeaders
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type RouteHandler<T extends RouteGenericInterface> = (
|
|
16
|
+
req: FastifyRequest<T>,
|
|
17
|
+
reply: FastifyReply,
|
|
18
|
+
) => Promise<unknown> | unknown
|
|
19
|
+
|
|
20
|
+
export type AppRouteHandler<
|
|
21
|
+
TBody = unknown,
|
|
22
|
+
TQuery = unknown,
|
|
23
|
+
TParams = unknown,
|
|
24
|
+
THeaders = unknown,
|
|
25
|
+
> = RouteHandler<AppRouteGeneric<TBody, TQuery, TParams, THeaders>>
|
|
26
|
+
|
|
27
|
+
type ParamsWithId = {
|
|
28
|
+
id: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type RHWithParamsId<TBody = unknown, TQuery = unknown> = AppRouteHandler<
|
|
32
|
+
TBody,
|
|
33
|
+
TQuery,
|
|
34
|
+
ParamsWithId
|
|
35
|
+
>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { env } from '@config'
|
|
2
|
+
import { InternalServerException } from '@errors'
|
|
3
|
+
import jwt, { SignOptions } from 'jsonwebtoken'
|
|
4
|
+
|
|
5
|
+
export type TokenPayload = {
|
|
6
|
+
userId: string
|
|
7
|
+
role?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const signToken = (payload: TokenPayload, secret = env.JWT_SECRET): string => {
|
|
11
|
+
const options: SignOptions = {
|
|
12
|
+
expiresIn: env.JWT_EXPIRES_IN as SignOptions['expiresIn'],
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return jwt.sign(payload, secret, options)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const verifyToken = (token: string): TokenPayload => {
|
|
19
|
+
const decoded = jwt.verify(token, env.JWT_SECRET)
|
|
20
|
+
|
|
21
|
+
if (typeof decoded === 'string') {
|
|
22
|
+
throw new InternalServerException('Invalid token')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return decoded as TokenPayload
|
|
26
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AnySchema, InferType } from 'yup'
|
|
2
|
+
|
|
3
|
+
export const validateSchema = async <TSchema extends AnySchema>(
|
|
4
|
+
schema: TSchema,
|
|
5
|
+
data: unknown,
|
|
6
|
+
): Promise<InferType<TSchema>> => {
|
|
7
|
+
return schema.validate(data, {
|
|
8
|
+
abortEarly: false,
|
|
9
|
+
stripUnknown: true,
|
|
10
|
+
})
|
|
11
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const toNumber = (defaultValue: number) => (_: unknown, originalValue: unknown) => {
|
|
2
|
+
const parsed = Number(originalValue)
|
|
3
|
+
return isNaN(parsed) ? defaultValue : parsed
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export const toClampedNumber =
|
|
7
|
+
(min: number, max: number, defaultValue: number) => (_: unknown, originalValue: unknown) => {
|
|
8
|
+
const parsed = Number(originalValue)
|
|
9
|
+
|
|
10
|
+
if (isNaN(parsed)) return defaultValue
|
|
11
|
+
if (parsed > max) return max
|
|
12
|
+
if (parsed < min) return min
|
|
13
|
+
|
|
14
|
+
return parsed
|
|
15
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { object, number } from 'yup'
|
|
2
|
+
import { toNumber, toClampedNumber } from './number.utils'
|
|
3
|
+
|
|
4
|
+
export const pageableSchema = object({
|
|
5
|
+
page: number().transform(toNumber(1)).min(1).default(1).optional(),
|
|
6
|
+
|
|
7
|
+
limit: number()
|
|
8
|
+
.transform(toClampedNumber(1, 100, 10))
|
|
9
|
+
.default(10)
|
|
10
|
+
.optional(),
|
|
11
|
+
})
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as yup from 'yup'
|
|
2
|
+
import { isValidObjectId } from './objectId'
|
|
3
|
+
|
|
4
|
+
yup.addMethod(yup.string, 'isObjectId', function (message?: string) {
|
|
5
|
+
return this.test('is-object-id', message || 'Invalid ObjectId', (value) => {
|
|
6
|
+
if (!value) return true
|
|
7
|
+
return isValidObjectId(value)
|
|
8
|
+
})
|
|
9
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "CommonJS",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"baseUrl": ".",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"paths": {
|
|
9
|
+
"@errors": ["./src/shared/errors/index.ts"],
|
|
10
|
+
"@errors/*": ["./src/shared/errors/*"],
|
|
11
|
+
"@constants": ["./src/shared/constants/index.ts"],
|
|
12
|
+
"@constants/*": ["./src/shared/constants/*"],
|
|
13
|
+
"@handlers": ["./src/shared/handlers/index.ts"],
|
|
14
|
+
"@handlers/*": ["./src/shared/handlers/*"],
|
|
15
|
+
"@helpers": ["./src/shared/helpers/index.ts"],
|
|
16
|
+
"@helpers/*": ["./src/shared/helpers/*"],
|
|
17
|
+
"@middlewares": ["./src/shared/middlewares/index.ts"],
|
|
18
|
+
"@middlewares/*": ["./src/shared/middlewares/*"],
|
|
19
|
+
"@types": ["./src/shared/types/index.ts"],
|
|
20
|
+
"@types/*": ["./src/shared/types/*"],
|
|
21
|
+
"@utils": ["./src/shared/utils/index.ts"],
|
|
22
|
+
"@utils/*": ["./src/shared/utils/*"],
|
|
23
|
+
"@validators": ["./src/shared/validators/index.ts"],
|
|
24
|
+
"@validators/*": ["./src/shared/validators/*"],
|
|
25
|
+
"@config": ["./src/config/index.ts"],
|
|
26
|
+
"@config/*": ["./src/config/*"],
|
|
27
|
+
"@Health": ["./src/modules/health/index.ts"],
|
|
28
|
+
"@Health/*": ["./src/modules/health/*"],
|
|
29
|
+
"@Example": ["./src/modules/example/index.ts"],
|
|
30
|
+
"@Example/*": ["./src/modules/example/*"],
|
|
31
|
+
"@routes": ["./src/routes/index.ts"],
|
|
32
|
+
"@routes/*": ["./src/routes/*"]
|
|
33
|
+
},
|
|
34
|
+
"outDir": "dist",
|
|
35
|
+
"esModuleInterop": true,
|
|
36
|
+
"strict": true,
|
|
37
|
+
"skipLibCheck": true
|
|
38
|
+
},
|
|
39
|
+
"include": ["src/**/*.ts"],
|
|
40
|
+
"exclude": ["dist", "node_modules"]
|
|
41
|
+
}
|