prisma-entity-gen 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/README.md +368 -0
- package/dist/cli/index.cjs +711 -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 +709 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.cjs +691 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +209 -0
- package/dist/index.d.ts +209 -0
- package/dist/index.js +680 -0
- package/dist/index.js.map +1 -0
- package/package.json +67 -0
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
|
|
4
|
+
import { resolve, dirname } from 'path';
|
|
5
|
+
import { z, ZodError } from 'zod';
|
|
6
|
+
import { getSchema } from '@mrleebo/prisma-ast';
|
|
7
|
+
|
|
8
|
+
var ConfigSchema = z.object({
|
|
9
|
+
/**
|
|
10
|
+
* Caminho para o schema.prisma
|
|
11
|
+
* @default "prisma/schema.prisma"
|
|
12
|
+
*/
|
|
13
|
+
schemaPath: z.string().default("prisma/schema.prisma"),
|
|
14
|
+
/**
|
|
15
|
+
* Diretório de saída das entities geradas
|
|
16
|
+
* @default "src/entities"
|
|
17
|
+
*/
|
|
18
|
+
outputDir: z.string().default("src/entities"),
|
|
19
|
+
/**
|
|
20
|
+
* Modo de geração:
|
|
21
|
+
* - "type" → type aliases e interfaces puras (melhor performance TS)
|
|
22
|
+
* - "class" → classes com decorators (NestJS / class-validator)
|
|
23
|
+
* - "both" → gera os dois em subpastas /types e /classes
|
|
24
|
+
*/
|
|
25
|
+
mode: z.enum(["type", "class", "both"]).default("type"),
|
|
26
|
+
/**
|
|
27
|
+
* Quando mode="class", inclui decorators do class-validator
|
|
28
|
+
* @default false
|
|
29
|
+
*/
|
|
30
|
+
classValidator: z.boolean().default(false),
|
|
31
|
+
/**
|
|
32
|
+
* Quando mode="class", inclui decorators do class-transformer
|
|
33
|
+
* @default false
|
|
34
|
+
*/
|
|
35
|
+
classTransformer: z.boolean().default(false),
|
|
36
|
+
/**
|
|
37
|
+
* Gera barrel index.ts na raiz do outputDir
|
|
38
|
+
* @default true
|
|
39
|
+
*/
|
|
40
|
+
barrel: z.boolean().default(true),
|
|
41
|
+
/**
|
|
42
|
+
* Modelos a ignorar na geração
|
|
43
|
+
* @default []
|
|
44
|
+
*/
|
|
45
|
+
exclude: z.array(z.string()).default([]),
|
|
46
|
+
/**
|
|
47
|
+
* Apenas esses modelos serão gerados (vazio = todos)
|
|
48
|
+
* @default []
|
|
49
|
+
*/
|
|
50
|
+
include: z.array(z.string()).default([]),
|
|
51
|
+
/**
|
|
52
|
+
* Formato de saída dos enums:
|
|
53
|
+
* - "enum" → export enum Role { ADMIN = "ADMIN" }
|
|
54
|
+
* - "union" → export type Role = "ADMIN" | "USER"
|
|
55
|
+
* Use "union" para evitar conflitos com enums do @prisma/client
|
|
56
|
+
* @default "enum"
|
|
57
|
+
*/
|
|
58
|
+
enumOutput: z.enum(["enum", "union"]).default("enum"),
|
|
59
|
+
/**
|
|
60
|
+
* Define se campos de relação são opcionais ou obrigatórios na entity gerada:
|
|
61
|
+
* - "optional" → relações sempre com ? (compatível com queries sem include)
|
|
62
|
+
* - "required" → relações sem ? (requer include em toda query que retornar o tipo)
|
|
63
|
+
* @default "optional"
|
|
64
|
+
*/
|
|
65
|
+
relations: z.enum(["optional", "required"]).default("optional"),
|
|
66
|
+
/**
|
|
67
|
+
* Sufixo dos arquivos gerados
|
|
68
|
+
* @default "entity"
|
|
69
|
+
* Resulta em: user.entity.ts
|
|
70
|
+
*/
|
|
71
|
+
suffix: z.string().default("entity"),
|
|
72
|
+
/**
|
|
73
|
+
* Simula a geração sem escrever arquivos
|
|
74
|
+
* @default false
|
|
75
|
+
*/
|
|
76
|
+
dryRun: z.boolean().default(false),
|
|
77
|
+
/**
|
|
78
|
+
* Nível de log: "silent" | "info" | "verbose"
|
|
79
|
+
* @default "info"
|
|
80
|
+
*/
|
|
81
|
+
logLevel: z.enum(["silent", "info", "verbose"]).default("info")
|
|
82
|
+
});
|
|
83
|
+
ConfigSchema.parse({});
|
|
84
|
+
|
|
85
|
+
// src/config/loader.ts
|
|
86
|
+
var RC_FILES = [
|
|
87
|
+
".entitygenrc.json",
|
|
88
|
+
"entitygen.config.json",
|
|
89
|
+
".entitygenrc"
|
|
90
|
+
];
|
|
91
|
+
var ConfigError = class extends Error {
|
|
92
|
+
constructor(message, cause) {
|
|
93
|
+
super(message, { cause });
|
|
94
|
+
this.name = "ConfigError";
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
function loadConfig(cwd = process.cwd()) {
|
|
98
|
+
const rcFile = RC_FILES.map((f) => resolve(cwd, f)).find(existsSync);
|
|
99
|
+
if (!rcFile) {
|
|
100
|
+
return ConfigSchema.parse({});
|
|
101
|
+
}
|
|
102
|
+
let raw;
|
|
103
|
+
try {
|
|
104
|
+
raw = JSON.parse(readFileSync(rcFile, "utf-8"));
|
|
105
|
+
} catch (err) {
|
|
106
|
+
throw new ConfigError(`Falha ao ler ${rcFile}: arquivo JSON inv\xE1lido`, err);
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
return ConfigSchema.parse(raw);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
if (err instanceof ZodError) {
|
|
112
|
+
const messages = err.errors.map((e) => ` \u2022 ${e.path.join(".")} \u2014 ${e.message}`).join("\n");
|
|
113
|
+
throw new ConfigError(
|
|
114
|
+
`Config inv\xE1lido em ${rcFile}:
|
|
115
|
+
${messages}`,
|
|
116
|
+
err
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
throw err;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/parser/types.ts
|
|
124
|
+
var PRISMA_TO_TS = {
|
|
125
|
+
String: "string",
|
|
126
|
+
Boolean: "boolean",
|
|
127
|
+
Int: "number",
|
|
128
|
+
BigInt: "bigint",
|
|
129
|
+
Float: "number",
|
|
130
|
+
Decimal: "string",
|
|
131
|
+
// Decimal não tem tipo nativo TS — string é mais seguro que number
|
|
132
|
+
DateTime: "Date",
|
|
133
|
+
Json: "Record<string, unknown>",
|
|
134
|
+
Bytes: "Buffer"
|
|
135
|
+
};
|
|
136
|
+
var SCALARS = new Set(Object.keys(PRISMA_TO_TS));
|
|
137
|
+
function isScalar(type) {
|
|
138
|
+
return SCALARS.has(type);
|
|
139
|
+
}
|
|
140
|
+
function resolveTsType(prismaType, knownEnums) {
|
|
141
|
+
if (isScalar(prismaType)) return PRISMA_TO_TS[prismaType];
|
|
142
|
+
if (knownEnums.has(prismaType)) return prismaType;
|
|
143
|
+
return prismaType;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/parser/relations.ts
|
|
147
|
+
function detectCircularRefs(graph) {
|
|
148
|
+
const color = /* @__PURE__ */ new Map();
|
|
149
|
+
const circular = /* @__PURE__ */ new Map();
|
|
150
|
+
for (const node of graph.keys()) {
|
|
151
|
+
color.set(node, "white");
|
|
152
|
+
circular.set(node, /* @__PURE__ */ new Set());
|
|
153
|
+
}
|
|
154
|
+
function addCircular(a, b) {
|
|
155
|
+
circular.get(a)?.add(b);
|
|
156
|
+
circular.get(b)?.add(a);
|
|
157
|
+
}
|
|
158
|
+
function dfs(node, ancestors) {
|
|
159
|
+
color.set(node, "gray");
|
|
160
|
+
for (const neighbor of graph.get(node) ?? []) {
|
|
161
|
+
if (!color.has(neighbor)) continue;
|
|
162
|
+
if (color.get(neighbor) === "gray") {
|
|
163
|
+
addCircular(node, neighbor);
|
|
164
|
+
for (const anc of ancestors) {
|
|
165
|
+
if (color.get(anc) === "gray") {
|
|
166
|
+
addCircular(anc, neighbor);
|
|
167
|
+
addCircular(node, anc);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} else if (color.get(neighbor) === "white") {
|
|
171
|
+
dfs(neighbor, [...ancestors, node]);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
color.set(node, "black");
|
|
175
|
+
}
|
|
176
|
+
for (const node of graph.keys()) {
|
|
177
|
+
if (color.get(node) === "white") {
|
|
178
|
+
dfs(node, []);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return circular;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// src/parser/index.ts
|
|
185
|
+
function hasAttribute(field, name) {
|
|
186
|
+
return field.attributes?.some((a) => a.name === name) ?? false;
|
|
187
|
+
}
|
|
188
|
+
function hasDefault(field) {
|
|
189
|
+
return hasAttribute(field, "default") || hasAttribute(field, "updatedAt");
|
|
190
|
+
}
|
|
191
|
+
function parseEnum(node) {
|
|
192
|
+
const values = node.enumerators.filter((e) => e.type === "enumerator").map((e) => e.name);
|
|
193
|
+
return { name: node.name, values, documentation: node.comment };
|
|
194
|
+
}
|
|
195
|
+
function parseField(field, knownModels, knownEnums) {
|
|
196
|
+
return {
|
|
197
|
+
name: field.name,
|
|
198
|
+
prismaType: field.fieldType,
|
|
199
|
+
tsType: resolveTsType(field.fieldType, knownEnums),
|
|
200
|
+
isOptional: field.optional,
|
|
201
|
+
isArray: field.array,
|
|
202
|
+
isRelation: knownModels.has(field.fieldType),
|
|
203
|
+
isCircular: false,
|
|
204
|
+
hasDefault: hasDefault(field),
|
|
205
|
+
isId: hasAttribute(field, "id"),
|
|
206
|
+
isUnique: hasAttribute(field, "unique"),
|
|
207
|
+
isUpdatedAt: hasAttribute(field, "updatedAt"),
|
|
208
|
+
decorators: []
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function parseModel(node, knownModels, knownEnums) {
|
|
212
|
+
const fields = node.properties.filter((p) => p.type === "field").map((f) => parseField(f, knownModels, knownEnums));
|
|
213
|
+
const relations = [...new Set(
|
|
214
|
+
fields.filter((f) => f.isRelation).map((f) => f.prismaType)
|
|
215
|
+
)];
|
|
216
|
+
return {
|
|
217
|
+
name: node.name,
|
|
218
|
+
fields,
|
|
219
|
+
relations,
|
|
220
|
+
circularRefs: [],
|
|
221
|
+
documentation: node.comment
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
var ParseError = class extends Error {
|
|
225
|
+
constructor(message, cause) {
|
|
226
|
+
super(message, { cause });
|
|
227
|
+
this.name = "ParseError";
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
function parseSchema(schemaPath) {
|
|
231
|
+
let raw;
|
|
232
|
+
try {
|
|
233
|
+
raw = readFileSync(schemaPath, "utf-8");
|
|
234
|
+
} catch (err) {
|
|
235
|
+
throw new ParseError(`N\xE3o foi poss\xEDvel ler o schema em "${schemaPath}"`, err);
|
|
236
|
+
}
|
|
237
|
+
return parseSchemaString(raw);
|
|
238
|
+
}
|
|
239
|
+
function parseSchemaString(schemaContent) {
|
|
240
|
+
let ast;
|
|
241
|
+
try {
|
|
242
|
+
ast = getSchema(schemaContent);
|
|
243
|
+
} catch (err) {
|
|
244
|
+
throw new ParseError("Falha ao fazer parse do schema Prisma", err);
|
|
245
|
+
}
|
|
246
|
+
const rawNodes = ast.list;
|
|
247
|
+
const knownModels = new Set(
|
|
248
|
+
rawNodes.filter((n) => n.type === "model").map((n) => n.name)
|
|
249
|
+
);
|
|
250
|
+
const knownEnums = new Set(
|
|
251
|
+
rawNodes.filter((n) => n.type === "enum").map((n) => n.name)
|
|
252
|
+
);
|
|
253
|
+
const enums = rawNodes.filter((n) => n.type === "enum").map((n) => parseEnum(n));
|
|
254
|
+
const models = rawNodes.filter((n) => n.type === "model").map((n) => parseModel(n, knownModels, knownEnums));
|
|
255
|
+
const graph = new Map(models.map((m) => [m.name, m.relations]));
|
|
256
|
+
const circularMap = detectCircularRefs(graph);
|
|
257
|
+
for (const model of models) {
|
|
258
|
+
const circulars = circularMap.get(model.name) ?? /* @__PURE__ */ new Set();
|
|
259
|
+
model.circularRefs = [...circulars];
|
|
260
|
+
for (const field of model.fields) {
|
|
261
|
+
if (field.isRelation && circulars.has(field.prismaType)) {
|
|
262
|
+
field.isCircular = true;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return { models, enums };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/emitters/base.ts
|
|
270
|
+
function fileHeader() {
|
|
271
|
+
return [
|
|
272
|
+
"/**",
|
|
273
|
+
" * ARQUIVO GERADO AUTOMATICAMENTE \u2014 N\xC3O EDITE MANUALMENTE.",
|
|
274
|
+
" * Gerado por prisma-entity-gen. Rode o gerador para atualizar.",
|
|
275
|
+
" */",
|
|
276
|
+
""
|
|
277
|
+
].join("\n");
|
|
278
|
+
}
|
|
279
|
+
function formatFieldType(tsType, isArray, isOptional, isCircular) {
|
|
280
|
+
const base = isArray ? `${tsType}[]` : tsType;
|
|
281
|
+
const nullable = isOptional ? ` | null` : "";
|
|
282
|
+
return `${base}${nullable}`;
|
|
283
|
+
}
|
|
284
|
+
function formatJsDoc(doc) {
|
|
285
|
+
if (!doc) return "";
|
|
286
|
+
const lines = doc.split("\n").map((l) => ` * ${l.trim()}`).join("\n");
|
|
287
|
+
return `/**
|
|
288
|
+
${lines}
|
|
289
|
+
*/
|
|
290
|
+
`;
|
|
291
|
+
}
|
|
292
|
+
var BaseEmitter = class {
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// src/emitters/type.emitter.ts
|
|
296
|
+
function toKebab(name) {
|
|
297
|
+
return name.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").toLowerCase();
|
|
298
|
+
}
|
|
299
|
+
var TypeEmitter = class extends BaseEmitter {
|
|
300
|
+
constructor(options = {}) {
|
|
301
|
+
super();
|
|
302
|
+
this.options = options;
|
|
303
|
+
}
|
|
304
|
+
options;
|
|
305
|
+
emit(model, enums) {
|
|
306
|
+
const enumNames = new Set(enums.map((e) => e.name));
|
|
307
|
+
const lines = [fileHeader()];
|
|
308
|
+
const relationImports = this.collectRelationImports(model, enumNames);
|
|
309
|
+
if (relationImports.length > 0) {
|
|
310
|
+
for (const imp of relationImports) lines.push(imp);
|
|
311
|
+
lines.push("");
|
|
312
|
+
}
|
|
313
|
+
const enumImports = this.collectEnumImports(model, enumNames);
|
|
314
|
+
if (enumImports.length > 0) {
|
|
315
|
+
for (const imp of enumImports) lines.push(imp);
|
|
316
|
+
lines.push("");
|
|
317
|
+
}
|
|
318
|
+
if (model.documentation) lines.push(formatJsDoc(model.documentation));
|
|
319
|
+
lines.push(`export interface ${model.name}Entity {`);
|
|
320
|
+
for (const field of model.fields) {
|
|
321
|
+
lines.push(` ${this.formatField(field)}`);
|
|
322
|
+
}
|
|
323
|
+
lines.push("}");
|
|
324
|
+
lines.push("");
|
|
325
|
+
return lines.join("\n");
|
|
326
|
+
}
|
|
327
|
+
formatField(field) {
|
|
328
|
+
const relationsOptional = this.options.relations !== "required";
|
|
329
|
+
const optional = field.isRelation && relationsOptional || field.isOptional ? "?" : "";
|
|
330
|
+
const tsType = field.isRelation ? `${field.prismaType}Entity` : field.tsType;
|
|
331
|
+
const type = formatFieldType(tsType, field.isArray, field.isOptional, field.isCircular);
|
|
332
|
+
return `${field.name}${optional}: ${type};`;
|
|
333
|
+
}
|
|
334
|
+
collectRelationImports(model, enumNames) {
|
|
335
|
+
const seen = /* @__PURE__ */ new Set();
|
|
336
|
+
const imports = [];
|
|
337
|
+
for (const field of model.fields) {
|
|
338
|
+
if (field.isRelation && !enumNames.has(field.prismaType) && !seen.has(field.prismaType) && field.prismaType !== model.name) {
|
|
339
|
+
seen.add(field.prismaType);
|
|
340
|
+
imports.push(
|
|
341
|
+
`import type { ${field.prismaType}Entity } from "./${toKebab(field.prismaType)}.entity.js";`
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return imports;
|
|
346
|
+
}
|
|
347
|
+
collectEnumImports(model, enumNames) {
|
|
348
|
+
const seen = /* @__PURE__ */ new Set();
|
|
349
|
+
const imports = [];
|
|
350
|
+
for (const field of model.fields) {
|
|
351
|
+
if (enumNames.has(field.prismaType) && !seen.has(field.prismaType)) {
|
|
352
|
+
seen.add(field.prismaType);
|
|
353
|
+
imports.push(
|
|
354
|
+
`import { ${field.prismaType} } from "./${toKebab(field.prismaType)}.enum.js";`
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return imports;
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
// src/emitters/class.emitter.ts
|
|
363
|
+
var ClassEmitter = class extends BaseEmitter {
|
|
364
|
+
constructor(options = {}) {
|
|
365
|
+
super();
|
|
366
|
+
this.options = options;
|
|
367
|
+
}
|
|
368
|
+
options;
|
|
369
|
+
emit(model, enums) {
|
|
370
|
+
const enumNames = new Set(enums.map((e) => e.name));
|
|
371
|
+
const lines = [fileHeader()];
|
|
372
|
+
const cvImports = this.collectCvImports(model, enumNames);
|
|
373
|
+
if (this.options.classValidator && cvImports.length > 0) {
|
|
374
|
+
lines.push(`import { ${cvImports.join(", ")} } from "class-validator";`);
|
|
375
|
+
}
|
|
376
|
+
if (this.options.classTransformer) {
|
|
377
|
+
lines.push(`import { Type } from "class-transformer";`);
|
|
378
|
+
}
|
|
379
|
+
if (this.options.classValidator && cvImports.length > 0 || this.options.classTransformer) {
|
|
380
|
+
lines.push("");
|
|
381
|
+
}
|
|
382
|
+
const relationImports = this.collectRelationImports(model, enumNames);
|
|
383
|
+
if (relationImports.length > 0) {
|
|
384
|
+
for (const imp of relationImports) lines.push(imp);
|
|
385
|
+
lines.push("");
|
|
386
|
+
}
|
|
387
|
+
const enumImports = this.collectEnumImports(model, enumNames);
|
|
388
|
+
if (enumImports.length > 0) {
|
|
389
|
+
for (const imp of enumImports) lines.push(imp);
|
|
390
|
+
lines.push("");
|
|
391
|
+
}
|
|
392
|
+
if (model.documentation) lines.push(formatJsDoc(model.documentation));
|
|
393
|
+
lines.push(`export class ${model.name}Entity {`);
|
|
394
|
+
for (const field of model.fields) {
|
|
395
|
+
const fieldLines = this.formatField(field, enumNames);
|
|
396
|
+
for (const l of fieldLines) lines.push(` ${l}`);
|
|
397
|
+
}
|
|
398
|
+
lines.push("}");
|
|
399
|
+
lines.push("");
|
|
400
|
+
return lines.join("\n");
|
|
401
|
+
}
|
|
402
|
+
formatField(field, enumNames) {
|
|
403
|
+
const lines = [];
|
|
404
|
+
if (this.options.classValidator) {
|
|
405
|
+
const decorators = this.getCvDecorators(field, enumNames);
|
|
406
|
+
for (const d of decorators) lines.push(d);
|
|
407
|
+
}
|
|
408
|
+
if (this.options.classTransformer && field.isRelation && !field.isCircular) {
|
|
409
|
+
lines.push(`@Type(() => ${field.tsType}Entity)`);
|
|
410
|
+
}
|
|
411
|
+
const optional = field.isOptional || field.hasDefault ? "?" : "!";
|
|
412
|
+
const type = formatFieldType(
|
|
413
|
+
field.isRelation && !field.isCircular ? `${field.tsType}Entity` : field.tsType,
|
|
414
|
+
field.isArray,
|
|
415
|
+
field.isOptional,
|
|
416
|
+
field.isCircular
|
|
417
|
+
);
|
|
418
|
+
const comment = field.isCircular ? " // circular ref \u2014 usar Ref" : "";
|
|
419
|
+
lines.push(`${field.name}${optional}: ${type};${comment}`);
|
|
420
|
+
lines.push("");
|
|
421
|
+
return lines;
|
|
422
|
+
}
|
|
423
|
+
getCvDecorators(field, enumNames) {
|
|
424
|
+
const decorators = [];
|
|
425
|
+
if (field.isOptional || field.hasDefault) {
|
|
426
|
+
decorators.push("@IsOptional()");
|
|
427
|
+
}
|
|
428
|
+
if (field.isArray) {
|
|
429
|
+
decorators.push("@IsArray()");
|
|
430
|
+
}
|
|
431
|
+
if (field.isRelation || enumNames.has(field.prismaType)) {
|
|
432
|
+
return decorators;
|
|
433
|
+
}
|
|
434
|
+
switch (field.prismaType) {
|
|
435
|
+
case "String":
|
|
436
|
+
decorators.push("@IsString()");
|
|
437
|
+
break;
|
|
438
|
+
case "Boolean":
|
|
439
|
+
decorators.push("@IsBoolean()");
|
|
440
|
+
break;
|
|
441
|
+
case "Int":
|
|
442
|
+
case "Float":
|
|
443
|
+
case "Decimal":
|
|
444
|
+
decorators.push("@IsNumber()");
|
|
445
|
+
break;
|
|
446
|
+
case "BigInt":
|
|
447
|
+
decorators.push("@IsInt()");
|
|
448
|
+
break;
|
|
449
|
+
case "DateTime":
|
|
450
|
+
decorators.push("@IsDate()");
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
453
|
+
return decorators;
|
|
454
|
+
}
|
|
455
|
+
collectCvImports(model, enumNames) {
|
|
456
|
+
const imports = /* @__PURE__ */ new Set();
|
|
457
|
+
for (const field of model.fields) {
|
|
458
|
+
if (field.isOptional || field.hasDefault) imports.add("IsOptional");
|
|
459
|
+
if (field.isArray) imports.add("IsArray");
|
|
460
|
+
if (field.isRelation || enumNames.has(field.prismaType)) continue;
|
|
461
|
+
switch (field.prismaType) {
|
|
462
|
+
case "String":
|
|
463
|
+
imports.add("IsString");
|
|
464
|
+
break;
|
|
465
|
+
case "Boolean":
|
|
466
|
+
imports.add("IsBoolean");
|
|
467
|
+
break;
|
|
468
|
+
case "Int":
|
|
469
|
+
case "Float":
|
|
470
|
+
case "Decimal":
|
|
471
|
+
imports.add("IsNumber");
|
|
472
|
+
break;
|
|
473
|
+
case "BigInt":
|
|
474
|
+
imports.add("IsInt");
|
|
475
|
+
break;
|
|
476
|
+
case "DateTime":
|
|
477
|
+
imports.add("IsDate");
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return [...imports].sort();
|
|
482
|
+
}
|
|
483
|
+
collectRelationImports(model, enumNames) {
|
|
484
|
+
const seen = /* @__PURE__ */ new Set();
|
|
485
|
+
const imports = [];
|
|
486
|
+
for (const field of model.fields) {
|
|
487
|
+
if (field.isRelation && !field.isCircular && !enumNames.has(field.prismaType) && !seen.has(field.prismaType)) {
|
|
488
|
+
seen.add(field.prismaType);
|
|
489
|
+
const fileName = field.prismaType.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").toLowerCase();
|
|
490
|
+
imports.push(
|
|
491
|
+
`import { ${field.prismaType}Entity } from "./${fileName}.entity.js";`
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return imports;
|
|
496
|
+
}
|
|
497
|
+
collectEnumImports(model, enumNames) {
|
|
498
|
+
const seen = /* @__PURE__ */ new Set();
|
|
499
|
+
const imports = [];
|
|
500
|
+
for (const field of model.fields) {
|
|
501
|
+
if (enumNames.has(field.prismaType) && !seen.has(field.prismaType)) {
|
|
502
|
+
seen.add(field.prismaType);
|
|
503
|
+
const fileName = field.prismaType.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").toLowerCase();
|
|
504
|
+
imports.push(
|
|
505
|
+
`import { ${field.prismaType} } from "./${fileName}.enum.js";`
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return imports;
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
var FileWriter = class {
|
|
513
|
+
constructor(outputDir, dryRun, logger) {
|
|
514
|
+
this.outputDir = outputDir;
|
|
515
|
+
this.dryRun = dryRun;
|
|
516
|
+
this.logger = logger;
|
|
517
|
+
}
|
|
518
|
+
outputDir;
|
|
519
|
+
dryRun;
|
|
520
|
+
logger;
|
|
521
|
+
writeAll(files) {
|
|
522
|
+
const result = { written: [], skipped: [] };
|
|
523
|
+
if (!this.dryRun) {
|
|
524
|
+
mkdirSync(resolve(this.outputDir), { recursive: true });
|
|
525
|
+
}
|
|
526
|
+
for (const file of files) {
|
|
527
|
+
const absPath = resolve(this.outputDir, file.relativePath);
|
|
528
|
+
if (this.dryRun) {
|
|
529
|
+
this.logger.info(`[dry-run] ${file.relativePath}`);
|
|
530
|
+
result.skipped.push(absPath);
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
mkdirSync(dirname(absPath), { recursive: true });
|
|
534
|
+
writeFileSync(absPath, file.content, "utf-8");
|
|
535
|
+
this.logger.success(file.relativePath);
|
|
536
|
+
result.written.push(absPath);
|
|
537
|
+
}
|
|
538
|
+
return result;
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
// src/writer/barrel.ts
|
|
543
|
+
function generateBarrel(files) {
|
|
544
|
+
const exports$1 = files.filter((f) => !f.relativePath.endsWith("index.ts")).map((f) => {
|
|
545
|
+
const path = f.relativePath.replace(/\.ts$/, ".js");
|
|
546
|
+
return `export * from "./${path}";`;
|
|
547
|
+
}).sort();
|
|
548
|
+
return [fileHeader(), ...exports$1, ""].join("\n");
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// src/logger.ts
|
|
552
|
+
var LEVELS = {
|
|
553
|
+
silent: 0,
|
|
554
|
+
info: 1,
|
|
555
|
+
verbose: 2
|
|
556
|
+
};
|
|
557
|
+
var Logger = class {
|
|
558
|
+
level;
|
|
559
|
+
constructor(logLevel = "info") {
|
|
560
|
+
this.level = LEVELS[logLevel];
|
|
561
|
+
}
|
|
562
|
+
info(msg) {
|
|
563
|
+
if (this.level >= LEVELS.info) {
|
|
564
|
+
console.log(` ${msg}`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
verbose(msg) {
|
|
568
|
+
if (this.level >= LEVELS.verbose) {
|
|
569
|
+
console.log(` [verbose] ${msg}`);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
success(msg) {
|
|
573
|
+
if (this.level >= LEVELS.info) {
|
|
574
|
+
console.log(` \u2713 ${msg}`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
warn(msg) {
|
|
578
|
+
if (this.level >= LEVELS.info) {
|
|
579
|
+
console.warn(` \u26A0 ${msg}`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
error(msg) {
|
|
583
|
+
console.error(` \u2717 ${msg}`);
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
// src/generator.ts
|
|
588
|
+
function toKebabCase(name) {
|
|
589
|
+
return name.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").toLowerCase();
|
|
590
|
+
}
|
|
591
|
+
function shouldInclude(name, config) {
|
|
592
|
+
if (config.include.length > 0 && !config.include.includes(name)) return false;
|
|
593
|
+
if (config.exclude.includes(name)) return false;
|
|
594
|
+
return true;
|
|
595
|
+
}
|
|
596
|
+
function buildFiles(models, enums, config) {
|
|
597
|
+
const files = [];
|
|
598
|
+
const useType = config.mode === "type" || config.mode === "both";
|
|
599
|
+
const useClass = config.mode === "class" || config.mode === "both";
|
|
600
|
+
const typeEmitter = useType ? new TypeEmitter({ relations: config.relations }) : null;
|
|
601
|
+
const classEmitter = useClass ? new ClassEmitter({
|
|
602
|
+
classValidator: config.classValidator,
|
|
603
|
+
classTransformer: config.classTransformer
|
|
604
|
+
}) : null;
|
|
605
|
+
const typeDir = config.mode === "both" ? "types/" : "";
|
|
606
|
+
const classDir = config.mode === "both" ? "classes/" : "";
|
|
607
|
+
for (const model of models) {
|
|
608
|
+
if (!shouldInclude(model.name, config)) continue;
|
|
609
|
+
const fileName = `${toKebabCase(model.name)}.${config.suffix}.ts`;
|
|
610
|
+
if (typeEmitter) {
|
|
611
|
+
files.push({
|
|
612
|
+
relativePath: `${typeDir}${fileName}`,
|
|
613
|
+
content: typeEmitter.emit(model, enums),
|
|
614
|
+
sourceName: model.name
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
if (classEmitter) {
|
|
618
|
+
files.push({
|
|
619
|
+
relativePath: `${classDir}${fileName}`,
|
|
620
|
+
content: classEmitter.emit(model, enums),
|
|
621
|
+
sourceName: model.name
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
for (const enumDef of enums) {
|
|
626
|
+
if (!shouldInclude(enumDef.name, config)) continue;
|
|
627
|
+
const fileName = `${toKebabCase(enumDef.name)}.enum.ts`;
|
|
628
|
+
const content = buildEnumFile(enumDef, config.enumOutput);
|
|
629
|
+
if (typeDir) files.push({ relativePath: `${typeDir}${fileName}`, content, sourceName: enumDef.name });
|
|
630
|
+
else files.push({ relativePath: fileName, content, sourceName: enumDef.name });
|
|
631
|
+
if (classDir) files.push({ relativePath: `${classDir}${fileName}`, content, sourceName: enumDef.name });
|
|
632
|
+
}
|
|
633
|
+
return files;
|
|
634
|
+
}
|
|
635
|
+
function buildEnumFile(enumDef, enumOutput) {
|
|
636
|
+
const header = [
|
|
637
|
+
"/**",
|
|
638
|
+
" * ARQUIVO GERADO AUTOMATICAMENTE \u2014 N\xC3O EDITE MANUALMENTE.",
|
|
639
|
+
" * Gerado por prisma-entity-gen. Rode o gerador para atualizar.",
|
|
640
|
+
" */",
|
|
641
|
+
""
|
|
642
|
+
];
|
|
643
|
+
if (enumOutput === "union") {
|
|
644
|
+
const union = enumDef.values.map((v) => `"${v}"`).join(" | ");
|
|
645
|
+
return [...header, `export type ${enumDef.name} = ${union};`, ""].join("\n");
|
|
646
|
+
}
|
|
647
|
+
return [
|
|
648
|
+
...header,
|
|
649
|
+
`export enum ${enumDef.name} {`,
|
|
650
|
+
...enumDef.values.map((v) => ` ${v} = "${v}",`),
|
|
651
|
+
"}",
|
|
652
|
+
""
|
|
653
|
+
].join("\n");
|
|
654
|
+
}
|
|
655
|
+
async function generate(config) {
|
|
656
|
+
const logger = new Logger(config.logLevel);
|
|
657
|
+
logger.verbose(`Lendo schema: ${config.schemaPath}`);
|
|
658
|
+
const { models, enums } = parseSchema(config.schemaPath);
|
|
659
|
+
logger.info(`${models.length} models encontrados`);
|
|
660
|
+
logger.info(`${enums.length} enums encontrados`);
|
|
661
|
+
const files = buildFiles(models, enums, config);
|
|
662
|
+
logger.verbose(`${files.length} arquivos a gerar`);
|
|
663
|
+
if (config.barrel) {
|
|
664
|
+
files.push({
|
|
665
|
+
relativePath: "index.ts",
|
|
666
|
+
content: generateBarrel(files),
|
|
667
|
+
sourceName: "barrel"
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
const writer = new FileWriter(config.outputDir, config.dryRun, logger);
|
|
671
|
+
const result = writer.writeAll(files);
|
|
672
|
+
if (config.dryRun) {
|
|
673
|
+
logger.info(`${result.skipped.length} arquivos seriam gerados`);
|
|
674
|
+
} else {
|
|
675
|
+
logger.success(`${result.written.length} arquivos gerados em ${config.outputDir}`);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// src/cli/index.ts
|
|
680
|
+
var program = new Command();
|
|
681
|
+
program.name("prisma-entity-gen").description("Gera entities TypeScript a partir do schema.prisma via AST").version("0.1.0");
|
|
682
|
+
program.command("generate", { isDefault: true }).description("Gera as entities do schema.prisma").option("-s, --schema <path>", "Caminho para o schema.prisma").option("-o, --output <dir>", "Diret\xF3rio de sa\xEDda").option("-m, --mode <mode>", "Modo: type | class | both").option("--relations <mode>", "Rela\xE7\xF5es nas entities: optional | required").option("--enum-output <format>", "Formato dos enums: enum | union").option("--dry-run", "Simula sem escrever arquivos").option("--log-level <level>", "silent | info | verbose").action(async (opts) => {
|
|
683
|
+
let config;
|
|
684
|
+
try {
|
|
685
|
+
config = loadConfig();
|
|
686
|
+
} catch (err) {
|
|
687
|
+
if (err instanceof ConfigError) {
|
|
688
|
+
console.error(err.message);
|
|
689
|
+
process.exit(1);
|
|
690
|
+
}
|
|
691
|
+
throw err;
|
|
692
|
+
}
|
|
693
|
+
if (opts.schema) config = { ...config, schemaPath: opts.schema };
|
|
694
|
+
if (opts.output) config = { ...config, outputDir: opts.output };
|
|
695
|
+
if (opts.mode) config = { ...config, mode: opts.mode };
|
|
696
|
+
if (opts.relations) config = { ...config, relations: opts.relations };
|
|
697
|
+
if (opts.enumOutput) config = { ...config, enumOutput: opts.enumOutput };
|
|
698
|
+
if (opts.dryRun) config = { ...config, dryRun: true };
|
|
699
|
+
if (opts.logLevel) config = { ...config, logLevel: opts.logLevel };
|
|
700
|
+
try {
|
|
701
|
+
await generate(config);
|
|
702
|
+
} catch (err) {
|
|
703
|
+
console.error(err.message);
|
|
704
|
+
process.exit(1);
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
program.parse();
|
|
708
|
+
//# sourceMappingURL=index.js.map
|
|
709
|
+
//# sourceMappingURL=index.js.map
|