@specverse/engines 6.5.3 → 6.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analyse-prepass/adapters/typescript-decorators.d.ts.map +1 -1
- package/dist/analyse-prepass/adapters/typescript-decorators.js +82 -20
- package/dist/analyse-prepass/adapters/typescript-decorators.js.map +1 -1
- package/dist/libs/instance-factories/services/mongodb-native-services.yaml +84 -0
- package/dist/libs/instance-factories/services/templates/mongodb-native/client-generator.js +43 -0
- package/dist/libs/instance-factories/services/templates/mongodb-native/controller-generator.js +259 -0
- package/dist/libs/instance-factories/services/templates/mongodb-native/service-generator.js +64 -0
- package/dist/realize/library/library.d.ts.map +1 -1
- package/dist/realize/library/library.js +11 -0
- package/dist/realize/library/library.js.map +1 -1
- package/libs/instance-factories/services/mongodb-native-services.yaml +84 -0
- package/libs/instance-factories/services/templates/mongodb-native/__tests__/controller-generator.test.ts +111 -0
- package/libs/instance-factories/services/templates/mongodb-native/client-generator.ts +51 -0
- package/libs/instance-factories/services/templates/mongodb-native/controller-generator.ts +316 -0
- package/libs/instance-factories/services/templates/mongodb-native/service-generator.ts +83 -0
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"typescript-decorators.d.ts","sourceRoot":"","sources":["../../../src/analyse-prepass/adapters/typescript-decorators.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,SAAS,EAAE,WAAW,GAAG,SAAS,GAAG,QAAQ,GAAG,UAAU,GAAG,KAAK,GAAG,SAAS,CAAC;IAC/E,UAAU,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACpG,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,WAAW,GAAG,SAAS,GAAG,QAAQ,GAAG,YAAY,CAAC;IACxD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,aAAa,EAAE,qBAAqB,EAAE,CAAC;IACvC,mDAAmD;IACnD,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED;;;;GAIG;AACH,wBAAsB,2BAA2B,CAC/C,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,cAAc,CAAC,
|
|
1
|
+
{"version":3,"file":"typescript-decorators.d.ts","sourceRoot":"","sources":["../../../src/analyse-prepass/adapters/typescript-decorators.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,SAAS,EAAE,WAAW,GAAG,SAAS,GAAG,QAAQ,GAAG,UAAU,GAAG,KAAK,GAAG,SAAS,CAAC;IAC/E,UAAU,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACpG,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,WAAW,GAAG,SAAS,GAAG,QAAQ,GAAG,YAAY,CAAC;IACxD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,aAAa,EAAE,qBAAqB,EAAE,CAAC;IACvC,mDAAmD;IACnD,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED;;;;GAIG;AACH,wBAAsB,2BAA2B,CAC/C,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,cAAc,CAAC,CA0CzB"}
|
|
@@ -17,13 +17,19 @@ export async function extractTypeScriptDecorators(prepass) {
|
|
|
17
17
|
catch {
|
|
18
18
|
continue;
|
|
19
19
|
}
|
|
20
|
-
const
|
|
21
|
-
if (!
|
|
20
|
+
const slice = sliceClass(body, cls.startLine, cls.endLine);
|
|
21
|
+
if (!slice)
|
|
22
22
|
continue;
|
|
23
|
-
|
|
23
|
+
// Detect framework from the class-level decorator block (immediately above
|
|
24
|
+
// `class X {`). Walking the decorator block specifically — rather than the
|
|
25
|
+
// first ~600 chars of an arbitrary lookback — handles long import blocks
|
|
26
|
+
// and multi-line decorator arguments (twenty: 9-16 lines of class-level
|
|
27
|
+
// decorators between @Entity and the class declaration).
|
|
28
|
+
const framework = detectFramework(slice.decorators);
|
|
24
29
|
if (framework === 'not-an-entity')
|
|
25
30
|
continue;
|
|
26
31
|
seenFrameworks.add(framework);
|
|
32
|
+
const classBody = slice.body;
|
|
27
33
|
const attributes = extractAttributes(classBody);
|
|
28
34
|
facts.entities.push({
|
|
29
35
|
name: cls.name,
|
|
@@ -45,22 +51,25 @@ export async function extractTypeScriptDecorators(prepass) {
|
|
|
45
51
|
// Detection + extraction helpers
|
|
46
52
|
// ─────────────────────────────────────────────────────────────────────
|
|
47
53
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
54
|
+
* Decide whether the class is an entity and which framework it uses, given
|
|
55
|
+
* the class-level decorator block (collected by `sliceClass`). The block is
|
|
56
|
+
* already trimmed to just `@...` decorators + their multi-line arguments +
|
|
57
|
+
* the `extends` clause from the class declaration, so framework probes can
|
|
58
|
+
* scan it directly without char-count guards.
|
|
59
|
+
*
|
|
60
|
+
* Returns `not-an-entity` to signal "skip this class".
|
|
51
61
|
*/
|
|
52
|
-
function detectFramework(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (/@Table\s*\(/.test(head))
|
|
62
|
+
function detectFramework(decoratorBlock) {
|
|
63
|
+
// Sequelize: @Table or `extends Model` (Model can appear on the class line)
|
|
64
|
+
if (/@Table\s*\(/.test(decoratorBlock))
|
|
56
65
|
return 'sequelize';
|
|
57
|
-
if (/\bextends\s+Model\b/.test(
|
|
66
|
+
if (/\bextends\s+Model\b/.test(decoratorBlock))
|
|
58
67
|
return 'sequelize';
|
|
59
68
|
// TypeORM / NestJS: @Entity
|
|
60
|
-
if (/@Entity\s*[\s(]/.test(
|
|
69
|
+
if (/@Entity\s*[\s(]/.test(decoratorBlock))
|
|
61
70
|
return 'typeorm';
|
|
62
71
|
// Mongoose with NestJS: @Schema
|
|
63
|
-
if (/@Schema\s*[\s(]/.test(
|
|
72
|
+
if (/@Schema\s*[\s(]/.test(decoratorBlock))
|
|
64
73
|
return 'mongoose';
|
|
65
74
|
// JPA (Java) — same @Entity but in a Java file. We err on the side of typeorm
|
|
66
75
|
// because we're only called on TypeScript codebases right now.
|
|
@@ -140,11 +149,29 @@ function extractRelationships(body, fromName) {
|
|
|
140
149
|
return out;
|
|
141
150
|
}
|
|
142
151
|
/**
|
|
143
|
-
* Slice
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
152
|
+
* Slice a class into two pieces: the class-level decorator block (immediately
|
|
153
|
+
* above `class X {`) and the class body (from `class X {` through endLine).
|
|
154
|
+
*
|
|
155
|
+
* The decorator block is built by walking UPWARD from the class line and
|
|
156
|
+
* collecting every line that is either:
|
|
157
|
+
* - blank,
|
|
158
|
+
* - starts with `@` (a decorator), or
|
|
159
|
+
* - is a continuation of an unclosed parenthesised decorator argument.
|
|
160
|
+
*
|
|
161
|
+
* It also includes the class-declaration line itself (so `extends Model` is
|
|
162
|
+
* visible to framework detection). The walk stops at the first
|
|
163
|
+
* non-decorator-non-blank line (typically an `import`, `}`, or comment).
|
|
164
|
+
*
|
|
165
|
+
* This handles real-world shapes that the previous fixed-line lookback
|
|
166
|
+
* missed — e.g. twenty's class-level decorator stacks like:
|
|
167
|
+
* @Entity({ name: 'view', schema: 'core' })
|
|
168
|
+
* @Index(...)
|
|
169
|
+
* @Unique('IDX_...', [...])
|
|
170
|
+
* @Index(...)
|
|
171
|
+
* ...
|
|
172
|
+
* export class ViewEntity { ... }
|
|
173
|
+
* which can run 9-16 lines before the class declaration, well past any
|
|
174
|
+
* fixed lookback or char-count slice.
|
|
148
175
|
*
|
|
149
176
|
* Returns null if line numbers are missing or out of range.
|
|
150
177
|
*/
|
|
@@ -152,7 +179,42 @@ function sliceClass(fileText, startLine, endLine) {
|
|
|
152
179
|
if (!startLine || !endLine || endLine <= startLine)
|
|
153
180
|
return null;
|
|
154
181
|
const lines = fileText.split('\n');
|
|
155
|
-
|
|
156
|
-
|
|
182
|
+
if (startLine - 1 >= lines.length)
|
|
183
|
+
return null;
|
|
184
|
+
const classLineIdx = startLine - 1;
|
|
185
|
+
const decoratorLines = [];
|
|
186
|
+
// Walk UP from class line, tracking paren balance contributed by lines we visit.
|
|
187
|
+
// Going backward, ')' tokens INCREASE depth (unclosed-from-above) and '(' tokens
|
|
188
|
+
// DECREASE depth. While depth > 0 we're inside a multi-line decorator argument.
|
|
189
|
+
let parenDepth = 0;
|
|
190
|
+
for (let i = classLineIdx - 1; i >= 0 && i >= classLineIdx - 60; i--) {
|
|
191
|
+
const line = lines[i];
|
|
192
|
+
const opens = (line.match(/\(/g) || []).length;
|
|
193
|
+
const closes = (line.match(/\)/g) || []).length;
|
|
194
|
+
parenDepth += closes - opens;
|
|
195
|
+
if (line.trim() === '') {
|
|
196
|
+
decoratorLines.unshift(line);
|
|
197
|
+
// blank lines OK as long as we haven't seen a decorator yet, OR we're inside parens
|
|
198
|
+
if (decoratorLines.some((l) => l.trim().startsWith('@')) || parenDepth > 0)
|
|
199
|
+
continue;
|
|
200
|
+
// a leading blank line (no decorators yet) is benign — keep walking
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (parenDepth > 0) {
|
|
204
|
+
decoratorLines.unshift(line);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (line.trim().startsWith('@')) {
|
|
208
|
+
decoratorLines.unshift(line);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
// Hit a non-decorator, non-continuation, non-blank line — stop.
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
// Include the class declaration line (covers `extends Model`).
|
|
215
|
+
decoratorLines.push(lines[classLineIdx]);
|
|
216
|
+
const decorators = decoratorLines.join('\n');
|
|
217
|
+
const body = lines.slice(classLineIdx, Math.min(lines.length, endLine + 1)).join('\n');
|
|
218
|
+
return { decorators, body };
|
|
157
219
|
}
|
|
158
220
|
//# sourceMappingURL=typescript-decorators.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"typescript-decorators.js","sourceRoot":"","sources":["../../../src/analyse-prepass/adapters/typescript-decorators.ts"],"names":[],"mappings":"AAyDA;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,2BAA2B,CAC/C,OAA0B;IAE1B,MAAM,KAAK,GAAmB,EAAE,QAAQ,EAAE,EAAE,EAAE,aAAa,EAAE,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC;IAElF,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,WAAW,EAAE,CAAC;IAC5C,MAAM,cAAc,GAAG,IAAI,GAAG,EAAU,CAAC;IAEzC,KAAK,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;QAC1B,IAAI,CAAC,GAAG,CAAC,QAAQ;YAAE,SAAS;QAC5B,IAAI,IAAY,CAAC;QACjB,IAAI,CAAC;YACH,IAAI,GAAG,MAAM,OAAO,CAAC,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACpD,CAAC;QAAC,MAAM,CAAC;YAAC,SAAS;QAAC,CAAC;QACrB,MAAM,
|
|
1
|
+
{"version":3,"file":"typescript-decorators.js","sourceRoot":"","sources":["../../../src/analyse-prepass/adapters/typescript-decorators.ts"],"names":[],"mappings":"AAyDA;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,2BAA2B,CAC/C,OAA0B;IAE1B,MAAM,KAAK,GAAmB,EAAE,QAAQ,EAAE,EAAE,EAAE,aAAa,EAAE,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC;IAElF,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,WAAW,EAAE,CAAC;IAC5C,MAAM,cAAc,GAAG,IAAI,GAAG,EAAU,CAAC;IAEzC,KAAK,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;QAC1B,IAAI,CAAC,GAAG,CAAC,QAAQ;YAAE,SAAS;QAC5B,IAAI,IAAY,CAAC;QACjB,IAAI,CAAC;YACH,IAAI,GAAG,MAAM,OAAO,CAAC,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACpD,CAAC;QAAC,MAAM,CAAC;YAAC,SAAS;QAAC,CAAC;QACrB,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;QAC3D,IAAI,CAAC,KAAK;YAAE,SAAS;QAErB,2EAA2E;QAC3E,2EAA2E;QAC3E,yEAAyE;QACzE,wEAAwE;QACxE,yDAAyD;QACzD,MAAM,SAAS,GAAG,eAAe,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QACpD,IAAI,SAAS,KAAK,eAAe;YAAE,SAAS;QAC5C,cAAc,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAE9B,MAAM,SAAS,GAAG,KAAK,CAAC,IAAI,CAAC;QAC7B,MAAM,UAAU,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;QAChD,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC;YAClB,IAAI,EAAE,GAAG,CAAC,IAAI;YACd,QAAQ,EAAE,GAAG,CAAC,QAAQ;YACtB,SAAS,EAAE,GAAG,CAAC,SAAS;YACxB,OAAO,EAAE,GAAG,CAAC,OAAO;YACpB,SAAS,EAAE,SAAyC;YACpD,UAAU;YACV,UAAU,EAAE,CAAC,CAAC,GAAG,CAAC,UAAU;SAC7B,CAAC,CAAC;QAEH,KAAK,MAAM,GAAG,IAAI,oBAAoB,CAAC,SAAS,EAAE,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YAC5D,KAAK,CAAC,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAChC,CAAC;IACH,CAAC;IACD,KAAK,CAAC,UAAU,GAAG,CAAC,GAAG,cAAc,CAAC,CAAC;IACvC,OAAO,KAAK,CAAC;AACf,CAAC;AAED,wEAAwE;AACxE,iCAAiC;AACjC,wEAAwE;AAExE;;;;;;;;GAQG;AACH,SAAS,eAAe,CAAC,cAAsB;IAC7C,4EAA4E;IAC5E,IAAI,aAAa,CAAC,IAAI,CAAC,cAAc,CAAC;QAAE,OAAO,WAAW,CAAC;IAC3D,IAAI,qBAAqB,CAAC,IAAI,CAAC,cAAc,CAAC;QAAE,OAAO,WAAW,CAAC;IACnE,4BAA4B;IAC5B,IAAI,iBAAiB,CAAC,IAAI,CAAC,cAAc,CAAC;QAAE,OAAO,SAAS,CAAC;IAC7D,gCAAgC;IAChC,IAAI,iBAAiB,CAAC,IAAI,CAAC,cAAc,CAAC;QAAE,OAAO,UAAU,CAAC;IAC9D,8EAA8E;IAC9E,+DAA+D;IAC/D,OAAO,eAAe,CAAC;AACzB,CAAC;AAED;;;;;;;;;;;GAWG;AACH,SAAS,iBAAiB,CAAC,IAAY;IACrC,MAAM,GAAG,GAAkC,EAAE,CAAC;IAC9C,iEAAiE;IACjE,0EAA0E;IAC1E,MAAM,EAAE,GAAG,2PAA2P,CAAC;IACvQ,IAAI,CAAyB,CAAC;IAC9B,OAAO,CAAC,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QACpC,MAAM,cAAc,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5B,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAClB,MAAM,YAAY,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACjC,GAAG,CAAC,IAAI,CAAC;YACP,IAAI;YACJ,YAAY;YACZ,SAAS,EAAE,6CAA6C,CAAC,IAAI,CAAC,cAAc,CAAC;YAC7E,QAAQ,EAAE,WAAW,CAAC,IAAI,CAAC,cAAc,CAAC,IAAI,oBAAoB,CAAC,IAAI,CAAC,cAAc,CAAC;SACxF,CAAC,CAAC;IACL,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;GAGG;AACH,SAAS,oBAAoB,CAAC,IAAY,EAAE,QAAgB;IAC1D,MAAM,GAAG,GAA4B,EAAE,CAAC;IACxC,sEAAsE;IACtE,gDAAgD;IAChD,qEAAqE;IACrE,4DAA4D;IAC5D,MAAM,EAAE,GAAG,mJAAmJ,CAAC;IAC/J,IAAI,CAAyB,CAAC;IAC9B,OAAO,CAAC,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QACpC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAClB,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAClB,MAAM,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QACvB,wEAAwE;QACxE,IAAI,EAAsB,CAAC;QAC3B,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,2BAA2B,CAAC,CAAC;QACtD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,8BAA8B,CAAC,CAAC;QAC1D,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,sCAAsC,CAAC,CAAC;QACrE,IAAI,KAAK;YAAE,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;aACpB,IAAI,MAAM;YAAE,EAAE,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;aAC3B,IAAI,SAAS;YAAE,EAAE,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC;QACtC,IAAI,CAAC,EAAE;YAAE,SAAS;QAElB,IAAI,IAAmC,CAAC;QACxC,IAAI,IAAI,KAAK,WAAW,IAAI,IAAI,KAAK,SAAS;YAAE,IAAI,GAAG,SAAS,CAAC;aAC5D,IAAI,IAAI,KAAK,WAAW,IAAI,IAAI,KAAK,WAAW;YAAE,IAAI,GAAG,WAAW,CAAC;aACrE,IAAI,IAAI,KAAK,UAAU,IAAI,IAAI,KAAK,QAAQ;YAAE,IAAI,GAAG,QAAQ,CAAC;;YAC9D,IAAI,GAAG,YAAY,CAAC;QAEzB,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC;IACpD,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,SAAS,UAAU,CACjB,QAAgB,EAChB,SAAiB,EACjB,OAAe;IAEf,IAAI,CAAC,SAAS,IAAI,CAAC,OAAO,IAAI,OAAO,IAAI,SAAS;QAAE,OAAO,IAAI,CAAC;IAChE,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACnC,IAAI,SAAS,GAAG,CAAC,IAAI,KAAK,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IAC/C,MAAM,YAAY,GAAG,SAAS,GAAG,CAAC,CAAC;IAEnC,MAAM,cAAc,GAAa,EAAE,CAAC;IACpC,iFAAiF;IACjF,iFAAiF;IACjF,gFAAgF;IAChF,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,KAAK,IAAI,CAAC,GAAG,YAAY,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,YAAY,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;QACrE,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACtB,MAAM,KAAK,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;QAC/C,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;QAChD,UAAU,IAAI,MAAM,GAAG,KAAK,CAAC;QAC7B,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YACvB,cAAc,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YAC7B,oFAAoF;YACpF,IAAI,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,IAAI,UAAU,GAAG,CAAC;gBAAE,SAAS;YACrF,oEAAoE;YACpE,SAAS;QACX,CAAC;QACD,IAAI,UAAU,GAAG,CAAC,EAAE,CAAC;YACnB,cAAc,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YAC7B,SAAS;QACX,CAAC;QACD,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YAChC,cAAc,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YAC7B,SAAS;QACX,CAAC;QACD,gEAAgE;QAChE,MAAM;IACR,CAAC;IAED,+DAA+D;IAC/D,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC;IACzC,MAAM,UAAU,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC7C,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,YAAY,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvF,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC;AAC9B,CAAC"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
name: MongoDBNativeDriver
|
|
2
|
+
version: "1.0.0"
|
|
3
|
+
category: service
|
|
4
|
+
description: "Business logic services using the native MongoDB driver for collection access (no ORM layer)"
|
|
5
|
+
|
|
6
|
+
metadata:
|
|
7
|
+
author: "SpecVerse Team"
|
|
8
|
+
license: "MIT"
|
|
9
|
+
tags: [services, business-logic, mongodb, native-driver, controllers]
|
|
10
|
+
|
|
11
|
+
compatibility:
|
|
12
|
+
specverse: ">=5.0.0"
|
|
13
|
+
node: ">=18.0.0"
|
|
14
|
+
|
|
15
|
+
# With the MongoDB native driver, collection access IS the data layer — there
|
|
16
|
+
# is no ORM layer to swap in independently. So this single factory provides
|
|
17
|
+
# both the "orm.client" capability AND the service-layer capabilities.
|
|
18
|
+
capabilities:
|
|
19
|
+
provides:
|
|
20
|
+
- "orm.schema" # Emits the MongoDB client connector (the client IS the schema layer)
|
|
21
|
+
- "orm.client"
|
|
22
|
+
- "orm.mongodb.native"
|
|
23
|
+
- "service.controller"
|
|
24
|
+
- "service.business"
|
|
25
|
+
- "service.crud"
|
|
26
|
+
requires:
|
|
27
|
+
- "storage.database.document"
|
|
28
|
+
|
|
29
|
+
technology:
|
|
30
|
+
runtime: "node"
|
|
31
|
+
language: "typescript"
|
|
32
|
+
orm: "mongodb-native"
|
|
33
|
+
version: "^6.0.0"
|
|
34
|
+
|
|
35
|
+
dependencies:
|
|
36
|
+
runtime:
|
|
37
|
+
- name: "mongodb"
|
|
38
|
+
version: "^6.3.0"
|
|
39
|
+
|
|
40
|
+
dev:
|
|
41
|
+
- name: "@types/node"
|
|
42
|
+
version: "^20.8.0"
|
|
43
|
+
- name: "typescript"
|
|
44
|
+
version: "^5.2.0"
|
|
45
|
+
|
|
46
|
+
codeTemplates:
|
|
47
|
+
# `schema` is the orm.schema-capability slot the realize engine drives for
|
|
48
|
+
# ORM-side artifacts. With Prisma this emits `schema.prisma`; with the
|
|
49
|
+
# MongoDB native driver there is no separate db-schema artifact, so this
|
|
50
|
+
# slot emits the singleton client connector instead — same intent
|
|
51
|
+
# ("how does the app reach the data layer"), different physical output.
|
|
52
|
+
schema:
|
|
53
|
+
engine: typescript
|
|
54
|
+
generator: "libs/instance-factories/services/templates/mongodb-native/client-generator.ts"
|
|
55
|
+
outputPattern: "{backendDir}/src/db/mongoClient.ts"
|
|
56
|
+
|
|
57
|
+
controllers:
|
|
58
|
+
engine: typescript
|
|
59
|
+
generator: "libs/instance-factories/services/templates/mongodb-native/controller-generator.ts"
|
|
60
|
+
outputPattern: "{backendDir}/src/controllers/{model}Controller.ts"
|
|
61
|
+
|
|
62
|
+
services:
|
|
63
|
+
engine: typescript
|
|
64
|
+
generator: "libs/instance-factories/services/templates/mongodb-native/service-generator.ts"
|
|
65
|
+
outputPattern: "{backendDir}/src/services/{service}.ts"
|
|
66
|
+
|
|
67
|
+
configuration:
|
|
68
|
+
outputStructure: "monorepo"
|
|
69
|
+
backendDir: "backend"
|
|
70
|
+
collectionNaming: "lowercase-pluralized" # User → users, OrderItem → orderitems
|
|
71
|
+
validation: true
|
|
72
|
+
eventPublishing: true
|
|
73
|
+
errorHandling: "throw"
|
|
74
|
+
|
|
75
|
+
requirements:
|
|
76
|
+
dependencies:
|
|
77
|
+
npm:
|
|
78
|
+
dependencies:
|
|
79
|
+
"mongodb": "^6.3.0"
|
|
80
|
+
environment:
|
|
81
|
+
- name: "MONGODB_URI"
|
|
82
|
+
description: "MongoDB connection string (e.g. mongodb://localhost:27017/myapp)"
|
|
83
|
+
required: true
|
|
84
|
+
configuration: {}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
function generateMongoClient(_context) {
|
|
2
|
+
return `/**
|
|
3
|
+
* MongoDB native driver \u2014 singleton client + helpers.
|
|
4
|
+
*
|
|
5
|
+
* Picks up MONGODB_URI / MONGODB_DB from the environment. The client is
|
|
6
|
+
* lazily initialised on first use and reused across requests; \`disconnect\`
|
|
7
|
+
* is exposed for graceful-shutdown wiring.
|
|
8
|
+
*/
|
|
9
|
+
import { MongoClient, type Db, type Collection, type Document } from 'mongodb';
|
|
10
|
+
|
|
11
|
+
const uri = process.env.MONGODB_URI || 'mongodb://localhost:27017';
|
|
12
|
+
const dbName = process.env.MONGODB_DB || 'specverse';
|
|
13
|
+
|
|
14
|
+
let client: MongoClient | null = null;
|
|
15
|
+
let database: Db | null = null;
|
|
16
|
+
|
|
17
|
+
export async function getDb(): Promise<Db> {
|
|
18
|
+
if (database) return database;
|
|
19
|
+
client = new MongoClient(uri);
|
|
20
|
+
await client.connect();
|
|
21
|
+
database = client.db(dbName);
|
|
22
|
+
return database;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function getCollection<T extends Document = Document>(
|
|
26
|
+
name: string,
|
|
27
|
+
): Promise<Collection<T>> {
|
|
28
|
+
const db = await getDb();
|
|
29
|
+
return db.collection<T>(name);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function disconnect(): Promise<void> {
|
|
33
|
+
if (client) {
|
|
34
|
+
await client.close();
|
|
35
|
+
client = null;
|
|
36
|
+
database = null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
`;
|
|
40
|
+
}
|
|
41
|
+
export {
|
|
42
|
+
generateMongoClient as default
|
|
43
|
+
};
|
package/dist/libs/instance-factories/services/templates/mongodb-native/controller-generator.js
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { buildTransitionMap, isAutoField } from "@specverse/types/spec-rules";
|
|
2
|
+
function generateMongoNativeController(context) {
|
|
3
|
+
const { controller, model } = context;
|
|
4
|
+
if (!controller) throw new Error("Controller is required in template context");
|
|
5
|
+
if (!model) throw new Error("Model is required for controller generation");
|
|
6
|
+
const controllerName = controller.name;
|
|
7
|
+
const modelName = model.name;
|
|
8
|
+
const modelVar = lowerFirst(modelName);
|
|
9
|
+
const collection = collectionName(model);
|
|
10
|
+
const curedOps = controller.cured || {};
|
|
11
|
+
const customActions = generateCustomActions(controller, modelName);
|
|
12
|
+
const validate = generateValidateMethod(model, modelName);
|
|
13
|
+
const create = curedOps.create ? generateCreateMethod(model, modelName, modelVar, collection) : "";
|
|
14
|
+
const retrieve = curedOps.retrieve ? generateRetrieveMethod(modelName, modelVar, collection) : "";
|
|
15
|
+
const update = curedOps.update ? generateUpdateMethod(modelName, modelVar, collection) : "";
|
|
16
|
+
const evolve = curedOps.evolve ? generateEvolveMethod(model, modelName, modelVar, collection) : "";
|
|
17
|
+
const del = curedOps.delete ? generateDeleteMethod(modelName, modelVar, collection) : "";
|
|
18
|
+
const hasEventPublishing = curedOps.create || curedOps.update || curedOps.evolve || curedOps.delete;
|
|
19
|
+
return `/**
|
|
20
|
+
* ${controllerName}
|
|
21
|
+
* Model-specific business logic for ${modelName} (MongoDB native driver)
|
|
22
|
+
* ${controller.description || ""}
|
|
23
|
+
*/
|
|
24
|
+
import { ObjectId, type Filter, type Document } from 'mongodb';
|
|
25
|
+
import { getCollection } from '../db/mongoClient.js';
|
|
26
|
+
${hasEventPublishing ? `import { eventBus } from '../events/eventBus.js';` : ""}
|
|
27
|
+
${customActions.needsAiBehaviors ? `import * as aiBehaviors from '../behaviors/${modelName}Controller.ai.js';` : ""}
|
|
28
|
+
|
|
29
|
+
const COLLECTION_NAME = '${collection}';
|
|
30
|
+
|
|
31
|
+
/** Parse an id string to the value the collection expects: ObjectId for
|
|
32
|
+
* 24-hex-character ids, otherwise pass-through (UUIDs, slugs, ints-as-string). */
|
|
33
|
+
function parseId(id: string): ObjectId | string {
|
|
34
|
+
if (typeof id === 'string' && /^[a-fA-F0-9]{24}$/.test(id)) return new ObjectId(id);
|
|
35
|
+
return id;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** _id-aware filter builder. */
|
|
39
|
+
function byId(id: string): Filter<Document> {
|
|
40
|
+
return { _id: parseId(id) as any };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class ${controllerName} {
|
|
44
|
+
${validate}
|
|
45
|
+
${create}
|
|
46
|
+
${retrieve}
|
|
47
|
+
${update}
|
|
48
|
+
${evolve}
|
|
49
|
+
${del}
|
|
50
|
+
${customActions.code}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const ${modelVar}Controller = new ${controllerName}();
|
|
54
|
+
export default ${modelVar}Controller;
|
|
55
|
+
`;
|
|
56
|
+
}
|
|
57
|
+
function lowerFirst(s) {
|
|
58
|
+
return s.charAt(0).toLowerCase() + s.slice(1);
|
|
59
|
+
}
|
|
60
|
+
function collectionName(model) {
|
|
61
|
+
if (model?.storage?.collection) return String(model.storage.collection);
|
|
62
|
+
return model.name.toLowerCase() + "s";
|
|
63
|
+
}
|
|
64
|
+
function generateValidateMethod(model, modelName) {
|
|
65
|
+
return `
|
|
66
|
+
/**
|
|
67
|
+
* Validate ${modelName} data \u2014 runs before create / update / evolve.
|
|
68
|
+
*/
|
|
69
|
+
public validate(
|
|
70
|
+
_data: any,
|
|
71
|
+
_context: { operation: 'create' | 'update' | 'evolve' }
|
|
72
|
+
): { valid: boolean; errors: string[] } {
|
|
73
|
+
const errors: string[] = [];
|
|
74
|
+
${generateValidationLogic(model)}
|
|
75
|
+
return { valid: errors.length === 0, errors };
|
|
76
|
+
}
|
|
77
|
+
`;
|
|
78
|
+
}
|
|
79
|
+
function generateValidationLogic(model) {
|
|
80
|
+
if (!model.attributes) return " // No validation rules defined";
|
|
81
|
+
const attrList = Array.isArray(model.attributes) ? model.attributes.map((a) => [a.name, a]) : Object.entries(model.attributes);
|
|
82
|
+
const out = [];
|
|
83
|
+
attrList.forEach(([name, attr]) => {
|
|
84
|
+
if (attr.required && !isAutoField(name, attr)) {
|
|
85
|
+
out.push(` if (_context.operation === 'create' && !_data.${name}) errors.push('${name} is required');`);
|
|
86
|
+
}
|
|
87
|
+
if (attr.type === "String" || attr.type === "string") {
|
|
88
|
+
if (attr.min) out.push(` if (_data.${name} && _data.${name}.length < ${attr.min}) errors.push('${name} must be at least ${attr.min} characters');`);
|
|
89
|
+
if (attr.max) out.push(` if (_data.${name} && _data.${name}.length > ${attr.max}) errors.push('${name} must be at most ${attr.max} characters');`);
|
|
90
|
+
}
|
|
91
|
+
if (attr.values && Array.isArray(attr.values)) {
|
|
92
|
+
const values = attr.values.map((v) => `'${v}'`).join(", ");
|
|
93
|
+
out.push(` if (_data.${name} && ![${values}].includes(_data.${name})) errors.push('${name} must be one of: ${attr.values.join(", ")}');`);
|
|
94
|
+
}
|
|
95
|
+
if (attr.format === "email") {
|
|
96
|
+
out.push(` if (_data.${name} && !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(_data.${name})) errors.push('${name} must be a valid email address');`);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
return out.join("\n") || " // No validation rules defined";
|
|
100
|
+
}
|
|
101
|
+
function generateCreateMethod(model, modelName, modelVar, collection) {
|
|
102
|
+
return `
|
|
103
|
+
/**
|
|
104
|
+
* Create a new ${modelName}.
|
|
105
|
+
*/
|
|
106
|
+
public async create(data: any): Promise<any> {
|
|
107
|
+
const validation = this.validate(data, { operation: 'create' });
|
|
108
|
+
if (!validation.valid) throw new Error(\`Validation failed: \${validation.errors.join(', ')}\`);
|
|
109
|
+
|
|
110
|
+
const collection = await getCollection('${collection}');
|
|
111
|
+
const result = await collection.insertOne({ ...data });
|
|
112
|
+
const ${modelVar} = { _id: result.insertedId, ...data };
|
|
113
|
+
|
|
114
|
+
await eventBus.publish('${modelName}Created', { ...${modelVar}, timestamp: new Date().toISOString() } as any);
|
|
115
|
+
return ${modelVar};
|
|
116
|
+
}
|
|
117
|
+
`;
|
|
118
|
+
}
|
|
119
|
+
function generateRetrieveMethod(modelName, modelVar, collection) {
|
|
120
|
+
return `
|
|
121
|
+
/**
|
|
122
|
+
* Retrieve ${modelName} by id. Returns null when not found.
|
|
123
|
+
*/
|
|
124
|
+
public async retrieve(id: string): Promise<any | null> {
|
|
125
|
+
const collection = await getCollection('${collection}');
|
|
126
|
+
return await collection.findOne(byId(id));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Retrieve a page of ${modelName}s.
|
|
131
|
+
*/
|
|
132
|
+
public async retrieveAll(options: { skip?: number; take?: number } = {}): Promise<any[]> {
|
|
133
|
+
const collection = await getCollection('${collection}');
|
|
134
|
+
const cursor = collection.find({});
|
|
135
|
+
if (options.skip) cursor.skip(options.skip);
|
|
136
|
+
if (options.take) cursor.limit(options.take);
|
|
137
|
+
return await cursor.toArray();
|
|
138
|
+
}
|
|
139
|
+
`;
|
|
140
|
+
}
|
|
141
|
+
function generateUpdateMethod(modelName, modelVar, collection) {
|
|
142
|
+
return `
|
|
143
|
+
/**
|
|
144
|
+
* Update ${modelName}.
|
|
145
|
+
*/
|
|
146
|
+
public async update(id: string, data: any): Promise<any> {
|
|
147
|
+
const validation = this.validate(data, { operation: 'update' });
|
|
148
|
+
if (!validation.valid) throw new Error(\`Validation failed: \${validation.errors.join(', ')}\`);
|
|
149
|
+
|
|
150
|
+
// Strip nested objects + id \u2014 only scalar fields are written.
|
|
151
|
+
const updateData: any = {};
|
|
152
|
+
for (const [key, value] of Object.entries(data)) {
|
|
153
|
+
if (key === 'id' || key === '_id') continue;
|
|
154
|
+
if (Array.isArray(value)) continue;
|
|
155
|
+
if (value !== null && typeof value === 'object' && !(value instanceof Date)) continue;
|
|
156
|
+
updateData[key] = value;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const collection = await getCollection('${collection}');
|
|
160
|
+
await collection.updateOne(byId(id), { $set: updateData });
|
|
161
|
+
const ${modelVar} = await collection.findOne(byId(id));
|
|
162
|
+
if (!${modelVar}) throw new Error('${modelName} not found after update');
|
|
163
|
+
|
|
164
|
+
await eventBus.publish('${modelName}Updated', { ...${modelVar}, timestamp: new Date().toISOString() } as any);
|
|
165
|
+
return ${modelVar};
|
|
166
|
+
}
|
|
167
|
+
`;
|
|
168
|
+
}
|
|
169
|
+
function generateEvolveMethod(model, modelName, modelVar, collection) {
|
|
170
|
+
const lifecycles = Array.isArray(model.lifecycles) ? model.lifecycles : model.lifecycles ? Object.entries(model.lifecycles).map(([name, lc]) => ({ name, ...lc })) : [];
|
|
171
|
+
const lifecycle = lifecycles[0];
|
|
172
|
+
const lifecycleName = lifecycle?.name || "status";
|
|
173
|
+
const validTransitions = lifecycle ? buildTransitionMap(lifecycle) : {};
|
|
174
|
+
const states = lifecycle?.states && lifecycle.states.length > 0 ? lifecycle.states.map((s) => typeof s === "string" ? s : s.name) : Array.from(/* @__PURE__ */ new Set([
|
|
175
|
+
...Object.keys(validTransitions),
|
|
176
|
+
...Object.values(validTransitions).flat()
|
|
177
|
+
]));
|
|
178
|
+
return `
|
|
179
|
+
/**
|
|
180
|
+
* Evolve ${modelName} through lifecycle "${lifecycleName}"
|
|
181
|
+
* States: ${states.join(" \u2192 ") || "(none declared)"}
|
|
182
|
+
*/
|
|
183
|
+
public async evolve(id: string, data: any): Promise<any> {
|
|
184
|
+
const collection = await getCollection('${collection}');
|
|
185
|
+
const current = await collection.findOne(byId(id));
|
|
186
|
+
if (!current) throw new Error('${modelName} not found');
|
|
187
|
+
|
|
188
|
+
const targetLifecycle = data?.lifecycleName || '${lifecycleName}';
|
|
189
|
+
const targetState = data?.toState ?? data?.state ?? data?.[targetLifecycle];
|
|
190
|
+
if (!targetState) throw new Error('evolve requires toState (or ${lifecycleName}) in the request body');
|
|
191
|
+
|
|
192
|
+
${states.length > 0 ? `
|
|
193
|
+
const currentState = (current as any)[targetLifecycle];
|
|
194
|
+
const validTransitions: Record<string, string[]> = ${JSON.stringify(validTransitions)};
|
|
195
|
+
const allowed = validTransitions[currentState] || [];
|
|
196
|
+
if (!allowed.includes(targetState)) {
|
|
197
|
+
throw new Error(\`Invalid transition: \${currentState} \u2192 \${targetState}. Allowed: \${allowed.join(', ') || 'none'}\`);
|
|
198
|
+
}
|
|
199
|
+
` : ""}
|
|
200
|
+
|
|
201
|
+
await collection.updateOne(byId(id), { $set: { [targetLifecycle]: targetState } });
|
|
202
|
+
const ${modelVar} = await collection.findOne(byId(id));
|
|
203
|
+
if (!${modelVar}) throw new Error('${modelName} not found after evolve');
|
|
204
|
+
|
|
205
|
+
await eventBus.publish('${modelName}Evolved', { ...${modelVar}, timestamp: new Date().toISOString() } as any);
|
|
206
|
+
return ${modelVar};
|
|
207
|
+
}
|
|
208
|
+
`;
|
|
209
|
+
}
|
|
210
|
+
function generateDeleteMethod(modelName, modelVar, collection) {
|
|
211
|
+
return `
|
|
212
|
+
/**
|
|
213
|
+
* Delete ${modelName}.
|
|
214
|
+
*/
|
|
215
|
+
public async delete(id: string): Promise<void> {
|
|
216
|
+
const collection = await getCollection('${collection}');
|
|
217
|
+
const ${modelVar} = await collection.findOne(byId(id));
|
|
218
|
+
await collection.deleteOne(byId(id));
|
|
219
|
+
if (${modelVar}) {
|
|
220
|
+
await eventBus.publish('${modelName}Deleted', { ...${modelVar}, timestamp: new Date().toISOString() } as any);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
`;
|
|
224
|
+
}
|
|
225
|
+
function generateCustomActions(controller, modelName) {
|
|
226
|
+
if (!controller.actions || Object.keys(controller.actions).length === 0) {
|
|
227
|
+
return { code: "", needsAiBehaviors: false };
|
|
228
|
+
}
|
|
229
|
+
const out = [];
|
|
230
|
+
for (const [actionName, action] of Object.entries(controller.actions)) {
|
|
231
|
+
const params = generateActionParams(action);
|
|
232
|
+
const stepsHeader = action.steps && action.steps.length > 0 ? action.steps.map((s) => ` * - ${typeof s === "string" ? s : s.action || JSON.stringify(s)}`).join("\n") : " * (no spec steps declared)";
|
|
233
|
+
out.push(`
|
|
234
|
+
/**
|
|
235
|
+
* ${actionName}
|
|
236
|
+
* ${action.description || ""}
|
|
237
|
+
*
|
|
238
|
+
* Spec steps:
|
|
239
|
+
${stepsHeader}
|
|
240
|
+
*/
|
|
241
|
+
public async ${actionName}(${params}): Promise<any> {
|
|
242
|
+
return await aiBehaviors.${actionName}({ controller: this, ...args });
|
|
243
|
+
}
|
|
244
|
+
`);
|
|
245
|
+
}
|
|
246
|
+
return { code: out.join("\n"), needsAiBehaviors: true };
|
|
247
|
+
}
|
|
248
|
+
function generateActionParams(action) {
|
|
249
|
+
if (Array.isArray(action.parameters) && action.parameters.length > 0) {
|
|
250
|
+
return "args: any";
|
|
251
|
+
}
|
|
252
|
+
if (action.parameters && typeof action.parameters === "object" && Object.keys(action.parameters).length > 0) {
|
|
253
|
+
return "args: any";
|
|
254
|
+
}
|
|
255
|
+
return "args: any = {}";
|
|
256
|
+
}
|
|
257
|
+
export {
|
|
258
|
+
generateMongoNativeController as default
|
|
259
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
function generateMongoNativeService(context) {
|
|
2
|
+
const { service } = context;
|
|
3
|
+
if (!service) throw new Error("Service is required in template context");
|
|
4
|
+
const serviceName = service.name;
|
|
5
|
+
const operationsCode = generateOperations(service);
|
|
6
|
+
const usesDb = /\bgetDb\b|\bgetCollection\b/.test(operationsCode);
|
|
7
|
+
const hasEvents = service.publishes && service.publishes.length > 0 || service.subscribes && service.subscribes.length > 0;
|
|
8
|
+
return `/**
|
|
9
|
+
* ${serviceName}
|
|
10
|
+
* Abstract business logic service (MongoDB native driver)
|
|
11
|
+
* ${service.description || ""}
|
|
12
|
+
*/
|
|
13
|
+
${usesDb ? `import { getDb, getCollection } from '../db/mongoClient.js';` : ""}
|
|
14
|
+
${hasEvents ? `import { eventBus } from '../events/eventBus.js';` : ""}
|
|
15
|
+
|
|
16
|
+
export class ${serviceName} {
|
|
17
|
+
${operationsCode}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const ${lowerFirst(serviceName)} = new ${serviceName}();
|
|
21
|
+
export const ${lowerFirst(serviceName)}Instance = ${lowerFirst(serviceName)};
|
|
22
|
+
export default ${lowerFirst(serviceName)};
|
|
23
|
+
`;
|
|
24
|
+
}
|
|
25
|
+
function lowerFirst(s) {
|
|
26
|
+
return s.charAt(0).toLowerCase() + s.slice(1);
|
|
27
|
+
}
|
|
28
|
+
function generateOperations(service) {
|
|
29
|
+
const ops = service.operations;
|
|
30
|
+
if (!ops || Array.isArray(ops) && ops.length === 0 || !Array.isArray(ops) && Object.keys(ops).length === 0) {
|
|
31
|
+
return `
|
|
32
|
+
/**
|
|
33
|
+
* Default service entrypoint \u2014 replace with real operations once declared
|
|
34
|
+
* on the service's spec.
|
|
35
|
+
*/
|
|
36
|
+
public async execute(_params: any = {}): Promise<any> {
|
|
37
|
+
throw new Error('${service.name}.execute is not implemented');
|
|
38
|
+
}
|
|
39
|
+
`;
|
|
40
|
+
}
|
|
41
|
+
const entries = Array.isArray(ops) ? ops.map((op) => [op.name, op]) : Object.entries(ops);
|
|
42
|
+
return entries.map(([name, op]) => generateOperation(name, op, service.name)).join("\n");
|
|
43
|
+
}
|
|
44
|
+
function generateOperation(operationName, operation, serviceName) {
|
|
45
|
+
const stepsHeader = operation.steps && operation.steps.length > 0 ? operation.steps.map((s) => ` * - ${typeof s === "string" ? s : s.action || JSON.stringify(s)}`).join("\n") : " * (no spec steps declared)";
|
|
46
|
+
return `
|
|
47
|
+
/**
|
|
48
|
+
* ${operationName}
|
|
49
|
+
* ${operation.description || ""}
|
|
50
|
+
*
|
|
51
|
+
* Spec steps:
|
|
52
|
+
${stepsHeader}
|
|
53
|
+
*/
|
|
54
|
+
public async ${operationName}(_args: any = {}): Promise<any> {
|
|
55
|
+
// TODO (#43 follow-up): translate spec steps into native MongoDB driver
|
|
56
|
+
// calls. For now this is a stub so realize completes and the service
|
|
57
|
+
// surface is callable for parity tests.
|
|
58
|
+
throw new Error('${serviceName}.${operationName} is not implemented');
|
|
59
|
+
}
|
|
60
|
+
`;
|
|
61
|
+
}
|
|
62
|
+
export {
|
|
63
|
+
generateMongoNativeService as default
|
|
64
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"library.d.ts","sourceRoot":"","sources":["../../../src/realize/library/library.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,EAAkD,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9F,OAAO,EAA4B,KAAK,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AACpF,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvF;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,8BAA8B;IAC9B,QAAQ,CAAC,EAAE,eAAe,CAAC,UAAU,CAAC,CAAC;IAEvC,gDAAgD;IAChD,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,iCAAiC;IACjC,UAAU,CAAC,EAAE;QACX,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;IAEF,8BAA8B;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,qBAAqB;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAED;;GAEG;AACH,qBAAa,sBAAsB;IACjC,OAAO,CAAC,KAAK,CAAwC;IACrD,OAAO,CAAC,SAAS,CAA2B;IAC5C,OAAO,CAAC,OAAO,CAAuB;;IAMtC;;OAEG;IACG,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAsB9F;;OAEG;IACG,eAAe,CAAC,OAAO,EAAE,aAAa,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"library.d.ts","sourceRoot":"","sources":["../../../src/realize/library/library.ts"],"names":[],"mappings":"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"}
|
|
@@ -48,6 +48,17 @@ export class InstanceFactoryLibrary {
|
|
|
48
48
|
// Sort by priority (highest first)
|
|
49
49
|
const sortedSources = [...sources].sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
|
50
50
|
const result = await loadFromMultipleSources(sortedSources.map(s => ({ path: s.path })), { validate: true, ignoreInvalid: true });
|
|
51
|
+
// Surface validation errors as warnings so a malformed factory doesn't
|
|
52
|
+
// disappear without a trace. ignoreInvalid: true means we keep loading
|
|
53
|
+
// the other factories — but the user still needs to see WHY one was
|
|
54
|
+
// dropped (otherwise the symptom is a confusing "Unknown factory X" at
|
|
55
|
+
// the manifest stage with no hint that the yaml was rejected upstream).
|
|
56
|
+
if (result.errors && result.errors.length > 0) {
|
|
57
|
+
for (const err of result.errors) {
|
|
58
|
+
const lines = (err.errors || []).map(e => ` - ${e}`).join('\n');
|
|
59
|
+
console.warn(`⚠️ Skipped instance factory ${err.filePath}\n${lines}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
51
62
|
// Add to library
|
|
52
63
|
for (const loadResult of result.types) {
|
|
53
64
|
const name = loadResult.type.name;
|