@gzl10/nexus-sdk 0.3.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/dist/index.d.ts +68 -10
- package/dist/index.js +223 -8
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -12,10 +12,26 @@ import { Logger } from 'pino';
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* Entidades que persisten en BD local
|
|
16
|
-
* Excluye: action, external, virtual, computed
|
|
15
|
+
* Entidades que persisten en BD local con tabla propia
|
|
16
|
+
* Excluye: action, external, virtual, computed, single (usa tabla compartida)
|
|
17
17
|
*/
|
|
18
|
-
type PersistentEntityDefinition = CollectionEntityDefinition |
|
|
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;
|
|
19
35
|
/**
|
|
20
36
|
* Genera código de migración Knex desde EntityDefinition
|
|
21
37
|
*
|
|
@@ -51,6 +67,15 @@ declare function generateZodSchema(entity: PersistentEntityDefinition): string;
|
|
|
51
67
|
* // }
|
|
52
68
|
*/
|
|
53
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;
|
|
54
79
|
/**
|
|
55
80
|
* Estructura de permiso generado para insertar en BD
|
|
56
81
|
*/
|
|
@@ -82,6 +107,29 @@ declare function generateCaslSeed(entities: EntityDefinition[]): string;
|
|
|
82
107
|
* Obtiene el subject CASL de una entidad
|
|
83
108
|
*/
|
|
84
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;
|
|
85
133
|
|
|
86
134
|
/**
|
|
87
135
|
* @gzl10/nexus-sdk
|
|
@@ -370,14 +418,24 @@ interface CollectionEntityDefinition extends BaseEntityDefinition {
|
|
|
370
418
|
indexes?: EntityIndex[];
|
|
371
419
|
}
|
|
372
420
|
/**
|
|
373
|
-
* Entidad singleton -
|
|
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: '...' } }
|
|
374
428
|
*/
|
|
375
|
-
interface SingleEntityDefinition
|
|
429
|
+
interface SingleEntityDefinition {
|
|
376
430
|
type: 'single';
|
|
377
|
-
/**
|
|
378
|
-
|
|
379
|
-
/**
|
|
380
|
-
|
|
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;
|
|
381
439
|
}
|
|
382
440
|
/**
|
|
383
441
|
* Entidad de referencia - Catálogos con CRUD admin (countries, currencies)
|
|
@@ -723,4 +781,4 @@ interface PluginManifest {
|
|
|
723
781
|
modules: ModuleManifest[];
|
|
724
782
|
}
|
|
725
783
|
|
|
726
|
-
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 OwnershipCondition, type PaginatedResult, type PaginationParams, 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, 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,7 +1,13 @@
|
|
|
1
1
|
// src/generators.ts
|
|
2
2
|
function isPersistentEntity(entity) {
|
|
3
|
-
const
|
|
4
|
-
return !
|
|
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";
|
|
5
11
|
}
|
|
6
12
|
function generateMigration(entity) {
|
|
7
13
|
const { table, fields } = entity;
|
|
@@ -291,19 +297,81 @@ function dbTypeToTsType(db) {
|
|
|
291
297
|
return "unknown";
|
|
292
298
|
}
|
|
293
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
|
+
}
|
|
294
308
|
function tableToEntityName(table) {
|
|
295
309
|
const withoutPrefix = table.replace(/^[a-z]{2,4}_/, "");
|
|
296
|
-
|
|
297
|
-
return singular.charAt(0).toUpperCase() + singular.slice(1);
|
|
310
|
+
return toPascalCase(toSingular(withoutPrefix));
|
|
298
311
|
}
|
|
299
312
|
function tableToSubject(table) {
|
|
300
313
|
const match = table.match(/^([a-z]{2,4})_(.+)$/);
|
|
301
314
|
if (!match) return tableToEntityName(table);
|
|
302
315
|
const [, prefix, rest] = match;
|
|
303
316
|
const prefixPascal = prefix.charAt(0).toUpperCase() + prefix.slice(1);
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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");
|
|
307
375
|
}
|
|
308
376
|
function getFieldsForRole(entity, role, action) {
|
|
309
377
|
const { fields, casl } = entity;
|
|
@@ -441,11 +509,158 @@ function generateCaslSeed(entities) {
|
|
|
441
509
|
function getEntitySubject(entity) {
|
|
442
510
|
return entity.casl?.subject ?? tableToSubject(entity.table);
|
|
443
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
|
+
}
|
|
444
651
|
export {
|
|
445
652
|
generateCaslPermissions,
|
|
446
653
|
generateCaslSeed,
|
|
447
654
|
generateMigration,
|
|
448
655
|
generateModel,
|
|
656
|
+
generateModelsFile,
|
|
657
|
+
generateReadOnlyModel,
|
|
658
|
+
generateReadOnlySchema,
|
|
659
|
+
generateSchemasFile,
|
|
449
660
|
generateZodSchema,
|
|
450
|
-
|
|
661
|
+
getEntityName,
|
|
662
|
+
getEntitySubject,
|
|
663
|
+
hasTable,
|
|
664
|
+
isPersistentEntity,
|
|
665
|
+
isSingletonEntity
|
|
451
666
|
};
|