@gzl10/nexus-sdk 0.5.0 → 0.6.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 CHANGED
@@ -160,35 +160,25 @@ export const postEntity: EntityDefinition = {
160
160
 
161
161
  ### Code Generators
162
162
 
163
- Generate migrations, Zod schemas, TypeScript models, and CASL permissions:
163
+ Generate TypeScript interfaces from EntityDefinitions:
164
164
 
165
165
  ```typescript
166
- import {
167
- generateMigration,
168
- generateZodSchema,
169
- generateModel,
170
- generateCaslPermissions
171
- } from '@gzl10/nexus-sdk'
166
+ import { generateModel, generateModelsFile } from '@gzl10/nexus-sdk'
172
167
 
173
- // Generate Knex migration code
174
- const migrationCode = generateMigration(postEntity)
175
-
176
- // Generate Zod validation schemas
177
- const zodCode = generateZodSchema(postEntity)
178
- // Creates: createPostSchema, updatePostSchema, postParamsSchema, postQuerySchema
179
-
180
- // Generate TypeScript interface
168
+ // Generate TypeScript interface for a single entity
181
169
  const modelCode = generateModel(postEntity)
182
170
  // Creates: export interface Post { ... }
183
171
 
184
- // Generate CASL permissions for seeding
185
- const permissions = generateCaslPermissions(postEntity)
186
- // Returns: Array<{ role, action, subject, conditions, fields, inverted }>
172
+ // Generate models file for multiple entities
173
+ const modelsFile = generateModelsFile([postEntity, userEntity])
174
+ // Creates consolidated models file with all interfaces
187
175
  ```
188
176
 
177
+ > **Note:** Zod schemas and migrations are now generated at runtime by `@gzl10/nexus-backend`.
178
+
189
179
  ## CLI (v0.5.0+)
190
180
 
191
- Auto-generate code from EntityDefinitions in your modules:
181
+ Generate TypeScript models from EntityDefinitions in your modules:
192
182
 
193
183
  ```bash
194
184
  # Install globally or use npx
@@ -216,10 +206,6 @@ nexus-sdk generate --dry-run --verbose
216
206
  | `-o, --output <dir>` | `__generated__` | Output directory name |
217
207
  | `-w, --watch` | - | Watch for changes and regenerate |
218
208
  | `--dry-run` | - | Show what would be generated |
219
- | `--skip-schemas` | - | Skip `.schemas.ts` generation |
220
- | `--skip-models` | - | Skip `.models.ts` generation |
221
- | `--skip-migrations` | - | Skip `.migrate.ts` generation |
222
- | `--skip-casl` | - | Skip `.casl.ts` generation |
223
209
  | `--verbose` | - | Show detailed logs |
224
210
 
225
211
  ### Generated Files
@@ -230,10 +216,7 @@ For each module with `definitions`, the CLI generates:
230
216
  src/modules/posts/
231
217
  ├── index.ts # Module manifest with definitions
232
218
  └── __generated__/
233
- ├── posts.schemas.ts # Zod validation schemas
234
- ├── posts.models.ts # TypeScript interfaces
235
- ├── posts.migrate.ts # Knex migration function
236
- └── posts.casl.ts # CASL permission seeds
219
+ └── posts.models.ts # TypeScript interfaces
237
220
  ```
238
221
 
239
222
  ### Module Structure
@@ -0,0 +1,186 @@
1
+ // src/generators.ts
2
+ var GENERATED_DIR = "__generated__";
3
+ function isPersistentEntity(entity) {
4
+ const withoutOwnTable = ["action", "external", "virtual", "computed", "single"];
5
+ return !withoutOwnTable.includes(entity.type ?? "collection");
6
+ }
7
+ function isSingletonEntity(entity) {
8
+ return entity.type === "single";
9
+ }
10
+ function hasTable(entity) {
11
+ return "table" in entity && typeof entity.table === "string";
12
+ }
13
+ function generateModel(entity) {
14
+ const { table, fields } = entity;
15
+ const timestamps = "timestamps" in entity ? entity.timestamps : false;
16
+ const audit = "audit" in entity ? entity.audit : false;
17
+ const entityName = tableToEntityName(table);
18
+ const lines = [
19
+ `/**`,
20
+ ` * ${entity.label}`,
21
+ ` * Generated from EntityDefinition`,
22
+ ` */`,
23
+ `export interface ${entityName} {`
24
+ ];
25
+ for (const [name, field] of Object.entries(fields)) {
26
+ const tsType = dbTypeToTsType(field.db);
27
+ const optional = field.db.nullable ? "?" : "";
28
+ lines.push(` ${name}${optional}: ${tsType}`);
29
+ }
30
+ if (timestamps) {
31
+ lines.push(` created_at: Date`);
32
+ lines.push(` updated_at: Date`);
33
+ }
34
+ if (audit) {
35
+ lines.push(` created_by: string | null`);
36
+ lines.push(` updated_by: string | null`);
37
+ }
38
+ lines.push(`}`);
39
+ lines.push(``);
40
+ return lines.join("\n");
41
+ }
42
+ function dbTypeToTsType(db) {
43
+ switch (db.type) {
44
+ case "string":
45
+ case "text":
46
+ case "uuid":
47
+ return "string";
48
+ case "integer":
49
+ case "decimal":
50
+ return "number";
51
+ case "boolean":
52
+ return "boolean";
53
+ case "date":
54
+ case "datetime":
55
+ return "Date";
56
+ case "json":
57
+ return "Record<string, unknown>";
58
+ default:
59
+ return "unknown";
60
+ }
61
+ }
62
+ function generateReadOnlyModel(entity) {
63
+ const { label, fields } = entity;
64
+ const entityName = labelToEntityName(label);
65
+ const lines = [
66
+ `/**`,
67
+ ` * ${label}`,
68
+ ` * Generated from EntityDefinition (${entity.type})`,
69
+ ` */`,
70
+ `export interface ${entityName} {`
71
+ ];
72
+ for (const [name, field] of Object.entries(fields)) {
73
+ const tsType = dbTypeToTsType(field.db);
74
+ const optional = field.db.nullable ? "?" : "";
75
+ lines.push(` ${name}${optional}: ${tsType}`);
76
+ }
77
+ lines.push(`}`);
78
+ lines.push(``);
79
+ return lines.join("\n");
80
+ }
81
+ function getEntityName(entity) {
82
+ if ("table" in entity) {
83
+ const withoutPrefix = entity.table.replace(/^[a-z]{2,4}_/, "");
84
+ return toPascalCase(toSingular(withoutPrefix));
85
+ } else if ("key" in entity) {
86
+ return toPascalCase(entity.key);
87
+ } else {
88
+ return toSingular(entity.label).replace(/\s+/g, "");
89
+ }
90
+ }
91
+ function getEntitySubject(entity) {
92
+ return entity.casl?.subject ?? tableToSubject(entity.table);
93
+ }
94
+ function dbTypeToTsSimple(type) {
95
+ switch (type) {
96
+ case "string":
97
+ case "text":
98
+ case "uuid":
99
+ return "string";
100
+ case "integer":
101
+ case "decimal":
102
+ return "number";
103
+ case "boolean":
104
+ return "boolean";
105
+ case "date":
106
+ case "datetime":
107
+ return "Date";
108
+ case "json":
109
+ return "Record<string, unknown>";
110
+ default:
111
+ return "unknown";
112
+ }
113
+ }
114
+ function generateModelsFile(definitions) {
115
+ const lines = [
116
+ "/**",
117
+ " * AUTO-GENERATED - Do not edit manually",
118
+ " * Generated from EntityDefinition via @gzl10/nexus-sdk generators",
119
+ " */",
120
+ ""
121
+ ];
122
+ for (const entity of definitions) {
123
+ const name = getEntityName(entity);
124
+ lines.push(`// ============================================================================`);
125
+ lines.push(`// ${name.toUpperCase()} (${entity.type ?? "collection"})`);
126
+ lines.push(`// ============================================================================`);
127
+ lines.push("");
128
+ if (isPersistentEntity(entity)) {
129
+ const model = generateModel(entity);
130
+ const modelLines = model.split("\n");
131
+ lines.push(...modelLines);
132
+ } else if (entity.type === "single") {
133
+ lines.push(`/**`);
134
+ lines.push(` * ${entity.label}`);
135
+ lines.push(` * Generated from EntityDefinition (single)`);
136
+ lines.push(` */`);
137
+ lines.push(`export interface ${name} {`);
138
+ for (const [fieldName, field] of Object.entries(entity.fields)) {
139
+ const tsType = dbTypeToTsSimple(field.db.type);
140
+ const optional = field.db.nullable ? "?" : "";
141
+ lines.push(` ${fieldName}${optional}: ${tsType}`);
142
+ }
143
+ lines.push("}");
144
+ } else {
145
+ const model = generateReadOnlyModel(entity);
146
+ lines.push(model);
147
+ }
148
+ lines.push("");
149
+ }
150
+ return lines.join("\n");
151
+ }
152
+ function toPascalCase(str) {
153
+ return str.split("_").map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
154
+ }
155
+ function toSingular(str) {
156
+ if (str.endsWith("ies")) return str.slice(0, -3) + "y";
157
+ if (str.endsWith("s")) return str.slice(0, -1);
158
+ return str;
159
+ }
160
+ function tableToEntityName(table) {
161
+ const withoutPrefix = table.replace(/^[a-z]{2,4}_/, "");
162
+ return toPascalCase(toSingular(withoutPrefix));
163
+ }
164
+ function tableToSubject(table) {
165
+ const match = table.match(/^([a-z]{2,4})_(.+)$/);
166
+ if (!match) return tableToEntityName(table);
167
+ const [, prefix, rest] = match;
168
+ const prefixPascal = prefix.charAt(0).toUpperCase() + prefix.slice(1);
169
+ return prefixPascal + toPascalCase(toSingular(rest));
170
+ }
171
+ function labelToEntityName(label) {
172
+ const singular = label.endsWith("ies") ? label.slice(0, -3) + "y" : label.endsWith("s") ? label.slice(0, -1) : label;
173
+ return singular.charAt(0).toUpperCase() + singular.slice(1).replace(/\s+/g, "");
174
+ }
175
+
176
+ export {
177
+ GENERATED_DIR,
178
+ isPersistentEntity,
179
+ isSingletonEntity,
180
+ hasTable,
181
+ generateModel,
182
+ generateReadOnlyModel,
183
+ getEntityName,
184
+ getEntitySubject,
185
+ generateModelsFile
186
+ };
package/dist/cli/index.js CHANGED
@@ -1,11 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  GENERATED_DIR,
4
- generateCaslSeed,
5
- generateMigrationsFile,
6
- generateModelsFile,
7
- generateSchemasFile
8
- } from "../chunk-XEPNN6IG.js";
4
+ generateModelsFile
5
+ } from "../chunk-UZT5CTOT.js";
9
6
 
10
7
  // src/cli/index.ts
11
8
  import { Command as Command2 } from "commander";
@@ -115,43 +112,21 @@ async function runGenerate(opts) {
115
112
  if (!opts.dryRun && !existsSync(outputDir)) {
116
113
  mkdirSync(outputDir, { recursive: true });
117
114
  }
118
- const files = [];
119
- if (!opts.skipSchemas) {
120
- const content = generateSchemasFile(mod.definitions);
121
- const filePath = join(outputDir, `${mod.manifest.name}.schemas.ts`);
122
- if (!opts.dryRun) writeFileSync(filePath, content);
123
- files.push("schemas");
124
- }
125
- if (!opts.skipModels) {
126
- const content = generateModelsFile(mod.definitions);
127
- const filePath = join(outputDir, `${mod.manifest.name}.models.ts`);
128
- if (!opts.dryRun) writeFileSync(filePath, content);
129
- files.push("models");
130
- }
131
- if (!opts.skipMigrations) {
132
- const content = generateMigrationsFile(mod.definitions);
133
- const filePath = join(outputDir, `${mod.manifest.name}.migrate.ts`);
134
- if (!opts.dryRun) writeFileSync(filePath, content);
135
- files.push("migrate");
136
- }
137
- if (!opts.skipCasl) {
138
- const content = generateCaslSeed(mod.definitions);
139
- const filePath = join(outputDir, `${mod.manifest.name}.casl.ts`);
140
- if (!opts.dryRun) writeFileSync(filePath, content);
141
- files.push("casl");
142
- }
143
- totalFiles += files.length;
115
+ const content = generateModelsFile(mod.definitions);
116
+ const filePath = join(outputDir, `${mod.manifest.name}.models.ts`);
117
+ if (!opts.dryRun) writeFileSync(filePath, content);
118
+ totalFiles++;
144
119
  const prefix = opts.dryRun ? pc.yellow("[dry-run]") : pc.green("\u2713");
145
- console.log(`${prefix} ${pc.bold(mod.manifest.name)}: ${files.join(", ")}`);
120
+ console.log(`${prefix} ${pc.bold(mod.manifest.name)}: models`);
146
121
  }
147
122
  const elapsed = Date.now() - startTime;
148
123
  console.log(
149
124
  `
150
- ${pc.green("\u2728")} Generated ${totalFiles} files in ${modules.length} module(s) (${elapsed}ms)`
125
+ ${pc.green("\u2728")} Generated ${totalFiles} file(s) in ${modules.length} module(s) (${elapsed}ms)`
151
126
  );
152
127
  }
153
128
  function createGenerateCommand() {
154
- return new Command("generate").description("Generate code from EntityDefinitions").option("-p, --path <dir>", "Base modules directory", "src/modules").option("-m, --module <name>", "Generate only specific module").option("-o, --output <dir>", "Output directory name", "__generated__").option("-w, --watch", "Watch for changes and regenerate").option("--dry-run", "Show what would be generated").option("--skip-schemas", "Skip .schemas.ts generation").option("--skip-models", "Skip .models.ts generation").option("--skip-migrations", "Skip .migrate.ts generation").option("--skip-casl", "Skip .casl.ts generation").option("--verbose", "Show detailed logs").action(async (opts) => {
129
+ return new Command("generate").description("Generate TypeScript models from EntityDefinitions").option("-p, --path <dir>", "Base modules directory", "src/modules").option("-m, --module <name>", "Generate only specific module").option("-o, --output <dir>", "Output directory name", "__generated__").option("-w, --watch", "Watch for changes and regenerate").option("--dry-run", "Show what would be generated").option("--verbose", "Show detailed logs").action(async (opts) => {
155
130
  await runGenerate(opts);
156
131
  if (opts.watch) {
157
132
  console.log(pc.blue("\nWatching for changes..."));
@@ -175,6 +150,6 @@ New file: ${path}`));
175
150
  }
176
151
 
177
152
  // src/cli/index.ts
178
- var program = new Command2().name("nexus-sdk").description("Nexus SDK CLI - Code generation from EntityDefinitions").version("0.5.0");
153
+ var program = new Command2().name("nexus-sdk").description("Nexus SDK CLI - Code generation from EntityDefinitions").version("0.6.0");
179
154
  program.addCommand(createGenerateCommand());
180
155
  program.parse();
package/dist/index.d.ts CHANGED
@@ -7,8 +7,8 @@ import { Logger } from 'pino';
7
7
  /**
8
8
  * Generadores de código desde EntityDefinition
9
9
  *
10
- * Estos generadores producen código TypeScript como strings
11
- * que pueden ser escritos a archivos o usados en runtime.
10
+ * Solo genera modelos TypeScript (interfaces).
11
+ * Schemas Zod y migraciones se generan en runtime por el backend.
12
12
  */
13
13
 
14
14
  /** Directorio estándar para código generado */
@@ -34,28 +34,6 @@ declare function isSingletonEntity(entity: EntityDefinition): entity is SingleEn
34
34
  * Type guard para verificar si una entidad tiene tabla propia (para migraciones)
35
35
  */
36
36
  declare function hasTable(entity: EntityDefinition): entity is PersistentEntityDefinition;
37
- /**
38
- * Genera código de migración Knex desde EntityDefinition
39
- *
40
- * @example
41
- * const code = generateMigration(postEntity)
42
- * // Genera:
43
- * // import type { ModuleContext, Knex } from '@gzl10/nexus-sdk'
44
- * //
45
- * // export async function migrate(ctx: ModuleContext): Promise<void> {
46
- * // const { db, logger, helpers } = ctx
47
- * // ...
48
- * // }
49
- */
50
- declare function generateMigration(entity: PersistentEntityDefinition): string;
51
- /**
52
- * Genera código de schemas Zod desde EntityDefinition
53
- *
54
- * @example
55
- * const code = generateZodSchema(postEntity)
56
- * // Genera createPostSchema, updatePostSchema, postParamsSchema, postQuerySchema
57
- */
58
- declare function generateZodSchema(entity: PersistentEntityDefinition): string;
59
37
  /**
60
38
  * Genera interface TypeScript desde EntityDefinition
61
39
  *
@@ -69,60 +47,19 @@ declare function generateZodSchema(entity: PersistentEntityDefinition): string;
69
47
  * // }
70
48
  */
71
49
  declare function generateModel(entity: PersistentEntityDefinition): string;
72
- /**
73
- * Genera schema Zod para entidades read-only
74
- * Solo genera el schema de output, no create/update
75
- */
76
- declare function generateReadOnlySchema(entity: ComputedEntityDefinition | ExternalEntityDefinition | VirtualEntityDefinition): string;
77
50
  /**
78
51
  * Genera interface TypeScript para entidades read-only
79
52
  */
80
53
  declare function generateReadOnlyModel(entity: ComputedEntityDefinition | ExternalEntityDefinition | VirtualEntityDefinition): string;
81
- /**
82
- * Estructura de permiso generado para insertar en BD
83
- */
84
- interface GeneratedPermission {
85
- role: string;
86
- action: string;
87
- subject: string;
88
- conditions: string | null;
89
- fields: string | null;
90
- inverted: boolean;
91
- }
92
- /**
93
- * Genera permisos CASL desde EntityDefinition
94
- *
95
- * @example
96
- * const permissions = generateCaslPermissions(postEntity)
97
- * // Genera array de permisos para insertar en rol_role_permissions
98
- */
99
- declare function generateCaslPermissions(entity: PersistentEntityDefinition): GeneratedPermission[];
100
- /**
101
- * Genera código de seed para permisos CASL
102
- *
103
- * @example
104
- * const code = generateCaslSeed([postEntity, pageEntity])
105
- * // Genera código para insertar permisos en rol_role_permissions
106
- */
107
- declare function generateCaslSeed(entities: EntityDefinition[]): string;
108
- /**
109
- * Obtiene el subject CASL de una entidad
110
- */
111
- declare function getEntitySubject(entity: PersistentEntityDefinition): string;
112
54
  /**
113
55
  * Obtiene el nombre de una entidad en PascalCase singular
114
56
  * 'cms_posts' → 'Post', 'rol_role_permissions' → 'RolePermission'
115
57
  */
116
58
  declare function getEntityName(entity: EntityDefinition): string;
117
59
  /**
118
- * Genera archivo completo de schemas Zod para múltiples entidades
119
- * Consolida todas las entidades de un módulo en un solo archivo
120
- *
121
- * @example
122
- * const code = generateSchemasFile([userEntity, roleEntity])
123
- * writeFileSync('users.schemas.ts', code)
60
+ * Obtiene el subject CASL de una entidad
124
61
  */
125
- declare function generateSchemasFile(definitions: EntityDefinition[]): string;
62
+ declare function getEntitySubject(entity: PersistentEntityDefinition): string;
126
63
  /**
127
64
  * Genera archivo completo de modelos TypeScript para múltiples entidades
128
65
  * Consolida todas las entidades de un módulo en un solo archivo
@@ -132,15 +69,6 @@ declare function generateSchemasFile(definitions: EntityDefinition[]): string;
132
69
  * writeFileSync('users.models.ts', code)
133
70
  */
134
71
  declare function generateModelsFile(definitions: EntityDefinition[]): string;
135
- /**
136
- * Genera archivo completo de migración Knex para múltiples entidades
137
- * Consolida todas las entidades de un módulo en un solo archivo de migración
138
- *
139
- * @example
140
- * const code = generateMigrationsFile([userEntity, roleEntity])
141
- * writeFileSync('users.migrate.ts', code)
142
- */
143
- declare function generateMigrationsFile(definitions: EntityDefinition[]): string;
144
72
 
145
73
  /**
146
74
  * @gzl10/nexus-sdk
@@ -412,6 +340,8 @@ interface BaseEntityDefinition {
412
340
  fields: Record<string, FieldDefinition>;
413
341
  /** Autorización CASL */
414
342
  casl?: EntityCaslConfig;
343
+ /** Prefijo de ruta para montar (default: inferido de table) */
344
+ routePrefix?: string;
415
345
  }
416
346
  /**
417
347
  * Entidad de colección - CRUD completo (users, posts, orders)
@@ -451,6 +381,8 @@ interface SingleEntityDefinition {
451
381
  defaults?: Record<string, unknown>;
452
382
  /** Autorización CASL */
453
383
  casl?: EntityCaslConfig;
384
+ /** Prefijo de ruta para montar (default: inferido de key) */
385
+ routePrefix?: string;
454
386
  }
455
387
  /**
456
388
  * Entidad de referencia - Catálogos con CRUD admin (countries, currencies)
@@ -502,6 +434,8 @@ interface ActionEntityDefinition {
502
434
  handler?: (ctx: ModuleContext, input: unknown) => Promise<unknown>;
503
435
  /** Autorización CASL */
504
436
  casl?: EntityCaslConfig;
437
+ /** Prefijo de ruta para montar */
438
+ routePrefix?: string;
505
439
  }
506
440
  /**
507
441
  * Entidad externa - Datos de APIs externas (stripe_customers, github_repos)
@@ -533,6 +467,8 @@ interface ExternalEntityDefinition {
533
467
  };
534
468
  /** Autorización CASL */
535
469
  casl?: EntityCaslConfig;
470
+ /** Prefijo de ruta para montar */
471
+ routePrefix?: string;
536
472
  }
537
473
  /**
538
474
  * Entidad virtual - Orquestación de múltiples fuentes (unified_customers)
@@ -552,6 +488,8 @@ interface VirtualEntityDefinition {
552
488
  resolver?: (sources: Record<string, unknown[]>, ctx: ModuleContext) => unknown[] | Promise<unknown[]>;
553
489
  /** Autorización CASL */
554
490
  casl?: EntityCaslConfig;
491
+ /** Prefijo de ruta para montar */
492
+ routePrefix?: string;
555
493
  }
556
494
  /**
557
495
  * Entidad computed - KPIs, estadísticas, métricas calculadas
@@ -574,6 +512,8 @@ interface ComputedEntityDefinition {
574
512
  };
575
513
  /** Autorización CASL */
576
514
  casl?: EntityCaslConfig;
515
+ /** Prefijo de ruta para montar */
516
+ routePrefix?: string;
577
517
  }
578
518
  /**
579
519
  * Entidad view - Vista optimizada para lectura (projections, denormalizaciones)
@@ -595,6 +535,8 @@ interface ViewEntityDefinition {
595
535
  query?: string | ((db: Knex) => Knex.QueryBuilder);
596
536
  /** Autorización CASL */
597
537
  casl?: EntityCaslConfig;
538
+ /** Prefijo de ruta para montar */
539
+ routePrefix?: string;
598
540
  }
599
541
  /**
600
542
  * Entidad config - Configuración por módulo/tenant
@@ -746,6 +688,9 @@ interface ModuleContext {
746
688
  UnauthorizedError: new (message?: string) => Error;
747
689
  ForbiddenError: new (message?: string) => Error;
748
690
  ConflictError: new (message?: string) => Error;
691
+ ValidationError: new (message?: string, errors?: unknown[]) => Error & {
692
+ errors: unknown[];
693
+ };
749
694
  };
750
695
  abilities: ModuleAbilities;
751
696
  /** Sistema de eventos (EventEmitter2 compatible, permite implementaciones tipadas) */
@@ -826,4 +771,4 @@ interface PluginManifest {
826
771
  modules: ModuleManifest[];
827
772
  }
828
773
 
829
- export { type AbilityLike, type ActionEntityDefinition, type AuthRequest, type BaseUser, type CaslAction, type CollectionEntityDefinition, type ComputedEntityDefinition, type ConfigEntityDefinition, type DbType, type EntityCaslConfig, type EntityDefinition, type EntityIndex, type EntityType, type EventEntityDefinition, type ExternalEntityDefinition, type FieldCaslAccess, type FieldDbConfig, type FieldDefinition, type FieldMeta, type FieldOptions, type FieldRelation, type FieldValidation, type FieldValidationConfig, type ForbiddenErrorConstructor, type ForbiddenErrorInstance, type FormField, type FormFieldType, GENERATED_DIR, type GeneratedPermission, type InputType, type KnexAlterTableBuilder, type KnexCreateTableBuilder, type KnexTransaction, type ListType, type MigrationHelpers, type ModuleAbilities, type ModuleContext, type ModuleManifest, type ModuleMiddlewares, type ModuleRequirements, type NonPersistentEntityDefinition, type OwnershipCondition, type PaginatedResult, type PaginationParams, type PersistentEntityDefinition, type PluginAuthRequest, type PluginCategory, type PluginManifest, type ReferenceEntityDefinition, type RolePermission, type SingleEntityDefinition, type TempEntityDefinition, type ValidateSchemas, type ValidationSchema, type ViewEntityDefinition, type VirtualEntityDefinition, generateCaslPermissions, generateCaslSeed, generateMigration, generateMigrationsFile, generateModel, generateModelsFile, generateReadOnlyModel, generateReadOnlySchema, generateSchemasFile, generateZodSchema, getEntityName, getEntitySubject, hasTable, isPersistentEntity, isSingletonEntity };
774
+ export { type AbilityLike, type ActionEntityDefinition, type AuthRequest, type BaseUser, type CaslAction, type CollectionEntityDefinition, type ComputedEntityDefinition, type ConfigEntityDefinition, type DbType, type EntityCaslConfig, type EntityDefinition, type EntityIndex, type EntityType, type EventEntityDefinition, type ExternalEntityDefinition, type FieldCaslAccess, type FieldDbConfig, type FieldDefinition, type FieldMeta, type FieldOptions, type FieldRelation, type FieldValidation, type FieldValidationConfig, type ForbiddenErrorConstructor, type ForbiddenErrorInstance, type FormField, type FormFieldType, GENERATED_DIR, type InputType, type KnexAlterTableBuilder, type KnexCreateTableBuilder, type KnexTransaction, type ListType, type MigrationHelpers, type ModuleAbilities, type ModuleContext, type ModuleManifest, type ModuleMiddlewares, type ModuleRequirements, type NonPersistentEntityDefinition, type OwnershipCondition, type PaginatedResult, type PaginationParams, type PersistentEntityDefinition, type PluginAuthRequest, type PluginCategory, type PluginManifest, type ReferenceEntityDefinition, type RolePermission, type SingleEntityDefinition, type TempEntityDefinition, type ValidateSchemas, type ValidationSchema, type ViewEntityDefinition, type VirtualEntityDefinition, generateModel, generateModelsFile, generateReadOnlyModel, getEntityName, getEntitySubject, hasTable, isPersistentEntity, isSingletonEntity };
package/dist/index.js CHANGED
@@ -1,33 +1,19 @@
1
1
  import {
2
2
  GENERATED_DIR,
3
- generateCaslPermissions,
4
- generateCaslSeed,
5
- generateMigration,
6
- generateMigrationsFile,
7
3
  generateModel,
8
4
  generateModelsFile,
9
5
  generateReadOnlyModel,
10
- generateReadOnlySchema,
11
- generateSchemasFile,
12
- generateZodSchema,
13
6
  getEntityName,
14
7
  getEntitySubject,
15
8
  hasTable,
16
9
  isPersistentEntity,
17
10
  isSingletonEntity
18
- } from "./chunk-XEPNN6IG.js";
11
+ } from "./chunk-UZT5CTOT.js";
19
12
  export {
20
13
  GENERATED_DIR,
21
- generateCaslPermissions,
22
- generateCaslSeed,
23
- generateMigration,
24
- generateMigrationsFile,
25
14
  generateModel,
26
15
  generateModelsFile,
27
16
  generateReadOnlyModel,
28
- generateReadOnlySchema,
29
- generateSchemasFile,
30
- generateZodSchema,
31
17
  getEntityName,
32
18
  getEntitySubject,
33
19
  hasTable,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gzl10/nexus-sdk",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "SDK types for creating Nexus plugins and modules",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,806 +0,0 @@
1
- // src/generators.ts
2
- var GENERATED_DIR = "__generated__";
3
- function isPersistentEntity(entity) {
4
- const withoutOwnTable = ["action", "external", "virtual", "computed", "single"];
5
- return !withoutOwnTable.includes(entity.type ?? "collection");
6
- }
7
- function isSingletonEntity(entity) {
8
- return entity.type === "single";
9
- }
10
- function hasTable(entity) {
11
- return "table" in entity && typeof entity.table === "string";
12
- }
13
- function generateMigration(entity) {
14
- const { table, fields } = entity;
15
- const timestamps = "timestamps" in entity ? entity.timestamps : false;
16
- const audit = "audit" in entity ? entity.audit : false;
17
- const indexes = "indexes" in entity ? entity.indexes : void 0;
18
- const lines = [
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
- }
788
-
789
- export {
790
- GENERATED_DIR,
791
- isPersistentEntity,
792
- isSingletonEntity,
793
- hasTable,
794
- generateMigration,
795
- generateZodSchema,
796
- generateModel,
797
- generateReadOnlySchema,
798
- generateReadOnlyModel,
799
- generateCaslPermissions,
800
- generateCaslSeed,
801
- getEntitySubject,
802
- getEntityName,
803
- generateSchemasFile,
804
- generateModelsFile,
805
- generateMigrationsFile
806
- };