@gzl10/nexus-sdk 0.1.10 → 0.2.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 +45 -5
- package/dist/index.d.ts +381 -10
- package/dist/index.js +440 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ pnpm add @gzl10/nexus-sdk
|
|
|
17
17
|
### Define a module
|
|
18
18
|
|
|
19
19
|
```typescript
|
|
20
|
-
import type { ModuleManifest } from '@gzl10/nexus-sdk'
|
|
20
|
+
import type { ModuleManifest, PluginAuthRequest } from '@gzl10/nexus-sdk'
|
|
21
21
|
|
|
22
22
|
export const tasksModule: ModuleManifest = {
|
|
23
23
|
name: 'tasks',
|
|
@@ -37,6 +37,7 @@ export const tasksModule: ModuleManifest = {
|
|
|
37
37
|
table.string('title').notNullable()
|
|
38
38
|
table.text('description')
|
|
39
39
|
table.boolean('completed').defaultTo(false)
|
|
40
|
+
table.string('created_by').references('id').inTable('users')
|
|
40
41
|
helpers.addTimestamps(table, db)
|
|
41
42
|
})
|
|
42
43
|
}
|
|
@@ -44,10 +45,19 @@ export const tasksModule: ModuleManifest = {
|
|
|
44
45
|
|
|
45
46
|
routes: (ctx) => {
|
|
46
47
|
const router = ctx.createRouter()
|
|
48
|
+
const { abilities, services } = ctx
|
|
49
|
+
|
|
50
|
+
router.get('/', async (req: PluginAuthRequest, res) => {
|
|
51
|
+
// Check permissions with CASL
|
|
52
|
+
abilities.ForbiddenError.from(req.ability).throwUnlessCan('read', 'tasks')
|
|
47
53
|
|
|
48
|
-
router.get('/', async (req, res) => {
|
|
49
54
|
const tasks = await ctx.db('tasks').select('*')
|
|
50
|
-
|
|
55
|
+
|
|
56
|
+
// Resolve user relations
|
|
57
|
+
const userIds = [...new Set(tasks.map(t => t.created_by).filter(Boolean))]
|
|
58
|
+
const users = await services.users?.findByIds(userIds) ?? []
|
|
59
|
+
|
|
60
|
+
res.json({ tasks, users })
|
|
51
61
|
})
|
|
52
62
|
|
|
53
63
|
return router
|
|
@@ -90,14 +100,35 @@ export const projectPlugin: PluginManifest = {
|
|
|
90
100
|
|
|
91
101
|
## Main Types
|
|
92
102
|
|
|
103
|
+
### Manifests & Context
|
|
104
|
+
|
|
93
105
|
| Type | Description |
|
|
94
106
|
|------|-------------|
|
|
95
107
|
| `ModuleManifest` | Defines a module: routes, migrations, seeds, CRUD entities |
|
|
96
108
|
| `PluginManifest` | Groups modules under a plugin with shared metadata |
|
|
97
|
-
| `ModuleContext` | Context injected by Nexus: `db`, `logger`, `helpers`, `
|
|
109
|
+
| `ModuleContext` | Context injected by Nexus: `db`, `logger`, `helpers`, `services`, `abilities` |
|
|
98
110
|
| `ModuleEntity` | Declarative entity configuration for CRUD UI |
|
|
99
111
|
| `FormField` | Form field configuration with validation |
|
|
100
112
|
|
|
113
|
+
### Request & Auth
|
|
114
|
+
|
|
115
|
+
| Type | Description |
|
|
116
|
+
|------|-------------|
|
|
117
|
+
| `AuthRequest<TUser, TAbility>` | Generic authenticated request |
|
|
118
|
+
| `PluginAuthRequest` | Pre-typed `AuthRequest<BaseUser, AbilityLike>` for plugins |
|
|
119
|
+
| `BaseUser` | User without password for relations |
|
|
120
|
+
| `AbilityLike` | Generic CASL ability interface (`can`, `cannot`) |
|
|
121
|
+
|
|
122
|
+
### Services & Utilities
|
|
123
|
+
|
|
124
|
+
| Type | Description |
|
|
125
|
+
|------|-------------|
|
|
126
|
+
| `CoreServices` | Core services container (`users`, etc.) |
|
|
127
|
+
| `UsersResolver` | User lookup service (`findById`, `findByIds`) |
|
|
128
|
+
| `PaginationParams` | Pagination input (`page`, `limit`) |
|
|
129
|
+
| `PaginatedResult<T>` | Paginated response with metadata |
|
|
130
|
+
| `ValidationSchema` | Generic Zod-compatible schema interface |
|
|
131
|
+
|
|
101
132
|
### ModuleContext
|
|
102
133
|
|
|
103
134
|
The context provides access to:
|
|
@@ -106,11 +137,20 @@ The context provides access to:
|
|
|
106
137
|
- `logger` - Pino logger
|
|
107
138
|
- `helpers` - Migration utilities (`addTimestamps`, `addColumnIfMissing`)
|
|
108
139
|
- `createRouter()` - Express Router factory
|
|
109
|
-
- `middleware` -
|
|
140
|
+
- `middleware.validate()` - Request validation with Zod schemas
|
|
110
141
|
- `errors` - Error classes (`AppError`, `NotFoundError`, `UnauthorizedError`, etc.)
|
|
142
|
+
- `abilities` - CASL helpers (`subject`, `ForbiddenError`)
|
|
143
|
+
- `services` - Core services (`users.findById`, `users.findByIds`)
|
|
111
144
|
- `events` - EventEmitter for inter-module communication
|
|
112
145
|
- `mail` - Email sending service
|
|
113
146
|
|
|
147
|
+
### Re-exported Types
|
|
148
|
+
|
|
149
|
+
For convenience, the SDK re-exports commonly used types:
|
|
150
|
+
|
|
151
|
+
- **Express:** `Request`, `Response`, `NextFunction`, `RequestHandler`, `Router`, `CookieOptions`
|
|
152
|
+
- **Knex:** `Knex`, `KnexCreateTableBuilder`, `KnexAlterTableBuilder`, `KnexTransaction`
|
|
153
|
+
|
|
114
154
|
## License
|
|
115
155
|
|
|
116
156
|
MIT © [Gonzalo Díez](https://www.gzl10.com)
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,80 @@ import { Request, RequestHandler, Router } from 'express';
|
|
|
4
4
|
export { CookieOptions, NextFunction, Request, RequestHandler, Response, Router } from 'express';
|
|
5
5
|
import { Logger } from 'pino';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Generadores de código desde EntityDefinition
|
|
9
|
+
*
|
|
10
|
+
* Estos generadores producen código TypeScript como strings
|
|
11
|
+
* que pueden ser escritos a archivos o usados en runtime.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Genera código de migración Knex desde EntityDefinition
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* const code = generateMigration(postEntity)
|
|
19
|
+
* // Genera:
|
|
20
|
+
* // import type { ModuleContext, Knex } from '@gzl10/nexus-sdk'
|
|
21
|
+
* //
|
|
22
|
+
* // export async function migrate(ctx: ModuleContext): Promise<void> {
|
|
23
|
+
* // const { db, logger, helpers } = ctx
|
|
24
|
+
* // ...
|
|
25
|
+
* // }
|
|
26
|
+
*/
|
|
27
|
+
declare function generateMigration(entity: EntityDefinition): string;
|
|
28
|
+
/**
|
|
29
|
+
* Genera código de schemas Zod desde EntityDefinition
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* const code = generateZodSchema(postEntity)
|
|
33
|
+
* // Genera createPostSchema, updatePostSchema, postParamsSchema, postQuerySchema
|
|
34
|
+
*/
|
|
35
|
+
declare function generateZodSchema(entity: EntityDefinition): string;
|
|
36
|
+
/**
|
|
37
|
+
* Genera interface TypeScript desde EntityDefinition
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* const code = generateModel(postEntity)
|
|
41
|
+
* // Genera:
|
|
42
|
+
* // export interface Post {
|
|
43
|
+
* // id: string
|
|
44
|
+
* // title: string
|
|
45
|
+
* // ...
|
|
46
|
+
* // }
|
|
47
|
+
*/
|
|
48
|
+
declare function generateModel(entity: EntityDefinition): string;
|
|
49
|
+
/**
|
|
50
|
+
* Estructura de permiso generado para insertar en BD
|
|
51
|
+
*/
|
|
52
|
+
interface GeneratedPermission {
|
|
53
|
+
role: string;
|
|
54
|
+
action: string;
|
|
55
|
+
subject: string;
|
|
56
|
+
conditions: string | null;
|
|
57
|
+
fields: string | null;
|
|
58
|
+
inverted: boolean;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Genera permisos CASL desde EntityDefinition
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* const permissions = generateCaslPermissions(postEntity)
|
|
65
|
+
* // Genera array de permisos para insertar en rol_role_permissions
|
|
66
|
+
*/
|
|
67
|
+
declare function generateCaslPermissions(entity: EntityDefinition): GeneratedPermission[];
|
|
68
|
+
/**
|
|
69
|
+
* Genera código de seed para permisos CASL
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* const code = generateCaslSeed([postEntity, pageEntity])
|
|
73
|
+
* // Genera código para insertar permisos en rol_role_permissions
|
|
74
|
+
*/
|
|
75
|
+
declare function generateCaslSeed(entities: EntityDefinition[]): string;
|
|
76
|
+
/**
|
|
77
|
+
* Obtiene el subject CASL de una entidad
|
|
78
|
+
*/
|
|
79
|
+
declare function getEntitySubject(entity: EntityDefinition): string;
|
|
80
|
+
|
|
7
81
|
/**
|
|
8
82
|
* @gzl10/nexus-sdk
|
|
9
83
|
*
|
|
@@ -93,18 +167,307 @@ interface FormField {
|
|
|
93
167
|
*/
|
|
94
168
|
type ListType = 'table' | 'list' | 'grid' | 'masonry';
|
|
95
169
|
/**
|
|
96
|
-
*
|
|
170
|
+
* Tipos de columna en base de datos
|
|
171
|
+
*/
|
|
172
|
+
type DbType = 'string' | 'text' | 'integer' | 'decimal' | 'boolean' | 'date' | 'datetime' | 'json' | 'uuid';
|
|
173
|
+
/**
|
|
174
|
+
* Tipos de input en UI
|
|
175
|
+
*/
|
|
176
|
+
type InputType = 'text' | 'email' | 'password' | 'url' | 'tel' | 'number' | 'decimal' | 'textarea' | 'markdown' | 'select' | 'multiselect' | 'checkbox' | 'switch' | 'date' | 'datetime' | 'file' | 'image' | 'hidden';
|
|
177
|
+
/**
|
|
178
|
+
* Configuración de base de datos para un campo
|
|
97
179
|
*/
|
|
98
|
-
interface
|
|
180
|
+
interface FieldDbConfig {
|
|
181
|
+
type: DbType;
|
|
182
|
+
size?: number;
|
|
183
|
+
precision?: [number, number];
|
|
184
|
+
nullable: boolean;
|
|
185
|
+
unique?: boolean;
|
|
186
|
+
default?: unknown;
|
|
187
|
+
defaultFn?: 'now' | 'uuid';
|
|
188
|
+
index?: boolean;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Configuración de relación (Foreign Key)
|
|
192
|
+
*/
|
|
193
|
+
interface FieldRelation {
|
|
194
|
+
table: string;
|
|
195
|
+
column?: string;
|
|
196
|
+
onDelete?: 'CASCADE' | 'RESTRICT' | 'SET NULL' | 'NO ACTION';
|
|
197
|
+
onUpdate?: 'CASCADE' | 'RESTRICT' | 'SET NULL' | 'NO ACTION';
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Configuración de validación para un campo
|
|
201
|
+
*/
|
|
202
|
+
interface FieldValidationConfig {
|
|
203
|
+
required?: boolean;
|
|
204
|
+
min?: number;
|
|
205
|
+
max?: number;
|
|
206
|
+
pattern?: string;
|
|
207
|
+
format?: 'email' | 'url' | 'uuid' | 'slug';
|
|
208
|
+
enum?: string[];
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Opciones para campos select/relaciones
|
|
212
|
+
*/
|
|
213
|
+
interface FieldOptions {
|
|
214
|
+
endpoint?: string;
|
|
215
|
+
valueField?: string;
|
|
216
|
+
labelField?: string;
|
|
217
|
+
static?: Array<{
|
|
218
|
+
value: string;
|
|
219
|
+
label: string;
|
|
220
|
+
}>;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Restricciones de acceso CASL para un campo
|
|
224
|
+
*/
|
|
225
|
+
interface FieldCaslAccess {
|
|
226
|
+
/** Roles que pueden leer este campo (vacío = todos, null = nadie excepto ADMIN) */
|
|
227
|
+
readRoles?: string[];
|
|
228
|
+
/** Roles que pueden escribir este campo (vacío = todos, null = nadie excepto ADMIN) */
|
|
229
|
+
writeRoles?: string[];
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Metadata del campo
|
|
233
|
+
*/
|
|
234
|
+
interface FieldMeta {
|
|
235
|
+
sortable?: boolean;
|
|
236
|
+
searchable?: boolean;
|
|
237
|
+
exportable?: boolean;
|
|
238
|
+
auditField?: 'created_by' | 'updated_by';
|
|
239
|
+
/** Restricciones de acceso CASL para este campo */
|
|
240
|
+
casl?: FieldCaslAccess;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Definición completa de un campo
|
|
244
|
+
*
|
|
245
|
+
* Contiene toda la información necesaria para:
|
|
246
|
+
* - UI: formularios, listas
|
|
247
|
+
* - BD: migraciones Knex
|
|
248
|
+
* - Validación: schemas Zod
|
|
249
|
+
*/
|
|
250
|
+
interface FieldDefinition {
|
|
99
251
|
name: string;
|
|
100
252
|
label: string;
|
|
253
|
+
input: InputType;
|
|
254
|
+
placeholder?: string;
|
|
255
|
+
hint?: string;
|
|
256
|
+
hidden?: boolean;
|
|
257
|
+
disabled?: boolean;
|
|
258
|
+
db: FieldDbConfig;
|
|
259
|
+
relation?: FieldRelation;
|
|
260
|
+
validation?: FieldValidationConfig;
|
|
261
|
+
options?: FieldOptions;
|
|
262
|
+
meta?: FieldMeta;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Índice compuesto de tabla
|
|
266
|
+
*/
|
|
267
|
+
interface EntityIndex {
|
|
268
|
+
columns: string[];
|
|
269
|
+
unique?: boolean;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Acciones CASL estándar
|
|
273
|
+
*/
|
|
274
|
+
type CaslAction = 'manage' | 'create' | 'read' | 'update' | 'delete';
|
|
275
|
+
/**
|
|
276
|
+
* Condición de ownership para permisos
|
|
277
|
+
* El campo de la entidad que referencia al usuario
|
|
278
|
+
*/
|
|
279
|
+
interface OwnershipCondition {
|
|
280
|
+
/** Campo que contiene el ID del propietario (ej: 'author_id', 'created_by') */
|
|
281
|
+
field: string;
|
|
282
|
+
/** Variable del usuario a comparar (default: 'id') */
|
|
283
|
+
userField?: string;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Permiso CASL para un rol específico
|
|
287
|
+
*/
|
|
288
|
+
interface RolePermission {
|
|
289
|
+
/** Acciones permitidas */
|
|
290
|
+
actions: CaslAction[];
|
|
291
|
+
/** Condiciones (ownership, status, etc.) */
|
|
292
|
+
conditions?: Record<string, unknown>;
|
|
293
|
+
/** Campos permitidos (null = todos) */
|
|
294
|
+
fields?: string[] | null;
|
|
295
|
+
/** Invertir permiso (cannot en lugar de can) */
|
|
296
|
+
inverted?: boolean;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Configuración de autorización CASL para una entidad
|
|
300
|
+
*/
|
|
301
|
+
interface EntityCaslConfig {
|
|
302
|
+
/**
|
|
303
|
+
* Subject CASL (default: inferido de table → 'cms_posts' → 'CmsPost')
|
|
304
|
+
*/
|
|
305
|
+
subject?: string;
|
|
306
|
+
/**
|
|
307
|
+
* Campo de ownership para conditions automáticas
|
|
308
|
+
* Si se define, se genera condition { [field]: '${user.id}' }
|
|
309
|
+
*/
|
|
310
|
+
ownership?: OwnershipCondition;
|
|
311
|
+
/**
|
|
312
|
+
* Permisos por rol
|
|
313
|
+
* Las keys son nombres de roles (ADMIN, EDITOR, VIEWER, etc.)
|
|
314
|
+
*/
|
|
315
|
+
permissions?: Record<string, RolePermission>;
|
|
316
|
+
/**
|
|
317
|
+
* Campos sensibles que requieren permiso especial
|
|
318
|
+
* Se excluyen de 'read' para roles sin acceso explícito
|
|
319
|
+
*/
|
|
320
|
+
sensitiveFields?: string[];
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
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
|
|
331
|
+
*/
|
|
332
|
+
interface EntityDefinition {
|
|
333
|
+
table: string;
|
|
334
|
+
label: string;
|
|
335
|
+
labelField: string;
|
|
336
|
+
fields: Record<string, FieldDefinition>;
|
|
337
|
+
timestamps?: boolean;
|
|
338
|
+
audit?: boolean;
|
|
339
|
+
indexes?: EntityIndex[];
|
|
340
|
+
casl?: EntityCaslConfig;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Propiedades base compartidas por todas las entidades
|
|
344
|
+
*/
|
|
345
|
+
interface BaseEntity {
|
|
346
|
+
name: string;
|
|
347
|
+
label: string;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Entidad de colección - CRUD completo (users, posts, orders)
|
|
351
|
+
* Default cuando no se especifica type
|
|
352
|
+
*/
|
|
353
|
+
interface CollectionEntity extends BaseEntity {
|
|
354
|
+
type?: 'collection';
|
|
355
|
+
listFields: Record<string, string>;
|
|
356
|
+
formFields?: Record<string, FormField>;
|
|
357
|
+
labelField: string;
|
|
358
|
+
editMode?: 'modal' | 'page';
|
|
359
|
+
listType?: ListType;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Entidad de referencia - Catálogos con CRUD admin (countries, currencies)
|
|
363
|
+
*/
|
|
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;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Entidad temporal - CRUD con TTL (otp_codes, cache)
|
|
374
|
+
*/
|
|
375
|
+
interface TempEntity extends BaseEntity {
|
|
376
|
+
type: 'temp';
|
|
101
377
|
listFields: Record<string, string>;
|
|
102
378
|
formFields?: Record<string, FormField>;
|
|
103
379
|
labelField: string;
|
|
104
|
-
routePrefix?: string;
|
|
105
380
|
editMode?: 'modal' | 'page';
|
|
106
381
|
listType?: ListType;
|
|
107
382
|
}
|
|
383
|
+
/**
|
|
384
|
+
* Entidad singleton - Solo Update/Read, sin lista (site_config)
|
|
385
|
+
*/
|
|
386
|
+
interface SingleEntity extends BaseEntity {
|
|
387
|
+
type: 'single';
|
|
388
|
+
formFields?: Record<string, FormField>;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Entidad de configuración - Config por módulo, singleton
|
|
392
|
+
*/
|
|
393
|
+
interface ConfigEntity extends BaseEntity {
|
|
394
|
+
type: 'config';
|
|
395
|
+
formFields?: Record<string, FormField>;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Entidad externa - Read-only desde sistemas externos (stripe_customers)
|
|
399
|
+
*/
|
|
400
|
+
interface ExternalEntity extends BaseEntity {
|
|
401
|
+
type: 'external';
|
|
402
|
+
listFields: Record<string, string>;
|
|
403
|
+
labelField: string;
|
|
404
|
+
listType?: ListType;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Entidad virtual - Orquestación de múltiples fuentes (unified_customers)
|
|
408
|
+
*/
|
|
409
|
+
interface VirtualEntity extends BaseEntity {
|
|
410
|
+
type: 'virtual';
|
|
411
|
+
listFields: Record<string, string>;
|
|
412
|
+
labelField: string;
|
|
413
|
+
listType?: ListType;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Entidad computada - KPIs, estadísticas calculadas
|
|
417
|
+
*/
|
|
418
|
+
interface ComputedEntity extends BaseEntity {
|
|
419
|
+
type: 'computed';
|
|
420
|
+
listFields: Record<string, string>;
|
|
421
|
+
labelField: string;
|
|
422
|
+
listType?: ListType;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Entidad vista - Proyección optimizada para lectura
|
|
426
|
+
*/
|
|
427
|
+
interface ViewEntity extends BaseEntity {
|
|
428
|
+
type: 'view';
|
|
429
|
+
listFields: Record<string, string>;
|
|
430
|
+
labelField: string;
|
|
431
|
+
listType?: ListType;
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Entidad de eventos - Logs de auditoría, append-only
|
|
435
|
+
*/
|
|
436
|
+
interface EventEntity extends BaseEntity {
|
|
437
|
+
type: 'event';
|
|
438
|
+
listFields: Record<string, string>;
|
|
439
|
+
labelField: string;
|
|
440
|
+
listType?: ListType;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Entidad de acción - Comandos/operaciones sin persistencia
|
|
444
|
+
*/
|
|
445
|
+
interface ActionEntity extends BaseEntity {
|
|
446
|
+
type: 'action';
|
|
447
|
+
formFields?: Record<string, FormField>;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Unión discriminada de todos los tipos de entidad
|
|
451
|
+
*
|
|
452
|
+
* | Type | Persistencia | CRUD | Uso principal |
|
|
453
|
+
* |------------|--------------|-----------------|----------------------------------|
|
|
454
|
+
* | collection | Sí (BD) | Completo | Datos de negocio (users, posts) |
|
|
455
|
+
* | single | Sí (BD) | Update/Read | Config global (site_config) |
|
|
456
|
+
* | external | No | Read | Datos externos (stripe_customers)|
|
|
457
|
+
* | virtual | No | Read | Orquestación (unified_customers) |
|
|
458
|
+
* | computed | No/opcional | Read | KPIs, estadísticas |
|
|
459
|
+
* | view | Sí/virtual | Read | Lectura optimizada (projections) |
|
|
460
|
+
* | reference | Sí | Read (admin) | Catálogos (countries, currencies)|
|
|
461
|
+
* | config | Sí | Update/Read | Config por módulo |
|
|
462
|
+
* | event | Sí | Append | Auditoría (audit_logs) |
|
|
463
|
+
* | temp | No (TTL) | Read/Write | Cache, sesiones (otp_codes) |
|
|
464
|
+
* | action | No | Execute | Operaciones, workflows |
|
|
465
|
+
*/
|
|
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'];
|
|
108
471
|
/**
|
|
109
472
|
* Requisitos para activar un módulo
|
|
110
473
|
*/
|
|
@@ -130,7 +493,7 @@ interface ForbiddenErrorInstance {
|
|
|
130
493
|
* Constructor de ForbiddenError con método from()
|
|
131
494
|
*/
|
|
132
495
|
interface ForbiddenErrorConstructor {
|
|
133
|
-
from: (ability:
|
|
496
|
+
from: (ability: any) => ForbiddenErrorInstance;
|
|
134
497
|
}
|
|
135
498
|
/**
|
|
136
499
|
* Abilities CASL disponibles en el contexto
|
|
@@ -138,11 +501,9 @@ interface ForbiddenErrorConstructor {
|
|
|
138
501
|
*/
|
|
139
502
|
interface ModuleAbilities {
|
|
140
503
|
/** Wrapper para verificar permisos contra instancias */
|
|
141
|
-
subject: (type: string, object:
|
|
504
|
+
subject: (type: string, object: Record<string, any>) => unknown;
|
|
142
505
|
/** Error de CASL para throwUnlessCan */
|
|
143
506
|
ForbiddenError: ForbiddenErrorConstructor;
|
|
144
|
-
/** Otros métodos que el backend pueda añadir */
|
|
145
|
-
[key: string]: unknown;
|
|
146
507
|
}
|
|
147
508
|
/**
|
|
148
509
|
* AuthRequest pre-tipado para uso en plugins
|
|
@@ -206,6 +567,7 @@ interface ModuleContext {
|
|
|
206
567
|
createRouter: () => Router;
|
|
207
568
|
middleware: ModuleMiddlewares;
|
|
208
569
|
registerMiddleware: (name: string, handler: RequestHandler) => void;
|
|
570
|
+
/** Configuración resuelta de la aplicación */
|
|
209
571
|
config: Record<string, unknown>;
|
|
210
572
|
errors: {
|
|
211
573
|
AppError: new (message: string, statusCode?: number) => Error;
|
|
@@ -227,7 +589,7 @@ interface ModuleContext {
|
|
|
227
589
|
text?: string;
|
|
228
590
|
template?: string;
|
|
229
591
|
data?: Record<string, unknown>;
|
|
230
|
-
}) => Promise<
|
|
592
|
+
}) => Promise<unknown>;
|
|
231
593
|
};
|
|
232
594
|
/** Servicios de módulos core (users, etc.) */
|
|
233
595
|
services: CoreServices & Record<string, unknown>;
|
|
@@ -264,7 +626,16 @@ interface ModuleManifest {
|
|
|
264
626
|
routePrefix?: string;
|
|
265
627
|
/** Subjects CASL adicionales (sin entity). Los subjects de entities se infieren automáticamente */
|
|
266
628
|
additionalSubjects?: string[];
|
|
267
|
-
/**
|
|
629
|
+
/**
|
|
630
|
+
* Definiciones de entidades (nuevo sistema unificado).
|
|
631
|
+
* Single source of truth para BD, validación, UI y CASL.
|
|
632
|
+
* Sus subjects se registran automáticamente.
|
|
633
|
+
*/
|
|
634
|
+
definitions?: EntityDefinition[];
|
|
635
|
+
/**
|
|
636
|
+
* @deprecated Usar `definitions` en su lugar.
|
|
637
|
+
* Entidades/tablas del módulo con config CRUD para UI.
|
|
638
|
+
*/
|
|
268
639
|
entities?: ModuleEntity[];
|
|
269
640
|
}
|
|
270
641
|
/**
|
|
@@ -293,4 +664,4 @@ interface PluginManifest {
|
|
|
293
664
|
modules: ModuleManifest[];
|
|
294
665
|
}
|
|
295
666
|
|
|
296
|
-
export type
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
// src/generators.ts
|
|
2
|
+
function generateMigration(entity) {
|
|
3
|
+
const { table, fields, timestamps, audit, indexes } = entity;
|
|
4
|
+
const lines = [
|
|
5
|
+
`import type { ModuleContext, Knex } from '@gzl10/nexus-sdk'`,
|
|
6
|
+
``,
|
|
7
|
+
`export async function migrate(ctx: ModuleContext): Promise<void> {`,
|
|
8
|
+
` const { db, logger, helpers } = ctx`
|
|
9
|
+
];
|
|
10
|
+
if (timestamps) {
|
|
11
|
+
lines.push(` const { addTimestamps } = helpers`);
|
|
12
|
+
}
|
|
13
|
+
if (audit) {
|
|
14
|
+
lines.push(` const { addAuditFieldsIfMissing } = helpers`);
|
|
15
|
+
}
|
|
16
|
+
lines.push(``);
|
|
17
|
+
lines.push(` if (!(await db.schema.hasTable('${table}'))) {`);
|
|
18
|
+
lines.push(` await db.schema.createTable('${table}', (table: Knex.CreateTableBuilder) => {`);
|
|
19
|
+
for (const [name, field] of Object.entries(fields)) {
|
|
20
|
+
const columnCode = generateColumnCode(name, field);
|
|
21
|
+
lines.push(` ${columnCode}`);
|
|
22
|
+
}
|
|
23
|
+
if (timestamps) {
|
|
24
|
+
lines.push(` addTimestamps(table, db)`);
|
|
25
|
+
}
|
|
26
|
+
if (indexes?.length) {
|
|
27
|
+
lines.push(``);
|
|
28
|
+
for (const idx of indexes) {
|
|
29
|
+
if (idx.unique) {
|
|
30
|
+
lines.push(` table.unique([${idx.columns.map((c) => `'${c}'`).join(", ")}])`);
|
|
31
|
+
} else {
|
|
32
|
+
lines.push(` table.index([${idx.columns.map((c) => `'${c}'`).join(", ")}])`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
lines.push(` })`);
|
|
37
|
+
lines.push(` logger.info('Created table: ${table}')`);
|
|
38
|
+
lines.push(` }`);
|
|
39
|
+
if (audit) {
|
|
40
|
+
lines.push(``);
|
|
41
|
+
lines.push(` await addAuditFieldsIfMissing(db, '${table}')`);
|
|
42
|
+
}
|
|
43
|
+
lines.push(`}`);
|
|
44
|
+
lines.push(``);
|
|
45
|
+
return lines.join("\n");
|
|
46
|
+
}
|
|
47
|
+
function generateColumnCode(name, field) {
|
|
48
|
+
const { db, relation } = field;
|
|
49
|
+
let code = "";
|
|
50
|
+
switch (db.type) {
|
|
51
|
+
case "string":
|
|
52
|
+
code = db.size ? `table.string('${name}', ${db.size})` : `table.string('${name}')`;
|
|
53
|
+
break;
|
|
54
|
+
case "text":
|
|
55
|
+
code = `table.text('${name}')`;
|
|
56
|
+
break;
|
|
57
|
+
case "integer":
|
|
58
|
+
code = `table.integer('${name}')`;
|
|
59
|
+
break;
|
|
60
|
+
case "decimal":
|
|
61
|
+
if (db.precision) {
|
|
62
|
+
code = `table.decimal('${name}', ${db.precision[0]}, ${db.precision[1]})`;
|
|
63
|
+
} else {
|
|
64
|
+
code = `table.decimal('${name}')`;
|
|
65
|
+
}
|
|
66
|
+
break;
|
|
67
|
+
case "boolean":
|
|
68
|
+
code = `table.boolean('${name}')`;
|
|
69
|
+
break;
|
|
70
|
+
case "date":
|
|
71
|
+
code = `table.date('${name}')`;
|
|
72
|
+
break;
|
|
73
|
+
case "datetime":
|
|
74
|
+
code = `table.timestamp('${name}')`;
|
|
75
|
+
break;
|
|
76
|
+
case "json":
|
|
77
|
+
code = `table.json('${name}')`;
|
|
78
|
+
break;
|
|
79
|
+
case "uuid":
|
|
80
|
+
code = `table.uuid('${name}')`;
|
|
81
|
+
break;
|
|
82
|
+
default:
|
|
83
|
+
code = `table.string('${name}')`;
|
|
84
|
+
}
|
|
85
|
+
if (name === "id") {
|
|
86
|
+
code += `.primary()`;
|
|
87
|
+
}
|
|
88
|
+
if (db.nullable) {
|
|
89
|
+
code += `.nullable()`;
|
|
90
|
+
} else {
|
|
91
|
+
code += `.notNullable()`;
|
|
92
|
+
}
|
|
93
|
+
if (db.unique) {
|
|
94
|
+
code += `.unique()`;
|
|
95
|
+
}
|
|
96
|
+
if (db.default !== void 0) {
|
|
97
|
+
if (typeof db.default === "string") {
|
|
98
|
+
code += `.defaultTo('${db.default}')`;
|
|
99
|
+
} else {
|
|
100
|
+
code += `.defaultTo(${JSON.stringify(db.default)})`;
|
|
101
|
+
}
|
|
102
|
+
} else if (db.defaultFn === "now") {
|
|
103
|
+
code += `.defaultTo(db.fn.now())`;
|
|
104
|
+
} else if (db.defaultFn === "uuid") {
|
|
105
|
+
code += `.defaultTo(db.raw('gen_random_uuid()'))`;
|
|
106
|
+
}
|
|
107
|
+
if (relation) {
|
|
108
|
+
const col = relation.column ?? "id";
|
|
109
|
+
code += `.references('${col}').inTable('${relation.table}')`;
|
|
110
|
+
if (relation.onDelete) {
|
|
111
|
+
code += `.onDelete('${relation.onDelete}')`;
|
|
112
|
+
}
|
|
113
|
+
if (relation.onUpdate) {
|
|
114
|
+
code += `.onUpdate('${relation.onUpdate}')`;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (db.index && !db.unique && name !== "id") {
|
|
118
|
+
}
|
|
119
|
+
return code;
|
|
120
|
+
}
|
|
121
|
+
function generateZodSchema(entity) {
|
|
122
|
+
const { table, fields } = entity;
|
|
123
|
+
const entityName = tableToEntityName(table);
|
|
124
|
+
const lines = [
|
|
125
|
+
`import { z } from 'zod'`,
|
|
126
|
+
``
|
|
127
|
+
];
|
|
128
|
+
lines.push(`// === CREATE ===`);
|
|
129
|
+
lines.push(`export const create${entityName}Schema = z.object({`);
|
|
130
|
+
for (const [name, field] of Object.entries(fields)) {
|
|
131
|
+
if (name === "id" || field.meta?.auditField) continue;
|
|
132
|
+
const zodCode = generateZodFieldCode(field, "create");
|
|
133
|
+
if (zodCode) {
|
|
134
|
+
lines.push(` ${name}: ${zodCode},`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
lines.push(`})`);
|
|
138
|
+
lines.push(``);
|
|
139
|
+
lines.push(`// === UPDATE ===`);
|
|
140
|
+
lines.push(`export const update${entityName}Schema = z.object({`);
|
|
141
|
+
for (const [name, field] of Object.entries(fields)) {
|
|
142
|
+
if (name === "id" || field.meta?.auditField) continue;
|
|
143
|
+
const zodCode = generateZodFieldCode(field, "update");
|
|
144
|
+
if (zodCode) {
|
|
145
|
+
lines.push(` ${name}: ${zodCode},`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
lines.push(`})`);
|
|
149
|
+
lines.push(``);
|
|
150
|
+
lines.push(`// === PARAMS ===`);
|
|
151
|
+
lines.push(`export const ${entityName.toLowerCase()}ParamsSchema = z.object({`);
|
|
152
|
+
lines.push(` id: z.string(),`);
|
|
153
|
+
lines.push(`})`);
|
|
154
|
+
lines.push(``);
|
|
155
|
+
lines.push(`// === QUERY ===`);
|
|
156
|
+
lines.push(`export const ${entityName.toLowerCase()}QuerySchema = z.object({`);
|
|
157
|
+
lines.push(` page: z.coerce.number().int().min(1).default(1),`);
|
|
158
|
+
lines.push(` limit: z.coerce.number().int().min(1).max(100).default(20),`);
|
|
159
|
+
for (const [name, field] of Object.entries(fields)) {
|
|
160
|
+
if (field.meta?.searchable) {
|
|
161
|
+
const zodType = dbTypeToZodType(field.db.type);
|
|
162
|
+
lines.push(` ${name}: ${zodType}.optional(),`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
lines.push(`})`);
|
|
166
|
+
lines.push(``);
|
|
167
|
+
lines.push(`// === INFERRED TYPES ===`);
|
|
168
|
+
lines.push(`export type Create${entityName}Input = z.infer<typeof create${entityName}Schema>`);
|
|
169
|
+
lines.push(`export type Update${entityName}Input = z.infer<typeof update${entityName}Schema>`);
|
|
170
|
+
lines.push(`export type ${entityName}Params = z.infer<typeof ${entityName.toLowerCase()}ParamsSchema>`);
|
|
171
|
+
lines.push(`export type ${entityName}Query = z.infer<typeof ${entityName.toLowerCase()}QuerySchema>`);
|
|
172
|
+
lines.push(``);
|
|
173
|
+
return lines.join("\n");
|
|
174
|
+
}
|
|
175
|
+
function generateZodFieldCode(field, mode) {
|
|
176
|
+
const { db, validation } = field;
|
|
177
|
+
let code = dbTypeToZodType(db.type);
|
|
178
|
+
if (validation) {
|
|
179
|
+
if (validation.format === "email") {
|
|
180
|
+
code += `.email()`;
|
|
181
|
+
} else if (validation.format === "url") {
|
|
182
|
+
code += `.url()`;
|
|
183
|
+
} else if (validation.format === "uuid") {
|
|
184
|
+
code += `.uuid()`;
|
|
185
|
+
}
|
|
186
|
+
if (validation.min !== void 0) {
|
|
187
|
+
if (db.type === "string" || db.type === "text") {
|
|
188
|
+
code += `.min(${validation.min})`;
|
|
189
|
+
} else if (db.type === "integer" || db.type === "decimal") {
|
|
190
|
+
code += `.min(${validation.min})`;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (validation.max !== void 0) {
|
|
194
|
+
if (db.type === "string" || db.type === "text") {
|
|
195
|
+
code += `.max(${validation.max})`;
|
|
196
|
+
} else if (db.type === "integer" || db.type === "decimal") {
|
|
197
|
+
code += `.max(${validation.max})`;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (validation.pattern) {
|
|
201
|
+
code += `.regex(/${validation.pattern}/)`;
|
|
202
|
+
}
|
|
203
|
+
if (validation.enum?.length) {
|
|
204
|
+
code = `z.enum([${validation.enum.map((v) => `'${v}'`).join(", ")}])`;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (db.nullable) {
|
|
208
|
+
code += `.nullable()`;
|
|
209
|
+
}
|
|
210
|
+
if (mode === "update") {
|
|
211
|
+
code += `.optional()`;
|
|
212
|
+
} else if (mode === "create" && !validation?.required && db.default !== void 0) {
|
|
213
|
+
code += `.optional()`;
|
|
214
|
+
}
|
|
215
|
+
return code;
|
|
216
|
+
}
|
|
217
|
+
function dbTypeToZodType(dbType) {
|
|
218
|
+
switch (dbType) {
|
|
219
|
+
case "string":
|
|
220
|
+
case "text":
|
|
221
|
+
case "uuid":
|
|
222
|
+
return "z.string()";
|
|
223
|
+
case "integer":
|
|
224
|
+
return "z.number().int()";
|
|
225
|
+
case "decimal":
|
|
226
|
+
return "z.number()";
|
|
227
|
+
case "boolean":
|
|
228
|
+
return "z.boolean()";
|
|
229
|
+
case "date":
|
|
230
|
+
case "datetime":
|
|
231
|
+
return "z.string().datetime()";
|
|
232
|
+
case "json":
|
|
233
|
+
return "z.record(z.unknown())";
|
|
234
|
+
default:
|
|
235
|
+
return "z.string()";
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function generateModel(entity) {
|
|
239
|
+
const { table, fields, timestamps, audit } = entity;
|
|
240
|
+
const entityName = tableToEntityName(table);
|
|
241
|
+
const lines = [
|
|
242
|
+
`/**`,
|
|
243
|
+
` * ${entity.label}`,
|
|
244
|
+
` * Generated from EntityDefinition`,
|
|
245
|
+
` */`,
|
|
246
|
+
`export interface ${entityName} {`
|
|
247
|
+
];
|
|
248
|
+
for (const [name, field] of Object.entries(fields)) {
|
|
249
|
+
const tsType = dbTypeToTsType(field.db);
|
|
250
|
+
const optional = field.db.nullable ? "?" : "";
|
|
251
|
+
lines.push(` ${name}${optional}: ${tsType}`);
|
|
252
|
+
}
|
|
253
|
+
if (timestamps) {
|
|
254
|
+
lines.push(` created_at: Date`);
|
|
255
|
+
lines.push(` updated_at: Date`);
|
|
256
|
+
}
|
|
257
|
+
if (audit) {
|
|
258
|
+
lines.push(` created_by: string | null`);
|
|
259
|
+
lines.push(` updated_by: string | null`);
|
|
260
|
+
}
|
|
261
|
+
lines.push(`}`);
|
|
262
|
+
lines.push(``);
|
|
263
|
+
return lines.join("\n");
|
|
264
|
+
}
|
|
265
|
+
function dbTypeToTsType(db) {
|
|
266
|
+
switch (db.type) {
|
|
267
|
+
case "string":
|
|
268
|
+
case "text":
|
|
269
|
+
case "uuid":
|
|
270
|
+
return "string";
|
|
271
|
+
case "integer":
|
|
272
|
+
case "decimal":
|
|
273
|
+
return "number";
|
|
274
|
+
case "boolean":
|
|
275
|
+
return "boolean";
|
|
276
|
+
case "date":
|
|
277
|
+
case "datetime":
|
|
278
|
+
return "Date";
|
|
279
|
+
case "json":
|
|
280
|
+
return "Record<string, unknown>";
|
|
281
|
+
default:
|
|
282
|
+
return "unknown";
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function tableToEntityName(table) {
|
|
286
|
+
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);
|
|
289
|
+
}
|
|
290
|
+
function tableToSubject(table) {
|
|
291
|
+
const match = table.match(/^([a-z]{2,4})_(.+)$/);
|
|
292
|
+
if (!match) return tableToEntityName(table);
|
|
293
|
+
const [, prefix, rest] = match;
|
|
294
|
+
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;
|
|
298
|
+
}
|
|
299
|
+
function getFieldsForRole(entity, role, action) {
|
|
300
|
+
const { fields, casl } = entity;
|
|
301
|
+
const isReadAction = action === "read";
|
|
302
|
+
const isWriteAction = action === "create" || action === "update";
|
|
303
|
+
if (action === "manage") return null;
|
|
304
|
+
const allowedFields = [];
|
|
305
|
+
const sensitiveFields = casl?.sensitiveFields ?? [];
|
|
306
|
+
for (const [fieldName, field] of Object.entries(fields)) {
|
|
307
|
+
const fieldCasl = field.meta?.casl;
|
|
308
|
+
if (sensitiveFields.includes(fieldName) && role !== "ADMIN") {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (isReadAction) {
|
|
312
|
+
if (fieldCasl?.readRoles !== void 0) {
|
|
313
|
+
if (fieldCasl.readRoles.length === 0 || fieldCasl.readRoles.includes(role)) {
|
|
314
|
+
allowedFields.push(fieldName);
|
|
315
|
+
}
|
|
316
|
+
} else {
|
|
317
|
+
allowedFields.push(fieldName);
|
|
318
|
+
}
|
|
319
|
+
} else if (isWriteAction) {
|
|
320
|
+
if (fieldCasl?.writeRoles !== void 0) {
|
|
321
|
+
if (fieldCasl.writeRoles.length === 0 || fieldCasl.writeRoles.includes(role)) {
|
|
322
|
+
allowedFields.push(fieldName);
|
|
323
|
+
}
|
|
324
|
+
} else {
|
|
325
|
+
allowedFields.push(fieldName);
|
|
326
|
+
}
|
|
327
|
+
} else {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const allFieldNames = Object.keys(fields).filter((f) => !sensitiveFields.includes(f) || role === "ADMIN");
|
|
332
|
+
if (allowedFields.length === allFieldNames.length) {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
return allowedFields.length > 0 ? allowedFields : null;
|
|
336
|
+
}
|
|
337
|
+
function generateCaslPermissions(entity) {
|
|
338
|
+
const { table, fields, casl } = entity;
|
|
339
|
+
if (!casl) return [];
|
|
340
|
+
const subject = casl.subject ?? tableToSubject(table);
|
|
341
|
+
const permissions = [];
|
|
342
|
+
for (const [role, config] of Object.entries(casl.permissions ?? {})) {
|
|
343
|
+
for (const action of config.actions) {
|
|
344
|
+
let conditions = config.conditions ?? null;
|
|
345
|
+
if (casl.ownership && (action === "update" || action === "delete") && !conditions) {
|
|
346
|
+
const userField = casl.ownership.userField ?? "id";
|
|
347
|
+
conditions = { [casl.ownership.field]: `\${user.${userField}}` };
|
|
348
|
+
}
|
|
349
|
+
let permFields = config.fields ?? null;
|
|
350
|
+
if (!permFields) {
|
|
351
|
+
permFields = getFieldsForRole(entity, role, action);
|
|
352
|
+
}
|
|
353
|
+
permissions.push({
|
|
354
|
+
role,
|
|
355
|
+
action,
|
|
356
|
+
subject,
|
|
357
|
+
conditions: conditions ? JSON.stringify(conditions) : null,
|
|
358
|
+
fields: permFields ? JSON.stringify(permFields) : null,
|
|
359
|
+
inverted: config.inverted ?? false
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return permissions;
|
|
364
|
+
}
|
|
365
|
+
function generateCaslSeed(entities) {
|
|
366
|
+
const allPermissions = [];
|
|
367
|
+
for (const entity of entities) {
|
|
368
|
+
allPermissions.push(...generateCaslPermissions(entity));
|
|
369
|
+
}
|
|
370
|
+
if (allPermissions.length === 0) {
|
|
371
|
+
return "// No CASL permissions defined in entities\n";
|
|
372
|
+
}
|
|
373
|
+
const lines = [
|
|
374
|
+
`import type { ModuleContext } from '@gzl10/nexus-sdk'`,
|
|
375
|
+
``,
|
|
376
|
+
`/**`,
|
|
377
|
+
` * Seed de permisos CASL generado desde EntityDefinition`,
|
|
378
|
+
` */`,
|
|
379
|
+
`export async function seedPermissions(ctx: ModuleContext): Promise<void> {`,
|
|
380
|
+
` const { db, generateId, logger } = ctx`,
|
|
381
|
+
``,
|
|
382
|
+
` // Obtener IDs de roles`,
|
|
383
|
+
` const roles = await db('rol_roles').select('id', 'name')`,
|
|
384
|
+
` const roleMap = Object.fromEntries(roles.map((r: { id: string; name: string }) => [r.name, r.id]))`,
|
|
385
|
+
``,
|
|
386
|
+
` const permissions = [`
|
|
387
|
+
];
|
|
388
|
+
for (const perm of allPermissions) {
|
|
389
|
+
lines.push(` {`);
|
|
390
|
+
lines.push(` role: '${perm.role}',`);
|
|
391
|
+
lines.push(` action: '${perm.action}',`);
|
|
392
|
+
lines.push(` subject: '${perm.subject}',`);
|
|
393
|
+
lines.push(` conditions: ${perm.conditions ? `'${perm.conditions}'` : "null"},`);
|
|
394
|
+
lines.push(` fields: ${perm.fields ? `'${perm.fields}'` : "null"},`);
|
|
395
|
+
lines.push(` inverted: ${perm.inverted}`);
|
|
396
|
+
lines.push(` },`);
|
|
397
|
+
}
|
|
398
|
+
lines.push(` ]`);
|
|
399
|
+
lines.push(``);
|
|
400
|
+
lines.push(` for (const perm of permissions) {`);
|
|
401
|
+
lines.push(` const roleId = roleMap[perm.role]`);
|
|
402
|
+
lines.push(` if (!roleId) {`);
|
|
403
|
+
lines.push(` logger.warn({ role: perm.role }, 'Role not found, skipping permission')`);
|
|
404
|
+
lines.push(` continue`);
|
|
405
|
+
lines.push(` }`);
|
|
406
|
+
lines.push(``);
|
|
407
|
+
lines.push(` // Check if permission exists`);
|
|
408
|
+
lines.push(` const existing = await db('rol_role_permissions')`);
|
|
409
|
+
lines.push(` .where({ role_id: roleId, action: perm.action, subject: perm.subject })`);
|
|
410
|
+
lines.push(` .first()`);
|
|
411
|
+
lines.push(``);
|
|
412
|
+
lines.push(` if (!existing) {`);
|
|
413
|
+
lines.push(` await db('rol_role_permissions').insert({`);
|
|
414
|
+
lines.push(` id: generateId(),`);
|
|
415
|
+
lines.push(` role_id: roleId,`);
|
|
416
|
+
lines.push(` action: perm.action,`);
|
|
417
|
+
lines.push(` subject: perm.subject,`);
|
|
418
|
+
lines.push(` conditions: perm.conditions,`);
|
|
419
|
+
lines.push(` fields: perm.fields,`);
|
|
420
|
+
lines.push(` inverted: perm.inverted`);
|
|
421
|
+
lines.push(` })`);
|
|
422
|
+
lines.push(` }`);
|
|
423
|
+
lines.push(` }`);
|
|
424
|
+
lines.push(``);
|
|
425
|
+
lines.push(` logger.info('Seeded CASL permissions from EntityDefinitions')`);
|
|
426
|
+
lines.push(`}`);
|
|
427
|
+
lines.push(``);
|
|
428
|
+
return lines.join("\n");
|
|
429
|
+
}
|
|
430
|
+
function getEntitySubject(entity) {
|
|
431
|
+
return entity.casl?.subject ?? tableToSubject(entity.table);
|
|
432
|
+
}
|
|
433
|
+
export {
|
|
434
|
+
generateCaslPermissions,
|
|
435
|
+
generateCaslSeed,
|
|
436
|
+
generateMigration,
|
|
437
|
+
generateModel,
|
|
438
|
+
generateZodSchema,
|
|
439
|
+
getEntitySubject
|
|
440
|
+
};
|