@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.
Files changed (42) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/src/cli/index.js +516 -73
  3. package/dist/src/cli/index.js.map +1 -1
  4. package/dist/src/index.d.ts +208 -1
  5. package/dist/src/index.js +147 -0
  6. package/dist/src/index.js.map +1 -1
  7. package/package.json +1 -1
  8. package/src/patterns/library/base-junction-fields.ts +32 -0
  9. package/src/patterns/library/index.ts +7 -0
  10. package/src/patterns/library/junction.pattern.ts +41 -0
  11. package/templates/entity/new/backend/application/queries/get-by-id.ejs.t +3 -3
  12. package/templates/entity/new/backend/application/queries/grouped-index.ejs.t +5 -5
  13. package/templates/entity/new/backend/application/queries/index.ejs.t +3 -0
  14. package/templates/entity/new/backend/application/queries/list.ejs.t +3 -3
  15. package/templates/entity/new/backend/application/queries/relationships.queries.ejs.t +147 -0
  16. package/templates/entity/new/backend/database/repository.ejs.t +36 -176
  17. package/templates/entity/new/backend/domain/entity.ejs.t +0 -44
  18. package/templates/entity/new/backend/domain/grouped-index.ejs.t +4 -60
  19. package/templates/entity/new/backend/domain/index.ejs.t +2 -2
  20. package/templates/entity/new/backend/domain/repository-interface.ejs.t +16 -17
  21. package/templates/entity/new/backend/modules/core/module.ejs.t +10 -0
  22. package/templates/entity/new/backend/presentation/controller.ejs.t +2 -34
  23. package/templates/entity/new/clean-lite-ps/entity.ejs.t +21 -2
  24. package/templates/entity/new/clean-lite-ps/module.ejs.t +27 -2
  25. package/templates/entity/new/clean-lite-ps/prompt-extension.js +108 -9
  26. package/templates/entity/new/clean-lite-ps/repository.ejs.t +33 -1
  27. package/templates/entity/new/clean-lite-ps/service.ejs.t +79 -0
  28. package/templates/entity/new/prompt.js +1 -0
  29. package/templates/junction/new/_inject-parent-module-clp-left.ejs.t +8 -0
  30. package/templates/junction/new/_inject-parent-module-clp-right.ejs.t +8 -0
  31. package/templates/junction/new/_inject-parent-module-import-clp-left.ejs.t +9 -0
  32. package/templates/junction/new/_inject-parent-module-import-clp-right.ejs.t +9 -0
  33. package/templates/junction/new/_inject-parent-service-clp-left.ejs.t +51 -0
  34. package/templates/junction/new/_inject-parent-service-clp-right.ejs.t +48 -0
  35. package/templates/junction/new/_inject-parent-service-import-clp-left.ejs.t +11 -0
  36. package/templates/junction/new/_inject-parent-service-import-clp-right.ejs.t +11 -0
  37. package/templates/junction/new/entity.ejs.t +111 -0
  38. package/templates/junction/new/index.ejs.t +15 -0
  39. package/templates/junction/new/module.ejs.t +37 -0
  40. package/templates/junction/new/prompt.js +492 -0
  41. package/templates/junction/new/repository.ejs.t +67 -0
  42. package/templates/junction/new/service.ejs.t +174 -0
@@ -16,23 +16,6 @@ force: true
16
16
  <% if (hasEntityRefFields) { -%>
17
17
  import type { EntityType } from '<%= locations.dbSchemaServer.import %>';
18
18
  <% } -%>
19
- <%
20
- // Collect unique non-self-referential imports
21
- const importedEntities = new Set();
22
- [...belongsToRelations, ...hasManyRelations, ...hasOneRelations].forEach((rel) => {
23
- if (rel.target !== name) {
24
- importedEntities.add(rel.target);
25
- }
26
- });
27
- -%>
28
- <% if (importedEntities.size > 0) { -%>
29
-
30
- <% importedEntities.forEach((target) => {
31
- const targetClass = target.charAt(0).toUpperCase() + target.slice(1).replace(/_([a-z])/g, (_, c) => c.toUpperCase());
32
- -%>
33
- import type { <%= targetClass %> } from '../<%= target %>/<%= target %>.entity';
34
- <% }) -%>
35
- <% } -%>
36
19
 
37
20
  export class <%= className %> {
38
21
  constructor(
@@ -51,28 +34,12 @@ export class <%= className %> {
51
34
  public readonly validFrom: Date | null,
52
35
  public readonly validTo: Date | null,
53
36
  public readonly isActive: boolean,
54
- <% } -%>
55
- <% if (hasRelationships) { -%>
56
- // Loaded relations (optional, populated when eager-loaded)
57
- <% belongsToRelations.forEach((rel) => { -%>
58
- public readonly <%= rel.name %>?: <%= rel.targetClass %>,
59
- <% }) -%>
60
- <% hasManyRelations.forEach((rel) => { -%>
61
- public readonly <%= rel.name %>?: <%= rel.targetClass %>[],
62
- <% }) -%>
63
- <% hasOneRelations.forEach((rel) => { -%>
64
- public readonly <%= rel.name %>?: <%= rel.targetClass %>,
65
- <% }) -%>
66
37
  <% } -%>
67
38
  ) {}
68
39
 
69
40
  static fromRecord(
70
41
  // biome-ignore lint/suspicious/noExplicitAny: Drizzle records have dynamic shape
71
42
  record: Record<string, any>,
72
- <% if (hasRelationships) { -%>
73
- // biome-ignore lint/suspicious/noExplicitAny: Returns different entity types
74
- mapRelation?: (name: string, data: unknown) => any,
75
- <% } -%>
76
43
  ): <%= className %> {
77
44
  return new <%= className %>(
78
45
  record.id,
@@ -90,17 +57,6 @@ export class <%= className %> {
90
57
  record.validFrom,
91
58
  record.validTo,
92
59
  record.isActive,
93
- <% } -%>
94
- <% if (hasRelationships) { -%>
95
- <% belongsToRelations.forEach((rel) => { -%>
96
- record.<%= rel.name %> ? mapRelation?.('<%= rel.name %>', record.<%= rel.name %>) : undefined,
97
- <% }) -%>
98
- <% hasManyRelations.forEach((rel) => { -%>
99
- record.<%= rel.name %> ? mapRelation?.('<%= rel.name %>', record.<%= rel.name %>) : undefined,
100
- <% }) -%>
101
- <% hasOneRelations.forEach((rel) => { -%>
102
- record.<%= rel.name %> ? mapRelation?.('<%= rel.name %>', record.<%= rel.name %>) : undefined,
103
- <% }) -%>
104
60
  <% } -%>
105
61
  );
106
62
  }
@@ -14,23 +14,6 @@ force: true
14
14
  <% if (hasEntityRefFields) { -%>
15
15
  import type { EntityType } from '<%= locations.dbSchemaServer.import %>';
16
16
  <% } -%>
17
- <%
18
- // Collect unique non-self-referential imports
19
- const importedEntities = new Set();
20
- [...belongsToRelations, ...hasManyRelations, ...hasOneRelations].forEach((rel) => {
21
- if (rel.target !== name) {
22
- importedEntities.add(rel.target);
23
- }
24
- });
25
- -%>
26
- <% if (importedEntities.size > 0) { -%>
27
-
28
- <% importedEntities.forEach((target) => {
29
- const targetClass = target.charAt(0).toUpperCase() + target.slice(1).replace(/_([a-z])/g, (_, c) => c.toUpperCase());
30
- -%>
31
- import type { <%= targetClass %> } from '../<%= target %>';
32
- <% }) -%>
33
- <% } -%>
34
17
 
35
18
  // ============================================================================
36
19
  // Entity
@@ -53,28 +36,12 @@ export class <%= className %> {
53
36
  public readonly validFrom: Date | null,
54
37
  public readonly validTo: Date | null,
55
38
  public readonly isActive: boolean,
56
- <% } -%>
57
- <% if (hasRelationships) { -%>
58
- // Loaded relations (optional, populated when eager-loaded)
59
- <% belongsToRelations.forEach((rel) => { -%>
60
- public readonly <%= rel.name %>?: <%= rel.targetClass %>,
61
- <% }) -%>
62
- <% hasManyRelations.forEach((rel) => { -%>
63
- public readonly <%= rel.name %>?: <%= rel.targetClass %>[],
64
- <% }) -%>
65
- <% hasOneRelations.forEach((rel) => { -%>
66
- public readonly <%= rel.name %>?: <%= rel.targetClass %>,
67
- <% }) -%>
68
39
  <% } -%>
69
40
  ) {}
70
41
 
71
42
  static fromRecord(
72
43
  // biome-ignore lint/suspicious/noExplicitAny: Drizzle records have dynamic shape
73
44
  record: Record<string, any>,
74
- <% if (hasRelationships) { -%>
75
- // biome-ignore lint/suspicious/noExplicitAny: Returns different entity types
76
- mapRelation?: (name: string, data: unknown) => any,
77
- <% } -%>
78
45
  ): <%= className %> {
79
46
  return new <%= className %>(
80
47
  record.id,
@@ -92,17 +59,6 @@ export class <%= className %> {
92
59
  record.validFrom,
93
60
  record.validTo,
94
61
  record.isActive,
95
- <% } -%>
96
- <% if (hasRelationships) { -%>
97
- <% belongsToRelations.forEach((rel) => { -%>
98
- record.<%= rel.name %> ? mapRelation?.('<%= rel.name %>', record.<%= rel.name %>) : undefined,
99
- <% }) -%>
100
- <% hasManyRelations.forEach((rel) => { -%>
101
- record.<%= rel.name %> ? mapRelation?.('<%= rel.name %>', record.<%= rel.name %>) : undefined,
102
- <% }) -%>
103
- <% hasOneRelations.forEach((rel) => { -%>
104
- record.<%= rel.name %> ? mapRelation?.('<%= rel.name %>', record.<%= rel.name %>) : undefined,
105
- <% }) -%>
106
62
  <% } -%>
107
63
  );
108
64
  }
@@ -111,18 +67,6 @@ export class <%= className %> {
111
67
  // ============================================================================
112
68
  // Repository Interface
113
69
  // ============================================================================
114
- <% if (hasRelationships) { -%>
115
-
116
- /**
117
- * Type-safe eager loading options.
118
- * Pass to repository methods to include related entities.
119
- */
120
- export type <%= className %>With = {
121
- <% relationships.forEach((rel) => { -%>
122
- <%= rel.name %>?: boolean;
123
- <% }) -%>
124
- };
125
- <% } -%>
126
70
 
127
71
  /**
128
72
  * Domain-level input types for repository operations.
@@ -143,8 +87,8 @@ export type Update<%= className %>Input = Partial<Create<%= className %>Input>;
143
87
 
144
88
  export interface I<%= className %>Repository {
145
89
  create(input: Create<%= className %>Input): Promise<<%= className %>>;
146
- findById(id: string<%= hasRelationships ? `, include?: ${className}With` : '' %>): Promise<<%= className %> | null>;
147
- findAll(<%= hasRelationships ? `include?: ${className}With` : '' %>): Promise<<%= className %>[]>;
90
+ findById(id: string): Promise<<%= className %> | null>;
91
+ findAll(): Promise<<%= className %>[]>;
148
92
  update(id: string, input: Update<%= className %>Input): Promise<<%= className %> | null>;
149
93
  delete(id: string): Promise<<%= className %> | null>;
150
94
  <% if (hasSoftDelete) { -%>
@@ -154,10 +98,10 @@ export interface I<%= className %>Repository {
154
98
  findOnlyDeleted(): Promise<<%= className %>[]>;
155
99
  <% } -%>
156
100
  <% belongsToRelations.forEach((rel) => { -%>
157
- findBy<%= rel.foreignKeyPascal %>(id: string<%= hasRelationships ? `, include?: ${className}With` : '' %>): Promise<<%= className %>[]>;
101
+ findBy<%= rel.foreignKeyPascal %>(id: string, opts?: { cursor?: string; limit?: number }): Promise<<%= className %>[]>;
158
102
  <% }) -%>
159
103
  <% entityRefFields.forEach((ref) => { -%>
160
- findBy<%= ref.pascalName %>(entityType: EntityType, entityId: string<%= hasRelationships ? `, include?: ${className}With` : '' %>): Promise<<%= className %>[]>;
104
+ findBy<%= ref.pascalName %>(entityType: EntityType, entityId: string): Promise<<%= className %>[]>;
161
105
  <% }) -%>
162
106
  }
163
107
  <% } -%>
@@ -11,5 +11,5 @@ force: true
11
11
  * when using separate file layout (file_grouping: "separate")
12
12
  */
13
13
 
14
- export * from './<%= name %>.entity';
15
- export * from './<%= name %>.repository.interface';
14
+ export * from './<%= fileNames.entity.replace('.ts', '') %>';
15
+ export * from './<%= fileNames.repositoryInterface.replace('.ts', '') %>';
@@ -9,25 +9,13 @@ force: true
9
9
  * Generated by entity codegen - do not edit directly
10
10
  */
11
11
 
12
- import type { <%= className %> } from './<%= name %>.entity';
12
+ import type { <%= className %> } from './<%= (typeof fileNames !== 'undefined' ? fileNames.entity : name + '.entity.ts').replace('.ts', '') %>';
13
13
  <% if (hasEntityRefFields) { -%>
14
14
  import type { EntityType } from '<%= locations.dbSchemaServer.import %>';
15
15
  <% } -%>
16
16
  <% if (hasEmits && (createEventType || updateEventType || deleteEventType)) { -%>
17
17
  import type { DrizzleTransaction } from '<%= eventsTokenImport %>';
18
18
  <% } -%>
19
- <% if (hasRelationships) { -%>
20
-
21
- /**
22
- * Type-safe eager loading options.
23
- * Pass to repository methods to include related entities.
24
- */
25
- export type <%= className %>With = {
26
- <% relationships.forEach((rel) => { -%>
27
- <%= rel.name %>?: boolean;
28
- <% }) -%>
29
- };
30
- <% } -%>
31
19
 
32
20
  /**
33
21
  * Domain-level input types for repository operations.
@@ -48,8 +36,8 @@ export type Update<%= className %>Input = Partial<Create<%= className %>Input>;
48
36
 
49
37
  export interface I<%= className %>Repository {
50
38
  create(input: Create<%= className %>Input<%= (hasEmits && createEventType) ? ', tx?: DrizzleTransaction' : '' %>): Promise<<%= className %>>;
51
- findById(id: string<%= hasRelationships ? `, include?: ${className}With` : '' %>): Promise<<%= className %> | null>;
52
- findAll(<%= hasRelationships ? `include?: ${className}With` : '' %>): Promise<<%= className %>[]>;
39
+ findById(id: string): Promise<<%= className %> | null>;
40
+ findAll(): Promise<<%= className %>[]>;
53
41
  update(id: string, input: Update<%= className %>Input<%= (hasEmits && updateEventType) ? ', tx?: DrizzleTransaction' : '' %>): Promise<<%= className %> | null>;
54
42
  delete(id: string<%= (hasEmits && deleteEventType) ? ', tx?: DrizzleTransaction' : '' %>): Promise<<%= className %> | null>;
55
43
  <% if (hasSoftDelete) { -%>
@@ -58,16 +46,27 @@ export interface I<%= className %>Repository {
58
46
  findWithDeleted(): Promise<<%= className %>[]>;
59
47
  findOnlyDeleted(): Promise<<%= className %>[]>;
60
48
  <% } -%>
49
+ <%_
50
+ // CGP-358: FK methods with opts take priority. Always emit FK signatures with opts.
51
+ // Skip declarative query signature when FK covers the same method name.
52
+ const _riFkMethodNames = new Set(belongsToRelations.map(rel => `findBy${rel.foreignKeyPascal}`));
53
+ _%>
61
54
  <% belongsToRelations.forEach((rel) => { -%>
62
- findBy<%= rel.foreignKeyPascal %>(id: string<%= hasRelationships ? `, include?: ${className}With` : '' %>): Promise<<%= className %>[]>;
55
+ findBy<%= rel.foreignKeyPascal %>(id: string, opts?: { cursor?: string; limit?: number }): Promise<<%= className %>[]>;
63
56
  <% }) -%>
64
57
  <% entityRefFields.forEach((ref) => { -%>
65
- findBy<%= ref.pascalName %>(entityType: EntityType, entityId: string<%= hasRelationships ? `, include?: ${className}With` : '' %>): Promise<<%= className %>[]>;
58
+ findBy<%= ref.pascalName %>(entityType: EntityType, entityId: string): Promise<<%= className %>[]>;
66
59
  <% }) -%>
67
60
  <% if (hasDeclarativeQueries) { -%>
68
61
  // Declarative queries (from queries: block in entity YAML)
69
62
  <% processedQueries.forEach((q) => { -%>
63
+ <%_
64
+ // Skip declarative signature when FK method covers it (opts is a superset of plain single-param).
65
+ const _skipRiDq = _riFkMethodNames.has(q.methodName) && !q.isUnique && !q.hasVia && !q.hasSelect;
66
+ _%>
67
+ <% if (!_skipRiDq) { -%>
70
68
  <%= q.methodName %>(<%- q.params.map(p => `${p.camelName}: ${p.tsType}`).join(', ') %>): Promise<<%- q.returnType %>>;
69
+ <% } -%>
71
70
  <% }) -%>
72
71
  <% } -%>
73
72
  }
@@ -32,6 +32,9 @@ import { <%= className %>Repository } from '<%= imports.moduleToRepository %>';
32
32
  <% if (hasDeclarativeQueries) { -%>
33
33
  import { declarativeQueryClasses } from '<%= imports.moduleToDeclarativeQueries %>';
34
34
  <% } -%>
35
+ <% if (hasRelationships && !isGrouped) { -%>
36
+ import { relationshipQueryClasses } from '<%= imports.moduleToRelationshipQueries %>';
37
+ <% } -%>
35
38
  <% if (exposeRest || exposeElectric) { -%>
36
39
  import { <%= classNamePlural %>Controller } from '<%= imports.moduleToController %>';
37
40
  <% } -%>
@@ -58,6 +61,10 @@ import { create<%= className %>Schema, update<%= className %>Schema, <%= camelNa
58
61
  <% if (hasDeclarativeQueries) { -%>
59
62
  // Declarative queries
60
63
  ...declarativeQueryClasses,
64
+ <% } -%>
65
+ <% if (hasRelationships && !isGrouped) { -%>
66
+ // Relationship composition queries
67
+ ...relationshipQueryClasses,
61
68
  <% } -%>
62
69
  ],
63
70
  exports: [
@@ -71,6 +78,9 @@ import { create<%= className %>Schema, update<%= className %>Schema, <%= camelNa
71
78
  <%= deleteCommandClass %>,
72
79
  <% if (hasDeclarativeQueries) { -%>
73
80
  ...declarativeQueryClasses,
81
+ <% } -%>
82
+ <% if (hasRelationships && !isGrouped) { -%>
83
+ ...relationshipQueryClasses,
74
84
  <% } -%>
75
85
  ],
76
86
  })
@@ -19,9 +19,6 @@ import {
19
19
  ParseUUIDPipe,
20
20
  Post,
21
21
  Put,
22
- <% if (hasRelationships) { -%>
23
- Query,
24
- <% } -%>
25
22
  UsePipes,
26
23
  } from '@nestjs/common';
27
24
  import {
@@ -44,9 +41,6 @@ import { <%= deleteCommandClass %> } from '<%= imports.controllerToDeleteCommand
44
41
  import { <%= updateCommandClass %> } from '<%= imports.controllerToUpdateCommand %>';
45
42
  import { ZodValidationPipe } from '../../core/pipes/zod-validation.pipe';
46
43
  import { <%= className %> } from '<%= imports.controllerToDomain %>';
47
- <% if (hasRelationships) { -%>
48
- import type { <%= className %>With } from '<%= imports.controllerToDomain %>';
49
- <% } -%>
50
44
 
51
45
  // OPENAPI-3: controller decorators reference schemas by `$ref` rather
52
46
  // than `type:` class references because generated DTOs are Zod-derived
@@ -72,12 +66,8 @@ export class <%= classNamePlural %>Controller {
72
66
  })
73
67
  @ApiResponse({ status: 401, schema: { $ref: '#/components/schemas/ErrorResponseDto' } })
74
68
  @Get()
75
- async findAll(<%- hasRelationships ? `@Query('include') include?: string` : '' %>): Promise<<%= className %>[]> {
76
- <% if (hasRelationships) { -%>
77
- return this.list<%= classNamePlural %>Query.execute(this.parseInclude(include));
78
- <% } else { -%>
69
+ async findAll(): Promise<<%= className %>[]> {
79
70
  return this.list<%= classNamePlural %>Query.execute();
80
- <% } -%>
81
71
  }
82
72
 
83
73
  @ApiOperation({ summary: 'Find <%= name %> by id', operationId: 'find<%= className %>ById' })
@@ -88,15 +78,8 @@ export class <%= classNamePlural %>Controller {
88
78
  @Get(':id')
89
79
  async findById(
90
80
  @Param('id', ParseUUIDPipe) id: string,
91
- <% if (hasRelationships) { -%>
92
- @Query('include') include?: string,
93
- <% } -%>
94
81
  ): Promise<<%= className %>> {
95
- <% if (hasRelationships) { -%>
96
- return this.get<%= className %>ByIdQuery.execute(id, this.parseInclude(include));
97
- <% } else { -%>
98
82
  return this.get<%= className %>ByIdQuery.execute(id);
99
- <% } -%>
100
83
  }
101
84
 
102
85
  @ApiOperation({ summary: 'Create <%= name %>', operationId: 'create<%= className %>' })
@@ -144,23 +127,8 @@ export class <%= classNamePlural %>Controller {
144
127
  ): Promise<<%= className %>> {
145
128
  return this.delete<%= className %>Command.execute(id, { actor: { tenantId, userId } });
146
129
  }
147
- <% if (hasRelationships) { -%>
148
-
149
- /**
150
- * Parse comma-separated include query param into typed options.
151
- * Example: ?include=account,owner → { account: true, owner: true }
152
- */
153
- private parseInclude(include?: string): <%= className %>With | undefined {
154
- if (!include) return undefined;
155
- const parts = include.split(',').map((s) => s.trim());
156
- return {
157
- <% relationships.forEach((rel) => { -%>
158
- <%= rel.name %>: parts.includes('<%= rel.name %>'),
159
- <% }) -%>
160
- };
161
- }
162
- <% } -%>
163
130
  }
131
+
164
132
  <% } -%>
165
133
  <% if (exposeElectric) { -%>
166
134
  /**
@@ -7,6 +7,9 @@ import {
7
7
  <%_ clpDrizzleImports.filter(i => i !== 'relations').forEach(i => { _%>
8
8
  <%= i %>,
9
9
  <%_ }) _%>
10
+ <%_ if (typeof clpHasSelfFk !== 'undefined' && clpHasSelfFk) { _%>
11
+ type AnyPgColumn,
12
+ <%_ } _%>
10
13
  } from 'drizzle-orm/pg-core';
11
14
  <%_ if (clpHasRelationsBlock) { _%>
12
15
  import { relations, type InferSelectModel } from 'drizzle-orm';
@@ -18,6 +21,18 @@ import { type InferSelectModel } from 'drizzle-orm';
18
21
  import { <%= rel.relatedTable %> } from '<%= rel.importPath %>';
19
22
  <%_ } _%>
20
23
  <%_ }) _%>
24
+ <%_ /* CGP-358b: import has_many target tables for many() relation const */ _%>
25
+ <%_ if (typeof clpExistingHasMany !== 'undefined') { _%>
26
+ <%_ clpExistingHasMany.filter(rel => !rel.isSelfRef).forEach(rel => { _%>
27
+ import { <%= rel.targetPlural %> } from '../<%= rel.targetPlural %>/<%= rel.target %>.entity';
28
+ <%_ }) _%>
29
+ <%_ } _%>
30
+ <%_ if (typeof clpEnumFields !== 'undefined' && clpEnumFields.length > 0) { _%>
31
+
32
+ <%_ clpEnumFields.forEach(ef => { _%>
33
+ export const <%= ef.enumName %> = pgEnum('<%= ef.dbName %>', [<%- ef.choices.map(c => `'${c}'`).join(', ') %>]);
34
+ <%_ }) _%>
35
+ <%_ } _%>
21
36
 
22
37
  export const <%= entityNamePlural %> = pgTable(
23
38
  '<%= entityNamePlural %>',
@@ -30,7 +45,7 @@ export const <%= entityNamePlural %> = pgTable(
30
45
  // cascade rules never fire for a soft-deleted parent. This FK constraint only applies on
31
46
  // hard-delete (e.g. admin purge). See ADR-021: docs/adrs/ADR-021-on-delete-semantics.md
32
47
  <%_ } _%>
33
- <%= rel.camelField %>: uuid('<%= rel.field %>')<%= rel.nullable ? '' : '.notNull()' %>.references(() => <%= rel.relatedTable %>.id, { onDelete: '<%= rel.onDelete %>' }),
48
+ <%= rel.camelField %>: uuid('<%= rel.field %>')<%= rel.nullable ? '' : '.notNull()' %>.references(<%= rel.isSelfFk ? '(): AnyPgColumn ' : '() ' %>=> <%= rel.relatedTable %>.id, { onDelete: '<%= rel.onDelete %>' }),
34
49
  <%_ }) _%>
35
50
  <%_ clpProcessedFields.forEach(field => { _%>
36
51
  <%= field.camelName %>: <%- field.drizzleChain %>,
@@ -51,14 +66,18 @@ export const <%= entityNamePlural %> = pgTable(
51
66
  },
52
67
  );
53
68
  <%_ if (clpHasRelationsBlock) { _%>
69
+ <%_ const needsMany = typeof clpExistingHasMany !== 'undefined' && clpExistingHasMany.length > 0; _%>
54
70
 
55
- export const <%= entityNamePlural %>Relations = relations(<%= entityNamePlural %>, ({ one }) => ({
71
+ export const <%= entityNamePlural %>Relations = relations(<%= entityNamePlural %>, ({ one<%= needsMany ? ', many' : '' %> }) => ({
56
72
  <%_ clpBelongsTo.forEach(rel => { _%>
57
73
  <%= rel.relationKey %>: one(<%= rel.relatedTable %>, {
58
74
  fields: [<%= entityNamePlural %>.<%= rel.camelField %>],
59
75
  references: [<%= rel.relatedTable %>.id],
60
76
  }),
61
77
  <%_ }) _%>
78
+ <%_ if (typeof clpExistingHasMany !== 'undefined') { clpExistingHasMany.forEach(rel => { _%>
79
+ <%= rel.name %>: many(<%= rel.targetPlural %>),
80
+ <%_ }) } _%>
62
81
  }));
63
82
  <%_ } _%>
64
83
 
@@ -13,9 +13,21 @@ force: true
13
13
  import { Inject, Module, type OnModuleInit } from '@nestjs/common';
14
14
  import { OPENAPI_REGISTRY, type OpenApiRegistry } from '@shared/openapi';
15
15
  import { DatabaseModule } from '@shared/database/database.module';
16
- <%_ clpBelongsTo.forEach(rel => { _%>
17
- // import { <%= rel.relatedEntityPascal %>sModule } from '../<%= rel.relatedPlural %>/<%= rel.relatedPlural %>.module';
16
+ <%_ /* CGP-358b: Import cross-entity repos needed for has_many composition */ _%>
17
+ <%_ if (typeof clpExistingHasMany !== 'undefined') { _%>
18
+ <%_ const hasManyNeedingImport = clpExistingHasMany.filter(r => !r.isSelfRef); _%>
19
+ <%_ const uniqueHasManyForModule = [...new Map(hasManyNeedingImport.map(r => [r.target, r])).values()]; _%>
20
+ <%_ uniqueHasManyForModule.forEach(rel => { _%>
21
+ import { <%= rel.targetClass %>Repository } from '../<%= rel.targetPlural %>/<%= rel.target %>.repository';
22
+ <%_ }) _%>
23
+ <%_ } _%>
24
+ <%_ /* CGP-358b: Import cross-entity repos needed for belongs_to composition */ _%>
25
+ <%_ if (typeof clpBelongsTo !== 'undefined') { _%>
26
+ <%_ const uniqueBelongsToForModule = [...new Map(clpBelongsTo.filter(r => !r.isSelfFk).map(r => [r.relatedEntity, r])).values()]; _%>
27
+ <%_ uniqueBelongsToForModule.forEach(rel => { _%>
28
+ import { <%= rel.relatedEntityPascal %>Repository } from '../<%= rel.relatedPlural %>/<%= rel.relatedEntity %>.repository';
18
29
  <%_ }) _%>
30
+ <%_ } _%>
19
31
  <% if (eavEnabled) { -%>
20
32
  import { FieldValuesModule } from '../field_values/field_values.module';
21
33
  <% } -%>
@@ -68,6 +80,19 @@ import { <%= classNames.searchController %> } from './<%= entityName %>-search.c
68
80
  providers: [
69
81
  <%= classNames.repository %>,
70
82
  <%= classNames.service %>,
83
+ <%_ /* CGP-358b: Register cross-entity repos as providers (needed for service DI) */ _%>
84
+ <%_ if (typeof clpExistingHasMany !== 'undefined') { _%>
85
+ <%_ const uniqueHasManyProviders = [...new Map(clpExistingHasMany.filter(r => !r.isSelfRef).map(r => [r.target, r])).values()]; _%>
86
+ <%_ uniqueHasManyProviders.forEach(rel => { _%>
87
+ <%= rel.targetClass %>Repository,
88
+ <%_ }) _%>
89
+ <%_ } _%>
90
+ <%_ if (typeof clpBelongsTo !== 'undefined') { _%>
91
+ <%_ const uniqueBelongsToProviders = [...new Map(clpBelongsTo.filter(r => !r.isSelfFk).map(r => [r.relatedEntity, r])).values()]; _%>
92
+ <%_ uniqueBelongsToProviders.forEach(rel => { _%>
93
+ <%= rel.relatedEntityPascal %>Repository,
94
+ <%_ }) _%>
95
+ <%_ } _%>
71
96
  <%= classNames.findByIdUseCase %>,
72
97
  <%= classNames.listUseCase %>,
73
98
  <% if (eavEnabled) { -%>