@gzl10/nexus-sdk 0.4.1 → 0.5.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 +73 -5
- package/dist/chunk-XEPNN6IG.js +806 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +180 -0
- package/dist/index.d.ts +62 -11
- package/dist/index.js +18 -787
- package/package.json +13 -3
package/dist/index.js
CHANGED
|
@@ -1,790 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
`import type { ModuleContext, Knex } from '@gzl10/nexus-sdk'`,
|
|
20
|
-
``,
|
|
21
|
-
`export async function migrate(ctx: ModuleContext): Promise<void> {`,
|
|
22
|
-
` const { db, logger, helpers } = ctx`
|
|
23
|
-
];
|
|
24
|
-
if (timestamps) {
|
|
25
|
-
lines.push(` const { addTimestamps } = helpers`);
|
|
26
|
-
}
|
|
27
|
-
if (audit) {
|
|
28
|
-
lines.push(` const { addAuditFieldsIfMissing } = helpers`);
|
|
29
|
-
}
|
|
30
|
-
lines.push(``);
|
|
31
|
-
lines.push(` if (!(await db.schema.hasTable('${table}'))) {`);
|
|
32
|
-
lines.push(` await db.schema.createTable('${table}', (table: Knex.CreateTableBuilder) => {`);
|
|
33
|
-
for (const [name, field] of Object.entries(fields)) {
|
|
34
|
-
const columnCode = generateColumnCode(name, field);
|
|
35
|
-
lines.push(` ${columnCode}`);
|
|
36
|
-
}
|
|
37
|
-
if (timestamps) {
|
|
38
|
-
lines.push(` addTimestamps(table, db)`);
|
|
39
|
-
}
|
|
40
|
-
if (indexes?.length) {
|
|
41
|
-
lines.push(``);
|
|
42
|
-
for (const idx of indexes) {
|
|
43
|
-
if (idx.unique) {
|
|
44
|
-
lines.push(` table.unique([${idx.columns.map((c) => `'${c}'`).join(", ")}])`);
|
|
45
|
-
} else {
|
|
46
|
-
lines.push(` table.index([${idx.columns.map((c) => `'${c}'`).join(", ")}])`);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
lines.push(` })`);
|
|
51
|
-
lines.push(` logger.info('Created table: ${table}')`);
|
|
52
|
-
lines.push(` }`);
|
|
53
|
-
if (audit) {
|
|
54
|
-
lines.push(``);
|
|
55
|
-
lines.push(` await addAuditFieldsIfMissing(db, '${table}')`);
|
|
56
|
-
}
|
|
57
|
-
lines.push(`}`);
|
|
58
|
-
lines.push(``);
|
|
59
|
-
return lines.join("\n");
|
|
60
|
-
}
|
|
61
|
-
function generateColumnCode(name, field) {
|
|
62
|
-
const { db, relation } = field;
|
|
63
|
-
let code = "";
|
|
64
|
-
switch (db.type) {
|
|
65
|
-
case "string":
|
|
66
|
-
code = db.size ? `table.string('${name}', ${db.size})` : `table.string('${name}')`;
|
|
67
|
-
break;
|
|
68
|
-
case "text":
|
|
69
|
-
code = `table.text('${name}')`;
|
|
70
|
-
break;
|
|
71
|
-
case "integer":
|
|
72
|
-
code = `table.integer('${name}')`;
|
|
73
|
-
break;
|
|
74
|
-
case "decimal":
|
|
75
|
-
if (db.precision) {
|
|
76
|
-
code = `table.decimal('${name}', ${db.precision[0]}, ${db.precision[1]})`;
|
|
77
|
-
} else {
|
|
78
|
-
code = `table.decimal('${name}')`;
|
|
79
|
-
}
|
|
80
|
-
break;
|
|
81
|
-
case "boolean":
|
|
82
|
-
code = `table.boolean('${name}')`;
|
|
83
|
-
break;
|
|
84
|
-
case "date":
|
|
85
|
-
code = `table.date('${name}')`;
|
|
86
|
-
break;
|
|
87
|
-
case "datetime":
|
|
88
|
-
code = `table.timestamp('${name}')`;
|
|
89
|
-
break;
|
|
90
|
-
case "json":
|
|
91
|
-
code = `table.json('${name}')`;
|
|
92
|
-
break;
|
|
93
|
-
case "uuid":
|
|
94
|
-
code = `table.uuid('${name}')`;
|
|
95
|
-
break;
|
|
96
|
-
default:
|
|
97
|
-
code = `table.string('${name}')`;
|
|
98
|
-
}
|
|
99
|
-
if (name === "id") {
|
|
100
|
-
code += `.primary()`;
|
|
101
|
-
}
|
|
102
|
-
if (db.nullable) {
|
|
103
|
-
code += `.nullable()`;
|
|
104
|
-
} else {
|
|
105
|
-
code += `.notNullable()`;
|
|
106
|
-
}
|
|
107
|
-
if (db.unique) {
|
|
108
|
-
code += `.unique()`;
|
|
109
|
-
}
|
|
110
|
-
if (db.default !== void 0) {
|
|
111
|
-
if (typeof db.default === "string") {
|
|
112
|
-
code += `.defaultTo('${db.default}')`;
|
|
113
|
-
} else {
|
|
114
|
-
code += `.defaultTo(${JSON.stringify(db.default)})`;
|
|
115
|
-
}
|
|
116
|
-
} else if (db.defaultFn === "now") {
|
|
117
|
-
code += `.defaultTo(db.fn.now())`;
|
|
118
|
-
} else if (db.defaultFn === "uuid") {
|
|
119
|
-
code += `.defaultTo(db.raw('gen_random_uuid()'))`;
|
|
120
|
-
}
|
|
121
|
-
if (relation) {
|
|
122
|
-
const col = relation.column ?? "id";
|
|
123
|
-
code += `.references('${col}').inTable('${relation.table}')`;
|
|
124
|
-
if (relation.onDelete) {
|
|
125
|
-
code += `.onDelete('${relation.onDelete}')`;
|
|
126
|
-
}
|
|
127
|
-
if (relation.onUpdate) {
|
|
128
|
-
code += `.onUpdate('${relation.onUpdate}')`;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
if (db.index && !db.unique && name !== "id") {
|
|
132
|
-
}
|
|
133
|
-
return code;
|
|
134
|
-
}
|
|
135
|
-
function generateZodSchema(entity) {
|
|
136
|
-
const { table, fields } = entity;
|
|
137
|
-
const entityName = tableToEntityName(table);
|
|
138
|
-
const lines = [
|
|
139
|
-
`import { z } from 'zod'`,
|
|
140
|
-
``
|
|
141
|
-
];
|
|
142
|
-
lines.push(`// === CREATE ===`);
|
|
143
|
-
lines.push(`export const create${entityName}Schema = z.object({`);
|
|
144
|
-
for (const [name, field] of Object.entries(fields)) {
|
|
145
|
-
if (name === "id" || field.meta?.auditField) continue;
|
|
146
|
-
const zodCode = generateZodFieldCode(field, "create");
|
|
147
|
-
if (zodCode) {
|
|
148
|
-
lines.push(` ${name}: ${zodCode},`);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
lines.push(`})`);
|
|
152
|
-
lines.push(``);
|
|
153
|
-
lines.push(`// === UPDATE ===`);
|
|
154
|
-
lines.push(`export const update${entityName}Schema = z.object({`);
|
|
155
|
-
for (const [name, field] of Object.entries(fields)) {
|
|
156
|
-
if (name === "id" || field.meta?.auditField) continue;
|
|
157
|
-
const zodCode = generateZodFieldCode(field, "update");
|
|
158
|
-
if (zodCode) {
|
|
159
|
-
lines.push(` ${name}: ${zodCode},`);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
lines.push(`})`);
|
|
163
|
-
lines.push(``);
|
|
164
|
-
lines.push(`// === PARAMS ===`);
|
|
165
|
-
lines.push(`export const ${entityName.toLowerCase()}ParamsSchema = z.object({`);
|
|
166
|
-
lines.push(` id: z.string(),`);
|
|
167
|
-
lines.push(`})`);
|
|
168
|
-
lines.push(``);
|
|
169
|
-
lines.push(`// === QUERY ===`);
|
|
170
|
-
lines.push(`export const ${entityName.toLowerCase()}QuerySchema = z.object({`);
|
|
171
|
-
lines.push(` page: z.coerce.number().int().min(1).default(1),`);
|
|
172
|
-
lines.push(` limit: z.coerce.number().int().min(1).max(100).default(20),`);
|
|
173
|
-
for (const [name, field] of Object.entries(fields)) {
|
|
174
|
-
if (field.meta?.searchable) {
|
|
175
|
-
const zodType = dbTypeToZodType(field.db.type);
|
|
176
|
-
lines.push(` ${name}: ${zodType}.optional(),`);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
lines.push(`})`);
|
|
180
|
-
lines.push(``);
|
|
181
|
-
lines.push(`// === INFERRED TYPES ===`);
|
|
182
|
-
lines.push(`export type Create${entityName}Input = z.infer<typeof create${entityName}Schema>`);
|
|
183
|
-
lines.push(`export type Update${entityName}Input = z.infer<typeof update${entityName}Schema>`);
|
|
184
|
-
lines.push(`export type ${entityName}Params = z.infer<typeof ${entityName.toLowerCase()}ParamsSchema>`);
|
|
185
|
-
lines.push(`export type ${entityName}Query = z.infer<typeof ${entityName.toLowerCase()}QuerySchema>`);
|
|
186
|
-
lines.push(``);
|
|
187
|
-
return lines.join("\n");
|
|
188
|
-
}
|
|
189
|
-
function generateZodFieldCode(field, mode) {
|
|
190
|
-
const { db, validation } = field;
|
|
191
|
-
let code = dbTypeToZodType(db.type);
|
|
192
|
-
if (validation) {
|
|
193
|
-
if (validation.format === "email") {
|
|
194
|
-
code += `.email()`;
|
|
195
|
-
} else if (validation.format === "url") {
|
|
196
|
-
code += `.url()`;
|
|
197
|
-
} else if (validation.format === "uuid") {
|
|
198
|
-
code += `.uuid()`;
|
|
199
|
-
}
|
|
200
|
-
if (validation.min !== void 0) {
|
|
201
|
-
if (db.type === "string" || db.type === "text") {
|
|
202
|
-
code += `.min(${validation.min})`;
|
|
203
|
-
} else if (db.type === "integer" || db.type === "decimal") {
|
|
204
|
-
code += `.min(${validation.min})`;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
if (validation.max !== void 0) {
|
|
208
|
-
if (db.type === "string" || db.type === "text") {
|
|
209
|
-
code += `.max(${validation.max})`;
|
|
210
|
-
} else if (db.type === "integer" || db.type === "decimal") {
|
|
211
|
-
code += `.max(${validation.max})`;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
if (validation.pattern) {
|
|
215
|
-
code += `.regex(/${validation.pattern}/)`;
|
|
216
|
-
}
|
|
217
|
-
if (validation.enum?.length) {
|
|
218
|
-
code = `z.enum([${validation.enum.map((v) => `'${v}'`).join(", ")}])`;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
if (db.nullable) {
|
|
222
|
-
code += `.nullable()`;
|
|
223
|
-
}
|
|
224
|
-
if (mode === "update") {
|
|
225
|
-
code += `.optional()`;
|
|
226
|
-
} else if (mode === "create" && !validation?.required && db.default !== void 0) {
|
|
227
|
-
code += `.optional()`;
|
|
228
|
-
}
|
|
229
|
-
return code;
|
|
230
|
-
}
|
|
231
|
-
function dbTypeToZodType(dbType) {
|
|
232
|
-
switch (dbType) {
|
|
233
|
-
case "string":
|
|
234
|
-
case "text":
|
|
235
|
-
case "uuid":
|
|
236
|
-
return "z.string()";
|
|
237
|
-
case "integer":
|
|
238
|
-
return "z.number().int()";
|
|
239
|
-
case "decimal":
|
|
240
|
-
return "z.number()";
|
|
241
|
-
case "boolean":
|
|
242
|
-
return "z.boolean()";
|
|
243
|
-
case "date":
|
|
244
|
-
case "datetime":
|
|
245
|
-
return "z.string().datetime()";
|
|
246
|
-
case "json":
|
|
247
|
-
return "z.record(z.unknown())";
|
|
248
|
-
default:
|
|
249
|
-
return "z.string()";
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
function generateModel(entity) {
|
|
253
|
-
const { table, fields } = entity;
|
|
254
|
-
const timestamps = "timestamps" in entity ? entity.timestamps : false;
|
|
255
|
-
const audit = "audit" in entity ? entity.audit : false;
|
|
256
|
-
const entityName = tableToEntityName(table);
|
|
257
|
-
const lines = [
|
|
258
|
-
`/**`,
|
|
259
|
-
` * ${entity.label}`,
|
|
260
|
-
` * Generated from EntityDefinition`,
|
|
261
|
-
` */`,
|
|
262
|
-
`export interface ${entityName} {`
|
|
263
|
-
];
|
|
264
|
-
for (const [name, field] of Object.entries(fields)) {
|
|
265
|
-
const tsType = dbTypeToTsType(field.db);
|
|
266
|
-
const optional = field.db.nullable ? "?" : "";
|
|
267
|
-
lines.push(` ${name}${optional}: ${tsType}`);
|
|
268
|
-
}
|
|
269
|
-
if (timestamps) {
|
|
270
|
-
lines.push(` created_at: Date`);
|
|
271
|
-
lines.push(` updated_at: Date`);
|
|
272
|
-
}
|
|
273
|
-
if (audit) {
|
|
274
|
-
lines.push(` created_by: string | null`);
|
|
275
|
-
lines.push(` updated_by: string | null`);
|
|
276
|
-
}
|
|
277
|
-
lines.push(`}`);
|
|
278
|
-
lines.push(``);
|
|
279
|
-
return lines.join("\n");
|
|
280
|
-
}
|
|
281
|
-
function dbTypeToTsType(db) {
|
|
282
|
-
switch (db.type) {
|
|
283
|
-
case "string":
|
|
284
|
-
case "text":
|
|
285
|
-
case "uuid":
|
|
286
|
-
return "string";
|
|
287
|
-
case "integer":
|
|
288
|
-
case "decimal":
|
|
289
|
-
return "number";
|
|
290
|
-
case "boolean":
|
|
291
|
-
return "boolean";
|
|
292
|
-
case "date":
|
|
293
|
-
case "datetime":
|
|
294
|
-
return "Date";
|
|
295
|
-
case "json":
|
|
296
|
-
return "Record<string, unknown>";
|
|
297
|
-
default:
|
|
298
|
-
return "unknown";
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
function toPascalCase(str) {
|
|
302
|
-
return str.split("_").map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
|
|
303
|
-
}
|
|
304
|
-
function toSingular(str) {
|
|
305
|
-
if (str.endsWith("ies")) return str.slice(0, -3) + "y";
|
|
306
|
-
if (str.endsWith("s")) return str.slice(0, -1);
|
|
307
|
-
return str;
|
|
308
|
-
}
|
|
309
|
-
function tableToEntityName(table) {
|
|
310
|
-
const withoutPrefix = table.replace(/^[a-z]{2,4}_/, "");
|
|
311
|
-
return toPascalCase(toSingular(withoutPrefix));
|
|
312
|
-
}
|
|
313
|
-
function tableToSubject(table) {
|
|
314
|
-
const match = table.match(/^([a-z]{2,4})_(.+)$/);
|
|
315
|
-
if (!match) return tableToEntityName(table);
|
|
316
|
-
const [, prefix, rest] = match;
|
|
317
|
-
const prefixPascal = prefix.charAt(0).toUpperCase() + prefix.slice(1);
|
|
318
|
-
return prefixPascal + toPascalCase(toSingular(rest));
|
|
319
|
-
}
|
|
320
|
-
function labelToEntityName(label) {
|
|
321
|
-
const singular = label.endsWith("ies") ? label.slice(0, -3) + "y" : label.endsWith("s") ? label.slice(0, -1) : label;
|
|
322
|
-
return singular.charAt(0).toUpperCase() + singular.slice(1).replace(/\s+/g, "");
|
|
323
|
-
}
|
|
324
|
-
function generateReadOnlySchema(entity) {
|
|
325
|
-
const { label, fields } = entity;
|
|
326
|
-
const entityName = labelToEntityName(label);
|
|
327
|
-
const lines = [
|
|
328
|
-
`import { z } from 'zod'`,
|
|
329
|
-
``,
|
|
330
|
-
`// === OUTPUT SCHEMA ===`,
|
|
331
|
-
`export const ${entityName.toLowerCase()}Schema = z.object({`
|
|
332
|
-
];
|
|
333
|
-
for (const [name, field] of Object.entries(fields)) {
|
|
334
|
-
const zodType = dbTypeToZodType(field.db.type);
|
|
335
|
-
const nullable = field.db.nullable ? `.nullable()` : "";
|
|
336
|
-
lines.push(` ${name}: ${zodType}${nullable},`);
|
|
337
|
-
}
|
|
338
|
-
lines.push(`})`);
|
|
339
|
-
lines.push(``);
|
|
340
|
-
lines.push(`// === QUERY ===`);
|
|
341
|
-
lines.push(`export const ${entityName.toLowerCase()}QuerySchema = z.object({`);
|
|
342
|
-
lines.push(` page: z.coerce.number().int().min(1).default(1),`);
|
|
343
|
-
lines.push(` limit: z.coerce.number().int().min(1).max(100).default(20),`);
|
|
344
|
-
for (const [name, field] of Object.entries(fields)) {
|
|
345
|
-
if (field.meta?.searchable) {
|
|
346
|
-
const zodType = dbTypeToZodType(field.db.type);
|
|
347
|
-
lines.push(` ${name}: ${zodType}.optional(),`);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
lines.push(`})`);
|
|
351
|
-
lines.push(``);
|
|
352
|
-
lines.push(`// === INFERRED TYPES ===`);
|
|
353
|
-
lines.push(`export type ${entityName} = z.infer<typeof ${entityName.toLowerCase()}Schema>`);
|
|
354
|
-
lines.push(`export type ${entityName}Query = z.infer<typeof ${entityName.toLowerCase()}QuerySchema>`);
|
|
355
|
-
lines.push(``);
|
|
356
|
-
return lines.join("\n");
|
|
357
|
-
}
|
|
358
|
-
function generateReadOnlyModel(entity) {
|
|
359
|
-
const { label, fields } = entity;
|
|
360
|
-
const entityName = labelToEntityName(label);
|
|
361
|
-
const lines = [
|
|
362
|
-
`/**`,
|
|
363
|
-
` * ${label}`,
|
|
364
|
-
` * Generated from EntityDefinition (${entity.type})`,
|
|
365
|
-
` */`,
|
|
366
|
-
`export interface ${entityName} {`
|
|
367
|
-
];
|
|
368
|
-
for (const [name, field] of Object.entries(fields)) {
|
|
369
|
-
const tsType = dbTypeToTsType(field.db);
|
|
370
|
-
const optional = field.db.nullable ? "?" : "";
|
|
371
|
-
lines.push(` ${name}${optional}: ${tsType}`);
|
|
372
|
-
}
|
|
373
|
-
lines.push(`}`);
|
|
374
|
-
lines.push(``);
|
|
375
|
-
return lines.join("\n");
|
|
376
|
-
}
|
|
377
|
-
function getFieldsForRole(entity, role, action) {
|
|
378
|
-
const { fields, casl } = entity;
|
|
379
|
-
const isReadAction = action === "read";
|
|
380
|
-
const isWriteAction = action === "create" || action === "update";
|
|
381
|
-
if (action === "manage") return null;
|
|
382
|
-
const allowedFields = [];
|
|
383
|
-
const sensitiveFields = casl?.sensitiveFields ?? [];
|
|
384
|
-
for (const [fieldName, field] of Object.entries(fields)) {
|
|
385
|
-
const fieldCasl = field.meta?.casl;
|
|
386
|
-
if (sensitiveFields.includes(fieldName) && role !== "ADMIN") {
|
|
387
|
-
continue;
|
|
388
|
-
}
|
|
389
|
-
if (isReadAction) {
|
|
390
|
-
if (fieldCasl?.readRoles !== void 0) {
|
|
391
|
-
if (fieldCasl.readRoles.length === 0 || fieldCasl.readRoles.includes(role)) {
|
|
392
|
-
allowedFields.push(fieldName);
|
|
393
|
-
}
|
|
394
|
-
} else {
|
|
395
|
-
allowedFields.push(fieldName);
|
|
396
|
-
}
|
|
397
|
-
} else if (isWriteAction) {
|
|
398
|
-
if (fieldCasl?.writeRoles !== void 0) {
|
|
399
|
-
if (fieldCasl.writeRoles.length === 0 || fieldCasl.writeRoles.includes(role)) {
|
|
400
|
-
allowedFields.push(fieldName);
|
|
401
|
-
}
|
|
402
|
-
} else {
|
|
403
|
-
allowedFields.push(fieldName);
|
|
404
|
-
}
|
|
405
|
-
} else {
|
|
406
|
-
return null;
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
const allFieldNames = Object.keys(fields).filter((f) => !sensitiveFields.includes(f) || role === "ADMIN");
|
|
410
|
-
if (allowedFields.length === allFieldNames.length) {
|
|
411
|
-
return null;
|
|
412
|
-
}
|
|
413
|
-
return allowedFields.length > 0 ? allowedFields : null;
|
|
414
|
-
}
|
|
415
|
-
function generateCaslPermissions(entity) {
|
|
416
|
-
const { table, fields, casl } = entity;
|
|
417
|
-
if (!casl) return [];
|
|
418
|
-
const subject = casl.subject ?? tableToSubject(table);
|
|
419
|
-
const permissions = [];
|
|
420
|
-
for (const [role, config] of Object.entries(casl.permissions ?? {})) {
|
|
421
|
-
for (const action of config.actions) {
|
|
422
|
-
let conditions = config.conditions ?? null;
|
|
423
|
-
if (casl.ownership && (action === "update" || action === "delete") && !conditions) {
|
|
424
|
-
const userField = casl.ownership.userField ?? "id";
|
|
425
|
-
conditions = { [casl.ownership.field]: `\${user.${userField}}` };
|
|
426
|
-
}
|
|
427
|
-
let permFields = config.fields ?? null;
|
|
428
|
-
if (!permFields) {
|
|
429
|
-
permFields = getFieldsForRole(entity, role, action);
|
|
430
|
-
}
|
|
431
|
-
permissions.push({
|
|
432
|
-
role,
|
|
433
|
-
action,
|
|
434
|
-
subject,
|
|
435
|
-
conditions: conditions ? JSON.stringify(conditions) : null,
|
|
436
|
-
fields: permFields ? JSON.stringify(permFields) : null,
|
|
437
|
-
inverted: config.inverted ?? false
|
|
438
|
-
});
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
return permissions;
|
|
442
|
-
}
|
|
443
|
-
function generateCaslSeed(entities) {
|
|
444
|
-
const allPermissions = [];
|
|
445
|
-
for (const entity of entities) {
|
|
446
|
-
if (isPersistentEntity(entity)) {
|
|
447
|
-
allPermissions.push(...generateCaslPermissions(entity));
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
if (allPermissions.length === 0) {
|
|
451
|
-
return "// No CASL permissions defined in entities\n";
|
|
452
|
-
}
|
|
453
|
-
const lines = [
|
|
454
|
-
`import type { ModuleContext } from '@gzl10/nexus-sdk'`,
|
|
455
|
-
``,
|
|
456
|
-
`/**`,
|
|
457
|
-
` * Seed de permisos CASL generado desde EntityDefinition`,
|
|
458
|
-
` */`,
|
|
459
|
-
`export async function seedPermissions(ctx: ModuleContext): Promise<void> {`,
|
|
460
|
-
` const { db, generateId, logger } = ctx`,
|
|
461
|
-
``,
|
|
462
|
-
` // Obtener IDs de roles`,
|
|
463
|
-
` const roles = await db('rol_roles').select('id', 'name')`,
|
|
464
|
-
` const roleMap = Object.fromEntries(roles.map((r: { id: string; name: string }) => [r.name, r.id]))`,
|
|
465
|
-
``,
|
|
466
|
-
` const permissions = [`
|
|
467
|
-
];
|
|
468
|
-
for (const perm of allPermissions) {
|
|
469
|
-
lines.push(` {`);
|
|
470
|
-
lines.push(` role: '${perm.role}',`);
|
|
471
|
-
lines.push(` action: '${perm.action}',`);
|
|
472
|
-
lines.push(` subject: '${perm.subject}',`);
|
|
473
|
-
lines.push(` conditions: ${perm.conditions ? `'${perm.conditions}'` : "null"},`);
|
|
474
|
-
lines.push(` fields: ${perm.fields ? `'${perm.fields}'` : "null"},`);
|
|
475
|
-
lines.push(` inverted: ${perm.inverted}`);
|
|
476
|
-
lines.push(` },`);
|
|
477
|
-
}
|
|
478
|
-
lines.push(` ]`);
|
|
479
|
-
lines.push(``);
|
|
480
|
-
lines.push(` for (const perm of permissions) {`);
|
|
481
|
-
lines.push(` const roleId = roleMap[perm.role]`);
|
|
482
|
-
lines.push(` if (!roleId) {`);
|
|
483
|
-
lines.push(` logger.warn({ role: perm.role }, 'Role not found, skipping permission')`);
|
|
484
|
-
lines.push(` continue`);
|
|
485
|
-
lines.push(` }`);
|
|
486
|
-
lines.push(``);
|
|
487
|
-
lines.push(` // Check if permission exists`);
|
|
488
|
-
lines.push(` const existing = await db('rol_role_permissions')`);
|
|
489
|
-
lines.push(` .where({ role_id: roleId, action: perm.action, subject: perm.subject })`);
|
|
490
|
-
lines.push(` .first()`);
|
|
491
|
-
lines.push(``);
|
|
492
|
-
lines.push(` if (!existing) {`);
|
|
493
|
-
lines.push(` await db('rol_role_permissions').insert({`);
|
|
494
|
-
lines.push(` id: generateId(),`);
|
|
495
|
-
lines.push(` role_id: roleId,`);
|
|
496
|
-
lines.push(` action: perm.action,`);
|
|
497
|
-
lines.push(` subject: perm.subject,`);
|
|
498
|
-
lines.push(` conditions: perm.conditions,`);
|
|
499
|
-
lines.push(` fields: perm.fields,`);
|
|
500
|
-
lines.push(` inverted: perm.inverted`);
|
|
501
|
-
lines.push(` })`);
|
|
502
|
-
lines.push(` }`);
|
|
503
|
-
lines.push(` }`);
|
|
504
|
-
lines.push(``);
|
|
505
|
-
lines.push(` logger.info('Seeded CASL permissions from EntityDefinitions')`);
|
|
506
|
-
lines.push(`}`);
|
|
507
|
-
lines.push(``);
|
|
508
|
-
return lines.join("\n");
|
|
509
|
-
}
|
|
510
|
-
function getEntitySubject(entity) {
|
|
511
|
-
return entity.casl?.subject ?? tableToSubject(entity.table);
|
|
512
|
-
}
|
|
513
|
-
function getEntityName(entity) {
|
|
514
|
-
if ("table" in entity) {
|
|
515
|
-
const withoutPrefix = entity.table.replace(/^[a-z]{2,4}_/, "");
|
|
516
|
-
return toPascalCase(toSingular(withoutPrefix));
|
|
517
|
-
} else if ("key" in entity) {
|
|
518
|
-
return toPascalCase(entity.key);
|
|
519
|
-
} else {
|
|
520
|
-
return toSingular(entity.label).replace(/\s+/g, "");
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
function dbTypeToZodSimple(type, nullable) {
|
|
524
|
-
let zod = "";
|
|
525
|
-
switch (type) {
|
|
526
|
-
case "string":
|
|
527
|
-
case "text":
|
|
528
|
-
case "uuid":
|
|
529
|
-
zod = "z.string()";
|
|
530
|
-
break;
|
|
531
|
-
case "integer":
|
|
532
|
-
zod = "z.number().int()";
|
|
533
|
-
break;
|
|
534
|
-
case "decimal":
|
|
535
|
-
zod = "z.number()";
|
|
536
|
-
break;
|
|
537
|
-
case "boolean":
|
|
538
|
-
zod = "z.boolean()";
|
|
539
|
-
break;
|
|
540
|
-
case "date":
|
|
541
|
-
case "datetime":
|
|
542
|
-
zod = "z.string().datetime()";
|
|
543
|
-
break;
|
|
544
|
-
case "json":
|
|
545
|
-
zod = "z.record(z.unknown())";
|
|
546
|
-
break;
|
|
547
|
-
default:
|
|
548
|
-
zod = "z.unknown()";
|
|
549
|
-
}
|
|
550
|
-
return nullable ? `${zod}.nullable()` : zod;
|
|
551
|
-
}
|
|
552
|
-
function dbTypeToTsSimple(type) {
|
|
553
|
-
switch (type) {
|
|
554
|
-
case "string":
|
|
555
|
-
case "text":
|
|
556
|
-
case "uuid":
|
|
557
|
-
return "string";
|
|
558
|
-
case "integer":
|
|
559
|
-
case "decimal":
|
|
560
|
-
return "number";
|
|
561
|
-
case "boolean":
|
|
562
|
-
return "boolean";
|
|
563
|
-
case "date":
|
|
564
|
-
case "datetime":
|
|
565
|
-
return "Date";
|
|
566
|
-
case "json":
|
|
567
|
-
return "Record<string, unknown>";
|
|
568
|
-
default:
|
|
569
|
-
return "unknown";
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
function generateSchemasFile(definitions) {
|
|
573
|
-
const lines = [
|
|
574
|
-
"/**",
|
|
575
|
-
" * AUTO-GENERATED - Do not edit manually",
|
|
576
|
-
" * Generated from EntityDefinition via @gzl10/nexus-sdk generators",
|
|
577
|
-
" */",
|
|
578
|
-
"",
|
|
579
|
-
"import { z } from 'zod'",
|
|
580
|
-
""
|
|
581
|
-
];
|
|
582
|
-
for (const entity of definitions) {
|
|
583
|
-
const name = getEntityName(entity);
|
|
584
|
-
lines.push(`// ============================================================================`);
|
|
585
|
-
lines.push(`// ${name.toUpperCase()} (${entity.type ?? "collection"})`);
|
|
586
|
-
lines.push(`// ============================================================================`);
|
|
587
|
-
lines.push("");
|
|
588
|
-
if (isPersistentEntity(entity)) {
|
|
589
|
-
const schema = generateZodSchema(entity);
|
|
590
|
-
const schemaLines = schema.split("\n").filter(
|
|
591
|
-
(l) => !l.startsWith("import") && l.trim() !== ""
|
|
592
|
-
);
|
|
593
|
-
lines.push(...schemaLines);
|
|
594
|
-
} else if (entity.type === "single") {
|
|
595
|
-
lines.push(`export const ${name.toLowerCase()}Schema = z.object({`);
|
|
596
|
-
for (const [fieldName, field] of Object.entries(entity.fields)) {
|
|
597
|
-
const zodType = dbTypeToZodSimple(field.db.type, field.db.nullable);
|
|
598
|
-
lines.push(` ${fieldName}: ${zodType},`);
|
|
599
|
-
}
|
|
600
|
-
lines.push("})");
|
|
601
|
-
lines.push("");
|
|
602
|
-
lines.push(`export type ${name} = z.infer<typeof ${name.toLowerCase()}Schema>`);
|
|
603
|
-
} else {
|
|
604
|
-
const schema = generateReadOnlySchema(entity);
|
|
605
|
-
const schemaLines = schema.split("\n").filter(
|
|
606
|
-
(l) => !l.startsWith("import") && l.trim() !== ""
|
|
607
|
-
);
|
|
608
|
-
lines.push(...schemaLines);
|
|
609
|
-
}
|
|
610
|
-
lines.push("");
|
|
611
|
-
}
|
|
612
|
-
return lines.join("\n");
|
|
613
|
-
}
|
|
614
|
-
function generateModelsFile(definitions) {
|
|
615
|
-
const lines = [
|
|
616
|
-
"/**",
|
|
617
|
-
" * AUTO-GENERATED - Do not edit manually",
|
|
618
|
-
" * Generated from EntityDefinition via @gzl10/nexus-sdk generators",
|
|
619
|
-
" */",
|
|
620
|
-
""
|
|
621
|
-
];
|
|
622
|
-
for (const entity of definitions) {
|
|
623
|
-
const name = getEntityName(entity);
|
|
624
|
-
lines.push(`// ============================================================================`);
|
|
625
|
-
lines.push(`// ${name.toUpperCase()} (${entity.type ?? "collection"})`);
|
|
626
|
-
lines.push(`// ============================================================================`);
|
|
627
|
-
lines.push("");
|
|
628
|
-
if (isPersistentEntity(entity)) {
|
|
629
|
-
const model = generateModel(entity);
|
|
630
|
-
const modelLines = model.split("\n");
|
|
631
|
-
lines.push(...modelLines);
|
|
632
|
-
} else if (entity.type === "single") {
|
|
633
|
-
lines.push(`/**`);
|
|
634
|
-
lines.push(` * ${entity.label}`);
|
|
635
|
-
lines.push(` * Generated from EntityDefinition (single)`);
|
|
636
|
-
lines.push(` */`);
|
|
637
|
-
lines.push(`export interface ${name} {`);
|
|
638
|
-
for (const [fieldName, field] of Object.entries(entity.fields)) {
|
|
639
|
-
const tsType = dbTypeToTsSimple(field.db.type);
|
|
640
|
-
const optional = field.db.nullable ? "?" : "";
|
|
641
|
-
lines.push(` ${fieldName}${optional}: ${tsType}`);
|
|
642
|
-
}
|
|
643
|
-
lines.push("}");
|
|
644
|
-
} else {
|
|
645
|
-
const model = generateReadOnlyModel(entity);
|
|
646
|
-
lines.push(model);
|
|
647
|
-
}
|
|
648
|
-
lines.push("");
|
|
649
|
-
}
|
|
650
|
-
return lines.join("\n");
|
|
651
|
-
}
|
|
652
|
-
function generateMigrationsFile(definitions) {
|
|
653
|
-
const persistentEntities = definitions.filter(isPersistentEntity);
|
|
654
|
-
if (persistentEntities.length === 0) {
|
|
655
|
-
return [
|
|
656
|
-
"/**",
|
|
657
|
-
" * AUTO-GENERATED - Do not edit manually",
|
|
658
|
-
" * Generated from EntityDefinition via @gzl10/nexus-sdk generators",
|
|
659
|
-
" */",
|
|
660
|
-
"",
|
|
661
|
-
"import type { ModuleContext } from '@gzl10/nexus-sdk'",
|
|
662
|
-
"",
|
|
663
|
-
"// No persistent entities to migrate",
|
|
664
|
-
"export async function migrate(_ctx: ModuleContext): Promise<void> {}",
|
|
665
|
-
""
|
|
666
|
-
].join("\n");
|
|
667
|
-
}
|
|
668
|
-
const lines = [
|
|
669
|
-
"/**",
|
|
670
|
-
" * AUTO-GENERATED - Do not edit manually",
|
|
671
|
-
" * Generated from EntityDefinition via @gzl10/nexus-sdk generators",
|
|
672
|
-
" */",
|
|
673
|
-
"",
|
|
674
|
-
"import type { ModuleContext, Knex } from '@gzl10/nexus-sdk'",
|
|
675
|
-
"",
|
|
676
|
-
"export async function migrate(ctx: ModuleContext): Promise<void> {",
|
|
677
|
-
" const { db, logger, helpers } = ctx",
|
|
678
|
-
" const { addTimestamps, addAuditFieldsIfMissing } = helpers",
|
|
679
|
-
""
|
|
680
|
-
];
|
|
681
|
-
for (const entity of persistentEntities) {
|
|
682
|
-
const { table, fields } = entity;
|
|
683
|
-
const timestamps = "timestamps" in entity ? entity.timestamps : false;
|
|
684
|
-
const audit = "audit" in entity ? entity.audit : false;
|
|
685
|
-
const indexes = "indexes" in entity ? entity.indexes : void 0;
|
|
686
|
-
const name = getEntityName(entity);
|
|
687
|
-
lines.push(` // === ${name.toUpperCase()} ===`);
|
|
688
|
-
lines.push(` if (!(await db.schema.hasTable('${table}'))) {`);
|
|
689
|
-
lines.push(` await db.schema.createTable('${table}', (table: Knex.CreateTableBuilder) => {`);
|
|
690
|
-
for (const [fieldName, field] of Object.entries(fields)) {
|
|
691
|
-
const columnCode = generateColumnCodeInline(fieldName, field);
|
|
692
|
-
lines.push(` ${columnCode}`);
|
|
693
|
-
}
|
|
694
|
-
if (timestamps) {
|
|
695
|
-
lines.push(` addTimestamps(table, db)`);
|
|
696
|
-
}
|
|
697
|
-
if (indexes?.length) {
|
|
698
|
-
lines.push("");
|
|
699
|
-
for (const idx of indexes) {
|
|
700
|
-
if (idx.unique) {
|
|
701
|
-
lines.push(` table.unique([${idx.columns.map((c) => `'${c}'`).join(", ")}])`);
|
|
702
|
-
} else {
|
|
703
|
-
lines.push(` table.index([${idx.columns.map((c) => `'${c}'`).join(", ")}])`);
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
lines.push(` })`);
|
|
708
|
-
lines.push(` logger.info('Created table: ${table}')`);
|
|
709
|
-
lines.push(` }`);
|
|
710
|
-
if (audit) {
|
|
711
|
-
lines.push(` await addAuditFieldsIfMissing(db, '${table}')`);
|
|
712
|
-
}
|
|
713
|
-
lines.push("");
|
|
714
|
-
}
|
|
715
|
-
lines.push("}");
|
|
716
|
-
lines.push("");
|
|
717
|
-
return lines.join("\n");
|
|
718
|
-
}
|
|
719
|
-
function generateColumnCodeInline(name, field) {
|
|
720
|
-
const { db } = field;
|
|
721
|
-
let code = "";
|
|
722
|
-
switch (db.type) {
|
|
723
|
-
case "string":
|
|
724
|
-
code = db.size ? `table.string('${name}', ${db.size})` : `table.string('${name}')`;
|
|
725
|
-
break;
|
|
726
|
-
case "text":
|
|
727
|
-
code = `table.text('${name}')`;
|
|
728
|
-
break;
|
|
729
|
-
case "integer":
|
|
730
|
-
code = `table.integer('${name}')`;
|
|
731
|
-
break;
|
|
732
|
-
case "decimal":
|
|
733
|
-
if (db.precision) {
|
|
734
|
-
code = `table.decimal('${name}', ${db.precision[0]}, ${db.precision[1]})`;
|
|
735
|
-
} else {
|
|
736
|
-
code = `table.decimal('${name}')`;
|
|
737
|
-
}
|
|
738
|
-
break;
|
|
739
|
-
case "boolean":
|
|
740
|
-
code = `table.boolean('${name}')`;
|
|
741
|
-
break;
|
|
742
|
-
case "date":
|
|
743
|
-
code = `table.date('${name}')`;
|
|
744
|
-
break;
|
|
745
|
-
case "datetime":
|
|
746
|
-
code = `table.datetime('${name}')`;
|
|
747
|
-
break;
|
|
748
|
-
case "json":
|
|
749
|
-
code = `table.json('${name}')`;
|
|
750
|
-
break;
|
|
751
|
-
case "uuid":
|
|
752
|
-
code = `table.uuid('${name}')`;
|
|
753
|
-
break;
|
|
754
|
-
default:
|
|
755
|
-
code = `table.string('${name}')`;
|
|
756
|
-
}
|
|
757
|
-
if (name === "id") {
|
|
758
|
-
code += ".primary()";
|
|
759
|
-
}
|
|
760
|
-
if (!db.nullable) {
|
|
761
|
-
code += ".notNullable()";
|
|
762
|
-
} else {
|
|
763
|
-
code += ".nullable()";
|
|
764
|
-
}
|
|
765
|
-
if (db.unique) {
|
|
766
|
-
code += ".unique()";
|
|
767
|
-
}
|
|
768
|
-
if (db.default !== void 0) {
|
|
769
|
-
if (typeof db.default === "string") {
|
|
770
|
-
code += `.defaultTo('${db.default}')`;
|
|
771
|
-
} else if (typeof db.default === "boolean") {
|
|
772
|
-
code += `.defaultTo(${db.default})`;
|
|
773
|
-
} else {
|
|
774
|
-
code += `.defaultTo(${db.default})`;
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
if (db.index) {
|
|
778
|
-
code += ".index()";
|
|
779
|
-
}
|
|
780
|
-
if (field.relation) {
|
|
781
|
-
code += `.references('${field.relation.column ?? "id"}').inTable('${field.relation.table}')`;
|
|
782
|
-
if (field.relation.onDelete) {
|
|
783
|
-
code += `.onDelete('${field.relation.onDelete}')`;
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
return code;
|
|
787
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
GENERATED_DIR,
|
|
3
|
+
generateCaslPermissions,
|
|
4
|
+
generateCaslSeed,
|
|
5
|
+
generateMigration,
|
|
6
|
+
generateMigrationsFile,
|
|
7
|
+
generateModel,
|
|
8
|
+
generateModelsFile,
|
|
9
|
+
generateReadOnlyModel,
|
|
10
|
+
generateReadOnlySchema,
|
|
11
|
+
generateSchemasFile,
|
|
12
|
+
generateZodSchema,
|
|
13
|
+
getEntityName,
|
|
14
|
+
getEntitySubject,
|
|
15
|
+
hasTable,
|
|
16
|
+
isPersistentEntity,
|
|
17
|
+
isSingletonEntity
|
|
18
|
+
} from "./chunk-XEPNN6IG.js";
|
|
788
19
|
export {
|
|
789
20
|
GENERATED_DIR,
|
|
790
21
|
generateCaslPermissions,
|