@pattern-stack/codegen 0.6.8 → 0.7.1
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 +16 -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 +15 -2
- package/templates/entity/new/clean-lite-ps/module.ejs.t +27 -2
- package/templates/entity/new/clean-lite-ps/prompt-extension.js +72 -5
- 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-forwardref-clp-left.ejs.t +8 -0
- package/templates/junction/new/_inject-parent-module-forwardref-clp-right.ejs.t +8 -0
- package/templates/junction/new/_inject-parent-module-import-clp-left.ejs.t +8 -0
- package/templates/junction/new/_inject-parent-module-import-clp-right.ejs.t +8 -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-counterparty-clp-left.ejs.t +7 -0
- package/templates/junction/new/_inject-parent-service-counterparty-clp-right.ejs.t +7 -0
- package/templates/junction/new/_inject-parent-service-forwardref-clp-left.ejs.t +8 -0
- package/templates/junction/new/_inject-parent-service-forwardref-clp-right.ejs.t +8 -0
- package/templates/junction/new/_inject-parent-service-import-clp-left.ejs.t +9 -0
- package/templates/junction/new/_inject-parent-service-import-clp-right.ejs.t +9 -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
package/package.json
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BaseJunctionFields — shared column shape every junction table carries.
|
|
3
|
+
*
|
|
4
|
+
* Per-pairing role columns and pairing-specific fields are declared in the
|
|
5
|
+
* consumer YAML's `fields:` block and are NOT part of this shape.
|
|
6
|
+
*
|
|
7
|
+
* Exposed as a TS const so two consumers can import it:
|
|
8
|
+
* - `junction.pattern.ts` — registers it as the pattern's
|
|
9
|
+
* column contribution so the
|
|
10
|
+
* registry's `assertHasContribution()`
|
|
11
|
+
* check passes structurally.
|
|
12
|
+
* - `junction-definition.schema.ts` — uses the name set for the
|
|
13
|
+
* reserved-column collision check
|
|
14
|
+
* on the consumer's `fields:` block.
|
|
15
|
+
*
|
|
16
|
+
* See ADR-031 and `.ai-docs/stacks/codegen-app-patterns/specs/58.md`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { PatternColumnContribution } from '../pattern-definition.js';
|
|
20
|
+
|
|
21
|
+
export const BaseJunctionFields: readonly PatternColumnContribution[] = [
|
|
22
|
+
{ name: 'is_primary', type: 'boolean' },
|
|
23
|
+
{ name: 'started_at', type: 'timestamp' },
|
|
24
|
+
{ name: 'ended_at', type: 'timestamp' },
|
|
25
|
+
{ name: 'sourced_from', type: 'text' },
|
|
26
|
+
{ name: 'confidence', type: 'numeric(5,4)' },
|
|
27
|
+
{ name: 'matched_at', type: 'timestamp' },
|
|
28
|
+
] as const;
|
|
29
|
+
|
|
30
|
+
export const BASE_JUNCTION_FIELD_NAMES: ReadonlySet<string> = new Set(
|
|
31
|
+
BaseJunctionFields.map((c) => c.name),
|
|
32
|
+
);
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { registerLibraryPattern } from '../registry.js';
|
|
12
12
|
import { ActivityPattern } from './activity.pattern.js';
|
|
13
13
|
import { BasePattern } from './base.pattern.js';
|
|
14
|
+
import { JunctionPattern } from './junction.pattern.js';
|
|
14
15
|
import { KnowledgePattern } from './knowledge.pattern.js';
|
|
15
16
|
import { MetadataPattern } from './metadata.pattern.js';
|
|
16
17
|
import { SyncedPattern } from './synced.pattern.js';
|
|
@@ -20,11 +21,17 @@ registerLibraryPattern(SyncedPattern);
|
|
|
20
21
|
registerLibraryPattern(ActivityPattern);
|
|
21
22
|
registerLibraryPattern(KnowledgePattern);
|
|
22
23
|
registerLibraryPattern(MetadataPattern);
|
|
24
|
+
registerLibraryPattern(JunctionPattern);
|
|
23
25
|
|
|
24
26
|
export {
|
|
25
27
|
ActivityPattern,
|
|
26
28
|
BasePattern,
|
|
29
|
+
JunctionPattern,
|
|
27
30
|
KnowledgePattern,
|
|
28
31
|
MetadataPattern,
|
|
29
32
|
SyncedPattern,
|
|
30
33
|
};
|
|
34
|
+
export {
|
|
35
|
+
BaseJunctionFields,
|
|
36
|
+
BASE_JUNCTION_FIELD_NAMES,
|
|
37
|
+
} from './base-junction-fields.js';
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JunctionPattern — top-level discriminator for explicit many-to-many
|
|
3
|
+
* junction YAML files.
|
|
4
|
+
*
|
|
5
|
+
* Unlike `Activity` / `Synced` / `Metadata` (which attach to an entity via
|
|
6
|
+
* `pattern:` / `patterns:`), `Junction` IS the top-level YAML shape — a
|
|
7
|
+
* junction file's discriminator is `pattern: Junction`, not `entity:`.
|
|
8
|
+
* It therefore does not declare `repositoryClass` / `serviceClass`: the
|
|
9
|
+
* downstream Hygen-template leaf emits a dedicated junction repo/service
|
|
10
|
+
* per pairing.
|
|
11
|
+
*
|
|
12
|
+
* `columns` is set to `BaseJunctionFields` for two reasons:
|
|
13
|
+
* 1. Registry-side declaration of the shared shape — discoverable through
|
|
14
|
+
* `getPattern('Junction').columns` by the downstream template leaf.
|
|
15
|
+
* 2. Satisfies the registry's `assertHasContribution()` check, which
|
|
16
|
+
* insists every pattern contribute at least one of columns / repo /
|
|
17
|
+
* service class. (See spec §"Open Questions Q3"; recommendation (a).)
|
|
18
|
+
*
|
|
19
|
+
* See `.ai-docs/stacks/codegen-app-patterns/specs/58.md`.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { z } from 'zod';
|
|
23
|
+
import { definePattern } from '../pattern-definition.js';
|
|
24
|
+
import { BaseJunctionFields } from './base-junction-fields.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The `pattern: Junction`-attached config block, validated at parse time.
|
|
28
|
+
*
|
|
29
|
+
* Surface is intentionally thin in this leaf — extensions land in later
|
|
30
|
+
* leaves (templates, association-codegen). `.strict()` rejects unknown
|
|
31
|
+
* keys so consumers who misspell a flag fail loudly.
|
|
32
|
+
*/
|
|
33
|
+
const JunctionPatternConfigSchema = z.object({}).strict();
|
|
34
|
+
|
|
35
|
+
export const JunctionPattern = definePattern({
|
|
36
|
+
name: 'Junction',
|
|
37
|
+
description:
|
|
38
|
+
'Explicit many-to-many junction with role + temporal + sourcing metadata',
|
|
39
|
+
columns: [...BaseJunctionFields],
|
|
40
|
+
configSchema: JunctionPatternConfigSchema,
|
|
41
|
+
});
|
|
@@ -16,7 +16,7 @@ force: true
|
|
|
16
16
|
|
|
17
17
|
import { Inject, Injectable, NotFoundException } from '@nestjs/common';
|
|
18
18
|
import { <%= repositoryToken %> } from '<%= imports.constants %>';
|
|
19
|
-
import type { I<%= className %>Repository
|
|
19
|
+
import type { I<%= className %>Repository } from '<%= imports.domain %>';
|
|
20
20
|
import { <%= className %> } from '<%= imports.domain %>';
|
|
21
21
|
|
|
22
22
|
@Injectable()
|
|
@@ -26,10 +26,10 @@ export class <%= getByIdQueryClass %> {
|
|
|
26
26
|
private readonly <%= camelName %>Repository: I<%= className %>Repository,
|
|
27
27
|
) {}
|
|
28
28
|
|
|
29
|
-
async execute(id: string
|
|
29
|
+
async execute(id: string): Promise<<%= className %>> {
|
|
30
30
|
// TODO: Add authorization check if needed (row-level security)
|
|
31
31
|
|
|
32
|
-
const entity = await this.<%= camelName %>Repository.findById(id
|
|
32
|
+
const entity = await this.<%= camelName %>Repository.findById(id);
|
|
33
33
|
if (!entity) {
|
|
34
34
|
throw new NotFoundException(`<%= className %> with id ${id} not found`);
|
|
35
35
|
}
|
|
@@ -14,7 +14,7 @@ force: true
|
|
|
14
14
|
|
|
15
15
|
import { Inject, Injectable, NotFoundException } from '@nestjs/common';
|
|
16
16
|
import { <%= repositoryToken %> } from '<%= imports.constants %>';
|
|
17
|
-
import type { I<%= className %>Repository
|
|
17
|
+
import type { I<%= className %>Repository } from '<%= imports.domain %>';
|
|
18
18
|
import { <%= className %> } from '<%= imports.domain %>';
|
|
19
19
|
|
|
20
20
|
// ============================================================================
|
|
@@ -37,10 +37,10 @@ export class <%= getByIdQueryClass %> {
|
|
|
37
37
|
private readonly <%= camelName %>Repository: I<%= className %>Repository,
|
|
38
38
|
) {}
|
|
39
39
|
|
|
40
|
-
async execute(id: string
|
|
40
|
+
async execute(id: string): Promise<<%= className %>> {
|
|
41
41
|
// TODO: Add authorization check if needed (row-level security)
|
|
42
42
|
|
|
43
|
-
const entity = await this.<%= camelName %>Repository.findById(id
|
|
43
|
+
const entity = await this.<%= camelName %>Repository.findById(id);
|
|
44
44
|
if (!entity) {
|
|
45
45
|
throw new NotFoundException(`<%= className %> with id ${id} not found`);
|
|
46
46
|
}
|
|
@@ -73,9 +73,9 @@ export class <%= listQueryClass %> {
|
|
|
73
73
|
private readonly <%= camelName %>Repository: I<%= className %>Repository,
|
|
74
74
|
) {}
|
|
75
75
|
|
|
76
|
-
async execute(
|
|
76
|
+
async execute(): Promise<<%= className %>[]> {
|
|
77
77
|
// TODO: Add filtering, pagination, sorting as needed
|
|
78
|
-
return this.<%= camelName %>Repository.findAll(
|
|
78
|
+
return this.<%= camelName %>Repository.findAll();
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
<% } -%>
|
|
@@ -11,4 +11,7 @@ force: true
|
|
|
11
11
|
|
|
12
12
|
export * from './<%= fileNames.getByIdQuery.replace('.ts', '') %>';
|
|
13
13
|
export * from './<%= fileNames.listQuery.replace('.ts', '') %>';
|
|
14
|
+
<% if (hasRelationships && !isGrouped) { -%>
|
|
15
|
+
export * from './relationships.queries';
|
|
16
|
+
<% } -%>
|
|
14
17
|
<% } -%>
|
|
@@ -18,7 +18,7 @@ force: true
|
|
|
18
18
|
|
|
19
19
|
import { Inject, Injectable } from '@nestjs/common';
|
|
20
20
|
import { <%= repositoryToken %> } from '<%= imports.constants %>';
|
|
21
|
-
import type { I<%= className %>Repository
|
|
21
|
+
import type { I<%= className %>Repository } from '<%= imports.domain %>';
|
|
22
22
|
import { <%= className %> } from '<%= imports.domain %>';
|
|
23
23
|
|
|
24
24
|
@Injectable()
|
|
@@ -28,9 +28,9 @@ export class <%= listQueryClass %> {
|
|
|
28
28
|
private readonly <%= camelName %>Repository: I<%= className %>Repository,
|
|
29
29
|
) {}
|
|
30
30
|
|
|
31
|
-
async execute(
|
|
31
|
+
async execute(): Promise<<%= className %>[]> {
|
|
32
32
|
// TODO: Add filtering, pagination, sorting as needed
|
|
33
|
-
return this.<%= camelName %>Repository.findAll(
|
|
33
|
+
return this.<%= camelName %>Repository.findAll();
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
<% } -%>
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: "<%= (isCleanArchitecture && hasRelationships && !isGrouped) ? `${basePaths.backendSrc}/${paths.queries}/relationships.queries.ts` : '' %>"
|
|
3
|
+
skip_if: <%= !(isCleanArchitecture && hasRelationships && !isGrouped) %>
|
|
4
|
+
force: true
|
|
5
|
+
---
|
|
6
|
+
<% if (isCleanArchitecture && hasRelationships && !isGrouped) { -%>
|
|
7
|
+
/**
|
|
8
|
+
* Relationship Composition Queries for <%= className %>
|
|
9
|
+
* Generated by entity codegen - do not edit directly
|
|
10
|
+
*
|
|
11
|
+
* These query classes implement service-layer composition via FK + single-table
|
|
12
|
+
* repo calls. No SQL JOINs. Two queries, no JOIN. (CGP-358 / CGP-62)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { Inject, Injectable } from '@nestjs/common';
|
|
16
|
+
import { <%= repositoryToken %> } from '<%= imports.constants %>';
|
|
17
|
+
import type { I<%= className %>Repository } from '<%= imports.domain %>/<%= name %>';
|
|
18
|
+
import type { <%= className %> } from '<%= imports.domain %>/<%= name %>';
|
|
19
|
+
<%
|
|
20
|
+
// Unique cross-entity targets (exclude self-refs which reuse same repo)
|
|
21
|
+
const crossEntityTargets = [...new Map(
|
|
22
|
+
[...existingBelongsTo, ...existingHasMany, ...existingHasOne]
|
|
23
|
+
.filter(r => r.target !== name)
|
|
24
|
+
.map(r => [r.target, r])
|
|
25
|
+
).values()];
|
|
26
|
+
-%>
|
|
27
|
+
<% crossEntityTargets.forEach((rel) => { -%>
|
|
28
|
+
import type { I<%= rel.targetClass %>Repository } from '<%= imports.domain %>/<%= rel.target %>';
|
|
29
|
+
import type { <%= rel.targetClass %> } from '<%= imports.domain %>/<%= rel.target %>';
|
|
30
|
+
<% }) -%>
|
|
31
|
+
<% existingBelongsTo.forEach((rel) => { -%>
|
|
32
|
+
<%
|
|
33
|
+
const relNamePascal = rel.name.charAt(0).toUpperCase() + rel.name.slice(1);
|
|
34
|
+
const targetCamel = rel.target.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
35
|
+
const isSelfRef = rel.target === name;
|
|
36
|
+
-%>
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolves the <%= rel.name %> (<%= rel.targetClass %>) for a given <%= className %>.
|
|
40
|
+
* Two repo calls: fetch <%= className %> by id → fetch <%= rel.targetClass %> by FK.
|
|
41
|
+
*/
|
|
42
|
+
@Injectable()
|
|
43
|
+
export class <%= className %><%= relNamePascal %>Query {
|
|
44
|
+
constructor(
|
|
45
|
+
@Inject(<%= repositoryToken %>)
|
|
46
|
+
private readonly repository: I<%= className %>Repository,
|
|
47
|
+
<% if (!isSelfRef) { -%>
|
|
48
|
+
@Inject('<%= rel.target.toUpperCase() %>_REPOSITORY')
|
|
49
|
+
private readonly <%= targetCamel %>Repo: I<%= rel.targetClass %>Repository,
|
|
50
|
+
<% } -%>
|
|
51
|
+
) {}
|
|
52
|
+
|
|
53
|
+
async execute(<%= camelName %>Id: string): Promise<<%= rel.targetClass %> | null> {
|
|
54
|
+
const entity = await this.repository.findById(<%= camelName %>Id);
|
|
55
|
+
<% if (isSelfRef) { -%>
|
|
56
|
+
return entity ? this.repository.findById(entity.<%= rel.foreignKeyCamel %>) : null;
|
|
57
|
+
<% } else { -%>
|
|
58
|
+
return entity ? this.<%= targetCamel %>Repo.findById(entity.<%= rel.foreignKeyCamel %>) : null;
|
|
59
|
+
<% } -%>
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
<% }) -%>
|
|
63
|
+
<% existingHasMany.forEach((rel) => { -%>
|
|
64
|
+
<%
|
|
65
|
+
const relNamePascal = rel.name.charAt(0).toUpperCase() + rel.name.slice(1);
|
|
66
|
+
const targetCamel = rel.target.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
67
|
+
const isSelfRef = rel.target === name;
|
|
68
|
+
// Derive the findBy method name from the inverseForeignKey
|
|
69
|
+
// inverseForeignKey = 'account_id' → foreignKeyPascal = 'AccountId'
|
|
70
|
+
const fkPascal = rel.inverseForeignKeyCamel.charAt(0).toUpperCase() + rel.inverseForeignKeyCamel.slice(1);
|
|
71
|
+
-%>
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Resolves the <%= rel.name %> (<%= rel.targetClass %>[]) for a given <%= className %>.
|
|
75
|
+
* Single repo call with FK filter + optional cursor/limit pagination.
|
|
76
|
+
*/
|
|
77
|
+
@Injectable()
|
|
78
|
+
export class <%= className %><%= relNamePascal %>Query {
|
|
79
|
+
constructor(
|
|
80
|
+
<% if (!isSelfRef) { -%>
|
|
81
|
+
@Inject('<%= rel.target.toUpperCase() %>_REPOSITORY')
|
|
82
|
+
private readonly <%= targetCamel %>Repo: I<%= rel.targetClass %>Repository,
|
|
83
|
+
<% } else { -%>
|
|
84
|
+
@Inject(<%= repositoryToken %>)
|
|
85
|
+
private readonly repository: I<%= className %>Repository,
|
|
86
|
+
<% } -%>
|
|
87
|
+
) {}
|
|
88
|
+
|
|
89
|
+
async execute(<%= camelName %>Id: string, opts?: { cursor?: string; limit?: number }): Promise<<%= rel.targetClass %>[]> {
|
|
90
|
+
<% if (!isSelfRef) { -%>
|
|
91
|
+
return this.<%= targetCamel %>Repo.findBy<%= fkPascal %>(<%= camelName %>Id, opts);
|
|
92
|
+
<% } else { -%>
|
|
93
|
+
return this.repository.findBy<%= fkPascal %>(<%= camelName %>Id, opts);
|
|
94
|
+
<% } -%>
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
<% }) -%>
|
|
98
|
+
<% existingHasOne.forEach((rel) => { -%>
|
|
99
|
+
<%
|
|
100
|
+
const relNamePascal = rel.name.charAt(0).toUpperCase() + rel.name.slice(1);
|
|
101
|
+
const targetCamel = rel.target.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
102
|
+
const isSelfRef = rel.target === name;
|
|
103
|
+
const fkPascal = rel.inverseForeignKeyCamel.charAt(0).toUpperCase() + rel.inverseForeignKeyCamel.slice(1);
|
|
104
|
+
-%>
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Resolves the <%= rel.name %> (<%= rel.targetClass %>) for a given <%= className %> (has_one inverse).
|
|
108
|
+
* Single repo call returning at most one record.
|
|
109
|
+
*/
|
|
110
|
+
@Injectable()
|
|
111
|
+
export class <%= className %><%= relNamePascal %>Query {
|
|
112
|
+
constructor(
|
|
113
|
+
<% if (!isSelfRef) { -%>
|
|
114
|
+
@Inject('<%= rel.target.toUpperCase() %>_REPOSITORY')
|
|
115
|
+
private readonly <%= targetCamel %>Repo: I<%= rel.targetClass %>Repository,
|
|
116
|
+
<% } else { -%>
|
|
117
|
+
@Inject(<%= repositoryToken %>)
|
|
118
|
+
private readonly repository: I<%= className %>Repository,
|
|
119
|
+
<% } -%>
|
|
120
|
+
) {}
|
|
121
|
+
|
|
122
|
+
async execute(<%= camelName %>Id: string): Promise<<%= rel.targetClass %> | null> {
|
|
123
|
+
<% if (!isSelfRef) { -%>
|
|
124
|
+
const results = await this.<%= targetCamel %>Repo.findBy<%= fkPascal %>(<%= camelName %>Id);
|
|
125
|
+
<% } else { -%>
|
|
126
|
+
const results = await this.repository.findBy<%= fkPascal %>(<%= camelName %>Id);
|
|
127
|
+
<% } -%>
|
|
128
|
+
return results[0] ?? null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
<% }) -%>
|
|
132
|
+
|
|
133
|
+
export const relationshipQueryClasses = [
|
|
134
|
+
<% existingBelongsTo.forEach((rel) => { -%>
|
|
135
|
+
<% const relNamePascal = rel.name.charAt(0).toUpperCase() + rel.name.slice(1); -%>
|
|
136
|
+
<%= className %><%= relNamePascal %>Query,
|
|
137
|
+
<% }) -%>
|
|
138
|
+
<% existingHasMany.forEach((rel) => { -%>
|
|
139
|
+
<% const relNamePascal = rel.name.charAt(0).toUpperCase() + rel.name.slice(1); -%>
|
|
140
|
+
<%= className %><%= relNamePascal %>Query,
|
|
141
|
+
<% }) -%>
|
|
142
|
+
<% existingHasOne.forEach((rel) => { -%>
|
|
143
|
+
<% const relNamePascal = rel.name.charAt(0).toUpperCase() + rel.name.slice(1); -%>
|
|
144
|
+
<%= className %><%= relNamePascal %>Query,
|
|
145
|
+
<% }) -%>
|
|
146
|
+
];
|
|
147
|
+
<% } -%>
|
|
@@ -19,20 +19,8 @@ import type {
|
|
|
19
19
|
Create<%= className %>Input,
|
|
20
20
|
I<%= className %>Repository,
|
|
21
21
|
Update<%= className %>Input,
|
|
22
|
-
<% if (hasRelationships) { -%>
|
|
23
|
-
<%= className %>With,
|
|
24
|
-
<% } -%>
|
|
25
22
|
} from '../../../domain';
|
|
26
|
-
|
|
27
|
-
// Collect unique entity imports - only include relationships with existing target entities
|
|
28
|
-
const entityImports = new Set([className]);
|
|
29
|
-
if (hasExistingRelationships) {
|
|
30
|
-
[...existingBelongsTo, ...existingHasMany, ...existingHasOne].forEach((rel) => {
|
|
31
|
-
entityImports.add(rel.targetClass);
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
-%>
|
|
35
|
-
import { <%- [...entityImports].join(', ') %> } from '../../../domain';
|
|
23
|
+
import { <%= className %> } from '../../../domain';
|
|
36
24
|
import { <%= plural %> } from '<%= locations.dbSchemaServer.import %>';
|
|
37
25
|
|
|
38
26
|
@Injectable()
|
|
@@ -57,12 +45,20 @@ export class <%= className %>Repository
|
|
|
57
45
|
protected toEntity(record: typeof <%= plural %>.$inferSelect): <%= className %> {
|
|
58
46
|
return <%= className %>.fromRecord(record);
|
|
59
47
|
}
|
|
48
|
+
<%_
|
|
49
|
+
// CGP-358: FK methods with opts take priority over same-named declarative query impl.
|
|
50
|
+
// Always emit FK methods with opts; skip the declarative query body when a FK method
|
|
51
|
+
// covers the same method name (simple single-FK, non-unique, non-via, non-select).
|
|
52
|
+
// The declarative use-case class still works because opts is optional.
|
|
53
|
+
const _fkMethodNames = new Set(belongsToRelations.map(rel => `findBy${rel.foreignKeyPascal}`));
|
|
54
|
+
_%>
|
|
60
55
|
<% belongsToRelations.forEach((rel) => { -%>
|
|
61
56
|
|
|
62
|
-
async findBy<%= rel.foreignKeyPascal %>(id: string): Promise<<%= className %>[]> {
|
|
63
|
-
|
|
57
|
+
async findBy<%= rel.foreignKeyPascal %>(id: string, opts?: { cursor?: string; limit?: number }): Promise<<%= className %>[]> {
|
|
58
|
+
let q = this.baseQuery()
|
|
64
59
|
.where(eq(this.table.<%= rel.foreignKeyCamel %>, id));
|
|
65
|
-
|
|
60
|
+
if (opts?.limit) q = (q as any).limit(opts.limit);
|
|
61
|
+
return (await q).map((r) => this.toEntity(r));
|
|
66
62
|
}
|
|
67
63
|
<% }) -%>
|
|
68
64
|
<% entityRefFields.forEach((ref) => { -%>
|
|
@@ -84,6 +80,12 @@ export class <%= className %>Repository
|
|
|
84
80
|
// Declarative queries (from queries: block in entity YAML)
|
|
85
81
|
// ═══════════════════════════════════════════════════════════════════════
|
|
86
82
|
<% processedQueries.forEach((q) => { -%>
|
|
83
|
+
<%_
|
|
84
|
+
// Skip declarative impl when a FK method already covers this method name.
|
|
85
|
+
// FK methods accept opts so they're a superset of a plain single-param non-unique query.
|
|
86
|
+
const _skipDqImpl = _fkMethodNames.has(q.methodName) && !q.isUnique && !q.hasVia && !q.hasSelect;
|
|
87
|
+
_%>
|
|
88
|
+
<% if (!_skipDqImpl) { -%>
|
|
87
89
|
|
|
88
90
|
async <%= q.methodName %>(<%- q.params.map(p => `${p.camelName}: ${p.tsType}`).join(', ') %>): Promise<<%- q.returnType %>> {
|
|
89
91
|
<% if (q.hasVia) { -%>
|
|
@@ -107,70 +109,8 @@ export class <%= className %>Repository
|
|
|
107
109
|
return records.map(r => this.toEntity(r));
|
|
108
110
|
<% } -%>
|
|
109
111
|
}
|
|
110
|
-
<% }) -%>
|
|
111
112
|
<% } -%>
|
|
112
|
-
<% if (hasRelationships) { -%>
|
|
113
|
-
|
|
114
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
115
|
-
// Relationship loading (extends base class)
|
|
116
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
117
|
-
|
|
118
|
-
async findByIdWithRelations(
|
|
119
|
-
id: string,
|
|
120
|
-
include?: <%= className %>With,
|
|
121
|
-
): Promise<<%= className %> | null> {
|
|
122
|
-
const record = await this.db.query.<%= plural %>.findFirst({
|
|
123
|
-
where: eq(<%= plural %>.id, id),
|
|
124
|
-
with: this.buildWithClause(include),
|
|
125
|
-
});
|
|
126
|
-
return record ? this.mapToEntityWithRelations(record) : null;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
async findAllWithRelations(include?: <%= className %>With): Promise<<%= className %>[]> {
|
|
130
|
-
const records = await this.db.query.<%= plural %>.findMany({
|
|
131
|
-
with: this.buildWithClause(include),
|
|
132
|
-
});
|
|
133
|
-
return records.map((r) => this.mapToEntityWithRelations(r));
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
private buildWithClause(include?: <%= className %>With) {
|
|
137
|
-
if (!include) return undefined;
|
|
138
|
-
const result: Record<string, true> = {};
|
|
139
|
-
<% relationships.forEach((rel) => { -%>
|
|
140
|
-
if (include.<%= rel.name %>) result.<%= rel.name %> = true;
|
|
141
113
|
<% }) -%>
|
|
142
|
-
return Object.keys(result).length > 0 ? result : undefined;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// biome-ignore lint/suspicious/noExplicitAny: Drizzle relational query returns dynamic shape
|
|
146
|
-
private mapToEntityWithRelations(record: any): <%= className %> {
|
|
147
|
-
<% if (hasExistingRelationships) { -%>
|
|
148
|
-
return <%= className %>.fromRecord(record, (name, data) => {
|
|
149
|
-
switch (name) {
|
|
150
|
-
<% existingBelongsTo.forEach((rel) => { -%>
|
|
151
|
-
case '<%= rel.name %>':
|
|
152
|
-
// biome-ignore lint/suspicious/noExplicitAny: Cast for Drizzle record
|
|
153
|
-
return <%= rel.targetClass %>.fromRecord(data as any);
|
|
154
|
-
<% }) -%>
|
|
155
|
-
<% existingHasMany.forEach((rel) => { -%>
|
|
156
|
-
case '<%= rel.name %>':
|
|
157
|
-
// biome-ignore lint/suspicious/noExplicitAny: Cast for Drizzle records
|
|
158
|
-
return (data as any[]).map((r) => <%= rel.targetClass %>.fromRecord(r));
|
|
159
|
-
<% }) -%>
|
|
160
|
-
<% existingHasOne.forEach((rel) => { -%>
|
|
161
|
-
case '<%= rel.name %>':
|
|
162
|
-
// biome-ignore lint/suspicious/noExplicitAny: Cast for Drizzle record
|
|
163
|
-
return <%= rel.targetClass %>.fromRecord(data as any);
|
|
164
|
-
<% }) -%>
|
|
165
|
-
default:
|
|
166
|
-
return data;
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
<% } else { -%>
|
|
170
|
-
// Related entities not yet generated - return entity without relationship mapping
|
|
171
|
-
return <%= className %>.fromRecord(record);
|
|
172
|
-
<% } -%>
|
|
173
|
-
}
|
|
174
114
|
<% } -%>
|
|
175
115
|
}
|
|
176
116
|
<% } else { -%>
|
|
@@ -191,20 +131,8 @@ import type {
|
|
|
191
131
|
Create<%= className %>Input,
|
|
192
132
|
I<%= className %>Repository,
|
|
193
133
|
Update<%= className %>Input,
|
|
194
|
-
<% if (hasRelationships) { -%>
|
|
195
|
-
<%= className %>With,
|
|
196
|
-
<% } -%>
|
|
197
134
|
} from '../../../domain';
|
|
198
|
-
|
|
199
|
-
// Collect unique entity imports - only include relationships with existing target entities
|
|
200
|
-
const entityImports = new Set([className]);
|
|
201
|
-
if (hasExistingRelationships) {
|
|
202
|
-
[...existingBelongsTo, ...existingHasMany, ...existingHasOne].forEach((rel) => {
|
|
203
|
-
entityImports.add(rel.targetClass);
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
-%>
|
|
207
|
-
import { <%- [...entityImports].join(', ') %> } from '../../../domain';
|
|
135
|
+
import { <%= className %> } from '../../../domain';
|
|
208
136
|
import type { DrizzleDB } from '../database.module';
|
|
209
137
|
import { <%= plural %> } from '<%= locations.dbSchemaServer.import %>';
|
|
210
138
|
|
|
@@ -238,35 +166,18 @@ export class <%= className %>Repository implements I<%= className %>Repository {
|
|
|
238
166
|
return <%= className %>.fromRecord(record);
|
|
239
167
|
}
|
|
240
168
|
|
|
241
|
-
async findById(id: string
|
|
242
|
-
<% if (hasRelationships) { -%>
|
|
243
|
-
const record = await this.db.query.<%= plural %>.findFirst({
|
|
244
|
-
where: eq(<%= plural %>.id, id),
|
|
245
|
-
with: this.buildWithClause(include),
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
return record ? this.mapToEntity(record) : null;
|
|
249
|
-
<% } else { -%>
|
|
169
|
+
async findById(id: string): Promise<<%= className %> | null> {
|
|
250
170
|
const result = await this.baseQuery()
|
|
251
171
|
.where(eq(<%= plural %>.id, id))
|
|
252
172
|
.limit(1);
|
|
253
173
|
|
|
254
174
|
const record = result[0];
|
|
255
175
|
return record ? <%= className %>.fromRecord(record) : null;
|
|
256
|
-
<% } -%>
|
|
257
176
|
}
|
|
258
177
|
|
|
259
|
-
async findAll(
|
|
260
|
-
<% if (hasRelationships) { -%>
|
|
261
|
-
const records = await this.db.query.<%= plural %>.findMany({
|
|
262
|
-
with: this.buildWithClause(include),
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
return records.map((r) => this.mapToEntity(r));
|
|
266
|
-
<% } else { -%>
|
|
178
|
+
async findAll(): Promise<<%= className %>[]> {
|
|
267
179
|
const records = await this.baseQuery();
|
|
268
180
|
return records.map(<%= className %>.fromRecord);
|
|
269
|
-
<% } -%>
|
|
270
181
|
}
|
|
271
182
|
|
|
272
183
|
async update(id: string, input: Update<%= className %>Input<%= (hasEmits && updateEventType) ? ', tx?: DrizzleTransaction' : '' %>): Promise<<%= className %> | null> {
|
|
@@ -359,38 +270,24 @@ export class <%= className %>Repository implements I<%= className %>Repository {
|
|
|
359
270
|
return this.db.select().from(<%= plural %>);
|
|
360
271
|
<% } -%>
|
|
361
272
|
}
|
|
273
|
+
<%_
|
|
274
|
+
// CGP-358: FK methods with opts take priority over same-named declarative query impl.
|
|
275
|
+
// Always emit FK methods with opts; skip declarative body when FK covers it.
|
|
276
|
+
const _fkMethodNamesInline = new Set(belongsToRelations.map(rel => `findBy${rel.foreignKeyPascal}`));
|
|
277
|
+
_%>
|
|
362
278
|
<% belongsToRelations.forEach((rel) => { -%>
|
|
363
279
|
|
|
364
|
-
async findBy<%= rel.foreignKeyPascal %>(id: string
|
|
365
|
-
|
|
366
|
-
const records = await this.db.query.<%= plural %>.findMany({
|
|
367
|
-
where: eq(<%= plural %>.<%= rel.foreignKeyCamel %>, id),
|
|
368
|
-
with: this.buildWithClause(include),
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
return records.map((r) => this.mapToEntity(r));
|
|
372
|
-
<% } else { -%>
|
|
373
|
-
const records = await this.baseQuery()
|
|
280
|
+
async findBy<%= rel.foreignKeyPascal %>(id: string, opts?: { cursor?: string; limit?: number }): Promise<<%= className %>[]> {
|
|
281
|
+
let q = this.baseQuery()
|
|
374
282
|
.where(eq(<%= plural %>.<%= rel.foreignKeyCamel %>, id));
|
|
375
|
-
|
|
283
|
+
if (opts?.limit) q = (q as any).limit(opts.limit);
|
|
284
|
+
const records = await q;
|
|
376
285
|
return records.map(<%= className %>.fromRecord);
|
|
377
|
-
<% } -%>
|
|
378
286
|
}
|
|
379
287
|
<% }) -%>
|
|
380
288
|
<% entityRefFields.forEach((ref) => { -%>
|
|
381
289
|
|
|
382
|
-
async findBy<%= ref.pascalName %>(entityType: EntityType, entityId: string
|
|
383
|
-
<% if (hasRelationships) { -%>
|
|
384
|
-
const records = await this.db.query.<%= plural %>.findMany({
|
|
385
|
-
where: and(
|
|
386
|
-
eq(<%= plural %>.<%= ref.camelName %>EntityType, entityType),
|
|
387
|
-
eq(<%= plural %>.<%= ref.camelName %>EntityId, entityId)
|
|
388
|
-
),
|
|
389
|
-
with: this.buildWithClause(include),
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
return records.map((r) => this.mapToEntity(r));
|
|
393
|
-
<% } else { -%>
|
|
290
|
+
async findBy<%= ref.pascalName %>(entityType: EntityType, entityId: string): Promise<<%= className %>[]> {
|
|
394
291
|
const records = await this.baseQuery()
|
|
395
292
|
.where(
|
|
396
293
|
and(
|
|
@@ -400,7 +297,6 @@ export class <%= className %>Repository implements I<%= className %>Repository {
|
|
|
400
297
|
);
|
|
401
298
|
|
|
402
299
|
return records.map(<%= className %>.fromRecord);
|
|
403
|
-
<% } -%>
|
|
404
300
|
}
|
|
405
301
|
<% }) -%>
|
|
406
302
|
<% if (hasDeclarativeQueries) { -%>
|
|
@@ -409,6 +305,11 @@ export class <%= className %>Repository implements I<%= className %>Repository {
|
|
|
409
305
|
// Declarative queries (from queries: block in entity YAML)
|
|
410
306
|
// ═══════════════════════════════════════════════════════════════════════
|
|
411
307
|
<% processedQueries.forEach((q) => { -%>
|
|
308
|
+
<%_
|
|
309
|
+
// Skip declarative impl when a FK method already covers this method name.
|
|
310
|
+
const _skipDqImplInline = _fkMethodNamesInline.has(q.methodName) && !q.isUnique && !q.hasVia && !q.hasSelect;
|
|
311
|
+
_%>
|
|
312
|
+
<% if (!_skipDqImplInline) { -%>
|
|
412
313
|
|
|
413
314
|
async <%= q.methodName %>(<%- q.params.map(p => `${p.camelName}: ${p.tsType}`).join(', ') %>): Promise<<%- q.returnType %>> {
|
|
414
315
|
<% if (q.hasVia) { -%>
|
|
@@ -432,49 +333,8 @@ export class <%= className %>Repository implements I<%= className %>Repository {
|
|
|
432
333
|
return records.map(<%= className %>.fromRecord);
|
|
433
334
|
<% } -%>
|
|
434
335
|
}
|
|
435
|
-
<% }) -%>
|
|
436
336
|
<% } -%>
|
|
437
|
-
<% if (hasRelationships) { -%>
|
|
438
|
-
|
|
439
|
-
private buildWithClause(include?: <%= className %>With) {
|
|
440
|
-
if (!include) return undefined;
|
|
441
|
-
// Drizzle expects `true` or object, not `false`. Only include truthy values.
|
|
442
|
-
const result: Record<string, true> = {};
|
|
443
|
-
<% relationships.forEach((rel) => { -%>
|
|
444
|
-
if (include.<%= rel.name %>) result.<%= rel.name %> = true;
|
|
445
|
-
<% }) -%>
|
|
446
|
-
return Object.keys(result).length > 0 ? result : undefined;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
// biome-ignore lint/suspicious/noExplicitAny: Drizzle relational query returns dynamic shape
|
|
450
|
-
private mapToEntity(record: any): <%= className %> {
|
|
451
|
-
<% if (hasExistingRelationships) { -%>
|
|
452
|
-
return <%= className %>.fromRecord(record, (name, data) => {
|
|
453
|
-
switch (name) {
|
|
454
|
-
<% existingBelongsTo.forEach((rel) => { -%>
|
|
455
|
-
case '<%= rel.name %>':
|
|
456
|
-
// biome-ignore lint/suspicious/noExplicitAny: Cast for Drizzle record
|
|
457
|
-
return <%= rel.targetClass %>.fromRecord(data as any);
|
|
458
337
|
<% }) -%>
|
|
459
|
-
<% existingHasMany.forEach((rel) => { -%>
|
|
460
|
-
case '<%= rel.name %>':
|
|
461
|
-
// biome-ignore lint/suspicious/noExplicitAny: Cast for Drizzle records
|
|
462
|
-
return (data as any[]).map((r) => <%= rel.targetClass %>.fromRecord(r));
|
|
463
|
-
<% }) -%>
|
|
464
|
-
<% existingHasOne.forEach((rel) => { -%>
|
|
465
|
-
case '<%= rel.name %>':
|
|
466
|
-
// biome-ignore lint/suspicious/noExplicitAny: Cast for Drizzle record
|
|
467
|
-
return <%= rel.targetClass %>.fromRecord(data as any);
|
|
468
|
-
<% }) -%>
|
|
469
|
-
default:
|
|
470
|
-
return data;
|
|
471
|
-
}
|
|
472
|
-
});
|
|
473
|
-
<% } else { -%>
|
|
474
|
-
// Related entities not yet generated - return entity without relationship mapping
|
|
475
|
-
return <%= className %>.fromRecord(record);
|
|
476
|
-
<% } -%>
|
|
477
|
-
}
|
|
478
338
|
<% } -%>
|
|
479
339
|
}
|
|
480
340
|
<% } -%>
|