servcraft 0.1.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/.dockerignore +45 -0
- package/.env.example +46 -0
- package/.husky/commit-msg +1 -0
- package/.husky/pre-commit +1 -0
- package/.prettierignore +4 -0
- package/.prettierrc +11 -0
- package/Dockerfile +76 -0
- package/Dockerfile.dev +31 -0
- package/README.md +232 -0
- package/commitlint.config.js +24 -0
- package/dist/cli/index.cjs +3968 -0
- package/dist/cli/index.cjs.map +1 -0
- package/dist/cli/index.d.cts +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +3945 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.cjs +2458 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +828 -0
- package/dist/index.d.ts +828 -0
- package/dist/index.js +2332 -0
- package/dist/index.js.map +1 -0
- package/docker-compose.prod.yml +118 -0
- package/docker-compose.yml +147 -0
- package/eslint.config.js +27 -0
- package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
- package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
- package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
- package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
- package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +5 -0
- package/npm-cache/_update-notifier-last-checked +0 -0
- package/package.json +112 -0
- package/prisma/schema.prisma +157 -0
- package/src/cli/commands/add-module.ts +422 -0
- package/src/cli/commands/db.ts +137 -0
- package/src/cli/commands/docs.ts +16 -0
- package/src/cli/commands/generate.ts +459 -0
- package/src/cli/commands/init.ts +640 -0
- package/src/cli/index.ts +32 -0
- package/src/cli/templates/controller.ts +67 -0
- package/src/cli/templates/dynamic-prisma.ts +89 -0
- package/src/cli/templates/dynamic-schemas.ts +232 -0
- package/src/cli/templates/dynamic-types.ts +60 -0
- package/src/cli/templates/module-index.ts +33 -0
- package/src/cli/templates/prisma-model.ts +17 -0
- package/src/cli/templates/repository.ts +104 -0
- package/src/cli/templates/routes.ts +70 -0
- package/src/cli/templates/schemas.ts +26 -0
- package/src/cli/templates/service.ts +58 -0
- package/src/cli/templates/types.ts +27 -0
- package/src/cli/utils/docs-generator.ts +47 -0
- package/src/cli/utils/field-parser.ts +315 -0
- package/src/cli/utils/helpers.ts +89 -0
- package/src/config/env.ts +80 -0
- package/src/config/index.ts +97 -0
- package/src/core/index.ts +5 -0
- package/src/core/logger.ts +43 -0
- package/src/core/server.ts +132 -0
- package/src/database/index.ts +7 -0
- package/src/database/prisma.ts +54 -0
- package/src/database/seed.ts +59 -0
- package/src/index.ts +63 -0
- package/src/middleware/error-handler.ts +73 -0
- package/src/middleware/index.ts +3 -0
- package/src/middleware/security.ts +116 -0
- package/src/modules/audit/audit.service.ts +192 -0
- package/src/modules/audit/index.ts +2 -0
- package/src/modules/audit/types.ts +37 -0
- package/src/modules/auth/auth.controller.ts +182 -0
- package/src/modules/auth/auth.middleware.ts +87 -0
- package/src/modules/auth/auth.routes.ts +123 -0
- package/src/modules/auth/auth.service.ts +142 -0
- package/src/modules/auth/index.ts +49 -0
- package/src/modules/auth/schemas.ts +52 -0
- package/src/modules/auth/types.ts +69 -0
- package/src/modules/email/email.service.ts +212 -0
- package/src/modules/email/index.ts +10 -0
- package/src/modules/email/templates.ts +213 -0
- package/src/modules/email/types.ts +57 -0
- package/src/modules/swagger/index.ts +3 -0
- package/src/modules/swagger/schema-builder.ts +263 -0
- package/src/modules/swagger/swagger.service.ts +169 -0
- package/src/modules/swagger/types.ts +68 -0
- package/src/modules/user/index.ts +30 -0
- package/src/modules/user/schemas.ts +49 -0
- package/src/modules/user/types.ts +78 -0
- package/src/modules/user/user.controller.ts +139 -0
- package/src/modules/user/user.repository.ts +156 -0
- package/src/modules/user/user.routes.ts +199 -0
- package/src/modules/user/user.service.ts +145 -0
- package/src/modules/validation/index.ts +18 -0
- package/src/modules/validation/validator.ts +104 -0
- package/src/types/common.ts +61 -0
- package/src/types/index.ts +10 -0
- package/src/utils/errors.ts +66 -0
- package/src/utils/index.ts +33 -0
- package/src/utils/pagination.ts +38 -0
- package/src/utils/response.ts +63 -0
- package/tests/integration/auth.test.ts +59 -0
- package/tests/setup.ts +17 -0
- package/tests/unit/modules/validation.test.ts +88 -0
- package/tests/unit/utils/errors.test.ts +113 -0
- package/tests/unit/utils/pagination.test.ts +82 -0
- package/tsconfig.json +33 -0
- package/tsup.config.ts +14 -0
- package/vitest.config.ts +34 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export function controllerTemplate(name: string, pascalName: string, camelName: string): string {
|
|
2
|
+
return `import type { FastifyRequest, FastifyReply } from 'fastify';
|
|
3
|
+
import type { ${pascalName}Service } from './${name}.service.js';
|
|
4
|
+
import { create${pascalName}Schema, update${pascalName}Schema, ${camelName}QuerySchema } from './${name}.schemas.js';
|
|
5
|
+
import { success, created, noContent } from '../../utils/response.js';
|
|
6
|
+
import { parsePaginationParams } from '../../utils/pagination.js';
|
|
7
|
+
import { validateBody, validateQuery } from '../validation/validator.js';
|
|
8
|
+
|
|
9
|
+
export class ${pascalName}Controller {
|
|
10
|
+
constructor(private ${camelName}Service: ${pascalName}Service) {}
|
|
11
|
+
|
|
12
|
+
async list(request: FastifyRequest, reply: FastifyReply): Promise<void> {
|
|
13
|
+
const query = validateQuery(${camelName}QuerySchema, request.query);
|
|
14
|
+
const pagination = parsePaginationParams(query);
|
|
15
|
+
const filters = {
|
|
16
|
+
search: query.search,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const result = await this.${camelName}Service.findMany(pagination, filters);
|
|
20
|
+
success(reply, result);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async getById(
|
|
24
|
+
request: FastifyRequest<{ Params: { id: string } }>,
|
|
25
|
+
reply: FastifyReply
|
|
26
|
+
): Promise<void> {
|
|
27
|
+
const item = await this.${camelName}Service.findById(request.params.id);
|
|
28
|
+
|
|
29
|
+
if (!item) {
|
|
30
|
+
return reply.status(404).send({
|
|
31
|
+
success: false,
|
|
32
|
+
message: '${pascalName} not found',
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
success(reply, item);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async create(request: FastifyRequest, reply: FastifyReply): Promise<void> {
|
|
40
|
+
const data = validateBody(create${pascalName}Schema, request.body);
|
|
41
|
+
const item = await this.${camelName}Service.create(data);
|
|
42
|
+
created(reply, item);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async update(
|
|
46
|
+
request: FastifyRequest<{ Params: { id: string } }>,
|
|
47
|
+
reply: FastifyReply
|
|
48
|
+
): Promise<void> {
|
|
49
|
+
const data = validateBody(update${pascalName}Schema, request.body);
|
|
50
|
+
const item = await this.${camelName}Service.update(request.params.id, data);
|
|
51
|
+
success(reply, item);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async delete(
|
|
55
|
+
request: FastifyRequest<{ Params: { id: string } }>,
|
|
56
|
+
reply: FastifyReply
|
|
57
|
+
): Promise<void> {
|
|
58
|
+
await this.${camelName}Service.delete(request.params.id);
|
|
59
|
+
noContent(reply);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function create${pascalName}Controller(${camelName}Service: ${pascalName}Service): ${pascalName}Controller {
|
|
64
|
+
return new ${pascalName}Controller(${camelName}Service);
|
|
65
|
+
}
|
|
66
|
+
`;
|
|
67
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { FieldDefinition } from '../utils/field-parser.js';
|
|
2
|
+
import { prismaTypeMap } from '../utils/field-parser.js';
|
|
3
|
+
|
|
4
|
+
export function dynamicPrismaTemplate(
|
|
5
|
+
modelName: string,
|
|
6
|
+
tableName: string,
|
|
7
|
+
fields: FieldDefinition[]
|
|
8
|
+
): string {
|
|
9
|
+
const fieldLines: string[] = [];
|
|
10
|
+
|
|
11
|
+
for (const field of fields) {
|
|
12
|
+
const prismaType = prismaTypeMap[field.type];
|
|
13
|
+
const optionalMark = field.isOptional ? '?' : '';
|
|
14
|
+
const arrayMark = field.isArray ? '[]' : '';
|
|
15
|
+
const annotations: string[] = [];
|
|
16
|
+
|
|
17
|
+
if (field.isUnique) {
|
|
18
|
+
annotations.push('@unique');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (field.defaultValue !== undefined) {
|
|
22
|
+
// Handle different default value types
|
|
23
|
+
if (field.type === 'boolean') {
|
|
24
|
+
annotations.push(`@default(${field.defaultValue})`);
|
|
25
|
+
} else if (field.type === 'number' || field.type === 'int' || field.type === 'float') {
|
|
26
|
+
annotations.push(`@default(${field.defaultValue})`);
|
|
27
|
+
} else {
|
|
28
|
+
annotations.push(`@default("${field.defaultValue}")`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Database-specific annotations
|
|
33
|
+
if (field.type === 'text') {
|
|
34
|
+
annotations.push('@db.Text');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (field.type === 'decimal') {
|
|
38
|
+
annotations.push('@db.Decimal(10, 2)');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const annotationStr = annotations.length > 0 ? ' ' + annotations.join(' ') : '';
|
|
42
|
+
const typePart = `${prismaType}${optionalMark}${arrayMark}`;
|
|
43
|
+
|
|
44
|
+
fieldLines.push(` ${field.name.padEnd(15)} ${typePart.padEnd(12)}${annotationStr}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Generate indexes
|
|
48
|
+
const indexLines: string[] = [];
|
|
49
|
+
|
|
50
|
+
// Index for unique fields
|
|
51
|
+
const uniqueFields = fields.filter((f) => f.isUnique);
|
|
52
|
+
for (const field of uniqueFields) {
|
|
53
|
+
indexLines.push(` @@index([${field.name}])`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Index for common search fields
|
|
57
|
+
const searchableFields = fields.filter(
|
|
58
|
+
(f) => ['string', 'email'].includes(f.type) && !f.isUnique
|
|
59
|
+
);
|
|
60
|
+
if (searchableFields.length > 0) {
|
|
61
|
+
const firstSearchable = searchableFields[0];
|
|
62
|
+
if (firstSearchable) {
|
|
63
|
+
indexLines.push(` @@index([${firstSearchable.name}])`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return `
|
|
68
|
+
// ==========================================
|
|
69
|
+
// Add this model to your prisma/schema.prisma file
|
|
70
|
+
// ==========================================
|
|
71
|
+
|
|
72
|
+
model ${modelName} {
|
|
73
|
+
id String @id @default(uuid())
|
|
74
|
+
|
|
75
|
+
${fieldLines.join('\n')}
|
|
76
|
+
|
|
77
|
+
createdAt DateTime @default(now())
|
|
78
|
+
updatedAt DateTime @updatedAt
|
|
79
|
+
|
|
80
|
+
${indexLines.join('\n')}
|
|
81
|
+
@@map("${tableName}")
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ==========================================
|
|
85
|
+
// After adding the model, run:
|
|
86
|
+
// npm run db:migrate -- --name add_${tableName}
|
|
87
|
+
// ==========================================
|
|
88
|
+
`;
|
|
89
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import type { FieldDefinition } from '../utils/field-parser.js';
|
|
2
|
+
import { zodTypeMap, joiTypeMap, yupTypeMap } from '../utils/field-parser.js';
|
|
3
|
+
|
|
4
|
+
export type ValidatorType = 'zod' | 'joi' | 'yup';
|
|
5
|
+
|
|
6
|
+
export function dynamicSchemasTemplate(
|
|
7
|
+
name: string,
|
|
8
|
+
pascalName: string,
|
|
9
|
+
camelName: string,
|
|
10
|
+
fields: FieldDefinition[],
|
|
11
|
+
validator: ValidatorType = 'zod'
|
|
12
|
+
): string {
|
|
13
|
+
switch (validator) {
|
|
14
|
+
case 'joi':
|
|
15
|
+
return generateJoiSchemas(pascalName, camelName, fields);
|
|
16
|
+
case 'yup':
|
|
17
|
+
return generateYupSchemas(pascalName, camelName, fields);
|
|
18
|
+
default:
|
|
19
|
+
return generateZodSchemas(pascalName, camelName, fields);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function generateZodSchemas(
|
|
24
|
+
pascalName: string,
|
|
25
|
+
camelName: string,
|
|
26
|
+
fields: FieldDefinition[]
|
|
27
|
+
): string {
|
|
28
|
+
const createFields = fields.map((field) => {
|
|
29
|
+
let validator = zodTypeMap[field.type];
|
|
30
|
+
|
|
31
|
+
if (field.isArray) {
|
|
32
|
+
validator = `z.array(${validator})`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (field.isOptional) {
|
|
36
|
+
validator += '.optional()';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (field.defaultValue) {
|
|
40
|
+
validator += `.default(${field.defaultValue})`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Add extra validations based on type
|
|
44
|
+
if (field.type === 'string' && !field.isOptional) {
|
|
45
|
+
validator = validator.replace('z.string()', 'z.string().min(1)');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return ` ${field.name}: ${validator},`;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const updateFields = fields.map((field) => {
|
|
52
|
+
let validator = zodTypeMap[field.type];
|
|
53
|
+
|
|
54
|
+
if (field.isArray) {
|
|
55
|
+
validator = `z.array(${validator})`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
validator += '.optional()';
|
|
59
|
+
|
|
60
|
+
return ` ${field.name}: ${validator},`;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return `import { z } from 'zod';
|
|
64
|
+
|
|
65
|
+
export const create${pascalName}Schema = z.object({
|
|
66
|
+
${createFields.join('\n')}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
export const update${pascalName}Schema = z.object({
|
|
70
|
+
${updateFields.join('\n')}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
export const ${camelName}QuerySchema = z.object({
|
|
74
|
+
page: z.string().transform(Number).optional(),
|
|
75
|
+
limit: z.string().transform(Number).optional(),
|
|
76
|
+
sortBy: z.string().optional(),
|
|
77
|
+
sortOrder: z.enum(['asc', 'desc']).optional(),
|
|
78
|
+
search: z.string().optional(),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
export type Create${pascalName}Input = z.infer<typeof create${pascalName}Schema>;
|
|
82
|
+
export type Update${pascalName}Input = z.infer<typeof update${pascalName}Schema>;
|
|
83
|
+
export type ${pascalName}QueryInput = z.infer<typeof ${camelName}QuerySchema>;
|
|
84
|
+
`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function generateJoiSchemas(
|
|
88
|
+
pascalName: string,
|
|
89
|
+
camelName: string,
|
|
90
|
+
fields: FieldDefinition[]
|
|
91
|
+
): string {
|
|
92
|
+
const createFields = fields.map((field) => {
|
|
93
|
+
let validator = joiTypeMap[field.type];
|
|
94
|
+
|
|
95
|
+
if (field.isArray) {
|
|
96
|
+
validator = `Joi.array().items(${validator})`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!field.isOptional) {
|
|
100
|
+
validator += '.required()';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (field.defaultValue) {
|
|
104
|
+
validator += `.default(${field.defaultValue})`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return ` ${field.name}: ${validator},`;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const updateFields = fields.map((field) => {
|
|
111
|
+
let validator = joiTypeMap[field.type];
|
|
112
|
+
|
|
113
|
+
if (field.isArray) {
|
|
114
|
+
validator = `Joi.array().items(${validator})`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return ` ${field.name}: ${validator},`;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return `import Joi from 'joi';
|
|
121
|
+
|
|
122
|
+
export const create${pascalName}Schema = Joi.object({
|
|
123
|
+
${createFields.join('\n')}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
export const update${pascalName}Schema = Joi.object({
|
|
127
|
+
${updateFields.join('\n')}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
export const ${camelName}QuerySchema = Joi.object({
|
|
131
|
+
page: Joi.number().integer().min(1),
|
|
132
|
+
limit: Joi.number().integer().min(1).max(100),
|
|
133
|
+
sortBy: Joi.string(),
|
|
134
|
+
sortOrder: Joi.string().valid('asc', 'desc'),
|
|
135
|
+
search: Joi.string(),
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
export type Create${pascalName}Input = {
|
|
139
|
+
${fields.map((f) => ` ${f.name}${f.isOptional ? '?' : ''}: ${getJsType(f)};`).join('\n')}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export type Update${pascalName}Input = Partial<Create${pascalName}Input>;
|
|
143
|
+
export type ${pascalName}QueryInput = {
|
|
144
|
+
page?: number;
|
|
145
|
+
limit?: number;
|
|
146
|
+
sortBy?: string;
|
|
147
|
+
sortOrder?: 'asc' | 'desc';
|
|
148
|
+
search?: string;
|
|
149
|
+
};
|
|
150
|
+
`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function generateYupSchemas(
|
|
154
|
+
pascalName: string,
|
|
155
|
+
camelName: string,
|
|
156
|
+
fields: FieldDefinition[]
|
|
157
|
+
): string {
|
|
158
|
+
const createFields = fields.map((field) => {
|
|
159
|
+
let validator = yupTypeMap[field.type];
|
|
160
|
+
|
|
161
|
+
if (field.isArray) {
|
|
162
|
+
validator = `yup.array().of(${validator})`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!field.isOptional) {
|
|
166
|
+
validator += '.required()';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (field.defaultValue) {
|
|
170
|
+
validator += `.default(${field.defaultValue})`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return ` ${field.name}: ${validator},`;
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const updateFields = fields.map((field) => {
|
|
177
|
+
let validator = yupTypeMap[field.type];
|
|
178
|
+
|
|
179
|
+
if (field.isArray) {
|
|
180
|
+
validator = `yup.array().of(${validator})`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
validator += '.optional()';
|
|
184
|
+
|
|
185
|
+
return ` ${field.name}: ${validator},`;
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
return `import * as yup from 'yup';
|
|
189
|
+
|
|
190
|
+
export const create${pascalName}Schema = yup.object({
|
|
191
|
+
${createFields.join('\n')}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
export const update${pascalName}Schema = yup.object({
|
|
195
|
+
${updateFields.join('\n')}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
export const ${camelName}QuerySchema = yup.object({
|
|
199
|
+
page: yup.number().integer().min(1),
|
|
200
|
+
limit: yup.number().integer().min(1).max(100),
|
|
201
|
+
sortBy: yup.string(),
|
|
202
|
+
sortOrder: yup.string().oneOf(['asc', 'desc']),
|
|
203
|
+
search: yup.string(),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
export type Create${pascalName}Input = yup.InferType<typeof create${pascalName}Schema>;
|
|
207
|
+
export type Update${pascalName}Input = yup.InferType<typeof update${pascalName}Schema>;
|
|
208
|
+
export type ${pascalName}QueryInput = yup.InferType<typeof ${camelName}QuerySchema>;
|
|
209
|
+
`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function getJsType(field: FieldDefinition): string {
|
|
213
|
+
const typeMap: Record<string, string> = {
|
|
214
|
+
string: 'string',
|
|
215
|
+
number: 'number',
|
|
216
|
+
boolean: 'boolean',
|
|
217
|
+
date: 'Date',
|
|
218
|
+
datetime: 'Date',
|
|
219
|
+
text: 'string',
|
|
220
|
+
json: 'Record<string, unknown>',
|
|
221
|
+
email: 'string',
|
|
222
|
+
url: 'string',
|
|
223
|
+
uuid: 'string',
|
|
224
|
+
int: 'number',
|
|
225
|
+
float: 'number',
|
|
226
|
+
decimal: 'number',
|
|
227
|
+
enum: 'string',
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const baseType = typeMap[field.type] || 'unknown';
|
|
231
|
+
return field.isArray ? `${baseType}[]` : baseType;
|
|
232
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { FieldDefinition } from '../utils/field-parser.js';
|
|
2
|
+
import { tsTypeMap } from '../utils/field-parser.js';
|
|
3
|
+
|
|
4
|
+
export function dynamicTypesTemplate(
|
|
5
|
+
name: string,
|
|
6
|
+
pascalName: string,
|
|
7
|
+
fields: FieldDefinition[]
|
|
8
|
+
): string {
|
|
9
|
+
const fieldLines = fields.map((field) => {
|
|
10
|
+
const tsType = tsTypeMap[field.type];
|
|
11
|
+
const arrayMark = field.isArray ? '[]' : '';
|
|
12
|
+
const optionalMark = field.isOptional ? '?' : '';
|
|
13
|
+
return ` ${field.name}${optionalMark}: ${tsType}${arrayMark};`;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const createFieldLines = fields
|
|
17
|
+
.filter((f) => !f.isOptional)
|
|
18
|
+
.map((field) => {
|
|
19
|
+
const tsType = tsTypeMap[field.type];
|
|
20
|
+
const arrayMark = field.isArray ? '[]' : '';
|
|
21
|
+
return ` ${field.name}: ${tsType}${arrayMark};`;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const createOptionalLines = fields
|
|
25
|
+
.filter((f) => f.isOptional)
|
|
26
|
+
.map((field) => {
|
|
27
|
+
const tsType = tsTypeMap[field.type];
|
|
28
|
+
const arrayMark = field.isArray ? '[]' : '';
|
|
29
|
+
return ` ${field.name}?: ${tsType}${arrayMark};`;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const updateFieldLines = fields.map((field) => {
|
|
33
|
+
const tsType = tsTypeMap[field.type];
|
|
34
|
+
const arrayMark = field.isArray ? '[]' : '';
|
|
35
|
+
return ` ${field.name}?: ${tsType}${arrayMark};`;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return `import type { BaseEntity } from '../../types/index.js';
|
|
39
|
+
|
|
40
|
+
export interface ${pascalName} extends BaseEntity {
|
|
41
|
+
${fieldLines.join('\n')}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface Create${pascalName}Data {
|
|
45
|
+
${[...createFieldLines, ...createOptionalLines].join('\n')}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface Update${pascalName}Data {
|
|
49
|
+
${updateFieldLines.join('\n')}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ${pascalName}Filters {
|
|
53
|
+
search?: string;
|
|
54
|
+
${fields
|
|
55
|
+
.filter((f) => ['string', 'enum', 'boolean'].includes(f.type))
|
|
56
|
+
.map((f) => ` ${f.name}?: ${tsTypeMap[f.type]};`)
|
|
57
|
+
.join('\n')}
|
|
58
|
+
}
|
|
59
|
+
`;
|
|
60
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export function moduleIndexTemplate(name: string, pascalName: string, camelName: string): string {
|
|
2
|
+
return `import type { FastifyInstance } from 'fastify';
|
|
3
|
+
import { logger } from '../../core/logger.js';
|
|
4
|
+
import { ${pascalName}Service, create${pascalName}Service } from './${name}.service.js';
|
|
5
|
+
import { ${pascalName}Controller, create${pascalName}Controller } from './${name}.controller.js';
|
|
6
|
+
import { ${pascalName}Repository, create${pascalName}Repository } from './${name}.repository.js';
|
|
7
|
+
import { register${pascalName}Routes } from './${name}.routes.js';
|
|
8
|
+
import type { AuthService } from '../auth/auth.service.js';
|
|
9
|
+
|
|
10
|
+
export async function register${pascalName}Module(
|
|
11
|
+
app: FastifyInstance,
|
|
12
|
+
authService: AuthService
|
|
13
|
+
): Promise<void> {
|
|
14
|
+
// Create repository and service
|
|
15
|
+
const repository = create${pascalName}Repository();
|
|
16
|
+
const ${camelName}Service = create${pascalName}Service(repository);
|
|
17
|
+
|
|
18
|
+
// Create controller
|
|
19
|
+
const ${camelName}Controller = create${pascalName}Controller(${camelName}Service);
|
|
20
|
+
|
|
21
|
+
// Register routes
|
|
22
|
+
register${pascalName}Routes(app, ${camelName}Controller, authService);
|
|
23
|
+
|
|
24
|
+
logger.info('${pascalName} module registered');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { ${pascalName}Service, create${pascalName}Service } from './${name}.service.js';
|
|
28
|
+
export { ${pascalName}Controller, create${pascalName}Controller } from './${name}.controller.js';
|
|
29
|
+
export { ${pascalName}Repository, create${pascalName}Repository } from './${name}.repository.js';
|
|
30
|
+
export * from './${name}.types.js';
|
|
31
|
+
export * from './${name}.schemas.js';
|
|
32
|
+
`;
|
|
33
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function prismaModelTemplate(name: string, pascalName: string, tableName: string): string {
|
|
2
|
+
return `
|
|
3
|
+
// Add this model to your prisma/schema.prisma file
|
|
4
|
+
|
|
5
|
+
model ${pascalName} {
|
|
6
|
+
id String @id @default(uuid())
|
|
7
|
+
name String
|
|
8
|
+
description String?
|
|
9
|
+
|
|
10
|
+
createdAt DateTime @default(now())
|
|
11
|
+
updatedAt DateTime @updatedAt
|
|
12
|
+
|
|
13
|
+
@@index([name])
|
|
14
|
+
@@map("${tableName}")
|
|
15
|
+
}
|
|
16
|
+
`;
|
|
17
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export function repositoryTemplate(name: string, pascalName: string, camelName: string, pluralName: string): string {
|
|
2
|
+
return `import { randomUUID } from 'crypto';
|
|
3
|
+
import type { PaginatedResult, PaginationParams } from '../../types/index.js';
|
|
4
|
+
import { createPaginatedResult, getSkip } from '../../utils/pagination.js';
|
|
5
|
+
import type { ${pascalName}, Create${pascalName}Data, Update${pascalName}Data, ${pascalName}Filters } from './${name}.types.js';
|
|
6
|
+
|
|
7
|
+
// In-memory storage (will be replaced by Prisma in production)
|
|
8
|
+
const ${pluralName} = new Map<string, ${pascalName}>();
|
|
9
|
+
|
|
10
|
+
export class ${pascalName}Repository {
|
|
11
|
+
async findById(id: string): Promise<${pascalName} | null> {
|
|
12
|
+
return ${pluralName}.get(id) || null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async findMany(
|
|
16
|
+
params: PaginationParams,
|
|
17
|
+
filters?: ${pascalName}Filters
|
|
18
|
+
): Promise<PaginatedResult<${pascalName}>> {
|
|
19
|
+
let items = Array.from(${pluralName}.values());
|
|
20
|
+
|
|
21
|
+
// Apply filters
|
|
22
|
+
if (filters?.search) {
|
|
23
|
+
const search = filters.search.toLowerCase();
|
|
24
|
+
items = items.filter((item) =>
|
|
25
|
+
JSON.stringify(item).toLowerCase().includes(search)
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Sort
|
|
30
|
+
if (params.sortBy) {
|
|
31
|
+
const sortKey = params.sortBy as keyof ${pascalName};
|
|
32
|
+
items.sort((a, b) => {
|
|
33
|
+
const aVal = a[sortKey];
|
|
34
|
+
const bVal = b[sortKey];
|
|
35
|
+
if (aVal === undefined || bVal === undefined) return 0;
|
|
36
|
+
if (aVal < bVal) return params.sortOrder === 'desc' ? 1 : -1;
|
|
37
|
+
if (aVal > bVal) return params.sortOrder === 'desc' ? -1 : 1;
|
|
38
|
+
return 0;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const total = items.length;
|
|
43
|
+
const skip = getSkip(params);
|
|
44
|
+
const data = items.slice(skip, skip + params.limit);
|
|
45
|
+
|
|
46
|
+
return createPaginatedResult(data, total, params);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async create(data: Create${pascalName}Data): Promise<${pascalName}> {
|
|
50
|
+
const now = new Date();
|
|
51
|
+
const item: ${pascalName} = {
|
|
52
|
+
id: randomUUID(),
|
|
53
|
+
...data,
|
|
54
|
+
createdAt: now,
|
|
55
|
+
updatedAt: now,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
${pluralName}.set(item.id, item);
|
|
59
|
+
return item;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async update(id: string, data: Update${pascalName}Data): Promise<${pascalName} | null> {
|
|
63
|
+
const item = ${pluralName}.get(id);
|
|
64
|
+
if (!item) return null;
|
|
65
|
+
|
|
66
|
+
const updated: ${pascalName} = {
|
|
67
|
+
...item,
|
|
68
|
+
...data,
|
|
69
|
+
updatedAt: new Date(),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
${pluralName}.set(id, updated);
|
|
73
|
+
return updated;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async delete(id: string): Promise<boolean> {
|
|
77
|
+
return ${pluralName}.delete(id);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async count(filters?: ${pascalName}Filters): Promise<number> {
|
|
81
|
+
if (!filters) return ${pluralName}.size;
|
|
82
|
+
|
|
83
|
+
let count = 0;
|
|
84
|
+
for (const item of ${pluralName}.values()) {
|
|
85
|
+
if (filters.search) {
|
|
86
|
+
const search = filters.search.toLowerCase();
|
|
87
|
+
if (!JSON.stringify(item).toLowerCase().includes(search)) continue;
|
|
88
|
+
}
|
|
89
|
+
count++;
|
|
90
|
+
}
|
|
91
|
+
return count;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Clear all (for testing)
|
|
95
|
+
async clear(): Promise<void> {
|
|
96
|
+
${pluralName}.clear();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function create${pascalName}Repository(): ${pascalName}Repository {
|
|
101
|
+
return new ${pascalName}Repository();
|
|
102
|
+
}
|
|
103
|
+
`;
|
|
104
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { FieldDefinition } from '../utils/field-parser.js';
|
|
2
|
+
|
|
3
|
+
export function routesTemplate(
|
|
4
|
+
name: string,
|
|
5
|
+
pascalName: string,
|
|
6
|
+
camelName: string,
|
|
7
|
+
pluralName: string,
|
|
8
|
+
fields: FieldDefinition[] = []
|
|
9
|
+
): string {
|
|
10
|
+
const serializedFields = JSON.stringify(fields, null, 2);
|
|
11
|
+
return `import type { FastifyInstance } from 'fastify';
|
|
12
|
+
import type { ${pascalName}Controller } from './${name}.controller.js';
|
|
13
|
+
import type { AuthService } from '../auth/auth.service.js';
|
|
14
|
+
import { createAuthMiddleware, createRoleMiddleware } from '../auth/auth.middleware.js';
|
|
15
|
+
import { generateRouteSchema } from '../swagger/schema-builder.js';
|
|
16
|
+
import type { FieldDefinition } from '../cli/utils/field-parser.js';
|
|
17
|
+
|
|
18
|
+
const ${camelName}Fields: FieldDefinition[] = ${serializedFields};
|
|
19
|
+
const ${camelName}Schemas = {
|
|
20
|
+
list: generateRouteSchema('${pascalName}', ${camelName}Fields, 'list'),
|
|
21
|
+
get: generateRouteSchema('${pascalName}', ${camelName}Fields, 'get'),
|
|
22
|
+
create: generateRouteSchema('${pascalName}', ${camelName}Fields, 'create'),
|
|
23
|
+
update: generateRouteSchema('${pascalName}', ${camelName}Fields, 'update'),
|
|
24
|
+
delete: generateRouteSchema('${pascalName}', ${camelName}Fields, 'delete'),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function register${pascalName}Routes(
|
|
28
|
+
app: FastifyInstance,
|
|
29
|
+
controller: ${pascalName}Controller,
|
|
30
|
+
authService: AuthService
|
|
31
|
+
): void {
|
|
32
|
+
const authenticate = createAuthMiddleware(authService);
|
|
33
|
+
const isAdmin = createRoleMiddleware(['admin', 'super_admin']);
|
|
34
|
+
|
|
35
|
+
// Public routes (if any)
|
|
36
|
+
// app.get('/${pluralName}/public', controller.publicList.bind(controller));
|
|
37
|
+
|
|
38
|
+
// Protected routes
|
|
39
|
+
app.get(
|
|
40
|
+
'/${pluralName}',
|
|
41
|
+
{ preHandler: [authenticate], ...${camelName}Schemas.list },
|
|
42
|
+
controller.list.bind(controller)
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
app.get(
|
|
46
|
+
'/${pluralName}/:id',
|
|
47
|
+
{ preHandler: [authenticate], ...${camelName}Schemas.get },
|
|
48
|
+
controller.getById.bind(controller)
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
app.post(
|
|
52
|
+
'/${pluralName}',
|
|
53
|
+
{ preHandler: [authenticate], ...${camelName}Schemas.create },
|
|
54
|
+
controller.create.bind(controller)
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
app.patch(
|
|
58
|
+
'/${pluralName}/:id',
|
|
59
|
+
{ preHandler: [authenticate], ...${camelName}Schemas.update },
|
|
60
|
+
controller.update.bind(controller)
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
app.delete(
|
|
64
|
+
'/${pluralName}/:id',
|
|
65
|
+
{ preHandler: [authenticate, isAdmin], ...${camelName}Schemas.delete },
|
|
66
|
+
controller.delete.bind(controller)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
`;
|
|
70
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function schemasTemplate(name: string, pascalName: string, camelName: string): string {
|
|
2
|
+
return `import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
export const create${pascalName}Schema = z.object({
|
|
5
|
+
name: z.string().min(1, 'Name is required').max(255),
|
|
6
|
+
description: z.string().max(1000).optional(),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export const update${pascalName}Schema = z.object({
|
|
10
|
+
name: z.string().min(1).max(255).optional(),
|
|
11
|
+
description: z.string().max(1000).optional(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const ${camelName}QuerySchema = z.object({
|
|
15
|
+
page: z.string().transform(Number).optional(),
|
|
16
|
+
limit: z.string().transform(Number).optional(),
|
|
17
|
+
sortBy: z.string().optional(),
|
|
18
|
+
sortOrder: z.enum(['asc', 'desc']).optional(),
|
|
19
|
+
search: z.string().optional(),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export type Create${pascalName}Input = z.infer<typeof create${pascalName}Schema>;
|
|
23
|
+
export type Update${pascalName}Input = z.infer<typeof update${pascalName}Schema>;
|
|
24
|
+
export type ${pascalName}QueryInput = z.infer<typeof ${camelName}QuerySchema>;
|
|
25
|
+
`;
|
|
26
|
+
}
|