@gzl10/nexus-sdk 0.4.0 → 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.
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ GENERATED_DIR,
4
+ generateCaslSeed,
5
+ generateMigrationsFile,
6
+ generateModelsFile,
7
+ generateSchemasFile
8
+ } from "../chunk-XEPNN6IG.js";
9
+
10
+ // src/cli/index.ts
11
+ import { Command as Command2 } from "commander";
12
+
13
+ // src/cli/commands/generate.ts
14
+ import { Command } from "commander";
15
+ import { join, dirname } from "path";
16
+ import { writeFileSync, mkdirSync, existsSync } from "fs";
17
+ import pc from "picocolors";
18
+ import { watch } from "chokidar";
19
+
20
+ // src/cli/discovery/module-finder.ts
21
+ import fg from "fast-glob";
22
+ async function findModules(opts) {
23
+ const pattern = opts.module ? `**/${opts.module}/index.ts` : "**/index.ts";
24
+ const files = await fg(pattern, {
25
+ cwd: opts.path,
26
+ absolute: true,
27
+ ignore: [
28
+ "**/node_modules/**",
29
+ "**/dist/**",
30
+ "**/__generated__/**",
31
+ "index.ts"
32
+ // Exclude root modules/index.ts (not nested ones)
33
+ ]
34
+ });
35
+ return files.filter((file) => {
36
+ const parts = file.split("/");
37
+ const fileName = parts.pop();
38
+ const folderName = parts.pop();
39
+ return fileName === "index.ts" && folderName && !folderName.startsWith("_");
40
+ });
41
+ }
42
+
43
+ // src/cli/discovery/manifest-extractor.ts
44
+ import { createJiti } from "jiti";
45
+ var jiti = createJiti(import.meta.url, {
46
+ interopDefault: true,
47
+ moduleCache: false
48
+ // Don't cache for watch mode
49
+ });
50
+ function isModuleManifest(value) {
51
+ return typeof value === "object" && value !== null && "name" in value && typeof value.name === "string";
52
+ }
53
+ async function extractManifest(modulePath, verbose = false) {
54
+ try {
55
+ const mod = await jiti.import(modulePath);
56
+ for (const value of Object.values(mod)) {
57
+ if (isModuleManifest(value)) {
58
+ const manifest = value;
59
+ if (manifest.definitions?.length) {
60
+ return {
61
+ path: modulePath,
62
+ manifest,
63
+ definitions: manifest.definitions
64
+ };
65
+ }
66
+ }
67
+ }
68
+ return null;
69
+ } catch (error) {
70
+ if (verbose) {
71
+ console.error(`Error loading ${modulePath}: ${getErrorMessage(error)}`);
72
+ }
73
+ return null;
74
+ }
75
+ }
76
+ function getErrorMessage(error) {
77
+ if (error instanceof Error) {
78
+ return error.message;
79
+ }
80
+ return String(error);
81
+ }
82
+
83
+ // src/cli/commands/generate.ts
84
+ async function runGenerate(opts) {
85
+ const startTime = Date.now();
86
+ console.log(pc.blue("Nexus SDK Generator"));
87
+ console.log(pc.dim(`Scanning ${opts.path}...
88
+ `));
89
+ const modulePaths = await findModules({ path: opts.path, module: opts.module });
90
+ if (opts.verbose) {
91
+ console.log(pc.dim(`Found ${modulePaths.length} potential module(s)`));
92
+ }
93
+ if (modulePaths.length === 0) {
94
+ console.log(pc.yellow("No modules found"));
95
+ return;
96
+ }
97
+ const modules = [];
98
+ for (const path of modulePaths) {
99
+ const extracted = await extractManifest(path, opts.verbose);
100
+ if (extracted) {
101
+ modules.push(extracted);
102
+ if (opts.verbose) {
103
+ console.log(pc.dim(` \u2713 ${extracted.manifest.name}: ${extracted.definitions.length} entities`));
104
+ }
105
+ }
106
+ }
107
+ if (modules.length === 0) {
108
+ console.log(pc.yellow("No modules with definitions found"));
109
+ return;
110
+ }
111
+ let totalFiles = 0;
112
+ for (const mod of modules) {
113
+ const moduleDir = dirname(mod.path);
114
+ const outputDir = join(moduleDir, opts.output || GENERATED_DIR);
115
+ if (!opts.dryRun && !existsSync(outputDir)) {
116
+ mkdirSync(outputDir, { recursive: true });
117
+ }
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;
144
+ const prefix = opts.dryRun ? pc.yellow("[dry-run]") : pc.green("\u2713");
145
+ console.log(`${prefix} ${pc.bold(mod.manifest.name)}: ${files.join(", ")}`);
146
+ }
147
+ const elapsed = Date.now() - startTime;
148
+ console.log(
149
+ `
150
+ ${pc.green("\u2728")} Generated ${totalFiles} files in ${modules.length} module(s) (${elapsed}ms)`
151
+ );
152
+ }
153
+ 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) => {
155
+ await runGenerate(opts);
156
+ if (opts.watch) {
157
+ console.log(pc.blue("\nWatching for changes..."));
158
+ const watcher = watch(["**/*.entity.ts", "**/index.ts"], {
159
+ cwd: opts.path,
160
+ ignored: ["**/node_modules/**", "**/__generated__/**", "**/dist/**"],
161
+ ignoreInitial: true
162
+ });
163
+ watcher.on("change", async (path) => {
164
+ console.log(pc.dim(`
165
+ Change detected: ${path}`));
166
+ await runGenerate({ ...opts, watch: false });
167
+ });
168
+ watcher.on("add", async (path) => {
169
+ console.log(pc.dim(`
170
+ New file: ${path}`));
171
+ await runGenerate({ ...opts, watch: false });
172
+ });
173
+ }
174
+ });
175
+ }
176
+
177
+ // src/cli/index.ts
178
+ var program = new Command2().name("nexus-sdk").description("Nexus SDK CLI - Code generation from EntityDefinitions").version("0.5.0");
179
+ program.addCommand(createGenerateCommand());
180
+ program.parse();
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Knex } from 'knex';
2
2
  export { Knex } from 'knex';
3
- import { Request, RequestHandler, Router } from 'express';
3
+ import { Request, Router, RequestHandler } from 'express';
4
4
  export { CookieOptions, NextFunction, Request, RequestHandler, Response, Router } from 'express';
5
5
  import { Logger } from 'pino';
6
6
 
@@ -11,6 +11,8 @@ import { Logger } from 'pino';
11
11
  * que pueden ser escritos a archivos o usados en runtime.
12
12
  */
13
13
 
14
+ /** Directorio estándar para código generado */
15
+ declare const GENERATED_DIR = "__generated__";
14
16
  /**
15
17
  * Entidades que persisten en BD local con tabla propia
16
18
  * Excluye: action, external, virtual, computed, single (usa tabla compartida)
@@ -130,6 +132,15 @@ declare function generateSchemasFile(definitions: EntityDefinition[]): string;
130
132
  * writeFileSync('users.models.ts', code)
131
133
  */
132
134
  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;
133
144
 
134
145
  /**
135
146
  * @gzl10/nexus-sdk
@@ -416,6 +427,8 @@ interface CollectionEntityDefinition extends BaseEntityDefinition {
416
427
  audit?: boolean;
417
428
  /** Índices compuestos */
418
429
  indexes?: EntityIndex[];
430
+ /** Usar deleted_at en vez de DELETE físico */
431
+ softDelete?: boolean;
419
432
  }
420
433
  /**
421
434
  * Entidad singleton - Un solo registro en tabla compartida sys_settings
@@ -434,6 +447,8 @@ interface SingleEntityDefinition {
434
447
  label: string;
435
448
  /** Definición de campos (estructura del JSON value) */
436
449
  fields: Record<string, FieldDefinition>;
450
+ /** Valores por defecto al crear */
451
+ defaults?: Record<string, unknown>;
437
452
  /** Autorización CASL */
438
453
  casl?: EntityCaslConfig;
439
454
  }
@@ -448,6 +463,10 @@ interface ReferenceEntityDefinition extends BaseEntityDefinition {
448
463
  timestamps?: boolean;
449
464
  /** Índices compuestos */
450
465
  indexes?: EntityIndex[];
466
+ /** Datos iniciales para seed */
467
+ seed?: Array<Record<string, unknown>>;
468
+ /** Permitir CRUD para admin (default: false, solo lectura) */
469
+ allowAdminEdit?: boolean;
451
470
  }
452
471
  /**
453
472
  * Entidad de eventos - Logs de auditoría, append-only
@@ -458,6 +477,13 @@ interface EventEntityDefinition extends BaseEntityDefinition {
458
477
  labelField: string;
459
478
  /** Siempre true para eventos */
460
479
  timestamps: true;
480
+ /** Política de retención automática */
481
+ retention?: {
482
+ /** Eliminar registros más antiguos que N días */
483
+ days?: number;
484
+ /** Mantener solo los últimos N registros */
485
+ maxRows?: number;
486
+ };
461
487
  }
462
488
  /**
463
489
  * Entidad de acción - Comandos/operaciones sin persistencia
@@ -466,8 +492,16 @@ interface ActionEntityDefinition {
466
492
  type: 'action';
467
493
  /** Nombre para mostrar en UI */
468
494
  label: string;
469
- /** Solo campos de formulario, sin BD */
495
+ /** Solo campos de formulario para input, sin BD */
470
496
  fields: Record<string, FieldDefinition>;
497
+ /** Schema Zod de input (se valida antes de ejecutar) */
498
+ inputSchema?: ValidationSchema;
499
+ /** Schema Zod de output (documenta respuesta) */
500
+ outputSchema?: ValidationSchema;
501
+ /** Handler que ejecuta la acción */
502
+ handler?: (ctx: ModuleContext, input: unknown) => Promise<unknown>;
503
+ /** Autorización CASL */
504
+ casl?: EntityCaslConfig;
471
505
  }
472
506
  /**
473
507
  * Entidad externa - Datos de APIs externas (stripe_customers, github_repos)
@@ -481,12 +515,21 @@ interface ExternalEntityDefinition {
481
515
  labelField: string;
482
516
  /** Definición de campos (estructura esperada del API externo) */
483
517
  fields: Record<string, FieldDefinition>;
484
- /** Configuración del origen externo */
485
- source: {
486
- /** Tipo de origen */
487
- provider: string;
488
- /** Endpoint o recurso */
518
+ /** Nombre del adapter registrado (ej: 'stripe', 'github') */
519
+ adapter: string;
520
+ /** Configuración del origen externo (pasada al adapter) */
521
+ source?: {
522
+ /** Endpoint o recurso específico */
489
523
  endpoint?: string;
524
+ /** Config adicional */
525
+ [key: string]: unknown;
526
+ };
527
+ /** Configuración de cache */
528
+ cache?: {
529
+ /** TTL en segundos */
530
+ ttl: number;
531
+ /** Campo para generar cache key */
532
+ key?: string;
490
533
  };
491
534
  /** Autorización CASL */
492
535
  casl?: EntityCaslConfig;
@@ -503,8 +546,10 @@ interface VirtualEntityDefinition {
503
546
  labelField: string;
504
547
  /** Definición de campos (esquema unificado) */
505
548
  fields: Record<string, FieldDefinition>;
506
- /** Fuentes de datos que se combinan */
549
+ /** Fuentes de datos que se combinan (nombres de entidades/tables) */
507
550
  sources: string[];
551
+ /** Función que combina los datos de las fuentes */
552
+ resolver?: (sources: Record<string, unknown[]>, ctx: ModuleContext) => unknown[] | Promise<unknown[]>;
508
553
  /** Autorización CASL */
509
554
  casl?: EntityCaslConfig;
510
555
  }
@@ -520,8 +565,13 @@ interface ComputedEntityDefinition {
520
565
  labelField?: string;
521
566
  /** Definición de campos (estructura del resultado) */
522
567
  fields: Record<string, FieldDefinition>;
523
- /** Tiempo de cache en segundos (0 = sin cache) */
524
- cacheTtl?: number;
568
+ /** Función que calcula los datos */
569
+ compute?: (ctx: ModuleContext, params?: Record<string, unknown>) => Promise<unknown[]>;
570
+ /** Configuración de cache */
571
+ cache?: {
572
+ /** TTL en segundos (0 = sin cache) */
573
+ ttl: number;
574
+ };
525
575
  /** Autorización CASL */
526
576
  casl?: EntityCaslConfig;
527
577
  }
@@ -541,6 +591,8 @@ interface ViewEntityDefinition {
541
591
  fields: Record<string, FieldDefinition>;
542
592
  /** Entidad fuente de la que deriva */
543
593
  sourceEntity?: string;
594
+ /** Query SQL o builder para crear la vista */
595
+ query?: string | ((db: Knex) => Knex.QueryBuilder);
544
596
  /** Autorización CASL */
545
597
  casl?: EntityCaslConfig;
546
598
  }
@@ -550,8 +602,12 @@ interface ViewEntityDefinition {
550
602
  */
551
603
  interface ConfigEntityDefinition extends BaseEntityDefinition {
552
604
  type: 'config';
605
+ /** Campo que identifica el scope (ej: 'module_name', 'tenant_id') */
606
+ scopeField?: string;
553
607
  /** Scope de la configuración */
554
608
  scope?: 'global' | 'module' | 'tenant' | 'user';
609
+ /** Valores por defecto */
610
+ defaults?: Record<string, unknown>;
555
611
  /** Añadir updated_at */
556
612
  timestamps?: boolean;
557
613
  /** Añadir updated_by */
@@ -565,11 +621,17 @@ interface TempEntityDefinition extends BaseEntityDefinition {
565
621
  type: 'temp';
566
622
  /** Tiempo de vida en segundos */
567
623
  ttl: number;
624
+ /** Campo que almacena la fecha de expiración (default: 'expires_at') */
625
+ ttlField?: string;
568
626
  /** Campo para mostrar en listas (opcional) */
569
627
  labelField?: string;
570
628
  /** Índices para búsqueda rápida */
571
629
  indexes?: EntityIndex[];
572
630
  }
631
+ /**
632
+ * Todos los tipos de entidad disponibles
633
+ */
634
+ type EntityType = 'collection' | 'single' | 'external' | 'virtual' | 'computed' | 'view' | 'reference' | 'config' | 'event' | 'temp' | 'action';
573
635
  /**
574
636
  * Union de todas las definiciones de entidad
575
637
  *
@@ -634,21 +696,6 @@ interface ModuleAbilities {
634
696
  * Usa BaseUser y AbilityLike para tipado básico sin depender del backend
635
697
  */
636
698
  type PluginAuthRequest = AuthRequest<BaseUser, AbilityLike>;
637
- /**
638
- * Resolver de usuarios para plugins
639
- * Permite acceder a usuarios sin conocer la implementación interna
640
- */
641
- interface UsersResolver {
642
- findById: (id: string) => Promise<BaseUser | null>;
643
- findByIds: (ids: string[]) => Promise<BaseUser[]>;
644
- }
645
- /**
646
- * Servicios core inyectados por el backend
647
- * Los plugins acceden a estos servicios via ctx.services
648
- */
649
- interface CoreServices {
650
- users?: UsersResolver;
651
- }
652
699
  /**
653
700
  * Helpers para migraciones de base de datos
654
701
  */
@@ -713,8 +760,8 @@ interface ModuleContext {
713
760
  data?: Record<string, unknown>;
714
761
  }) => Promise<unknown>;
715
762
  };
716
- /** Servicios de módulos core (users, etc.) */
717
- services: CoreServices & Record<string, unknown>;
763
+ /** Servicios de módulos (inyectados por backend) */
764
+ services: Record<string, unknown>;
718
765
  }
719
766
  /**
720
767
  * Manifest de un módulo Nexus
@@ -722,8 +769,6 @@ interface ModuleContext {
722
769
  interface ModuleManifest {
723
770
  /** Identificador único del módulo (ej: 'users', 'posts') */
724
771
  name: string;
725
- /** Código único del módulo (ej: 'USR', 'PST'). Opcional si pertenece a un plugin */
726
- code?: string;
727
772
  /** Nombre para mostrar en UI. Opcional si pertenece a un plugin */
728
773
  label?: string;
729
774
  /** Icono del módulo (Iconify MDI: 'mdi:account-group') */
@@ -736,7 +781,7 @@ interface ModuleManifest {
736
781
  dependencies?: string[];
737
782
  /** Requisitos para activar el módulo */
738
783
  required?: ModuleRequirements;
739
- /** Función de migración de base de datos */
784
+ /** Función de migración de base de datos. Normalmente se genera desde definitions (EntityDefinition) */
740
785
  migrate?: (ctx: ModuleContext) => Promise<void>;
741
786
  /** Función de seed de datos iniciales */
742
787
  seed?: (ctx: ModuleContext) => Promise<void>;
@@ -781,4 +826,4 @@ interface PluginManifest {
781
826
  modules: ModuleManifest[];
782
827
  }
783
828
 
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 };
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 };