@pattern-stack/codegen 0.6.7 → 0.7.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/CHANGELOG.md +12 -0
- package/dist/src/cli/index.js +516 -73
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/index.d.ts +208 -1
- package/dist/src/index.js +147 -0
- package/dist/src/index.js.map +1 -1
- package/package.json +1 -1
- package/src/patterns/library/base-junction-fields.ts +32 -0
- package/src/patterns/library/index.ts +7 -0
- package/src/patterns/library/junction.pattern.ts +41 -0
- package/templates/entity/new/backend/application/queries/get-by-id.ejs.t +3 -3
- package/templates/entity/new/backend/application/queries/grouped-index.ejs.t +5 -5
- package/templates/entity/new/backend/application/queries/index.ejs.t +3 -0
- package/templates/entity/new/backend/application/queries/list.ejs.t +3 -3
- package/templates/entity/new/backend/application/queries/relationships.queries.ejs.t +147 -0
- package/templates/entity/new/backend/database/repository.ejs.t +36 -176
- package/templates/entity/new/backend/domain/entity.ejs.t +0 -44
- package/templates/entity/new/backend/domain/grouped-index.ejs.t +4 -60
- package/templates/entity/new/backend/domain/index.ejs.t +2 -2
- package/templates/entity/new/backend/domain/repository-interface.ejs.t +16 -17
- package/templates/entity/new/backend/modules/core/module.ejs.t +10 -0
- package/templates/entity/new/backend/presentation/controller.ejs.t +2 -34
- package/templates/entity/new/clean-lite-ps/entity.ejs.t +21 -2
- package/templates/entity/new/clean-lite-ps/module.ejs.t +27 -2
- package/templates/entity/new/clean-lite-ps/prompt-extension.js +108 -9
- package/templates/entity/new/clean-lite-ps/repository.ejs.t +33 -1
- package/templates/entity/new/clean-lite-ps/service.ejs.t +79 -0
- package/templates/entity/new/prompt.js +1 -0
- package/templates/junction/new/_inject-parent-module-clp-left.ejs.t +8 -0
- package/templates/junction/new/_inject-parent-module-clp-right.ejs.t +8 -0
- package/templates/junction/new/_inject-parent-module-import-clp-left.ejs.t +9 -0
- package/templates/junction/new/_inject-parent-module-import-clp-right.ejs.t +9 -0
- package/templates/junction/new/_inject-parent-service-clp-left.ejs.t +51 -0
- package/templates/junction/new/_inject-parent-service-clp-right.ejs.t +48 -0
- package/templates/junction/new/_inject-parent-service-import-clp-left.ejs.t +11 -0
- package/templates/junction/new/_inject-parent-service-import-clp-right.ejs.t +11 -0
- package/templates/junction/new/entity.ejs.t +111 -0
- package/templates/junction/new/index.ejs.t +15 -0
- package/templates/junction/new/module.ejs.t +37 -0
- package/templates/junction/new/prompt.js +492 -0
- package/templates/junction/new/repository.ejs.t +67 -0
- package/templates/junction/new/service.ejs.t +174 -0
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* all variables required by the clean-lite-ps template set.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
8
10
|
import pluralizePkg from 'pluralize';
|
|
9
11
|
// The patterns barrel has the side effect of pre-registering the five
|
|
10
12
|
// library-shipped patterns (Base / Synced / Activity / Knowledge /
|
|
@@ -201,7 +203,7 @@ const EXTERNAL_ID_TRACKING_FIELDS = new Set([
|
|
|
201
203
|
/**
|
|
202
204
|
* Build a Drizzle column chain for a field
|
|
203
205
|
*/
|
|
204
|
-
function buildDrizzleChain(fieldName, field, drizzleType) {
|
|
206
|
+
function buildDrizzleChain(fieldName, field, drizzleType, enumName) {
|
|
205
207
|
const nullable = field.nullable ?? false;
|
|
206
208
|
const required = field.required ?? false;
|
|
207
209
|
const hasDefault = field.default !== undefined && field.default !== null;
|
|
@@ -211,7 +213,11 @@ function buildDrizzleChain(fieldName, field, drizzleType) {
|
|
|
211
213
|
// schemas using z.coerce.date() align with the entity type.
|
|
212
214
|
// `timestamp` already defaults to Date — no mode override needed.
|
|
213
215
|
let chain;
|
|
214
|
-
if (drizzleType === '
|
|
216
|
+
if (drizzleType === 'enum' && enumName) {
|
|
217
|
+
// Reference the pgEnum declaration emitted at the top of the entity file.
|
|
218
|
+
// The column name argument keeps the snake_case YAML field name.
|
|
219
|
+
chain = `${enumName}('${fieldName}')`;
|
|
220
|
+
} else if (drizzleType === 'date') {
|
|
215
221
|
chain = `${drizzleType}('${fieldName}', { mode: 'date' })`;
|
|
216
222
|
} else {
|
|
217
223
|
chain = `${drizzleType}('${fieldName}')`;
|
|
@@ -247,7 +253,15 @@ function processFields(fields) {
|
|
|
247
253
|
const choices = field.choices;
|
|
248
254
|
const hasChoices = Array.isArray(choices) && choices.length > 0;
|
|
249
255
|
|
|
250
|
-
|
|
256
|
+
// Enum-typed fields (or any field with a `choices` list) emit a
|
|
257
|
+
// Postgres-native pgEnum declaration + column reference, so the
|
|
258
|
+
// generated `InferSelectModel` type narrows to the literal union
|
|
259
|
+
// instead of falling back to `string`. Matches the backend pipeline
|
|
260
|
+
// (templates/entity/new/backend/database/schema.ejs.t:66-104).
|
|
261
|
+
const drizzleType = hasChoices
|
|
262
|
+
? 'enum'
|
|
263
|
+
: (DRIZZLE_TYPE_MAP[type] || 'text');
|
|
264
|
+
const enumName = hasChoices ? camelCase(fieldName) + 'Enum' : null;
|
|
251
265
|
const tsType = hasChoices
|
|
252
266
|
? choices.map((c) => `'${c}'`).join(' | ')
|
|
253
267
|
: (TS_TYPE_MAP[type] || 'unknown');
|
|
@@ -255,7 +269,7 @@ function processFields(fields) {
|
|
|
255
269
|
? `z.enum([${choices.map((c) => `'${c}'`).join(', ')}])`
|
|
256
270
|
: (ZOD_TYPE_MAP[type] || 'z.unknown()');
|
|
257
271
|
|
|
258
|
-
const drizzleChain = buildDrizzleChain(fieldName, field, drizzleType);
|
|
272
|
+
const drizzleChain = buildDrizzleChain(fieldName, field, drizzleType, enumName);
|
|
259
273
|
|
|
260
274
|
processed.push({
|
|
261
275
|
name: fieldName,
|
|
@@ -271,6 +285,7 @@ function processFields(fields) {
|
|
|
271
285
|
drizzleChain,
|
|
272
286
|
choices,
|
|
273
287
|
hasChoices,
|
|
288
|
+
enumName,
|
|
274
289
|
});
|
|
275
290
|
}
|
|
276
291
|
|
|
@@ -293,6 +308,57 @@ function mapOnDelete(onDelete) {
|
|
|
293
308
|
return map[onDelete] ?? 'restrict';
|
|
294
309
|
}
|
|
295
310
|
|
|
311
|
+
/**
|
|
312
|
+
* Process has_many relationships into HasManyRelation[].
|
|
313
|
+
*
|
|
314
|
+
* Mirrors processBelongsTo. The `foreign_key` declared on a has_many
|
|
315
|
+
* relationship is the inverse FK living on the *target* entity's table —
|
|
316
|
+
* e.g. `account.relationships.contacts: { foreign_key: account_id }` means
|
|
317
|
+
* contacts.account_id. The method name on AccountRepository would be
|
|
318
|
+
* `findByAccountId`.
|
|
319
|
+
*/
|
|
320
|
+
function processHasMany(relationships, parentEntityNamePlural, fs, path, srcRoot) {
|
|
321
|
+
if (!relationships) return [];
|
|
322
|
+
|
|
323
|
+
const result = [];
|
|
324
|
+
|
|
325
|
+
for (const [relName, rel] of Object.entries(relationships)) {
|
|
326
|
+
if (rel.type !== 'has_many') continue;
|
|
327
|
+
|
|
328
|
+
const target = rel.target;
|
|
329
|
+
const inverseForeignKey = rel.foreign_key;
|
|
330
|
+
const targetPlural = pluralize(target);
|
|
331
|
+
const isSelfRef = targetPlural === parentEntityNamePlural;
|
|
332
|
+
|
|
333
|
+
// Check whether the target entity has already been generated.
|
|
334
|
+
// Only include targets that exist so the import block doesn't
|
|
335
|
+
// reference files that aren't on disk yet (two-pass generation).
|
|
336
|
+
let targetExists = false;
|
|
337
|
+
if (fs && path && srcRoot) {
|
|
338
|
+
const nestedPath = path.resolve(srcRoot, 'modules', targetPlural, `${target}.entity.ts`);
|
|
339
|
+
const flatPath = path.resolve(srcRoot, 'modules', `${target}.entity.ts`);
|
|
340
|
+
targetExists = fs.existsSync(nestedPath) || fs.existsSync(flatPath) || isSelfRef;
|
|
341
|
+
} else {
|
|
342
|
+
targetExists = isSelfRef;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
result.push({
|
|
346
|
+
name: relName,
|
|
347
|
+
target,
|
|
348
|
+
targetClass: pascalCase(target),
|
|
349
|
+
targetPlural,
|
|
350
|
+
inverseForeignKey,
|
|
351
|
+
inverseForeignKeyCamel: camelCase(inverseForeignKey),
|
|
352
|
+
inverseForeignKeyPascal: pascalCase(inverseForeignKey),
|
|
353
|
+
isSelfRef,
|
|
354
|
+
targetExists,
|
|
355
|
+
importPath: `../${targetPlural}/${target}.repository`,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return result;
|
|
360
|
+
}
|
|
361
|
+
|
|
296
362
|
/**
|
|
297
363
|
* Process belongs_to relationships into BelongsToRelation[]
|
|
298
364
|
*/
|
|
@@ -351,10 +417,16 @@ function processBelongsTo(relationships, parentEntityNamePlural) {
|
|
|
351
417
|
/**
|
|
352
418
|
* Collect drizzle imports needed for entity fields
|
|
353
419
|
*/
|
|
354
|
-
function collectDrizzleImports(processedFields, belongsTo, hasTimestamps, hasSoftDelete, hasExternalIdTracking) {
|
|
420
|
+
function collectDrizzleImports(processedFields, belongsTo, hasTimestamps, hasSoftDelete, hasExternalIdTracking, hasMany = []) {
|
|
355
421
|
const imports = new Set(['pgTable', 'uuid']);
|
|
356
422
|
|
|
357
423
|
for (const field of processedFields) {
|
|
424
|
+
if (field.drizzleType === 'enum') {
|
|
425
|
+
// Enum columns reference a `pgEnum` declaration emitted at the top
|
|
426
|
+
// of the entity file; the helper itself comes from drizzle-orm/pg-core.
|
|
427
|
+
imports.add('pgEnum');
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
358
430
|
const importName = DRIZZLE_IMPORT_MAP[field.drizzleType];
|
|
359
431
|
if (importName) imports.add(importName);
|
|
360
432
|
}
|
|
@@ -375,7 +447,7 @@ function collectDrizzleImports(processedFields, belongsTo, hasTimestamps, hasSof
|
|
|
375
447
|
imports.add('jsonb');
|
|
376
448
|
}
|
|
377
449
|
|
|
378
|
-
if (belongsTo.length > 0) {
|
|
450
|
+
if (belongsTo.length > 0 || hasMany.length > 0) {
|
|
379
451
|
imports.add('relations');
|
|
380
452
|
}
|
|
381
453
|
|
|
@@ -750,6 +822,9 @@ export function buildCleanLitePsLocals(definition, baseLocals) {
|
|
|
750
822
|
// Process belongs_to relationships
|
|
751
823
|
const belongsTo = processBelongsTo(relationships, entityNamePlural);
|
|
752
824
|
|
|
825
|
+
// Process has_many relationships (CGP-358b)
|
|
826
|
+
const hasMany = processHasMany(relationships, entityNamePlural, fs, path, srcRoot);
|
|
827
|
+
|
|
753
828
|
// Issue #41 — warn when a soft-delete entity declares non-restrict on_delete on any
|
|
754
829
|
// belongs_to relation. The FK constraint applies to hard-delete only;
|
|
755
830
|
// developers expecting soft-delete cascade must use activeParentFilter() instead.
|
|
@@ -783,10 +858,22 @@ export function buildCleanLitePsLocals(definition, baseLocals) {
|
|
|
783
858
|
const fkFieldNames = new Set(belongsTo.map((r) => r.field));
|
|
784
859
|
const nonFkFields = processedFields.filter((f) => !fkFieldNames.has(f.name));
|
|
785
860
|
|
|
861
|
+
// Enum field declarations — surface a separate collection so the entity
|
|
862
|
+
// template can emit `export const xEnum = pgEnum('x', [...])` ahead of
|
|
863
|
+
// the `pgTable(...)` block. Both FK-filtered and unfiltered processing
|
|
864
|
+
// include the same enum fields; they're never FKs.
|
|
865
|
+
const clpEnumFields = processedFields
|
|
866
|
+
.filter((f) => f.hasChoices && f.enumName)
|
|
867
|
+
.map((f) => ({
|
|
868
|
+
enumName: f.enumName,
|
|
869
|
+
dbName: f.name,
|
|
870
|
+
choices: f.choices,
|
|
871
|
+
}));
|
|
872
|
+
|
|
786
873
|
// Drizzle imports needed
|
|
787
|
-
const drizzleEntityImports = collectDrizzleImports(processedFields, belongsTo, hasTimestamps, hasSoftDelete, hasExternalIdTracking);
|
|
788
|
-
// Whether relations() import is needed
|
|
789
|
-
const hasRelationsBlock = belongsTo.length > 0;
|
|
874
|
+
const drizzleEntityImports = collectDrizzleImports(processedFields, belongsTo, hasTimestamps, hasSoftDelete, hasExternalIdTracking, hasMany);
|
|
875
|
+
// Whether relations() import is needed (CGP-358b: also trigger on has_many)
|
|
876
|
+
const hasRelationsBlock = belongsTo.length > 0 || hasMany.length > 0;
|
|
790
877
|
|
|
791
878
|
// Output paths
|
|
792
879
|
const outputPaths = {
|
|
@@ -980,6 +1067,13 @@ export function buildCleanLitePsLocals(definition, baseLocals) {
|
|
|
980
1067
|
// Drizzle
|
|
981
1068
|
clpDrizzleImports: drizzleEntityImports,
|
|
982
1069
|
clpHasRelationsBlock: hasRelationsBlock,
|
|
1070
|
+
// A self-referential belongs_to FK requires the `references()` callback
|
|
1071
|
+
// to carry a `: AnyPgColumn` return-type annotation; otherwise TypeScript's
|
|
1072
|
+
// strict mode flags the table const with TS7022/TS7024 (circular initializer).
|
|
1073
|
+
// Surfaced by the cgp-62 relationship-scenario smoke when generating a CRM
|
|
1074
|
+
// account with a `parent_account_id` self-FK.
|
|
1075
|
+
clpHasSelfFk: belongsTo.some((rel) => rel.isSelfFk),
|
|
1076
|
+
clpEnumFields,
|
|
983
1077
|
|
|
984
1078
|
// Declarative queries
|
|
985
1079
|
processedQueries,
|
|
@@ -988,5 +1082,10 @@ export function buildCleanLitePsLocals(definition, baseLocals) {
|
|
|
988
1082
|
hasMultiFieldQuery,
|
|
989
1083
|
hasOrderedQuery,
|
|
990
1084
|
hasViaQuery,
|
|
1085
|
+
|
|
1086
|
+
// CGP-358b: has_many relationships for service-layer composition
|
|
1087
|
+
clpHasMany: hasMany,
|
|
1088
|
+
clpHasManyRelations: hasMany.length > 0,
|
|
1089
|
+
clpExistingHasMany: hasMany.filter((r) => r.targetExists),
|
|
991
1090
|
};
|
|
992
1091
|
}
|
|
@@ -4,7 +4,17 @@ skip_if: "<%= typeof clpOutputPaths === 'undefined' %>"
|
|
|
4
4
|
force: true
|
|
5
5
|
---
|
|
6
6
|
import { Injectable, Inject } from '@nestjs/common';
|
|
7
|
-
<%
|
|
7
|
+
<%_
|
|
8
|
+
// CGP-358: FK methods with opts take priority over same-named declarative query impl.
|
|
9
|
+
// Always emit FK methods; skip declarative body when FK covers same name.
|
|
10
|
+
const _fkMethods = (typeof clpBelongsTo !== 'undefined') ? clpBelongsTo : [];
|
|
11
|
+
const _fkMethodNamesCLP = new Set(_fkMethods.map(rel => {
|
|
12
|
+
const _p = rel.camelField.charAt(0).toUpperCase() + rel.camelField.slice(1);
|
|
13
|
+
return `findBy${_p}`;
|
|
14
|
+
}));
|
|
15
|
+
const _needsEq = hasDeclarativeQueries || _fkMethods.length > 0;
|
|
16
|
+
_%>
|
|
17
|
+
<% if (_needsEq) { -%>
|
|
8
18
|
import { eq<%= hasMultiFieldQuery ? ', and' : '' %><%= hasOrderedQuery ? ', desc, asc' : '' %> } from 'drizzle-orm';
|
|
9
19
|
<% } -%>
|
|
10
20
|
<% if (eavValueTable) { -%>
|
|
@@ -48,6 +58,12 @@ export class <%= classNames.repository %> extends <%= repositoryBaseClass %><<%=
|
|
|
48
58
|
// Declarative queries (from queries: block in entity YAML)
|
|
49
59
|
// ═══════════════════════════════════════════════════════════════════════
|
|
50
60
|
<%_ processedQueries.forEach((q) => { _%>
|
|
61
|
+
<%_
|
|
62
|
+
// CGP-358: Skip declarative impl when a FK method covers this method name.
|
|
63
|
+
// FK methods accept opts, making them a superset of a plain non-unique single-param query.
|
|
64
|
+
const _skipClpDq = _fkMethodNamesCLP.has(q.methodName) && !q.isUnique && !q.hasVia && !q.hasSelect;
|
|
65
|
+
_%>
|
|
66
|
+
<%_ if (!_skipClpDq) { _%>
|
|
51
67
|
|
|
52
68
|
async <%= q.methodName %>(<%- q.params.map(p => `${p.camelName}: ${p.tsType}`).join(', ') %>): Promise<<%- q.returnType %>> {
|
|
53
69
|
<% if (q.isUnique) { -%>
|
|
@@ -61,11 +77,27 @@ export class <%= classNames.repository %> extends <%= repositoryBaseClass %><<%=
|
|
|
61
77
|
return rows as <%= classNames.entity %>[];
|
|
62
78
|
<% } -%>
|
|
63
79
|
}
|
|
80
|
+
<%_ } _%>
|
|
64
81
|
<%_ }) _%>
|
|
65
82
|
<% } else { -%>
|
|
66
83
|
|
|
67
84
|
// TODO: Add entity-specific query methods here.
|
|
68
85
|
<% } -%>
|
|
86
|
+
<%_ if (_fkMethods.length > 0) { _%>
|
|
87
|
+
|
|
88
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
89
|
+
// FK traversal methods (from belongs_to relationships — CGP-358b)
|
|
90
|
+
// Called by service-layer composition methods on the inverse (has_many) side.
|
|
91
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
92
|
+
<%_ _fkMethods.forEach(rel => { _%>
|
|
93
|
+
|
|
94
|
+
async findBy<%= rel.camelField.charAt(0).toUpperCase() + rel.camelField.slice(1) %>(id: string, opts?: { cursor?: string; limit?: number }): Promise<<%= classNames.entity %>[]> {
|
|
95
|
+
let q = this.baseQuery().where(eq(this.table['<%= rel.camelField %>'], id));
|
|
96
|
+
if (opts?.limit) q = (q as any).limit(opts.limit);
|
|
97
|
+
return (await q) as <%= classNames.entity %>[];
|
|
98
|
+
}
|
|
99
|
+
<%_ }) _%>
|
|
100
|
+
<%_ } _%>
|
|
69
101
|
<% if (eavValueTable) { -%>
|
|
70
102
|
|
|
71
103
|
// ═══════════════════════════════════════════════════════════════════════
|
|
@@ -17,6 +17,22 @@ import { toEavRows, mergeEavRows } from '@shared/eav-helpers';
|
|
|
17
17
|
import type { DrizzleTx } from '@shared/types/drizzle';
|
|
18
18
|
import { <%= eavDefinitionPascal %>Repository } from '../<%= eavDefinitionEntityPlural %>/<%= eavDefinitionEntity %>.repository';
|
|
19
19
|
<% } -%>
|
|
20
|
+
<%_ /* CGP-358b — service-layer composition: import target repos for belongs_to relationships */ _%>
|
|
21
|
+
<%_ if (typeof clpBelongsTo !== 'undefined') { _%>
|
|
22
|
+
<%_ const uniqueBelongsToTargets = [...new Map(clpBelongsTo.filter(r => !r.isSelfFk).map(r => [r.relatedEntity, r])).values()]; _%>
|
|
23
|
+
<%_ uniqueBelongsToTargets.forEach(rel => { _%>
|
|
24
|
+
import { <%= rel.relatedEntityPascal %>Repository } from '../<%= rel.relatedPlural %>/<%= rel.relatedEntity %>.repository';
|
|
25
|
+
import type { <%= rel.relatedEntityPascal %> } from '../<%= rel.relatedPlural %>/<%= rel.relatedEntity %>.entity';
|
|
26
|
+
<%_ }) _%>
|
|
27
|
+
<%_ } _%>
|
|
28
|
+
<%_ /* CGP-358b — import target repos for has_many relationships */ _%>
|
|
29
|
+
<%_ if (typeof clpExistingHasMany !== 'undefined') { _%>
|
|
30
|
+
<%_ const uniqueHasManyTargets = [...new Map(clpExistingHasMany.filter(r => !r.isSelfRef).map(r => [r.target, r])).values()]; _%>
|
|
31
|
+
<%_ uniqueHasManyTargets.forEach(rel => { _%>
|
|
32
|
+
import { <%= rel.targetClass %>Repository } from '../<%= rel.targetPlural %>/<%= rel.target %>.repository';
|
|
33
|
+
import type { <%= rel.targetClass %> } from '../<%= rel.targetPlural %>/<%= rel.target %>.entity';
|
|
34
|
+
<%_ }) _%>
|
|
35
|
+
<%_ } _%>
|
|
20
36
|
|
|
21
37
|
@Injectable()
|
|
22
38
|
export class <%= classNames.service %> extends WithAnalytics(
|
|
@@ -43,6 +59,20 @@ export class <%= classNames.service %> extends WithAnalytics(
|
|
|
43
59
|
<% if (eavValueTable) { -%>
|
|
44
60
|
private readonly definitionRepo: <%= eavDefinitionPascal %>Repository,
|
|
45
61
|
<% } -%>
|
|
62
|
+
<%_ /* CGP-358b — inject target repos for belongs_to (non-self-ref) */ _%>
|
|
63
|
+
<%_ if (typeof clpBelongsTo !== 'undefined') { _%>
|
|
64
|
+
<%_ const uniqueBelongsToTargets2 = [...new Map(clpBelongsTo.filter(r => !r.isSelfFk).map(r => [r.relatedEntity, r])).values()]; _%>
|
|
65
|
+
<%_ uniqueBelongsToTargets2.forEach(rel => { _%>
|
|
66
|
+
private readonly <%= rel.relatedEntity.replace(/_([a-z])/g, (_, c) => c.toUpperCase()) %>Repo: <%= rel.relatedEntityPascal %>Repository,
|
|
67
|
+
<%_ }) _%>
|
|
68
|
+
<%_ } _%>
|
|
69
|
+
<%_ /* CGP-358b — inject target repos for has_many (non-self-ref) */ _%>
|
|
70
|
+
<%_ if (typeof clpExistingHasMany !== 'undefined') { _%>
|
|
71
|
+
<%_ const uniqueHasManyTargets2 = [...new Map(clpExistingHasMany.filter(r => !r.isSelfRef).map(r => [r.target, r])).values()]; _%>
|
|
72
|
+
<%_ uniqueHasManyTargets2.forEach(rel => { _%>
|
|
73
|
+
private readonly <%= rel.target.replace(/_([a-z])/g, (_, c) => c.toUpperCase()) %>Repo: <%= rel.targetClass %>Repository,
|
|
74
|
+
<%_ }) _%>
|
|
75
|
+
<%_ } _%>
|
|
46
76
|
) {
|
|
47
77
|
super(repository);
|
|
48
78
|
}
|
|
@@ -67,6 +97,55 @@ export class <%= classNames.service %> extends WithAnalytics(
|
|
|
67
97
|
}
|
|
68
98
|
<%_ }) _%>
|
|
69
99
|
<% } %>
|
|
100
|
+
<%_ /* CGP-358b — service-layer composition methods for relationships */ _%>
|
|
101
|
+
<%_ const hasBelongsToComposition = typeof clpBelongsTo !== 'undefined' && clpBelongsTo.length > 0; _%>
|
|
102
|
+
<%_ const hasHasManyComposition = typeof clpExistingHasMany !== 'undefined' && clpExistingHasMany.length > 0; _%>
|
|
103
|
+
<%_ if (hasBelongsToComposition || hasHasManyComposition) { _%>
|
|
104
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
105
|
+
// Relationship composition methods (CGP-358b / CGP-62)
|
|
106
|
+
// Two queries, no SQL JOIN. Core-contract path; relations() const stays
|
|
107
|
+
// as opt-in extension for hand-written Drizzle queries.
|
|
108
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
109
|
+
<%_ } _%>
|
|
110
|
+
<%_ if (hasBelongsToComposition) { _%>
|
|
111
|
+
<%_ clpBelongsTo.forEach(rel => { _%>
|
|
112
|
+
<%_ const relCamel = rel.relatedEntity.replace(/_([a-z])/g, (_, c) => c.toUpperCase()); _%>
|
|
113
|
+
<%_ const entityCamel = entityName.replace(/_([a-z])/g, (_, c) => c.toUpperCase()); _%>
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Fetch the <%= rel.relatedEntityPascal %> parent for this <%= entityNamePascal %>.
|
|
117
|
+
* Two repo calls: find self by id → find target by FK.
|
|
118
|
+
*/
|
|
119
|
+
async <%= rel.relationKey %>(<%- entityCamel %>Id: string): Promise<<%= rel.relatedEntityPascal %> | null> {
|
|
120
|
+
const entity = await this.repository.findById(<%- entityCamel %>Id);
|
|
121
|
+
if (!entity) return null;
|
|
122
|
+
<%_ if (rel.isSelfFk) { _%>
|
|
123
|
+
return entity.<%= rel.camelField %> ? this.repository.findById(entity.<%= rel.camelField %>) : null;
|
|
124
|
+
<%_ } else { _%>
|
|
125
|
+
return entity.<%= rel.camelField %> ? this.<%= relCamel %>Repo.findById(entity.<%= rel.camelField %>) : null;
|
|
126
|
+
<%_ } _%>
|
|
127
|
+
}
|
|
128
|
+
<%_ }) _%>
|
|
129
|
+
<%_ } _%>
|
|
130
|
+
<%_ if (hasHasManyComposition) { _%>
|
|
131
|
+
<%_ clpExistingHasMany.forEach(rel => { _%>
|
|
132
|
+
<%_ const relCamel = rel.target.replace(/_([a-z])/g, (_, c) => c.toUpperCase()); _%>
|
|
133
|
+
<%_ const entityCamel = entityName.replace(/_([a-z])/g, (_, c) => c.toUpperCase()); _%>
|
|
134
|
+
<%_ const fkPascal = rel.inverseForeignKeyPascal; _%>
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Fetch <%= rel.name %> for this <%= entityNamePascal %> by FK traversal.
|
|
138
|
+
* Single repo call with optional cursor/limit pagination.
|
|
139
|
+
*/
|
|
140
|
+
async <%= rel.name %>(<%- entityCamel %>Id: string, opts?: { cursor?: string; limit?: number }): Promise<<%= rel.targetClass %>[]> {
|
|
141
|
+
<%_ if (rel.isSelfRef) { _%>
|
|
142
|
+
return this.repository.findBy<%= fkPascal %>(<%- entityCamel %>Id, opts);
|
|
143
|
+
<%_ } else { _%>
|
|
144
|
+
return this.<%= relCamel %>Repo.findBy<%= fkPascal %>(<%- entityCamel %>Id, opts);
|
|
145
|
+
<%_ } _%>
|
|
146
|
+
}
|
|
147
|
+
<%_ }) _%>
|
|
148
|
+
<%_ } _%>
|
|
70
149
|
<% if (eavEnabled) { %>
|
|
71
150
|
/**
|
|
72
151
|
* EAV paired read (ADR-13): fetch the entity and merge dynamic `field_values`
|
|
@@ -568,6 +568,7 @@ export default {
|
|
|
568
568
|
moduleToGetByIdQuery: importHelpers.moduleToQuery(name, fileNames.getByIdQuery.replace('.ts', '')),
|
|
569
569
|
moduleToListQuery: importHelpers.moduleToQuery(name, fileNames.listQuery.replace('.ts', '')),
|
|
570
570
|
moduleToDeclarativeQueries: importHelpers.moduleToQuery(name, 'declarative-queries'),
|
|
571
|
+
moduleToRelationshipQueries: importHelpers.moduleToQuery(name, 'relationships.queries'),
|
|
571
572
|
moduleToCreateCommand: importHelpers.moduleToCommand(name, fileNames.createCommand.replace('.ts', '')),
|
|
572
573
|
moduleToUpdateCommand: importHelpers.moduleToCommand(name, fileNames.updateCommand.replace('.ts', '')),
|
|
573
574
|
moduleToDeleteCommand: importHelpers.moduleToCommand(name, fileNames.deleteCommand.replace('.ts', '')),
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: "<%= architecture === 'clean-lite-ps' && exposeOnParent.left ? parentModulePathLeft : '' %>"
|
|
3
|
+
inject: true
|
|
4
|
+
after: " DatabaseModule,"
|
|
5
|
+
skip_if: "<%= classNames.module %>"
|
|
6
|
+
---
|
|
7
|
+
// CGP-60 — junction module (forwardRef breaks the parent↔junction module cycle)
|
|
8
|
+
forwardRef(() => <%= classNames.module %>),
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: "<%= architecture === 'clean-lite-ps' && exposeOnParent.right ? parentModulePathRight : '' %>"
|
|
3
|
+
inject: true
|
|
4
|
+
after: " DatabaseModule,"
|
|
5
|
+
skip_if: "<%= classNames.module %>"
|
|
6
|
+
---
|
|
7
|
+
// CGP-60 — junction module (forwardRef breaks the parent↔junction module cycle)
|
|
8
|
+
forwardRef(() => <%= classNames.module %>),
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: "<%= architecture === 'clean-lite-ps' && exposeOnParent.left ? parentModulePathLeft : '' %>"
|
|
3
|
+
inject: true
|
|
4
|
+
after: "from '@nestjs/common';"
|
|
5
|
+
skip_if: "<%= classNames.module %> }"
|
|
6
|
+
---
|
|
7
|
+
// CGP-60 — junction module wiring
|
|
8
|
+
import { forwardRef } from '@nestjs/common';
|
|
9
|
+
import { <%= classNames.module %> } from '<%= junctionModuleImportFromLeft %>';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: "<%= architecture === 'clean-lite-ps' && exposeOnParent.right ? parentModulePathRight : '' %>"
|
|
3
|
+
inject: true
|
|
4
|
+
after: "from '@nestjs/common';"
|
|
5
|
+
skip_if: "<%= classNames.module %> }"
|
|
6
|
+
---
|
|
7
|
+
// CGP-60 — junction module wiring
|
|
8
|
+
import { forwardRef } from '@nestjs/common';
|
|
9
|
+
import { <%= classNames.module %> } from '<%= junctionModuleImportFromRight %>';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: "<%= architecture === 'clean-lite-ps' && exposeOnParent.left ? parentServicePathLeft : '' %>"
|
|
3
|
+
inject: true
|
|
4
|
+
before: "// Inherited from"
|
|
5
|
+
skip_if: "<%= injectionMarkerLeft %>"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
9
|
+
// CGP-60 — fan-out to <%= rightEntityPascal %> (junction: <%= name %>)
|
|
10
|
+
// Delegates to <%= classNames.service %>. Per-junction marker keeps
|
|
11
|
+
// idempotency; multiple junctions on the same parent each emit their own
|
|
12
|
+
// block. `forwardRef` resolves the circular module import (parent module
|
|
13
|
+
// imports junction module; junction module imports parent modules for
|
|
14
|
+
// repo DI).
|
|
15
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
16
|
+
<%= injectionMarkerLeft %>
|
|
17
|
+
|
|
18
|
+
@Inject(forwardRef(() => <%= classNames.service %>))
|
|
19
|
+
private readonly <%= entityNameCamel %>Service!: <%= classNames.service %>;
|
|
20
|
+
|
|
21
|
+
async attach<%= rightEntityPascal %>(
|
|
22
|
+
<%= leftEntityCamel %>Id: string,
|
|
23
|
+
<%= rightEntityCamel %>Id: string,
|
|
24
|
+
link?: <%= entityNamePascal %>LinkInput,
|
|
25
|
+
): Promise<<%= entityNamePascal %>> {
|
|
26
|
+
return this.<%= entityNameCamel %>Service.attach(<%= leftEntityCamel %>Id, <%= rightEntityCamel %>Id, link);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async detach<%= rightEntityPascal %>(
|
|
30
|
+
<%= leftEntityCamel %>Id: string,
|
|
31
|
+
<%= rightEntityCamel %>Id: string,
|
|
32
|
+
): Promise<void> {
|
|
33
|
+
return this.<%= entityNameCamel %>Service.detach(<%= leftEntityCamel %>Id, <%= rightEntityCamel %>Id);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async <%= rightEntityPlural %>List(
|
|
37
|
+
<%= leftEntityCamel %>Id: string,
|
|
38
|
+
opts?: { cursor?: string; limit?: number },
|
|
39
|
+
): Promise<Array<{ entity: <%= rightEntityPascal %>; link: <%= entityNamePascal %> }>> {
|
|
40
|
+
return this.<%= entityNameCamel %>Service.listAssoc('left', <%= leftEntityCamel %>Id, opts) as Promise<
|
|
41
|
+
Array<{ entity: <%= rightEntityPascal %>; link: <%= entityNamePascal %> }>
|
|
42
|
+
>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async <%= rightEntityPlural %>SetPrimary(
|
|
46
|
+
<%= leftEntityCamel %>Id: string,
|
|
47
|
+
<%= rightEntityCamel %>Id: string,
|
|
48
|
+
): Promise<void> {
|
|
49
|
+
return this.<%= entityNameCamel %>Service.setPrimary(<%= leftEntityCamel %>Id, <%= rightEntityCamel %>Id);
|
|
50
|
+
}
|
|
51
|
+
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: "<%= architecture === 'clean-lite-ps' && exposeOnParent.right ? parentServicePathRight : '' %>"
|
|
3
|
+
inject: true
|
|
4
|
+
before: "// Inherited from"
|
|
5
|
+
skip_if: "<%= injectionMarkerRight %>"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
9
|
+
// CGP-60 — fan-out to <%= leftEntityPascal %> (junction: <%= name %>)
|
|
10
|
+
// Delegates to <%= classNames.service %>. See left-side block for the
|
|
11
|
+
// forwardRef + per-junction marker rationale.
|
|
12
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
13
|
+
<%= injectionMarkerRight %>
|
|
14
|
+
|
|
15
|
+
@Inject(forwardRef(() => <%= classNames.service %>))
|
|
16
|
+
private readonly <%= entityNameCamel %>Service!: <%= classNames.service %>;
|
|
17
|
+
|
|
18
|
+
async addTo<%= leftEntityPascal %>(
|
|
19
|
+
<%= rightEntityCamel %>Id: string,
|
|
20
|
+
<%= leftEntityCamel %>Id: string,
|
|
21
|
+
link?: <%= entityNamePascal %>LinkInput,
|
|
22
|
+
): Promise<<%= entityNamePascal %>> {
|
|
23
|
+
return this.<%= entityNameCamel %>Service.attach(<%= leftEntityCamel %>Id, <%= rightEntityCamel %>Id, link);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async removeFrom<%= leftEntityPascal %>(
|
|
27
|
+
<%= rightEntityCamel %>Id: string,
|
|
28
|
+
<%= leftEntityCamel %>Id: string,
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
return this.<%= entityNameCamel %>Service.detach(<%= leftEntityCamel %>Id, <%= rightEntityCamel %>Id);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async <%= leftEntityPlural %>List(
|
|
34
|
+
<%= rightEntityCamel %>Id: string,
|
|
35
|
+
opts?: { cursor?: string; limit?: number },
|
|
36
|
+
): Promise<Array<{ entity: <%= leftEntityPascal %>; link: <%= entityNamePascal %> }>> {
|
|
37
|
+
return this.<%= entityNameCamel %>Service.listAssoc('right', <%= rightEntityCamel %>Id, opts) as Promise<
|
|
38
|
+
Array<{ entity: <%= leftEntityPascal %>; link: <%= entityNamePascal %> }>
|
|
39
|
+
>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async <%= leftEntityPlural %>SetPrimary(
|
|
43
|
+
<%= rightEntityCamel %>Id: string,
|
|
44
|
+
<%= leftEntityCamel %>Id: string,
|
|
45
|
+
): Promise<void> {
|
|
46
|
+
return this.<%= entityNameCamel %>Service.setPrimary(<%= leftEntityCamel %>Id, <%= rightEntityCamel %>Id);
|
|
47
|
+
}
|
|
48
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: "<%= architecture === 'clean-lite-ps' && exposeOnParent.left ? parentServicePathLeft : '' %>"
|
|
3
|
+
inject: true
|
|
4
|
+
after: "from '@nestjs/common';"
|
|
5
|
+
skip_if: "<%= classNames.service %> }"
|
|
6
|
+
---
|
|
7
|
+
// CGP-60 — junction service + types (forwardRef resolves circular module dep)
|
|
8
|
+
import { forwardRef } from '@nestjs/common';
|
|
9
|
+
import { <%= classNames.service %>, <%= entityNamePascal %>LinkInput } from '<%= junctionServiceImportFromLeft %>';
|
|
10
|
+
import type { <%= entityNamePascal %> } from '../<%= entityNamePlural %>/<%= name %>.entity';
|
|
11
|
+
import type { <%= rightEntityPascal %> } from '<%= rightEntityImportFromJunction %>';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: "<%= architecture === 'clean-lite-ps' && exposeOnParent.right ? parentServicePathRight : '' %>"
|
|
3
|
+
inject: true
|
|
4
|
+
after: "from '@nestjs/common';"
|
|
5
|
+
skip_if: "<%= classNames.service %> }"
|
|
6
|
+
---
|
|
7
|
+
// CGP-60 — junction service + types (forwardRef resolves circular module dep)
|
|
8
|
+
import { forwardRef } from '@nestjs/common';
|
|
9
|
+
import { <%= classNames.service %>, <%= entityNamePascal %>LinkInput } from '<%= junctionServiceImportFromRight %>';
|
|
10
|
+
import type { <%= entityNamePascal %> } from '../<%= entityNamePlural %>/<%= name %>.entity';
|
|
11
|
+
import type { <%= leftEntityPascal %> } from '<%= leftEntityImportFromJunction %>';
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: "<%= outputPaths.entity %>"
|
|
3
|
+
force: true
|
|
4
|
+
---
|
|
5
|
+
import {
|
|
6
|
+
<%_ drizzleImports.filter(i => i !== 'relations').forEach(i => { _%>
|
|
7
|
+
<%= i %>,
|
|
8
|
+
<%_ }) _%>
|
|
9
|
+
} from 'drizzle-orm/pg-core';
|
|
10
|
+
import { relations, type InferSelectModel } from 'drizzle-orm';
|
|
11
|
+
import { <%= leftTable %> } from '../<%= leftTable %>/<%= leftEntity %>.entity';
|
|
12
|
+
<%_ if (leftEntity !== rightEntity) { _%>
|
|
13
|
+
import { <%= rightTable %> } from '../<%= rightTable %>/<%= rightEntity %>.entity';
|
|
14
|
+
<%_ } _%>
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Enums
|
|
18
|
+
// ============================================================================
|
|
19
|
+
<%_ if (hasRole) { _%>
|
|
20
|
+
|
|
21
|
+
export const <%= roleEnumName %> = pgEnum('<%= name %>_role', [
|
|
22
|
+
<%_ roleEnumValues.forEach(v => { _%>
|
|
23
|
+
'<%= v %>',
|
|
24
|
+
<%_ }) _%>
|
|
25
|
+
]);
|
|
26
|
+
<%_ } _%>
|
|
27
|
+
<%_ processedCustomFields.filter(f => f.hasChoices).forEach(field => { _%>
|
|
28
|
+
|
|
29
|
+
export const <%= field.enumName %> = pgEnum('<%= name %>_<%= field.name %>', [
|
|
30
|
+
<%_ field.choices.forEach(c => { _%>
|
|
31
|
+
'<%= c %>',
|
|
32
|
+
<%_ }) _%>
|
|
33
|
+
]);
|
|
34
|
+
<%_ }) _%>
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// Table
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
export const <%= tableVarName %> = pgTable(
|
|
41
|
+
'<%= tableName %>',
|
|
42
|
+
{
|
|
43
|
+
// FK columns — composite primary key (no surrogate id: Q4 resolution)
|
|
44
|
+
<%= leftColumnCamel %>: uuid('<%= leftColumn %>').notNull().references(() => <%= leftTable %>.id, { onDelete: '<%= onDeleteLeft %>' }),
|
|
45
|
+
<%= rightColumnCamel %>: uuid('<%= rightColumn %>').notNull().references(() => <%= rightTable %>.id, { onDelete: '<%= onDeleteRight %>' }),
|
|
46
|
+
<%_ if (hasRole) { _%>
|
|
47
|
+
|
|
48
|
+
// Role enum (per-pairing; declared in junction YAML's fields.role.choices)
|
|
49
|
+
role: <%= roleEnumName %>('role'),
|
|
50
|
+
<%_ } _%>
|
|
51
|
+
|
|
52
|
+
// BaseJunctionFields — is_primary is always emitted
|
|
53
|
+
isPrimary: boolean('is_primary').notNull().default(false),
|
|
54
|
+
<%_ if (temporal) { _%>
|
|
55
|
+
|
|
56
|
+
// Temporal window (temporal: true, default)
|
|
57
|
+
startedAt: timestamp('started_at'),
|
|
58
|
+
endedAt: timestamp('ended_at'),
|
|
59
|
+
<%_ } _%>
|
|
60
|
+
<%_ if (sourced) { _%>
|
|
61
|
+
|
|
62
|
+
// Provenance (sourced: true, default)
|
|
63
|
+
sourcedFrom: text('sourced_from'),
|
|
64
|
+
confidence: numeric('confidence', { precision: 5, scale: 4 }),
|
|
65
|
+
matchedAt: timestamp('matched_at'),
|
|
66
|
+
<%_ } _%>
|
|
67
|
+
<%_ if (hasCustomFields) { _%>
|
|
68
|
+
|
|
69
|
+
// Custom fields
|
|
70
|
+
<%_ processedCustomFields.forEach(field => { _%>
|
|
71
|
+
<%_ if (field.hasChoices) { _%>
|
|
72
|
+
<%= field.camelName %>: <%= field.enumName %>('<%= field.name %>'),
|
|
73
|
+
<%_ } else if (field.drizzleType === 'uuid') { _%>
|
|
74
|
+
<%= field.camelName %>: uuid('<%= field.name %>'),
|
|
75
|
+
<%_ } else { _%>
|
|
76
|
+
<%= field.camelName %>: <%= field.drizzleType %>('<%= field.name %>'),
|
|
77
|
+
<%_ } _%>
|
|
78
|
+
<%_ }) _%>
|
|
79
|
+
<%_ } _%>
|
|
80
|
+
|
|
81
|
+
// Timestamps
|
|
82
|
+
createdAt: timestamp('created_at').notNull().defaultNow(),
|
|
83
|
+
updatedAt: timestamp('updated_at').notNull().defaultNow(),
|
|
84
|
+
},
|
|
85
|
+
(table) => [
|
|
86
|
+
// Composite primary key on the two FK columns (Q4 resolution: no surrogate id)
|
|
87
|
+
primaryKey({ columns: [table.<%= leftColumnCamel %>, table.<%= rightColumnCamel %>] }),
|
|
88
|
+
],
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
export type <%= classNames.entity %> = InferSelectModel<typeof <%= tableVarName %>>;
|
|
92
|
+
export type <%= classNames.entity %>Insert = typeof <%= tableVarName %>.$inferInsert;
|
|
93
|
+
|
|
94
|
+
// ============================================================================
|
|
95
|
+
// Relations — extension-path metadata for db.query.X.findMany({ with: ... })
|
|
96
|
+
// Generated code does NOT consume these; they exist for hand-written admin
|
|
97
|
+
// queries and for #60's fan-out methods once they land.
|
|
98
|
+
// ============================================================================
|
|
99
|
+
|
|
100
|
+
export const <%= tableVarName %>Relations = relations(<%= tableVarName %>, ({ one }) => ({
|
|
101
|
+
<%= leftEntity %>: one(<%= leftTable %>, {
|
|
102
|
+
fields: [<%= tableVarName %>.<%= leftColumnCamel %>],
|
|
103
|
+
references: [<%= leftTable %>.id],
|
|
104
|
+
}),
|
|
105
|
+
<%_ if (leftEntity !== rightEntity) { _%>
|
|
106
|
+
<%= rightEntity %>: one(<%= rightTable %>, {
|
|
107
|
+
fields: [<%= tableVarName %>.<%= rightColumnCamel %>],
|
|
108
|
+
references: [<%= rightTable %>.id],
|
|
109
|
+
}),
|
|
110
|
+
<%_ } _%>
|
|
111
|
+
}));
|