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,58 @@
|
|
|
1
|
+
export function serviceTemplate(name: string, pascalName: string, camelName: string): string {
|
|
2
|
+
return `import type { PaginatedResult, PaginationParams } from '../../types/index.js';
|
|
3
|
+
import { NotFoundError, ConflictError } from '../../utils/errors.js';
|
|
4
|
+
import { ${pascalName}Repository, create${pascalName}Repository } from './${name}.repository.js';
|
|
5
|
+
import type { ${pascalName}, Create${pascalName}Data, Update${pascalName}Data, ${pascalName}Filters } from './${name}.types.js';
|
|
6
|
+
import { logger } from '../../core/logger.js';
|
|
7
|
+
|
|
8
|
+
export class ${pascalName}Service {
|
|
9
|
+
constructor(private repository: ${pascalName}Repository) {}
|
|
10
|
+
|
|
11
|
+
async findById(id: string): Promise<${pascalName} | null> {
|
|
12
|
+
return this.repository.findById(id);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async findMany(
|
|
16
|
+
params: PaginationParams,
|
|
17
|
+
filters?: ${pascalName}Filters
|
|
18
|
+
): Promise<PaginatedResult<${pascalName}>> {
|
|
19
|
+
return this.repository.findMany(params, filters);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async create(data: Create${pascalName}Data): Promise<${pascalName}> {
|
|
23
|
+
const item = await this.repository.create(data);
|
|
24
|
+
logger.info({ ${camelName}Id: item.id }, '${pascalName} created');
|
|
25
|
+
return item;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async update(id: string, data: Update${pascalName}Data): Promise<${pascalName}> {
|
|
29
|
+
const existing = await this.repository.findById(id);
|
|
30
|
+
if (!existing) {
|
|
31
|
+
throw new NotFoundError('${pascalName}');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const updated = await this.repository.update(id, data);
|
|
35
|
+
if (!updated) {
|
|
36
|
+
throw new NotFoundError('${pascalName}');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
logger.info({ ${camelName}Id: id }, '${pascalName} updated');
|
|
40
|
+
return updated;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async delete(id: string): Promise<void> {
|
|
44
|
+
const existing = await this.repository.findById(id);
|
|
45
|
+
if (!existing) {
|
|
46
|
+
throw new NotFoundError('${pascalName}');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
await this.repository.delete(id);
|
|
50
|
+
logger.info({ ${camelName}Id: id }, '${pascalName} deleted');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function create${pascalName}Service(repository?: ${pascalName}Repository): ${pascalName}Service {
|
|
55
|
+
return new ${pascalName}Service(repository || create${pascalName}Repository());
|
|
56
|
+
}
|
|
57
|
+
`;
|
|
58
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function typesTemplate(name: string, pascalName: string): string {
|
|
2
|
+
return `import type { BaseEntity } from '../../types/index.js';
|
|
3
|
+
|
|
4
|
+
export interface ${pascalName} extends BaseEntity {
|
|
5
|
+
// Add your ${pascalName} specific fields here
|
|
6
|
+
name: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
// status?: string;
|
|
9
|
+
// metadata?: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface Create${pascalName}Data {
|
|
13
|
+
name: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface Update${pascalName}Data {
|
|
18
|
+
name?: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ${pascalName}Filters {
|
|
23
|
+
search?: string;
|
|
24
|
+
// Add more filters as needed
|
|
25
|
+
}
|
|
26
|
+
`;
|
|
27
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { createServer } from '../../core/server.js';
|
|
5
|
+
import { registerErrorHandler, registerSecurity } from '../../middleware/index.js';
|
|
6
|
+
import { registerSwagger } from '../../modules/swagger/index.js';
|
|
7
|
+
import { registerAuthModule } from '../../modules/auth/index.js';
|
|
8
|
+
import { registerUserModule } from '../../modules/user/index.js';
|
|
9
|
+
import { config } from '../../config/index.js';
|
|
10
|
+
|
|
11
|
+
export async function generateDocs(outputPath = 'openapi.json', silent = false): Promise<string> {
|
|
12
|
+
const spinner = silent ? null : ora('Generating OpenAPI documentation...').start();
|
|
13
|
+
try {
|
|
14
|
+
const server = createServer({
|
|
15
|
+
port: config.server.port,
|
|
16
|
+
host: config.server.host,
|
|
17
|
+
});
|
|
18
|
+
const app = server.instance;
|
|
19
|
+
|
|
20
|
+
registerErrorHandler(app);
|
|
21
|
+
await registerSecurity(app);
|
|
22
|
+
await registerSwagger(app, {
|
|
23
|
+
enabled: true,
|
|
24
|
+
route: config.swagger.route,
|
|
25
|
+
title: config.swagger.title,
|
|
26
|
+
description: config.swagger.description,
|
|
27
|
+
version: config.swagger.version,
|
|
28
|
+
});
|
|
29
|
+
const authService = await registerAuthModule(app);
|
|
30
|
+
await registerUserModule(app, authService);
|
|
31
|
+
|
|
32
|
+
await app.ready();
|
|
33
|
+
const spec = app.swagger();
|
|
34
|
+
|
|
35
|
+
const absoluteOutput = path.resolve(outputPath);
|
|
36
|
+
await fs.mkdir(path.dirname(absoluteOutput), { recursive: true });
|
|
37
|
+
await fs.writeFile(absoluteOutput, JSON.stringify(spec, null, 2), 'utf8');
|
|
38
|
+
|
|
39
|
+
spinner?.succeed(`OpenAPI spec generated at ${absoluteOutput}`);
|
|
40
|
+
|
|
41
|
+
await app.close();
|
|
42
|
+
return absoluteOutput;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
spinner?.fail('Failed to generate OpenAPI documentation');
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
export interface FieldDefinition {
|
|
2
|
+
name: string;
|
|
3
|
+
type: FieldType;
|
|
4
|
+
isOptional: boolean;
|
|
5
|
+
isArray: boolean;
|
|
6
|
+
isUnique: boolean;
|
|
7
|
+
defaultValue?: string;
|
|
8
|
+
relation?: {
|
|
9
|
+
model: string;
|
|
10
|
+
type: 'one-to-one' | 'one-to-many' | 'many-to-one';
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type FieldType =
|
|
15
|
+
| 'string'
|
|
16
|
+
| 'number'
|
|
17
|
+
| 'boolean'
|
|
18
|
+
| 'date'
|
|
19
|
+
| 'datetime'
|
|
20
|
+
| 'text'
|
|
21
|
+
| 'json'
|
|
22
|
+
| 'email'
|
|
23
|
+
| 'url'
|
|
24
|
+
| 'uuid'
|
|
25
|
+
| 'int'
|
|
26
|
+
| 'float'
|
|
27
|
+
| 'decimal'
|
|
28
|
+
| 'enum';
|
|
29
|
+
|
|
30
|
+
// Map field types to TypeScript types
|
|
31
|
+
export const tsTypeMap: Record<FieldType, string> = {
|
|
32
|
+
string: 'string',
|
|
33
|
+
number: 'number',
|
|
34
|
+
boolean: 'boolean',
|
|
35
|
+
date: 'Date',
|
|
36
|
+
datetime: 'Date',
|
|
37
|
+
text: 'string',
|
|
38
|
+
json: 'Record<string, unknown>',
|
|
39
|
+
email: 'string',
|
|
40
|
+
url: 'string',
|
|
41
|
+
uuid: 'string',
|
|
42
|
+
int: 'number',
|
|
43
|
+
float: 'number',
|
|
44
|
+
decimal: 'number',
|
|
45
|
+
enum: 'string',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Map field types to Prisma types
|
|
49
|
+
export const prismaTypeMap: Record<FieldType, string> = {
|
|
50
|
+
string: 'String',
|
|
51
|
+
number: 'Int',
|
|
52
|
+
boolean: 'Boolean',
|
|
53
|
+
date: 'DateTime',
|
|
54
|
+
datetime: 'DateTime',
|
|
55
|
+
text: 'String',
|
|
56
|
+
json: 'Json',
|
|
57
|
+
email: 'String',
|
|
58
|
+
url: 'String',
|
|
59
|
+
uuid: 'String',
|
|
60
|
+
int: 'Int',
|
|
61
|
+
float: 'Float',
|
|
62
|
+
decimal: 'Decimal',
|
|
63
|
+
enum: 'String',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Map field types to Zod validators
|
|
67
|
+
export const zodTypeMap: Record<FieldType, string> = {
|
|
68
|
+
string: 'z.string()',
|
|
69
|
+
number: 'z.number()',
|
|
70
|
+
boolean: 'z.boolean()',
|
|
71
|
+
date: 'z.coerce.date()',
|
|
72
|
+
datetime: 'z.coerce.date()',
|
|
73
|
+
text: 'z.string()',
|
|
74
|
+
json: 'z.record(z.unknown())',
|
|
75
|
+
email: 'z.string().email()',
|
|
76
|
+
url: 'z.string().url()',
|
|
77
|
+
uuid: 'z.string().uuid()',
|
|
78
|
+
int: 'z.number().int()',
|
|
79
|
+
float: 'z.number()',
|
|
80
|
+
decimal: 'z.number()',
|
|
81
|
+
enum: 'z.string()',
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Map field types to Joi validators
|
|
85
|
+
export const joiTypeMap: Record<FieldType, string> = {
|
|
86
|
+
string: 'Joi.string()',
|
|
87
|
+
number: 'Joi.number()',
|
|
88
|
+
boolean: 'Joi.boolean()',
|
|
89
|
+
date: 'Joi.date()',
|
|
90
|
+
datetime: 'Joi.date()',
|
|
91
|
+
text: 'Joi.string()',
|
|
92
|
+
json: 'Joi.object()',
|
|
93
|
+
email: 'Joi.string().email()',
|
|
94
|
+
url: 'Joi.string().uri()',
|
|
95
|
+
uuid: 'Joi.string().uuid()',
|
|
96
|
+
int: 'Joi.number().integer()',
|
|
97
|
+
float: 'Joi.number()',
|
|
98
|
+
decimal: 'Joi.number()',
|
|
99
|
+
enum: 'Joi.string()',
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Map field types to Yup validators
|
|
103
|
+
export const yupTypeMap: Record<FieldType, string> = {
|
|
104
|
+
string: 'yup.string()',
|
|
105
|
+
number: 'yup.number()',
|
|
106
|
+
boolean: 'yup.boolean()',
|
|
107
|
+
date: 'yup.date()',
|
|
108
|
+
datetime: 'yup.date()',
|
|
109
|
+
text: 'yup.string()',
|
|
110
|
+
json: 'yup.object()',
|
|
111
|
+
email: 'yup.string().email()',
|
|
112
|
+
url: 'yup.string().url()',
|
|
113
|
+
uuid: 'yup.string().uuid()',
|
|
114
|
+
int: 'yup.number().integer()',
|
|
115
|
+
float: 'yup.number()',
|
|
116
|
+
decimal: 'yup.number()',
|
|
117
|
+
enum: 'yup.string()',
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Parse field definition string
|
|
122
|
+
* Format: name:type[:modifiers]
|
|
123
|
+
* Examples:
|
|
124
|
+
* - title:string
|
|
125
|
+
* - price:number
|
|
126
|
+
* - email:email:unique
|
|
127
|
+
* - description:text?
|
|
128
|
+
* - tags:string[]
|
|
129
|
+
* - isActive:boolean:default=true
|
|
130
|
+
* - category:relation:Category
|
|
131
|
+
*/
|
|
132
|
+
export function parseField(fieldStr: string): FieldDefinition {
|
|
133
|
+
const parts = fieldStr.split(':');
|
|
134
|
+
let name = parts[0] || '';
|
|
135
|
+
let typeStr = parts[1] || 'string';
|
|
136
|
+
const modifiers = parts.slice(2);
|
|
137
|
+
|
|
138
|
+
// Check for optional (?)
|
|
139
|
+
const isOptional = name.endsWith('?') || typeStr.endsWith('?');
|
|
140
|
+
name = name.replace('?', '');
|
|
141
|
+
typeStr = typeStr.replace('?', '');
|
|
142
|
+
|
|
143
|
+
// Check for array ([])
|
|
144
|
+
const isArray = typeStr.endsWith('[]');
|
|
145
|
+
typeStr = typeStr.replace('[]', '');
|
|
146
|
+
|
|
147
|
+
// Validate type
|
|
148
|
+
const validTypes: FieldType[] = [
|
|
149
|
+
'string', 'number', 'boolean', 'date', 'datetime',
|
|
150
|
+
'text', 'json', 'email', 'url', 'uuid', 'int', 'float', 'decimal', 'enum'
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
let type: FieldType = 'string';
|
|
154
|
+
if (validTypes.includes(typeStr as FieldType)) {
|
|
155
|
+
type = typeStr as FieldType;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Parse modifiers
|
|
159
|
+
let isUnique = false;
|
|
160
|
+
let defaultValue: string | undefined;
|
|
161
|
+
let relation: FieldDefinition['relation'];
|
|
162
|
+
|
|
163
|
+
for (const mod of modifiers) {
|
|
164
|
+
if (mod === 'unique') {
|
|
165
|
+
isUnique = true;
|
|
166
|
+
} else if (mod.startsWith('default=')) {
|
|
167
|
+
defaultValue = mod.replace('default=', '');
|
|
168
|
+
} else if (typeStr === 'relation') {
|
|
169
|
+
relation = {
|
|
170
|
+
model: mod,
|
|
171
|
+
type: 'many-to-one',
|
|
172
|
+
};
|
|
173
|
+
type = 'string'; // Relations use string IDs
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
name,
|
|
179
|
+
type,
|
|
180
|
+
isOptional,
|
|
181
|
+
isArray,
|
|
182
|
+
isUnique,
|
|
183
|
+
defaultValue,
|
|
184
|
+
relation,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Parse multiple fields from command line
|
|
190
|
+
* Example: "title:string price:number description:text?"
|
|
191
|
+
*/
|
|
192
|
+
export function parseFields(fieldsStr: string): FieldDefinition[] {
|
|
193
|
+
if (!fieldsStr) return [];
|
|
194
|
+
|
|
195
|
+
return fieldsStr
|
|
196
|
+
.split(/\s+/)
|
|
197
|
+
.filter(Boolean)
|
|
198
|
+
.map(parseField);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Generate TypeScript interface from fields
|
|
203
|
+
*/
|
|
204
|
+
export function generateTypeInterface(
|
|
205
|
+
name: string,
|
|
206
|
+
fields: FieldDefinition[],
|
|
207
|
+
includeBase = true
|
|
208
|
+
): string {
|
|
209
|
+
const lines: string[] = [];
|
|
210
|
+
|
|
211
|
+
if (includeBase) {
|
|
212
|
+
lines.push(`import type { BaseEntity } from '../../types/index.js';`);
|
|
213
|
+
lines.push('');
|
|
214
|
+
lines.push(`export interface ${name} extends BaseEntity {`);
|
|
215
|
+
} else {
|
|
216
|
+
lines.push(`export interface ${name} {`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
for (const field of fields) {
|
|
220
|
+
const tsType = tsTypeMap[field.type];
|
|
221
|
+
const arrayMark = field.isArray ? '[]' : '';
|
|
222
|
+
const optionalMark = field.isOptional ? '?' : '';
|
|
223
|
+
lines.push(` ${field.name}${optionalMark}: ${tsType}${arrayMark};`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
lines.push('}');
|
|
227
|
+
|
|
228
|
+
return lines.join('\n');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Generate Zod schema from fields
|
|
233
|
+
*/
|
|
234
|
+
export function generateZodSchema(
|
|
235
|
+
name: string,
|
|
236
|
+
fields: FieldDefinition[],
|
|
237
|
+
schemaType: 'create' | 'update' = 'create'
|
|
238
|
+
): string {
|
|
239
|
+
const lines: string[] = [];
|
|
240
|
+
|
|
241
|
+
lines.push(`export const ${schemaType}${name}Schema = z.object({`);
|
|
242
|
+
|
|
243
|
+
for (const field of fields) {
|
|
244
|
+
let validator = zodTypeMap[field.type];
|
|
245
|
+
|
|
246
|
+
if (field.isArray) {
|
|
247
|
+
validator = `z.array(${validator})`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (field.isOptional || schemaType === 'update') {
|
|
251
|
+
validator += '.optional()';
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (field.defaultValue && schemaType === 'create') {
|
|
255
|
+
validator += `.default(${field.defaultValue})`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
lines.push(` ${field.name}: ${validator},`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
lines.push('});');
|
|
262
|
+
|
|
263
|
+
return lines.join('\n');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Generate Prisma model from fields
|
|
268
|
+
*/
|
|
269
|
+
export function generatePrismaModel(
|
|
270
|
+
modelName: string,
|
|
271
|
+
tableName: string,
|
|
272
|
+
fields: FieldDefinition[]
|
|
273
|
+
): string {
|
|
274
|
+
const lines: string[] = [];
|
|
275
|
+
|
|
276
|
+
lines.push(`model ${modelName} {`);
|
|
277
|
+
lines.push(' id String @id @default(uuid())');
|
|
278
|
+
|
|
279
|
+
for (const field of fields) {
|
|
280
|
+
const prismaType = prismaTypeMap[field.type];
|
|
281
|
+
const optionalMark = field.isOptional ? '?' : '';
|
|
282
|
+
const annotations: string[] = [];
|
|
283
|
+
|
|
284
|
+
if (field.isUnique) {
|
|
285
|
+
annotations.push('@unique');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (field.defaultValue) {
|
|
289
|
+
annotations.push(`@default(${field.defaultValue})`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (field.type === 'text') {
|
|
293
|
+
annotations.push('@db.Text');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const annotationStr = annotations.length > 0 ? ' ' + annotations.join(' ') : '';
|
|
297
|
+
lines.push(` ${field.name.padEnd(11)} ${prismaType}${optionalMark}${annotationStr}`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
lines.push('');
|
|
301
|
+
lines.push(' createdAt DateTime @default(now())');
|
|
302
|
+
lines.push(' updatedAt DateTime @updatedAt');
|
|
303
|
+
lines.push('');
|
|
304
|
+
|
|
305
|
+
// Add indexes for unique fields
|
|
306
|
+
const uniqueFields = fields.filter(f => f.isUnique);
|
|
307
|
+
for (const field of uniqueFields) {
|
|
308
|
+
lines.push(` @@index([${field.name}])`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
lines.push(` @@map("${tableName}")`);
|
|
312
|
+
lines.push('}');
|
|
313
|
+
|
|
314
|
+
return lines.join('\n');
|
|
315
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
export function toPascalCase(str: string): string {
|
|
6
|
+
return str
|
|
7
|
+
.split(/[-_\s]+/)
|
|
8
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
9
|
+
.join('');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function toCamelCase(str: string): string {
|
|
13
|
+
const pascal = toPascalCase(str);
|
|
14
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function toKebabCase(str: string): string {
|
|
18
|
+
return str
|
|
19
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
20
|
+
.replace(/[\s_]+/g, '-')
|
|
21
|
+
.toLowerCase();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function toSnakeCase(str: string): string {
|
|
25
|
+
return str
|
|
26
|
+
.replace(/([a-z])([A-Z])/g, '$1_$2')
|
|
27
|
+
.replace(/[\s-]+/g, '_')
|
|
28
|
+
.toLowerCase();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function pluralize(str: string): string {
|
|
32
|
+
if (str.endsWith('y')) {
|
|
33
|
+
return str.slice(0, -1) + 'ies';
|
|
34
|
+
}
|
|
35
|
+
if (str.endsWith('s') || str.endsWith('x') || str.endsWith('ch') || str.endsWith('sh')) {
|
|
36
|
+
return str + 'es';
|
|
37
|
+
}
|
|
38
|
+
return str + 's';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function fileExists(filePath: string): Promise<boolean> {
|
|
42
|
+
try {
|
|
43
|
+
await fs.access(filePath);
|
|
44
|
+
return true;
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function ensureDir(dirPath: string): Promise<void> {
|
|
51
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function writeFile(filePath: string, content: string): Promise<void> {
|
|
55
|
+
await ensureDir(path.dirname(filePath));
|
|
56
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function log(message: string): void {
|
|
60
|
+
console.log(message);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function success(message: string): void {
|
|
64
|
+
console.log(chalk.green('✓'), message);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function error(message: string): void {
|
|
68
|
+
console.error(chalk.red('✗'), message);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function warn(message: string): void {
|
|
72
|
+
console.log(chalk.yellow('⚠'), message);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function info(message: string): void {
|
|
76
|
+
console.log(chalk.blue('ℹ'), message);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function getProjectRoot(): string {
|
|
80
|
+
return process.cwd();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function getSourceDir(): string {
|
|
84
|
+
return path.join(getProjectRoot(), 'src');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function getModulesDir(): string {
|
|
88
|
+
return path.join(getSourceDir(), 'modules');
|
|
89
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import dotenv from 'dotenv';
|
|
3
|
+
import { logger } from '../core/logger.js';
|
|
4
|
+
|
|
5
|
+
// Load .env file
|
|
6
|
+
dotenv.config();
|
|
7
|
+
|
|
8
|
+
const envSchema = z.object({
|
|
9
|
+
// Server
|
|
10
|
+
NODE_ENV: z.enum(['development', 'staging', 'production', 'test']).default('development'),
|
|
11
|
+
PORT: z.string().transform(Number).default('3000'),
|
|
12
|
+
HOST: z.string().default('0.0.0.0'),
|
|
13
|
+
|
|
14
|
+
// Database
|
|
15
|
+
DATABASE_URL: z.string().optional(),
|
|
16
|
+
|
|
17
|
+
// JWT
|
|
18
|
+
JWT_SECRET: z.string().min(32).optional(),
|
|
19
|
+
JWT_ACCESS_EXPIRES_IN: z.string().default('15m'),
|
|
20
|
+
JWT_REFRESH_EXPIRES_IN: z.string().default('7d'),
|
|
21
|
+
|
|
22
|
+
// Security
|
|
23
|
+
CORS_ORIGIN: z.string().default('*'),
|
|
24
|
+
RATE_LIMIT_MAX: z.string().transform(Number).default('100'),
|
|
25
|
+
RATE_LIMIT_WINDOW_MS: z.string().transform(Number).default('60000'),
|
|
26
|
+
|
|
27
|
+
// Email
|
|
28
|
+
SMTP_HOST: z.string().optional(),
|
|
29
|
+
SMTP_PORT: z.string().transform(Number).optional(),
|
|
30
|
+
SMTP_USER: z.string().optional(),
|
|
31
|
+
SMTP_PASS: z.string().optional(),
|
|
32
|
+
SMTP_FROM: z.string().optional(),
|
|
33
|
+
|
|
34
|
+
// Redis (optional)
|
|
35
|
+
REDIS_URL: z.string().optional(),
|
|
36
|
+
|
|
37
|
+
// Swagger/OpenAPI
|
|
38
|
+
SWAGGER_ENABLED: z
|
|
39
|
+
.union([z.literal('true'), z.literal('false')])
|
|
40
|
+
.default('true')
|
|
41
|
+
.transform((val) => val === 'true'),
|
|
42
|
+
SWAGGER_ROUTE: z.string().default('/docs'),
|
|
43
|
+
SWAGGER_TITLE: z.string().default('Servcraft API'),
|
|
44
|
+
SWAGGER_DESCRIPTION: z.string().default('API documentation'),
|
|
45
|
+
SWAGGER_VERSION: z.string().default('1.0.0'),
|
|
46
|
+
|
|
47
|
+
// Logging
|
|
48
|
+
LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export type Env = z.infer<typeof envSchema>;
|
|
52
|
+
|
|
53
|
+
function validateEnv(): Env {
|
|
54
|
+
const parsed = envSchema.safeParse(process.env);
|
|
55
|
+
|
|
56
|
+
if (!parsed.success) {
|
|
57
|
+
logger.error({ errors: parsed.error.flatten().fieldErrors }, 'Invalid environment variables');
|
|
58
|
+
throw new Error('Invalid environment variables');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return parsed.data;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const env = validateEnv();
|
|
65
|
+
|
|
66
|
+
export function isDevelopment(): boolean {
|
|
67
|
+
return env.NODE_ENV === 'development';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function isProduction(): boolean {
|
|
71
|
+
return env.NODE_ENV === 'production';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function isTest(): boolean {
|
|
75
|
+
return env.NODE_ENV === 'test';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function isStaging(): boolean {
|
|
79
|
+
return env.NODE_ENV === 'staging';
|
|
80
|
+
}
|