@gzl10/nexus-sdk 0.2.0 → 0.4.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
@@ -98,17 +98,131 @@ export const projectPlugin: PluginManifest = {
98
98
  }
99
99
  ```
100
100
 
101
+ ## EntityDefinition (v0.2.0+)
102
+
103
+ Define entities as single source of truth for DB, validation, UI, and CASL:
104
+
105
+ ```typescript
106
+ import type { EntityDefinition } from '@gzl10/nexus-sdk'
107
+
108
+ export const postEntity: EntityDefinition = {
109
+ table: 'cms_posts',
110
+ label: 'Posts',
111
+ labelField: 'title',
112
+ timestamps: true,
113
+ audit: true,
114
+
115
+ fields: {
116
+ id: {
117
+ name: 'id',
118
+ label: 'ID',
119
+ input: 'hidden',
120
+ db: { type: 'uuid', nullable: false, defaultFn: 'uuid' }
121
+ },
122
+ title: {
123
+ name: 'title',
124
+ label: 'Title',
125
+ input: 'text',
126
+ db: { type: 'string', size: 200, nullable: false },
127
+ validation: { required: true, min: 3, max: 200 },
128
+ meta: { searchable: true, sortable: true }
129
+ },
130
+ content: {
131
+ name: 'content',
132
+ label: 'Content',
133
+ input: 'markdown',
134
+ db: { type: 'text', nullable: true }
135
+ },
136
+ status: {
137
+ name: 'status',
138
+ label: 'Status',
139
+ input: 'select',
140
+ db: { type: 'string', nullable: false, default: 'draft' },
141
+ validation: { enum: ['draft', 'published', 'archived'] },
142
+ options: {
143
+ static: [
144
+ { value: 'draft', label: 'Draft' },
145
+ { value: 'published', label: 'Published' },
146
+ { value: 'archived', label: 'Archived' }
147
+ ]
148
+ }
149
+ }
150
+ },
151
+
152
+ casl: {
153
+ ownership: { field: 'created_by' },
154
+ permissions: {
155
+ ADMIN: { actions: ['manage'] },
156
+ EDITOR: { actions: ['create', 'read', 'update'] },
157
+ VIEWER: { actions: ['read'] }
158
+ }
159
+ }
160
+ }
161
+ ```
162
+
163
+ ### Code Generators
164
+
165
+ Generate migrations, Zod schemas, TypeScript models, and CASL permissions:
166
+
167
+ ```typescript
168
+ import {
169
+ generateMigration,
170
+ generateZodSchema,
171
+ generateModel,
172
+ generateCaslPermissions
173
+ } from '@gzl10/nexus-sdk'
174
+
175
+ // Generate Knex migration code
176
+ const migrationCode = generateMigration(postEntity)
177
+
178
+ // Generate Zod validation schemas
179
+ const zodCode = generateZodSchema(postEntity)
180
+ // Creates: createPostSchema, updatePostSchema, postParamsSchema, postQuerySchema
181
+
182
+ // Generate TypeScript interface
183
+ const modelCode = generateModel(postEntity)
184
+ // Creates: export interface Post { ... }
185
+
186
+ // Generate CASL permissions for seeding
187
+ const permissions = generateCaslPermissions(postEntity)
188
+ // Returns: Array<{ role, action, subject, conditions, fields, inverted }>
189
+ ```
190
+
101
191
  ## Main Types
102
192
 
103
193
  ### Manifests & Context
104
194
 
105
195
  | Type | Description |
106
196
  |------|-------------|
107
- | `ModuleManifest` | Defines a module: routes, migrations, seeds, CRUD entities |
197
+ | `ModuleManifest` | Defines a module: routes, migrations, seeds, entities |
108
198
  | `PluginManifest` | Groups modules under a plugin with shared metadata |
109
199
  | `ModuleContext` | Context injected by Nexus: `db`, `logger`, `helpers`, `services`, `abilities` |
110
- | `ModuleEntity` | Declarative entity configuration for CRUD UI |
111
- | `FormField` | Form field configuration with validation |
200
+
201
+ ### Entity Definition System
202
+
203
+ | Type | Description |
204
+ |------|-------------|
205
+ | `EntityDefinition` | Complete entity definition (DB, UI, validation, CASL) |
206
+ | `FieldDefinition` | Field configuration with DB, UI, and validation settings |
207
+ | `FieldDbConfig` | Database column configuration |
208
+ | `FieldRelation` | Foreign key configuration |
209
+ | `EntityCaslConfig` | CASL authorization configuration |
210
+
211
+ ### Entity Types (Discriminated Union)
212
+
213
+ | Type | Description |
214
+ |------|-------------|
215
+ | `CollectionEntity` | Full CRUD (users, posts) |
216
+ | `ReferenceEntity` | Catalogs with admin CRUD (countries) |
217
+ | `SingleEntity` | Singleton, update/read only (site_config) |
218
+ | `ConfigEntity` | Per-module config, singleton |
219
+ | `ExternalEntity` | Read-only from external systems |
220
+ | `VirtualEntity` | Orchestration of multiple sources |
221
+ | `ComputedEntity` | KPIs, calculated stats |
222
+ | `ViewEntity` | Optimized read projections |
223
+ | `EventEntity` | Audit logs, append-only |
224
+ | `TempEntity` | TTL-based (cache, sessions) |
225
+ | `ActionEntity` | Commands without persistence |
112
226
 
113
227
  ### Request & Auth
114
228
 
package/dist/index.d.ts CHANGED
@@ -11,6 +11,27 @@ import { Logger } from 'pino';
11
11
  * que pueden ser escritos a archivos o usados en runtime.
12
12
  */
13
13
 
14
+ /**
15
+ * Entidades que persisten en BD local con tabla propia
16
+ * Excluye: action, external, virtual, computed, single (usa tabla compartida)
17
+ */
18
+ type PersistentEntityDefinition = CollectionEntityDefinition | ReferenceEntityDefinition | EventEntityDefinition | ConfigEntityDefinition | TempEntityDefinition | ViewEntityDefinition;
19
+ /**
20
+ * Entidades sin persistencia local (read-only o sin BD)
21
+ */
22
+ type NonPersistentEntityDefinition = ActionEntityDefinition | ExternalEntityDefinition | VirtualEntityDefinition | ComputedEntityDefinition;
23
+ /**
24
+ * Type guard para verificar si una entidad persiste en BD local con tabla propia
25
+ */
26
+ declare function isPersistentEntity(entity: EntityDefinition): entity is PersistentEntityDefinition;
27
+ /**
28
+ * Type guard para verificar si es un singleton (usa tabla compartida sys_settings)
29
+ */
30
+ declare function isSingletonEntity(entity: EntityDefinition): entity is SingleEntityDefinition;
31
+ /**
32
+ * Type guard para verificar si una entidad tiene tabla propia (para migraciones)
33
+ */
34
+ declare function hasTable(entity: EntityDefinition): entity is PersistentEntityDefinition;
14
35
  /**
15
36
  * Genera código de migración Knex desde EntityDefinition
16
37
  *
@@ -24,7 +45,7 @@ import { Logger } from 'pino';
24
45
  * // ...
25
46
  * // }
26
47
  */
27
- declare function generateMigration(entity: EntityDefinition): string;
48
+ declare function generateMigration(entity: PersistentEntityDefinition): string;
28
49
  /**
29
50
  * Genera código de schemas Zod desde EntityDefinition
30
51
  *
@@ -32,7 +53,7 @@ declare function generateMigration(entity: EntityDefinition): string;
32
53
  * const code = generateZodSchema(postEntity)
33
54
  * // Genera createPostSchema, updatePostSchema, postParamsSchema, postQuerySchema
34
55
  */
35
- declare function generateZodSchema(entity: EntityDefinition): string;
56
+ declare function generateZodSchema(entity: PersistentEntityDefinition): string;
36
57
  /**
37
58
  * Genera interface TypeScript desde EntityDefinition
38
59
  *
@@ -45,7 +66,16 @@ declare function generateZodSchema(entity: EntityDefinition): string;
45
66
  * // ...
46
67
  * // }
47
68
  */
48
- declare function generateModel(entity: EntityDefinition): string;
69
+ declare function generateModel(entity: PersistentEntityDefinition): string;
70
+ /**
71
+ * Genera schema Zod para entidades read-only
72
+ * Solo genera el schema de output, no create/update
73
+ */
74
+ declare function generateReadOnlySchema(entity: ComputedEntityDefinition | ExternalEntityDefinition | VirtualEntityDefinition): string;
75
+ /**
76
+ * Genera interface TypeScript para entidades read-only
77
+ */
78
+ declare function generateReadOnlyModel(entity: ComputedEntityDefinition | ExternalEntityDefinition | VirtualEntityDefinition): string;
49
79
  /**
50
80
  * Estructura de permiso generado para insertar en BD
51
81
  */
@@ -64,7 +94,7 @@ interface GeneratedPermission {
64
94
  * const permissions = generateCaslPermissions(postEntity)
65
95
  * // Genera array de permisos para insertar en rol_role_permissions
66
96
  */
67
- declare function generateCaslPermissions(entity: EntityDefinition): GeneratedPermission[];
97
+ declare function generateCaslPermissions(entity: PersistentEntityDefinition): GeneratedPermission[];
68
98
  /**
69
99
  * Genera código de seed para permisos CASL
70
100
  *
@@ -76,7 +106,30 @@ declare function generateCaslSeed(entities: EntityDefinition[]): string;
76
106
  /**
77
107
  * Obtiene el subject CASL de una entidad
78
108
  */
79
- declare function getEntitySubject(entity: EntityDefinition): string;
109
+ declare function getEntitySubject(entity: PersistentEntityDefinition): string;
110
+ /**
111
+ * Obtiene el nombre de una entidad en PascalCase singular
112
+ * 'cms_posts' → 'Post', 'rol_role_permissions' → 'RolePermission'
113
+ */
114
+ declare function getEntityName(entity: EntityDefinition): string;
115
+ /**
116
+ * Genera archivo completo de schemas Zod para múltiples entidades
117
+ * Consolida todas las entidades de un módulo en un solo archivo
118
+ *
119
+ * @example
120
+ * const code = generateSchemasFile([userEntity, roleEntity])
121
+ * writeFileSync('users.schemas.ts', code)
122
+ */
123
+ declare function generateSchemasFile(definitions: EntityDefinition[]): string;
124
+ /**
125
+ * Genera archivo completo de modelos TypeScript para múltiples entidades
126
+ * Consolida todas las entidades de un módulo en un solo archivo
127
+ *
128
+ * @example
129
+ * const code = generateModelsFile([userEntity, roleEntity])
130
+ * writeFileSync('users.models.ts', code)
131
+ */
132
+ declare function generateModelsFile(definitions: EntityDefinition[]): string;
80
133
 
81
134
  /**
82
135
  * @gzl10/nexus-sdk
@@ -85,6 +138,23 @@ declare function getEntitySubject(entity: EntityDefinition): string;
85
138
  * Use this package to define plugin/module manifests without
86
139
  * depending on the full @gzl10/nexus-backend package.
87
140
  */
141
+ /**
142
+ * Unión discriminada de todos los tipos de entidad
143
+ *
144
+ * | Type | Persistencia | CRUD | Uso principal |
145
+ * |------------|--------------|-----------------|----------------------------------|
146
+ * | collection | Sí (BD) | Completo | Datos de negocio (users, posts) |
147
+ * | single | Sí (BD) | Update/Read | Config global (site_config) |
148
+ * | external | No | Read | Datos externos (stripe_customers)|
149
+ * | virtual | No | Read | Orquestación (unified_customers) |
150
+ * | computed | No/opcional | Read | KPIs, estadísticas |
151
+ * | view | Sí/virtual | Read | Lectura optimizada (projections) |
152
+ * | reference | Sí | Read (admin) | Catálogos (countries, currencies)|
153
+ * | config | Sí | Update/Read | Config por módulo |
154
+ * | event | Sí | Append | Auditoría (audit_logs) |
155
+ * | temp | No (TTL) | Read/Write | Cache, sesiones (otp_codes) |
156
+ * | action | No | Execute | Operaciones, workflows |
157
+ */
88
158
 
89
159
  type KnexCreateTableBuilder = Knex.CreateTableBuilder;
90
160
  type KnexAlterTableBuilder = Knex.AlterTableBuilder;
@@ -320,134 +390,188 @@ interface EntityCaslConfig {
320
390
  sensitiveFields?: string[];
321
391
  }
322
392
  /**
323
- * Definición completa de una entidad
324
- *
325
- * Contiene toda la información necesaria para:
326
- * - Generar migraciones Knex
327
- * - Generar schemas Zod
328
- * - Generar tipos TypeScript
329
- * - Configurar UI CRUD
330
- * - Configurar permisos CASL
393
+ * Propiedades base compartidas por todas las EntityDefinition
331
394
  */
332
- interface EntityDefinition {
395
+ interface BaseEntityDefinition {
396
+ /** Nombre de tabla en BD (con prefijo: 'cms_posts') */
333
397
  table: string;
398
+ /** Nombre para mostrar en UI */
334
399
  label: string;
335
- labelField: string;
400
+ /** Definición de campos */
336
401
  fields: Record<string, FieldDefinition>;
337
- timestamps?: boolean;
338
- audit?: boolean;
339
- indexes?: EntityIndex[];
402
+ /** Autorización CASL */
340
403
  casl?: EntityCaslConfig;
341
404
  }
342
- /**
343
- * Propiedades base compartidas por todas las entidades
344
- */
345
- interface BaseEntity {
346
- name: string;
347
- label: string;
348
- }
349
405
  /**
350
406
  * Entidad de colección - CRUD completo (users, posts, orders)
351
407
  * Default cuando no se especifica type
352
408
  */
353
- interface CollectionEntity extends BaseEntity {
409
+ interface CollectionEntityDefinition extends BaseEntityDefinition {
354
410
  type?: 'collection';
355
- listFields: Record<string, string>;
356
- formFields?: Record<string, FormField>;
411
+ /** Campo para mostrar en selects/referencias */
357
412
  labelField: string;
358
- editMode?: 'modal' | 'page';
359
- listType?: ListType;
413
+ /** Añadir created_at, updated_at */
414
+ timestamps?: boolean;
415
+ /** Añadir created_by, updated_by */
416
+ audit?: boolean;
417
+ /** Índices compuestos */
418
+ indexes?: EntityIndex[];
360
419
  }
361
420
  /**
362
- * Entidad de referencia - Catálogos con CRUD admin (countries, currencies)
421
+ * Entidad singleton - Un solo registro en tabla compartida sys_settings
422
+ * Usa key-value: key identifica el registro, value es JSON con la estructura de fields
423
+ *
424
+ * @example
425
+ * // site_config singleton
426
+ * { type: 'single', key: 'site_config', label: 'Site Config', fields: { siteName: {...}, logo: {...} } }
427
+ * // Se guarda en sys_settings como: { key: 'site_config', value: { siteName: 'Mi App', logo: '...' } }
363
428
  */
364
- interface ReferenceEntity extends BaseEntity {
365
- type: 'reference';
366
- listFields: Record<string, string>;
367
- formFields?: Record<string, FormField>;
368
- labelField: string;
369
- editMode?: 'modal' | 'page';
370
- listType?: ListType;
429
+ interface SingleEntityDefinition {
430
+ type: 'single';
431
+ /** Clave única en tabla sys_settings (ej: 'site_config', 'smtp_settings') */
432
+ key: string;
433
+ /** Nombre para mostrar en UI */
434
+ label: string;
435
+ /** Definición de campos (estructura del JSON value) */
436
+ fields: Record<string, FieldDefinition>;
437
+ /** Autorización CASL */
438
+ casl?: EntityCaslConfig;
371
439
  }
372
440
  /**
373
- * Entidad temporal - CRUD con TTL (otp_codes, cache)
441
+ * Entidad de referencia - Catálogos con CRUD admin (countries, currencies)
374
442
  */
375
- interface TempEntity extends BaseEntity {
376
- type: 'temp';
377
- listFields: Record<string, string>;
378
- formFields?: Record<string, FormField>;
443
+ interface ReferenceEntityDefinition extends BaseEntityDefinition {
444
+ type: 'reference';
445
+ /** Campo para mostrar en selects/referencias */
379
446
  labelField: string;
380
- editMode?: 'modal' | 'page';
381
- listType?: ListType;
447
+ /** Añadir created_at, updated_at */
448
+ timestamps?: boolean;
449
+ /** Índices compuestos */
450
+ indexes?: EntityIndex[];
382
451
  }
383
452
  /**
384
- * Entidad singleton - Solo Update/Read, sin lista (site_config)
453
+ * Entidad de eventos - Logs de auditoría, append-only
385
454
  */
386
- interface SingleEntity extends BaseEntity {
387
- type: 'single';
388
- formFields?: Record<string, FormField>;
455
+ interface EventEntityDefinition extends BaseEntityDefinition {
456
+ type: 'event';
457
+ /** Campo para mostrar en listas */
458
+ labelField: string;
459
+ /** Siempre true para eventos */
460
+ timestamps: true;
389
461
  }
390
462
  /**
391
- * Entidad de configuración - Config por módulo, singleton
463
+ * Entidad de acción - Comandos/operaciones sin persistencia
392
464
  */
393
- interface ConfigEntity extends BaseEntity {
394
- type: 'config';
395
- formFields?: Record<string, FormField>;
465
+ interface ActionEntityDefinition {
466
+ type: 'action';
467
+ /** Nombre para mostrar en UI */
468
+ label: string;
469
+ /** Solo campos de formulario, sin BD */
470
+ fields: Record<string, FieldDefinition>;
396
471
  }
397
472
  /**
398
- * Entidad externa - Read-only desde sistemas externos (stripe_customers)
473
+ * Entidad externa - Datos de APIs externas (stripe_customers, github_repos)
474
+ * Read-only, sin persistencia local
399
475
  */
400
- interface ExternalEntity extends BaseEntity {
476
+ interface ExternalEntityDefinition {
401
477
  type: 'external';
402
- listFields: Record<string, string>;
478
+ /** Nombre para mostrar en UI */
479
+ label: string;
480
+ /** Campo para mostrar en selects/referencias */
403
481
  labelField: string;
404
- listType?: ListType;
482
+ /** Definición de campos (estructura esperada del API externo) */
483
+ fields: Record<string, FieldDefinition>;
484
+ /** Configuración del origen externo */
485
+ source: {
486
+ /** Tipo de origen */
487
+ provider: string;
488
+ /** Endpoint o recurso */
489
+ endpoint?: string;
490
+ };
491
+ /** Autorización CASL */
492
+ casl?: EntityCaslConfig;
405
493
  }
406
494
  /**
407
495
  * Entidad virtual - Orquestación de múltiples fuentes (unified_customers)
496
+ * Read-only, combina datos de varias entidades
408
497
  */
409
- interface VirtualEntity extends BaseEntity {
498
+ interface VirtualEntityDefinition {
410
499
  type: 'virtual';
411
- listFields: Record<string, string>;
500
+ /** Nombre para mostrar en UI */
501
+ label: string;
502
+ /** Campo para mostrar en selects/referencias */
412
503
  labelField: string;
413
- listType?: ListType;
504
+ /** Definición de campos (esquema unificado) */
505
+ fields: Record<string, FieldDefinition>;
506
+ /** Fuentes de datos que se combinan */
507
+ sources: string[];
508
+ /** Autorización CASL */
509
+ casl?: EntityCaslConfig;
414
510
  }
415
511
  /**
416
- * Entidad computada - KPIs, estadísticas calculadas
512
+ * Entidad computed - KPIs, estadísticas, métricas calculadas
513
+ * Read-only, puede cachear opcionalmente
417
514
  */
418
- interface ComputedEntity extends BaseEntity {
515
+ interface ComputedEntityDefinition {
419
516
  type: 'computed';
420
- listFields: Record<string, string>;
421
- labelField: string;
422
- listType?: ListType;
517
+ /** Nombre para mostrar en UI */
518
+ label: string;
519
+ /** Campo para mostrar en selects/referencias */
520
+ labelField?: string;
521
+ /** Definición de campos (estructura del resultado) */
522
+ fields: Record<string, FieldDefinition>;
523
+ /** Tiempo de cache en segundos (0 = sin cache) */
524
+ cacheTtl?: number;
525
+ /** Autorización CASL */
526
+ casl?: EntityCaslConfig;
423
527
  }
424
528
  /**
425
- * Entidad vista - Proyección optimizada para lectura
529
+ * Entidad view - Vista optimizada para lectura (projections, denormalizaciones)
530
+ * Read-only, puede ser vista de BD o virtual
426
531
  */
427
- interface ViewEntity extends BaseEntity {
532
+ interface ViewEntityDefinition {
428
533
  type: 'view';
429
- listFields: Record<string, string>;
534
+ /** Tabla o vista en BD (puede ser VIEW SQL) */
535
+ table: string;
536
+ /** Nombre para mostrar en UI */
537
+ label: string;
538
+ /** Campo para mostrar en selects/referencias */
430
539
  labelField: string;
431
- listType?: ListType;
540
+ /** Definición de campos */
541
+ fields: Record<string, FieldDefinition>;
542
+ /** Entidad fuente de la que deriva */
543
+ sourceEntity?: string;
544
+ /** Autorización CASL */
545
+ casl?: EntityCaslConfig;
432
546
  }
433
547
  /**
434
- * Entidad de eventos - Logs de auditoría, append-only
548
+ * Entidad config - Configuración por módulo/tenant
549
+ * Similar a single pero con scope
435
550
  */
436
- interface EventEntity extends BaseEntity {
437
- type: 'event';
438
- listFields: Record<string, string>;
439
- labelField: string;
440
- listType?: ListType;
551
+ interface ConfigEntityDefinition extends BaseEntityDefinition {
552
+ type: 'config';
553
+ /** Scope de la configuración */
554
+ scope?: 'global' | 'module' | 'tenant' | 'user';
555
+ /** Añadir updated_at */
556
+ timestamps?: boolean;
557
+ /** Añadir updated_by */
558
+ audit?: boolean;
441
559
  }
442
560
  /**
443
- * Entidad de acción - Comandos/operaciones sin persistencia
561
+ * Entidad temporal - Cache, sesiones, OTP codes
562
+ * Con TTL automático, sin auditoría
444
563
  */
445
- interface ActionEntity extends BaseEntity {
446
- type: 'action';
447
- formFields?: Record<string, FormField>;
564
+ interface TempEntityDefinition extends BaseEntityDefinition {
565
+ type: 'temp';
566
+ /** Tiempo de vida en segundos */
567
+ ttl: number;
568
+ /** Campo para mostrar en listas (opcional) */
569
+ labelField?: string;
570
+ /** Índices para búsqueda rápida */
571
+ indexes?: EntityIndex[];
448
572
  }
449
573
  /**
450
- * Unión discriminada de todos los tipos de entidad
574
+ * Union de todas las definiciones de entidad
451
575
  *
452
576
  * | Type | Persistencia | CRUD | Uso principal |
453
577
  * |------------|--------------|-----------------|----------------------------------|
@@ -463,11 +587,7 @@ interface ActionEntity extends BaseEntity {
463
587
  * | temp | No (TTL) | Read/Write | Cache, sesiones (otp_codes) |
464
588
  * | action | No | Execute | Operaciones, workflows |
465
589
  */
466
- type ModuleEntity = CollectionEntity | ReferenceEntity | TempEntity | SingleEntity | ConfigEntity | ExternalEntity | VirtualEntity | ComputedEntity | ViewEntity | EventEntity | ActionEntity;
467
- /**
468
- * Helper type para extraer el tipo de entidad
469
- */
470
- type EntityType = ModuleEntity['type'];
590
+ type EntityDefinition = CollectionEntityDefinition | SingleEntityDefinition | ExternalEntityDefinition | VirtualEntityDefinition | ComputedEntityDefinition | ViewEntityDefinition | ReferenceEntityDefinition | ConfigEntityDefinition | EventEntityDefinition | TempEntityDefinition | ActionEntityDefinition;
471
591
  /**
472
592
  * Requisitos para activar un módulo
473
593
  */
@@ -500,6 +620,10 @@ interface ForbiddenErrorConstructor {
500
620
  * Permite usar CASL en plugins sin importar @casl/ability directamente
501
621
  */
502
622
  interface ModuleAbilities {
623
+ /** Crea una ability CASL para un usuario (acepta argumentos adicionales como permissions) */
624
+ defineAbilityFor: (user: any, ...args: any[]) => any;
625
+ /** Empaqueta reglas CASL para enviar al cliente (recibe ability, retorna reglas) */
626
+ packRules: (ability: any) => unknown[];
503
627
  /** Wrapper para verificar permisos contra instancias */
504
628
  subject: (type: string, object: Record<string, any>) => unknown;
505
629
  /** Error de CASL para throwUnlessCan */
@@ -567,8 +691,8 @@ interface ModuleContext {
567
691
  createRouter: () => Router;
568
692
  middleware: ModuleMiddlewares;
569
693
  registerMiddleware: (name: string, handler: RequestHandler) => void;
570
- /** Configuración resuelta de la aplicación */
571
- config: Record<string, unknown>;
694
+ /** Configuración resuelta de la aplicación (permite propiedades tipadas del backend) */
695
+ config: any;
572
696
  errors: {
573
697
  AppError: new (message: string, statusCode?: number) => Error;
574
698
  NotFoundError: new (message?: string) => Error;
@@ -577,10 +701,8 @@ interface ModuleContext {
577
701
  ConflictError: new (message?: string) => Error;
578
702
  };
579
703
  abilities: ModuleAbilities;
580
- events: {
581
- emit: (event: string, ...args: unknown[]) => boolean;
582
- on: (event: string, listener: (...args: unknown[]) => void) => unknown;
583
- };
704
+ /** Sistema de eventos (EventEmitter2 compatible, permite implementaciones tipadas) */
705
+ events: any;
584
706
  mail: {
585
707
  send: (options: {
586
708
  to: string | string[];
@@ -632,11 +754,6 @@ interface ModuleManifest {
632
754
  * Sus subjects se registran automáticamente.
633
755
  */
634
756
  definitions?: EntityDefinition[];
635
- /**
636
- * @deprecated Usar `definitions` en su lugar.
637
- * Entidades/tablas del módulo con config CRUD para UI.
638
- */
639
- entities?: ModuleEntity[];
640
757
  }
641
758
  /**
642
759
  * Categorías disponibles para plugins
@@ -664,4 +781,4 @@ interface PluginManifest {
664
781
  modules: ModuleManifest[];
665
782
  }
666
783
 
667
- export { type AbilityLike, type ActionEntity, type AuthRequest, type BaseUser, type CaslAction, type CollectionEntity, type ComputedEntity, type ConfigEntity, type CoreServices, type DbType, type EntityCaslConfig, type EntityDefinition, type EntityIndex, type EntityType, type EventEntity, type ExternalEntity, type FieldCaslAccess, type FieldDbConfig, type FieldDefinition, type FieldMeta, type FieldOptions, type FieldRelation, type FieldValidation, type FieldValidationConfig, type ForbiddenErrorConstructor, type ForbiddenErrorInstance, type FormField, type FormFieldType, type GeneratedPermission, type InputType, type KnexAlterTableBuilder, type KnexCreateTableBuilder, type KnexTransaction, type ListType, type MigrationHelpers, type ModuleAbilities, type ModuleContext, type ModuleEntity, type ModuleManifest, type ModuleMiddlewares, type ModuleRequirements, type OwnershipCondition, type PaginatedResult, type PaginationParams, type PluginAuthRequest, type PluginCategory, type PluginManifest, type ReferenceEntity, type RolePermission, type SingleEntity, type TempEntity, type UsersResolver, type ValidateSchemas, type ValidationSchema, type ViewEntity, type VirtualEntity, generateCaslPermissions, generateCaslSeed, generateMigration, generateModel, generateZodSchema, getEntitySubject };
784
+ export { type AbilityLike, type ActionEntityDefinition, type AuthRequest, type BaseUser, type CaslAction, type CollectionEntityDefinition, type ComputedEntityDefinition, type ConfigEntityDefinition, type CoreServices, type DbType, type EntityCaslConfig, type EntityDefinition, type EntityIndex, 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, 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 UsersResolver, type ValidateSchemas, type ValidationSchema, type ViewEntityDefinition, type VirtualEntityDefinition, generateCaslPermissions, generateCaslSeed, generateMigration, generateModel, generateModelsFile, generateReadOnlyModel, generateReadOnlySchema, generateSchemasFile, generateZodSchema, getEntityName, getEntitySubject, hasTable, isPersistentEntity, isSingletonEntity };
package/dist/index.js CHANGED
@@ -1,6 +1,19 @@
1
1
  // src/generators.ts
2
+ function isPersistentEntity(entity) {
3
+ const withoutOwnTable = ["action", "external", "virtual", "computed", "single"];
4
+ return !withoutOwnTable.includes(entity.type ?? "collection");
5
+ }
6
+ function isSingletonEntity(entity) {
7
+ return entity.type === "single";
8
+ }
9
+ function hasTable(entity) {
10
+ return "table" in entity && typeof entity.table === "string";
11
+ }
2
12
  function generateMigration(entity) {
3
- const { table, fields, timestamps, audit, indexes } = entity;
13
+ const { table, fields } = entity;
14
+ const timestamps = "timestamps" in entity ? entity.timestamps : false;
15
+ const audit = "audit" in entity ? entity.audit : false;
16
+ const indexes = "indexes" in entity ? entity.indexes : void 0;
4
17
  const lines = [
5
18
  `import type { ModuleContext, Knex } from '@gzl10/nexus-sdk'`,
6
19
  ``,
@@ -236,7 +249,9 @@ function dbTypeToZodType(dbType) {
236
249
  }
237
250
  }
238
251
  function generateModel(entity) {
239
- const { table, fields, timestamps, audit } = entity;
252
+ const { table, fields } = entity;
253
+ const timestamps = "timestamps" in entity ? entity.timestamps : false;
254
+ const audit = "audit" in entity ? entity.audit : false;
240
255
  const entityName = tableToEntityName(table);
241
256
  const lines = [
242
257
  `/**`,
@@ -282,19 +297,81 @@ function dbTypeToTsType(db) {
282
297
  return "unknown";
283
298
  }
284
299
  }
300
+ function toPascalCase(str) {
301
+ return str.split("_").map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
302
+ }
303
+ function toSingular(str) {
304
+ if (str.endsWith("ies")) return str.slice(0, -3) + "y";
305
+ if (str.endsWith("s")) return str.slice(0, -1);
306
+ return str;
307
+ }
285
308
  function tableToEntityName(table) {
286
309
  const withoutPrefix = table.replace(/^[a-z]{2,4}_/, "");
287
- const singular = withoutPrefix.endsWith("ies") ? withoutPrefix.slice(0, -3) + "y" : withoutPrefix.endsWith("s") ? withoutPrefix.slice(0, -1) : withoutPrefix;
288
- return singular.charAt(0).toUpperCase() + singular.slice(1);
310
+ return toPascalCase(toSingular(withoutPrefix));
289
311
  }
290
312
  function tableToSubject(table) {
291
313
  const match = table.match(/^([a-z]{2,4})_(.+)$/);
292
314
  if (!match) return tableToEntityName(table);
293
315
  const [, prefix, rest] = match;
294
316
  const prefixPascal = prefix.charAt(0).toUpperCase() + prefix.slice(1);
295
- const singular = rest.endsWith("ies") ? rest.slice(0, -3) + "y" : rest.endsWith("s") ? rest.slice(0, -1) : rest;
296
- const restPascal = singular.charAt(0).toUpperCase() + singular.slice(1);
297
- return prefixPascal + restPascal;
317
+ return prefixPascal + toPascalCase(toSingular(rest));
318
+ }
319
+ function labelToEntityName(label) {
320
+ const singular = label.endsWith("ies") ? label.slice(0, -3) + "y" : label.endsWith("s") ? label.slice(0, -1) : label;
321
+ return singular.charAt(0).toUpperCase() + singular.slice(1).replace(/\s+/g, "");
322
+ }
323
+ function generateReadOnlySchema(entity) {
324
+ const { label, fields } = entity;
325
+ const entityName = labelToEntityName(label);
326
+ const lines = [
327
+ `import { z } from 'zod'`,
328
+ ``,
329
+ `// === OUTPUT SCHEMA ===`,
330
+ `export const ${entityName.toLowerCase()}Schema = z.object({`
331
+ ];
332
+ for (const [name, field] of Object.entries(fields)) {
333
+ const zodType = dbTypeToZodType(field.db.type);
334
+ const nullable = field.db.nullable ? `.nullable()` : "";
335
+ lines.push(` ${name}: ${zodType}${nullable},`);
336
+ }
337
+ lines.push(`})`);
338
+ lines.push(``);
339
+ lines.push(`// === QUERY ===`);
340
+ lines.push(`export const ${entityName.toLowerCase()}QuerySchema = z.object({`);
341
+ lines.push(` page: z.coerce.number().int().min(1).default(1),`);
342
+ lines.push(` limit: z.coerce.number().int().min(1).max(100).default(20),`);
343
+ for (const [name, field] of Object.entries(fields)) {
344
+ if (field.meta?.searchable) {
345
+ const zodType = dbTypeToZodType(field.db.type);
346
+ lines.push(` ${name}: ${zodType}.optional(),`);
347
+ }
348
+ }
349
+ lines.push(`})`);
350
+ lines.push(``);
351
+ lines.push(`// === INFERRED TYPES ===`);
352
+ lines.push(`export type ${entityName} = z.infer<typeof ${entityName.toLowerCase()}Schema>`);
353
+ lines.push(`export type ${entityName}Query = z.infer<typeof ${entityName.toLowerCase()}QuerySchema>`);
354
+ lines.push(``);
355
+ return lines.join("\n");
356
+ }
357
+ function generateReadOnlyModel(entity) {
358
+ const { label, fields } = entity;
359
+ const entityName = labelToEntityName(label);
360
+ const lines = [
361
+ `/**`,
362
+ ` * ${label}`,
363
+ ` * Generated from EntityDefinition (${entity.type})`,
364
+ ` */`,
365
+ `export interface ${entityName} {`
366
+ ];
367
+ for (const [name, field] of Object.entries(fields)) {
368
+ const tsType = dbTypeToTsType(field.db);
369
+ const optional = field.db.nullable ? "?" : "";
370
+ lines.push(` ${name}${optional}: ${tsType}`);
371
+ }
372
+ lines.push(`}`);
373
+ lines.push(``);
374
+ return lines.join("\n");
298
375
  }
299
376
  function getFieldsForRole(entity, role, action) {
300
377
  const { fields, casl } = entity;
@@ -365,7 +442,9 @@ function generateCaslPermissions(entity) {
365
442
  function generateCaslSeed(entities) {
366
443
  const allPermissions = [];
367
444
  for (const entity of entities) {
368
- allPermissions.push(...generateCaslPermissions(entity));
445
+ if (isPersistentEntity(entity)) {
446
+ allPermissions.push(...generateCaslPermissions(entity));
447
+ }
369
448
  }
370
449
  if (allPermissions.length === 0) {
371
450
  return "// No CASL permissions defined in entities\n";
@@ -430,11 +509,158 @@ function generateCaslSeed(entities) {
430
509
  function getEntitySubject(entity) {
431
510
  return entity.casl?.subject ?? tableToSubject(entity.table);
432
511
  }
512
+ function getEntityName(entity) {
513
+ if ("table" in entity) {
514
+ const withoutPrefix = entity.table.replace(/^[a-z]{2,4}_/, "");
515
+ return toPascalCase(toSingular(withoutPrefix));
516
+ } else if ("key" in entity) {
517
+ return toPascalCase(entity.key);
518
+ } else {
519
+ return toSingular(entity.label).replace(/\s+/g, "");
520
+ }
521
+ }
522
+ function dbTypeToZodSimple(type, nullable) {
523
+ let zod = "";
524
+ switch (type) {
525
+ case "string":
526
+ case "text":
527
+ case "uuid":
528
+ zod = "z.string()";
529
+ break;
530
+ case "integer":
531
+ zod = "z.number().int()";
532
+ break;
533
+ case "decimal":
534
+ zod = "z.number()";
535
+ break;
536
+ case "boolean":
537
+ zod = "z.boolean()";
538
+ break;
539
+ case "date":
540
+ case "datetime":
541
+ zod = "z.string().datetime()";
542
+ break;
543
+ case "json":
544
+ zod = "z.record(z.unknown())";
545
+ break;
546
+ default:
547
+ zod = "z.unknown()";
548
+ }
549
+ return nullable ? `${zod}.nullable()` : zod;
550
+ }
551
+ function dbTypeToTsSimple(type) {
552
+ switch (type) {
553
+ case "string":
554
+ case "text":
555
+ case "uuid":
556
+ return "string";
557
+ case "integer":
558
+ case "decimal":
559
+ return "number";
560
+ case "boolean":
561
+ return "boolean";
562
+ case "date":
563
+ case "datetime":
564
+ return "Date";
565
+ case "json":
566
+ return "Record<string, unknown>";
567
+ default:
568
+ return "unknown";
569
+ }
570
+ }
571
+ function generateSchemasFile(definitions) {
572
+ const lines = [
573
+ "/**",
574
+ " * AUTO-GENERATED - Do not edit manually",
575
+ " * Generated from EntityDefinition via @gzl10/nexus-sdk generators",
576
+ " */",
577
+ "",
578
+ "import { z } from 'zod'",
579
+ ""
580
+ ];
581
+ for (const entity of definitions) {
582
+ const name = getEntityName(entity);
583
+ lines.push(`// ============================================================================`);
584
+ lines.push(`// ${name.toUpperCase()} (${entity.type ?? "collection"})`);
585
+ lines.push(`// ============================================================================`);
586
+ lines.push("");
587
+ if (isPersistentEntity(entity)) {
588
+ const schema = generateZodSchema(entity);
589
+ const schemaLines = schema.split("\n").filter(
590
+ (l) => !l.startsWith("import") && l.trim() !== ""
591
+ );
592
+ lines.push(...schemaLines);
593
+ } else if (entity.type === "single") {
594
+ lines.push(`export const ${name.toLowerCase()}Schema = z.object({`);
595
+ for (const [fieldName, field] of Object.entries(entity.fields)) {
596
+ const zodType = dbTypeToZodSimple(field.db.type, field.db.nullable);
597
+ lines.push(` ${fieldName}: ${zodType},`);
598
+ }
599
+ lines.push("})");
600
+ lines.push("");
601
+ lines.push(`export type ${name} = z.infer<typeof ${name.toLowerCase()}Schema>`);
602
+ } else {
603
+ const schema = generateReadOnlySchema(entity);
604
+ const schemaLines = schema.split("\n").filter(
605
+ (l) => !l.startsWith("import") && l.trim() !== ""
606
+ );
607
+ lines.push(...schemaLines);
608
+ }
609
+ lines.push("");
610
+ }
611
+ return lines.join("\n");
612
+ }
613
+ function generateModelsFile(definitions) {
614
+ const lines = [
615
+ "/**",
616
+ " * AUTO-GENERATED - Do not edit manually",
617
+ " * Generated from EntityDefinition via @gzl10/nexus-sdk generators",
618
+ " */",
619
+ ""
620
+ ];
621
+ for (const entity of definitions) {
622
+ const name = getEntityName(entity);
623
+ lines.push(`// ============================================================================`);
624
+ lines.push(`// ${name.toUpperCase()} (${entity.type ?? "collection"})`);
625
+ lines.push(`// ============================================================================`);
626
+ lines.push("");
627
+ if (isPersistentEntity(entity)) {
628
+ const model = generateModel(entity);
629
+ const modelLines = model.split("\n");
630
+ lines.push(...modelLines);
631
+ } else if (entity.type === "single") {
632
+ lines.push(`/**`);
633
+ lines.push(` * ${entity.label}`);
634
+ lines.push(` * Generated from EntityDefinition (single)`);
635
+ lines.push(` */`);
636
+ lines.push(`export interface ${name} {`);
637
+ for (const [fieldName, field] of Object.entries(entity.fields)) {
638
+ const tsType = dbTypeToTsSimple(field.db.type);
639
+ const optional = field.db.nullable ? "?" : "";
640
+ lines.push(` ${fieldName}${optional}: ${tsType}`);
641
+ }
642
+ lines.push("}");
643
+ } else {
644
+ const model = generateReadOnlyModel(entity);
645
+ lines.push(model);
646
+ }
647
+ lines.push("");
648
+ }
649
+ return lines.join("\n");
650
+ }
433
651
  export {
434
652
  generateCaslPermissions,
435
653
  generateCaslSeed,
436
654
  generateMigration,
437
655
  generateModel,
656
+ generateModelsFile,
657
+ generateReadOnlyModel,
658
+ generateReadOnlySchema,
659
+ generateSchemasFile,
438
660
  generateZodSchema,
439
- getEntitySubject
661
+ getEntityName,
662
+ getEntitySubject,
663
+ hasTable,
664
+ isPersistentEntity,
665
+ isSingletonEntity
440
666
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gzl10/nexus-sdk",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "SDK types for creating Nexus plugins and modules",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",