@specverse/engines 6.7.8 → 6.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai/behavior-ai-service.js +2 -2
- package/dist/ai/behavior-ai-service.js.map +1 -1
- package/dist/inference/core/specly-converter.d.ts.map +1 -1
- package/dist/inference/core/specly-converter.js +20 -0
- package/dist/inference/core/specly-converter.js.map +1 -1
- package/dist/inference/index.d.ts.map +1 -1
- package/dist/inference/index.js +72 -22
- package/dist/inference/index.js.map +1 -1
- package/dist/inference/logical/generators/controller-generator.d.ts.map +1 -1
- package/dist/inference/logical/generators/controller-generator.js +26 -4
- package/dist/inference/logical/generators/controller-generator.js.map +1 -1
- package/dist/libs/instance-factories/applications/templates/generic/backend-package-json-generator.js +22 -5
- package/dist/libs/instance-factories/controllers/templates/fastify/routes-generator.js +50 -15
- package/dist/libs/instance-factories/controllers/templates/fastify/server-generator.js +26 -6
- package/dist/libs/instance-factories/services/postgres-native-services.yaml +90 -0
- package/dist/libs/instance-factories/services/templates/_shared/step-matching.js +44 -0
- package/dist/libs/instance-factories/services/templates/mongodb-native/controller-generator.js +68 -13
- package/dist/libs/instance-factories/services/templates/mongodb-native/step-conventions.js +515 -0
- package/dist/libs/instance-factories/services/templates/postgres-native/client-generator.js +165 -0
- package/dist/libs/instance-factories/services/templates/postgres-native/controller-generator.js +300 -0
- package/dist/libs/instance-factories/services/templates/postgres-native/ddl-generator.js +169 -0
- package/dist/libs/instance-factories/services/templates/postgres-native/service-generator.js +65 -0
- package/dist/libs/instance-factories/services/templates/postgres-native/step-conventions.js +433 -0
- package/dist/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.js +27 -4
- package/dist/libs/instance-factories/services/templates/prisma/step-conventions.js +7 -34
- package/dist/parser/processors/ExecutableProcessor.d.ts.map +1 -1
- package/dist/parser/processors/ExecutableProcessor.js +14 -1
- package/dist/parser/processors/ExecutableProcessor.js.map +1 -1
- package/dist/realize/index.d.ts.map +1 -1
- package/dist/realize/index.js +30 -3
- package/dist/realize/index.js.map +1 -1
- package/libs/instance-factories/applications/templates/generic/backend-package-json-generator.ts +46 -24
- package/libs/instance-factories/controllers/templates/fastify/routes-generator.ts +80 -21
- package/libs/instance-factories/controllers/templates/fastify/server-generator.ts +48 -7
- package/libs/instance-factories/services/postgres-native-services.yaml +90 -0
- package/libs/instance-factories/services/templates/_shared/step-matching.ts +103 -0
- package/libs/instance-factories/services/templates/mongodb-native/controller-generator.ts +97 -23
- package/libs/instance-factories/services/templates/mongodb-native/step-conventions.ts +691 -0
- package/libs/instance-factories/services/templates/postgres-native/__tests__/controller-generator.test.ts +193 -0
- package/libs/instance-factories/services/templates/postgres-native/client-generator.ts +178 -0
- package/libs/instance-factories/services/templates/postgres-native/controller-generator.ts +372 -0
- package/libs/instance-factories/services/templates/postgres-native/ddl-generator.ts +236 -0
- package/libs/instance-factories/services/templates/postgres-native/service-generator.ts +84 -0
- package/libs/instance-factories/services/templates/postgres-native/step-conventions.ts +539 -0
- package/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.ts +61 -7
- package/libs/instance-factories/services/templates/prisma/step-conventions.ts +21 -68
- package/package.json +4 -3
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step Conventions for MongoDB native driver — mirrors the prisma library's
|
|
3
|
+
* pattern set but emits native-driver collection code instead of `prisma.X`.
|
|
4
|
+
*
|
|
5
|
+
* Each convention maps a natural-language step pattern to a generated code
|
|
6
|
+
* block. Patterns are tried in order; first match wins. Unmatched steps
|
|
7
|
+
* fall through to the AI-behaviors layer (a separate concern).
|
|
8
|
+
*
|
|
9
|
+
* The emitted code assumes the controller's surrounding scope provides:
|
|
10
|
+
* - `getCollection(name)` (from `../db/mongoClient.js`)
|
|
11
|
+
* - `byId(id)` filter helper for `_id`-shaped queries
|
|
12
|
+
* - `eventBus` for `publish`
|
|
13
|
+
* These are imported by the mongo-native controller-generator regardless,
|
|
14
|
+
* so step-emitted code compiles inline.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
toMethod,
|
|
19
|
+
toVar,
|
|
20
|
+
matchAgainstConventions,
|
|
21
|
+
type SharedConvention,
|
|
22
|
+
type SharedStepContext,
|
|
23
|
+
} from '../_shared/step-matching.js';
|
|
24
|
+
|
|
25
|
+
export type MongoStepConvention = SharedConvention<MongoStepContext>;
|
|
26
|
+
|
|
27
|
+
export interface MongoStepContext extends SharedStepContext {
|
|
28
|
+
/** Collection name for the model (lowercased + 's' or storage.collection). */
|
|
29
|
+
collectionName: string;
|
|
30
|
+
/** The full model registry (name → ModelSpec) so conventions can read
|
|
31
|
+
* attributes for default values, types, and FK target collections. */
|
|
32
|
+
models?: Record<string, any>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Compute sensible default values for a model's attributes — used by
|
|
37
|
+
* mechanical create/insertMany conventions to fill required fields the
|
|
38
|
+
* action's `args` doesn't supply.
|
|
39
|
+
*
|
|
40
|
+
* Defaults are derived from the spec's attribute declarations:
|
|
41
|
+
* - `default=<value>` annotation → that value (literal or expression)
|
|
42
|
+
* - `Integer required` with `min=N` → N
|
|
43
|
+
* - `Integer required` no min → 0
|
|
44
|
+
* - `String required` → '""' (empty)
|
|
45
|
+
* - `Boolean required` → false
|
|
46
|
+
* - `DateTime auto=now` or with `auto=now` → new Date().toISOString()
|
|
47
|
+
* - Foreign keys (fields ending in `Id` matching another model) → resolved
|
|
48
|
+
* from a declared variable of that type (e.g. `userId` ← `user._id`)
|
|
49
|
+
* - Anything optional or already present in args → omitted
|
|
50
|
+
*
|
|
51
|
+
* Returns an array of `key: expression` strings ready to inline into an
|
|
52
|
+
* object-literal record body. Empty if the model isn't in the registry.
|
|
53
|
+
*/
|
|
54
|
+
export function deriveModelDefaults(
|
|
55
|
+
modelName: string,
|
|
56
|
+
ctx: MongoStepContext,
|
|
57
|
+
): string[] {
|
|
58
|
+
if (!ctx.models) return [];
|
|
59
|
+
const model = ctx.models[modelName] || ctx.models[modelName.charAt(0).toUpperCase() + modelName.slice(1)];
|
|
60
|
+
if (!model) return [];
|
|
61
|
+
const attrs = model.attributes;
|
|
62
|
+
if (!attrs) return [];
|
|
63
|
+
const list = Array.isArray(attrs)
|
|
64
|
+
? attrs.map((a: any) => [a.name, a])
|
|
65
|
+
: Object.entries(attrs);
|
|
66
|
+
|
|
67
|
+
const declaredVars = ctx.declaredVars || new Set();
|
|
68
|
+
const out: string[] = [];
|
|
69
|
+
|
|
70
|
+
// Names emitted explicitly by the convention's record-tail (createdAt /
|
|
71
|
+
// updatedAt) — skip in defaults to avoid TS1117 duplicate-property errors.
|
|
72
|
+
const conventionManaged = new Set(['createdAt', 'updatedAt', 'id']);
|
|
73
|
+
for (const [name, attr] of list as [string, any][]) {
|
|
74
|
+
if (!name) continue;
|
|
75
|
+
if (conventionManaged.has(name)) continue;
|
|
76
|
+
// Skip optional fields without explicit defaults
|
|
77
|
+
const required = !!attr.required;
|
|
78
|
+
const hasDefault = attr.default !== undefined;
|
|
79
|
+
if (!required && !hasDefault) continue;
|
|
80
|
+
// Skip if already supplied via spread args (caller overlays)
|
|
81
|
+
// — handled at convention call site by spreading args first.
|
|
82
|
+
if (hasDefault) {
|
|
83
|
+
out.push(`${name}: ${formatDefault(attr.default, attr.type)}`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
// Required field — derive a default from type
|
|
87
|
+
const type = (attr.type || 'String').toLowerCase();
|
|
88
|
+
if (type === 'integer' || type === 'int' || type === 'number' || type === 'float') {
|
|
89
|
+
const min = attr.min ?? 0;
|
|
90
|
+
out.push(`${name}: ${min}`);
|
|
91
|
+
} else if (type === 'boolean') {
|
|
92
|
+
out.push(`${name}: false`);
|
|
93
|
+
} else if (type === 'datetime' || type === 'date') {
|
|
94
|
+
if (attr.auto === 'now') {
|
|
95
|
+
out.push(`${name}: new Date().toISOString()`);
|
|
96
|
+
} else {
|
|
97
|
+
out.push(`${name}: new Date().toISOString()`);
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
// String / UUID / Text / etc. — try FK resolution first.
|
|
101
|
+
// Exclude self-reference (current model would FK-resolve to itself
|
|
102
|
+
// and emit a self-loop). The current model name's lowercase form
|
|
103
|
+
// is the conventionally-bound variable name.
|
|
104
|
+
const selfVar = modelName.charAt(0).toLowerCase() + modelName.slice(1);
|
|
105
|
+
const fkMatch = name.match(/^(.+)Id$/);
|
|
106
|
+
if (fkMatch && fkMatch[1] !== selfVar && declaredVars.has(fkMatch[1])) {
|
|
107
|
+
out.push(`${name}: (${fkMatch[1]} as any)?._id ?? (${fkMatch[1]} as any)?.id`);
|
|
108
|
+
} else {
|
|
109
|
+
out.push(`${name}: ''`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function formatDefault(value: any, type?: string): string {
|
|
117
|
+
if (value === null || value === undefined) return 'null';
|
|
118
|
+
if (typeof value === 'boolean') return String(value);
|
|
119
|
+
if (typeof value === 'number') return String(value);
|
|
120
|
+
if (typeof value === 'string') {
|
|
121
|
+
// Numeric defaults are sometimes parsed as strings
|
|
122
|
+
if (/^-?\d+(\.\d+)?$/.test(value)) return value;
|
|
123
|
+
if (value === 'true' || value === 'false') return value;
|
|
124
|
+
if (value === 'now' && (type === 'DateTime' || type === 'Date')) {
|
|
125
|
+
return 'new Date().toISOString()';
|
|
126
|
+
}
|
|
127
|
+
return JSON.stringify(value);
|
|
128
|
+
}
|
|
129
|
+
return JSON.stringify(value);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function toCollection(modelName: string): string {
|
|
133
|
+
return modelName.toLowerCase() + 's';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Resolve a value reference in step text to a TS expression. */
|
|
137
|
+
function resolveValue(rawValue: string, ctx: MongoStepContext): string {
|
|
138
|
+
const value = rawValue.trim().replace(/^['"]|['"]$/g, '');
|
|
139
|
+
if (/^(current\s*time|now|timestamp)$/i.test(value)) return 'new Date().toISOString()';
|
|
140
|
+
if (value === 'true' || value === 'false') return value;
|
|
141
|
+
if (/^-?\d+(\.\d+)?$/.test(value)) return value;
|
|
142
|
+
|
|
143
|
+
const declared = ctx.declaredVars || new Set();
|
|
144
|
+
const params = ctx.parameterNames || [];
|
|
145
|
+
|
|
146
|
+
const rootMatch = value.match(/^([a-zA-Z_][a-zA-Z0-9_]*)(\.[a-zA-Z0-9_.]+)?$/);
|
|
147
|
+
if (rootMatch) {
|
|
148
|
+
const root = rootMatch[1];
|
|
149
|
+
if (declared.has(root) || params.includes(root)) return value;
|
|
150
|
+
if (params.length > 0) return `args.${value}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Fallback: quoted string literal so generated code stays valid TS
|
|
154
|
+
if (/\s/.test(value)) return `/* TODO: resolve "${value}" */ null`;
|
|
155
|
+
return `'${value.replace(/'/g, "\\'")}'`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Find the most-recent step{N}Result variable in declaredVars — used as
|
|
159
|
+
* the source for `Persist X` / `Save X` patterns where the previous step
|
|
160
|
+
* (an AI pure transform) computed the record to write. */
|
|
161
|
+
function mostRecentStepResult(ctx: MongoStepContext): string | null {
|
|
162
|
+
const declared = Array.from(ctx.declaredVars || []);
|
|
163
|
+
const stepResults = declared.filter((v) => /^step\d+Result$/.test(v))
|
|
164
|
+
.sort((a, b) => parseInt(b.slice(4), 10) - parseInt(a.slice(4), 10));
|
|
165
|
+
return stepResults[0] ?? null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function pascal(model: string): string {
|
|
169
|
+
return model.charAt(0).toUpperCase() + model.slice(1);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export const MONGO_STEP_CONVENTIONS: MongoStepConvention[] = [
|
|
173
|
+
// --- Find / Lookup by single field ---
|
|
174
|
+
// Matches: "Look up X by Y", "Find X by Y", "Find X by Y or fail with 404"
|
|
175
|
+
{
|
|
176
|
+
name: 'find-by-field',
|
|
177
|
+
pattern: /^(?:look\s+up|find|fetch|get)\s+(\w+)\s+by\s+(\w+)(?:\s+or\s+fail.*)?$/i,
|
|
178
|
+
generateCall: (m, ctx) => {
|
|
179
|
+
const model = m[1];
|
|
180
|
+
const field = m[2];
|
|
181
|
+
const modelVar = toVar(model);
|
|
182
|
+
const collection = toCollection(model);
|
|
183
|
+
const params = ctx.parameterNames || [];
|
|
184
|
+
const declared = ctx.declaredVars || new Set();
|
|
185
|
+
const idVal = field === 'id'
|
|
186
|
+
? (params.includes(`${modelVar}Id`) ? `args.${modelVar}Id` : 'args.id')
|
|
187
|
+
: (params.includes(field) ? `args.${field}` : `args.${field}`);
|
|
188
|
+
|
|
189
|
+
if (declared.has(modelVar)) {
|
|
190
|
+
return ` // Step ${ctx.stepNum}: Find ${model} by ${field} (already loaded)`;
|
|
191
|
+
}
|
|
192
|
+
declared.add(modelVar);
|
|
193
|
+
const failOnMissing = /or\s+fail/i.test(m[0]);
|
|
194
|
+
// Emit as `let` so subsequent conditional-create steps can reassign
|
|
195
|
+
// the variable when the lookup returned null.
|
|
196
|
+
// Also surface `id` (string) alongside `_id` (ObjectId) for AI-body
|
|
197
|
+
// compatibility — LLMs trained on prisma routinely check `.id`.
|
|
198
|
+
return ` // Step ${ctx.stepNum}: Find ${model} by ${field}
|
|
199
|
+
const ${modelVar}_collection = await getCollection('${collection}');
|
|
200
|
+
let ${modelVar} = await ${modelVar}_collection.findOne({ ${field}: ${idVal} });
|
|
201
|
+
if (${modelVar} && !(${modelVar} as any).id && (${modelVar} as any)._id) (${modelVar} as any).id = String((${modelVar} as any)._id);${failOnMissing ? `
|
|
202
|
+
if (!${modelVar}) throw new Error('${model} not found');` : ''}`;
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
// --- Find by composite (two fields) ---
|
|
207
|
+
// Matches: "Look up X by Y and Z", "Find X by Y and Z"
|
|
208
|
+
{
|
|
209
|
+
name: 'find-by-composite',
|
|
210
|
+
pattern: /^(?:look\s+up|find|fetch|get)(?:\s+existing)?\s+(\w+)\s+by\s+(\w+)\s+and\s+(\w+)$/i,
|
|
211
|
+
generateCall: (m, ctx) => {
|
|
212
|
+
const model = m[1];
|
|
213
|
+
const f1 = m[2];
|
|
214
|
+
const f2 = m[3];
|
|
215
|
+
const modelVar = toVar(model);
|
|
216
|
+
const collection = toCollection(model);
|
|
217
|
+
const declared = ctx.declaredVars || new Set();
|
|
218
|
+
if (declared.has(modelVar)) {
|
|
219
|
+
return ` // Step ${ctx.stepNum}: Find ${model} by ${f1} and ${f2} (already loaded)`;
|
|
220
|
+
}
|
|
221
|
+
declared.add(modelVar);
|
|
222
|
+
// `let` for downstream conditional-create reassignment.
|
|
223
|
+
// Field-source resolution: a field like `userId` typically comes
|
|
224
|
+
// from a prior find/create (`user._id`), not from the action's
|
|
225
|
+
// request body. Check declaredVars first — if there's a model var
|
|
226
|
+
// matching the field's prefix (userId → user, gameId → game),
|
|
227
|
+
// use its `_id`. Otherwise fall back to args. Exclude the CURRENT
|
|
228
|
+
// model var (we're loading it now; it can't reference itself).
|
|
229
|
+
const resolveFieldSource = (f: string) => {
|
|
230
|
+
const stripIdMatch = f.match(/^(.+)Id$/);
|
|
231
|
+
if (stripIdMatch) {
|
|
232
|
+
const candidate = stripIdMatch[1];
|
|
233
|
+
if (candidate !== modelVar && declared.has(candidate)) {
|
|
234
|
+
return `(${candidate} as any)?._id ?? (${candidate} as any)?.id`;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return `args.${f}`;
|
|
238
|
+
};
|
|
239
|
+
const f1Src = resolveFieldSource(f1);
|
|
240
|
+
const f2Src = resolveFieldSource(f2);
|
|
241
|
+
return ` // Step ${ctx.stepNum}: Find ${model} by ${f1} and ${f2}
|
|
242
|
+
const ${modelVar}_collection = await getCollection('${collection}');
|
|
243
|
+
let ${modelVar} = await ${modelVar}_collection.findOne({ ${f1}: ${f1Src}, ${f2}: ${f2Src} });
|
|
244
|
+
if (${modelVar} && !(${modelVar} as any).id && (${modelVar} as any)._id) (${modelVar} as any).id = String((${modelVar} as any)._id);`;
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
// --- Create model record ---
|
|
249
|
+
// Matches: "Create X", "Create new X with ..."
|
|
250
|
+
{
|
|
251
|
+
name: 'create',
|
|
252
|
+
pattern: /^create\s+(?:new\s+)?(\w+)(?:\s+(?:with\s+)?(.+))?/i,
|
|
253
|
+
generateCall: (m, ctx) => {
|
|
254
|
+
const model = m[1];
|
|
255
|
+
const modelVar = toVar(model);
|
|
256
|
+
const collection = toCollection(model);
|
|
257
|
+
const params = ctx.parameterNames || [];
|
|
258
|
+
const declared = ctx.declaredVars || new Set();
|
|
259
|
+
declared.add(modelVar);
|
|
260
|
+
const dataExpr = params.length > 0 ? `{ ${params.join(', ')} }` : 'args';
|
|
261
|
+
return ` // Step ${ctx.stepNum}: Create ${model}
|
|
262
|
+
const ${modelVar}_collection = await getCollection('${collection}');
|
|
263
|
+
const ${modelVar}_inserted = await ${modelVar}_collection.insertOne(${dataExpr});
|
|
264
|
+
const ${modelVar} = { _id: ${modelVar}_inserted.insertedId, ...${dataExpr} };`;
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
// --- Update specific field on previously-loaded model ---
|
|
269
|
+
// Matches: "Update X field to value". Only fires when X has been
|
|
270
|
+
// declared by a prior matched find — otherwise the model var doesn't
|
|
271
|
+
// exist in scope and tsc fails. Falling through to the unmatched path
|
|
272
|
+
// lets the AI layer handle it.
|
|
273
|
+
{
|
|
274
|
+
name: 'update-field',
|
|
275
|
+
pattern: /^update\s+(\w+)\s+(\w+)\s+to\s+(.+)/i,
|
|
276
|
+
generateCall: (m, ctx) => {
|
|
277
|
+
const model = m[1];
|
|
278
|
+
const field = m[2];
|
|
279
|
+
const rawValue = m[3];
|
|
280
|
+
const modelVar = toVar(model);
|
|
281
|
+
if (!ctx.declaredVars?.has(modelVar)) return ''; // signal "not really matched"
|
|
282
|
+
const collection = toCollection(model);
|
|
283
|
+
const val = resolveValue(rawValue, ctx);
|
|
284
|
+
return ` // Step ${ctx.stepNum}: Update ${model}.${field} to ${rawValue.trim()}
|
|
285
|
+
{
|
|
286
|
+
const ${modelVar}_collection = await getCollection('${collection}');
|
|
287
|
+
await ${modelVar}_collection.updateOne({ _id: ${modelVar}._id }, { $set: { ${field}: ${val} } });
|
|
288
|
+
}`;
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
// --- Update field timestamp (e.g. "Update device lastLoginAt timestamp") ---
|
|
293
|
+
{
|
|
294
|
+
name: 'update-field-timestamp',
|
|
295
|
+
pattern: /^update\s+(\w+)\s+(\w+)\s+timestamp$/i,
|
|
296
|
+
generateCall: (m, ctx) => {
|
|
297
|
+
const model = m[1];
|
|
298
|
+
const field = m[2];
|
|
299
|
+
const modelVar = toVar(model);
|
|
300
|
+
if (!ctx.declaredVars?.has(modelVar)) return '';
|
|
301
|
+
const collection = toCollection(model);
|
|
302
|
+
return ` // Step ${ctx.stepNum}: Update ${model}.${field} timestamp
|
|
303
|
+
{
|
|
304
|
+
const ${modelVar}_collection = await getCollection('${collection}');
|
|
305
|
+
await ${modelVar}_collection.updateOne({ _id: ${modelVar}._id }, { $set: { ${field}: new Date().toISOString() } });
|
|
306
|
+
}`;
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
|
|
310
|
+
// --- Generic "Update X" (writes args back to record) ---
|
|
311
|
+
{
|
|
312
|
+
name: 'update',
|
|
313
|
+
pattern: /^update\s+(\w+)(?:\s+(.+))?$/i,
|
|
314
|
+
generateCall: (m, ctx) => {
|
|
315
|
+
const model = m[1];
|
|
316
|
+
const modelVar = toVar(model);
|
|
317
|
+
if (!ctx.declaredVars?.has(modelVar)) return '';
|
|
318
|
+
const collection = toCollection(model);
|
|
319
|
+
return ` // Step ${ctx.stepNum}: Update ${model}
|
|
320
|
+
{
|
|
321
|
+
const ${modelVar}_collection = await getCollection('${collection}');
|
|
322
|
+
await ${modelVar}_collection.updateOne({ _id: ${modelVar}._id }, { $set: args });
|
|
323
|
+
}`;
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
// --- Delete model record ---
|
|
328
|
+
{
|
|
329
|
+
name: 'delete',
|
|
330
|
+
pattern: /^delete\s+(\w+)/i,
|
|
331
|
+
generateCall: (m, ctx) => {
|
|
332
|
+
const model = m[1];
|
|
333
|
+
const modelVar = toVar(model);
|
|
334
|
+
if (!ctx.declaredVars?.has(modelVar)) return '';
|
|
335
|
+
const collection = toCollection(model);
|
|
336
|
+
return ` // Step ${ctx.stepNum}: Delete ${model}
|
|
337
|
+
{
|
|
338
|
+
const ${modelVar}_collection = await getCollection('${collection}');
|
|
339
|
+
await ${modelVar}_collection.deleteOne({ _id: ${modelVar}._id });
|
|
340
|
+
}`;
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
|
|
344
|
+
// --- Transition to lifecycle state ---
|
|
345
|
+
// Matches: "Transition X to state"
|
|
346
|
+
{
|
|
347
|
+
name: 'transition',
|
|
348
|
+
pattern: /^transition\s+(\w+)\s+to\s+(\w+)/i,
|
|
349
|
+
generateCall: (m, ctx) => {
|
|
350
|
+
const model = m[1];
|
|
351
|
+
const state = m[2];
|
|
352
|
+
const modelVar = toVar(model);
|
|
353
|
+
if (!ctx.declaredVars?.has(modelVar)) return '';
|
|
354
|
+
const collection = toCollection(model);
|
|
355
|
+
return ` // Step ${ctx.stepNum}: Transition ${model} to ${state}
|
|
356
|
+
if ((${modelVar} as any).status === '${state}') throw new Error('${model} is already ${state}');
|
|
357
|
+
{
|
|
358
|
+
const ${modelVar}_collection = await getCollection('${collection}');
|
|
359
|
+
await ${modelVar}_collection.updateOne({ _id: ${modelVar}._id }, { $set: { status: '${state}' } });
|
|
360
|
+
}`;
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
|
|
364
|
+
// --- Set field to value (on the controller's primary model) ---
|
|
365
|
+
{
|
|
366
|
+
name: 'set',
|
|
367
|
+
pattern: /^set\s+(\w+)\s+to\s+(.+)/i,
|
|
368
|
+
generateCall: (m, ctx) => {
|
|
369
|
+
const field = m[1];
|
|
370
|
+
const rawValue = m[2];
|
|
371
|
+
const modelVar = toVar(ctx.modelName);
|
|
372
|
+
const val = resolveValue(rawValue, ctx);
|
|
373
|
+
return ` // Step ${ctx.stepNum}: Set ${field} to ${rawValue.trim()}
|
|
374
|
+
{
|
|
375
|
+
const ${modelVar}_collection = await getCollection(COLLECTION_NAME);
|
|
376
|
+
await ${modelVar}_collection.updateOne({ _id: ${modelVar}._id }, { $set: { ${field}: ${val} } });
|
|
377
|
+
}`;
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
|
|
381
|
+
// --- Increment / Decrement ---
|
|
382
|
+
{
|
|
383
|
+
name: 'increment',
|
|
384
|
+
pattern: /^increment\s+(\w+)\s+by\s+(\w+)/i,
|
|
385
|
+
generateCall: (m, ctx) => {
|
|
386
|
+
const field = m[1];
|
|
387
|
+
const amount = m[2];
|
|
388
|
+
const modelVar = toVar(ctx.modelName);
|
|
389
|
+
return ` // Step ${ctx.stepNum}: Increment ${field} by ${amount}
|
|
390
|
+
{
|
|
391
|
+
const ${modelVar}_collection = await getCollection(COLLECTION_NAME);
|
|
392
|
+
await ${modelVar}_collection.updateOne({ _id: ${modelVar}._id }, { $inc: { ${field}: ${amount} } });
|
|
393
|
+
}`;
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
name: 'decrement',
|
|
398
|
+
pattern: /^decrement\s+(\w+)\s+by\s+(\w+)/i,
|
|
399
|
+
generateCall: (m, ctx) => {
|
|
400
|
+
const field = m[1];
|
|
401
|
+
const amount = m[2];
|
|
402
|
+
const modelVar = toVar(ctx.modelName);
|
|
403
|
+
return ` // Step ${ctx.stepNum}: Decrement ${field} by ${amount}
|
|
404
|
+
{
|
|
405
|
+
const ${modelVar}_collection = await getCollection(COLLECTION_NAME);
|
|
406
|
+
await ${modelVar}_collection.updateOne({ _id: ${modelVar}._id }, { $inc: { ${field}: -${amount} } });
|
|
407
|
+
}`;
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
|
|
411
|
+
// ──────────────────────────────────────────────────────────────────
|
|
412
|
+
// SIDE-EFFECT CONVENTIONS — patterns that the generator handles
|
|
413
|
+
// mechanically. The LLM never sees these; persistence happens
|
|
414
|
+
// deterministically from the spec text.
|
|
415
|
+
// ──────────────────────────────────────────────────────────────────
|
|
416
|
+
|
|
417
|
+
// --- Persist / Save / Store {Model} record ---
|
|
418
|
+
// Matches: "Persist refresh token for revocation tracking",
|
|
419
|
+
// "Save user record", "Store session"
|
|
420
|
+
// Source of the record: most-recent step{N}Result if any (typical case
|
|
421
|
+
// — the prior AI step computed what to persist), else `args`.
|
|
422
|
+
{
|
|
423
|
+
name: 'persist',
|
|
424
|
+
pattern: /^(?:persist|save|store)\s+(\w+(?:\s+\w+)?)(?:\s+(?:for|to|record).*)?$/i,
|
|
425
|
+
generateCall: (m, ctx) => {
|
|
426
|
+
// m[1] may be "refresh token" — collapse to camelCase + collection
|
|
427
|
+
const target = toVar(m[1].replace(/\s+(.)/g, (_, c) => c.toUpperCase()));
|
|
428
|
+
const collection = toCollection(target);
|
|
429
|
+
const recordSrc = mostRecentStepResult(ctx) ?? 'args';
|
|
430
|
+
return ` // Step ${ctx.stepNum}: Persist ${m[1]}
|
|
431
|
+
{
|
|
432
|
+
const _coll = await getCollection('${collection}');
|
|
433
|
+
await _coll.insertOne(${recordSrc} && typeof ${recordSrc} === 'object' && !Array.isArray(${recordSrc}) ? ${recordSrc} as any : { value: ${recordSrc} });
|
|
434
|
+
}`;
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
|
|
438
|
+
// --- Conditional create: "If X does not exist, create new X with ..." ---
|
|
439
|
+
// Reuses the model variable already declared by a prior find; mutates
|
|
440
|
+
// it to point at the newly-created record so subsequent steps see it.
|
|
441
|
+
{
|
|
442
|
+
name: 'conditional-create',
|
|
443
|
+
pattern: /^if\s+(\w+)\s+does\s+not\s+exist,?\s+create\s+new\s+(\w+)(?:\s+with\s+.+)?$/i,
|
|
444
|
+
generateCall: (m, ctx) => {
|
|
445
|
+
const modelVar = toVar(m[1]);
|
|
446
|
+
const Model = pascal(m[2]);
|
|
447
|
+
const collection = toCollection(Model);
|
|
448
|
+
if (!ctx.declaredVars?.has(modelVar)) return ''; // need prior find
|
|
449
|
+
// Reassigns the original `let ${modelVar}` declared by find — so
|
|
450
|
+
// subsequent steps (AI or convention) see the newly-created record
|
|
451
|
+
// when the lookup originally returned null. Defaults come from the
|
|
452
|
+
// model spec (#43K-A): required fields not present in args get
|
|
453
|
+
// their type-default (level: 1, totalResources: '', etc.).
|
|
454
|
+
const defaults = deriveModelDefaults(Model, ctx);
|
|
455
|
+
const defaultsBlock = defaults.length > 0 ? defaults.join(', ') + ',' : '';
|
|
456
|
+
return ` // Step ${ctx.stepNum}: If ${modelVar} does not exist, create new ${Model}
|
|
457
|
+
if (!${modelVar}) {
|
|
458
|
+
const _newRecord = { ${defaultsBlock} ...args, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() };
|
|
459
|
+
const _ins = await (await getCollection('${collection}')).insertOne(_newRecord);
|
|
460
|
+
${modelVar} = { _id: _ins.insertedId, id: String(_ins.insertedId), ..._newRecord } as any;
|
|
461
|
+
} else if (${modelVar} && !(${modelVar} as any).id && (${modelVar} as any)._id) {
|
|
462
|
+
(${modelVar} as any).id = String((${modelVar} as any)._id);
|
|
463
|
+
}`;
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
|
|
467
|
+
// --- Conditional update: "If X exists, update Y" ---
|
|
468
|
+
// Updates a single field on the previously-loaded record. Field defaults
|
|
469
|
+
// to lastLoginAt-style timestamp when the step says "update Y" without
|
|
470
|
+
// a "to <value>" clause.
|
|
471
|
+
{
|
|
472
|
+
name: 'conditional-update',
|
|
473
|
+
pattern: /^if\s+(\w+)\s+exists,?\s+update\s+(\w+)(?:\s+(.+))?$/i,
|
|
474
|
+
generateCall: (m, ctx) => {
|
|
475
|
+
const modelVar = toVar(m[1]);
|
|
476
|
+
const field = m[2];
|
|
477
|
+
const collection = toCollection(m[1]);
|
|
478
|
+
if (!ctx.declaredVars?.has(modelVar)) return '';
|
|
479
|
+
return ` // Step ${ctx.stepNum}: If ${modelVar} exists, update ${field}
|
|
480
|
+
if (${modelVar}) {
|
|
481
|
+
await (await getCollection('${collection}')).updateOne(
|
|
482
|
+
{ _id: ${modelVar}._id },
|
|
483
|
+
{ $set: { ${field}: new Date().toISOString() } }
|
|
484
|
+
);
|
|
485
|
+
}`;
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
|
|
489
|
+
// --- Auto-create / Bulk fan-out create ---
|
|
490
|
+
// Matches: "Auto-create player profiles for all available games"
|
|
491
|
+
// "Create X records for all Y"
|
|
492
|
+
// Pure-mechanical: enumerate the source collection, build a record per
|
|
493
|
+
// item linking it to the most-recently-loaded model var (typically
|
|
494
|
+
// `user`), insertMany. No AI hop — domain-specific field defaults are
|
|
495
|
+
// limited to the link fields (userId, gameId) plus createdAt/updatedAt.
|
|
496
|
+
// Anything richer should be done in a follow-up step (e.g. "Set displayName...").
|
|
497
|
+
{
|
|
498
|
+
name: 'auto-create-loop',
|
|
499
|
+
pattern: /^(?:auto-create|bulk\s+create)\s+(\w+)\s+(?:profile|record|entry|entries)?s?\s+for\s+(?:all\s+)?(?:available\s+)?(\w+)$/i,
|
|
500
|
+
generateCall: (m, ctx) => {
|
|
501
|
+
const Model = pascal(m[1]);
|
|
502
|
+
const targetCollection = toCollection(Model);
|
|
503
|
+
const sourceCollection = m[2].toLowerCase();
|
|
504
|
+
// Identify the "owner" var. Owner is the FIRST-declared non-stepResult
|
|
505
|
+
// var (e.g. `user`); spec authors load the owner before enumerating
|
|
506
|
+
// sources of its children.
|
|
507
|
+
const declared = Array.from(ctx.declaredVars || []);
|
|
508
|
+
const ownerVar = declared.find((v) => !/^step\d+Result$/.test(v) && v !== 'args');
|
|
509
|
+
const sourceSingular = sourceCollection.replace(/s$/, '');
|
|
510
|
+
const linkField = sourceSingular + 'Id';
|
|
511
|
+
const ownerLinkField = ownerVar ? ownerVar + 'Id' : 'ownerId';
|
|
512
|
+
|
|
513
|
+
// Pull required-field defaults from the target model spec (#43K-A).
|
|
514
|
+
// Conventions can now read model attributes via ctx.models, so a
|
|
515
|
+
// Player record gets `level: 1, experience: 0, totalResources: '""'`
|
|
516
|
+
// automatically — no AI hop, no missing-required-field errors.
|
|
517
|
+
const modelDefaults = deriveModelDefaults(Model, ctx);
|
|
518
|
+
const defaultsBlock = modelDefaults.length > 0
|
|
519
|
+
? modelDefaults.filter((d) => !d.startsWith(`${ownerLinkField}:`) && !d.startsWith(`${linkField}:`)).join(', ')
|
|
520
|
+
: '';
|
|
521
|
+
|
|
522
|
+
return ` // Step ${ctx.stepNum}: Auto-create ${m[1]} ${m[2]} for all ${m[2]}
|
|
523
|
+
{
|
|
524
|
+
const _allItems = await (await getCollection('${sourceCollection}')).find({}).toArray();
|
|
525
|
+
const _ownerId = ${ownerVar ? `(${ownerVar} as any)?.id ?? (${ownerVar} as any)?._id` : 'null'};
|
|
526
|
+
const _records = _allItems.map((_item: any) => ({
|
|
527
|
+
${defaultsBlock ? defaultsBlock + ',' : ''}
|
|
528
|
+
${ownerLinkField}: _ownerId,
|
|
529
|
+
${linkField}: (_item as any).${sourceSingular}Id ?? (_item as any).id ?? String((_item as any)._id),
|
|
530
|
+
createdAt: new Date().toISOString(),
|
|
531
|
+
updatedAt: new Date().toISOString(),
|
|
532
|
+
}));
|
|
533
|
+
if (_records.length > 0) {
|
|
534
|
+
await (await getCollection('${targetCollection}')).insertMany(_records as any);
|
|
535
|
+
}
|
|
536
|
+
}`;
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
|
|
540
|
+
// --- "Otherwise create new X record" — pairs with prior conditional ---
|
|
541
|
+
// Emitted as an `else` branch attached to the conditional that came
|
|
542
|
+
// before. We don't enforce ordering at the convention level; if the
|
|
543
|
+
// author writes "Otherwise" without a prior "If ... does not exist,
|
|
544
|
+
// create" the emitted else lands without an if and tsc catches it.
|
|
545
|
+
{
|
|
546
|
+
name: 'otherwise-create',
|
|
547
|
+
pattern: /^otherwise\s+create\s+(?:new\s+)?(\w+)\s+record$/i,
|
|
548
|
+
generateCall: (m, ctx) => {
|
|
549
|
+
const Model = pascal(m[1]);
|
|
550
|
+
const modelVar = toVar(Model);
|
|
551
|
+
const collection = toCollection(Model);
|
|
552
|
+
// The model var was declared (as `let`) by the find that paired
|
|
553
|
+
// with the prior conditional-update; reassign it here so downstream
|
|
554
|
+
// steps see the new record.
|
|
555
|
+
const wasDeclared = ctx.declaredVars?.has(modelVar);
|
|
556
|
+
const declared = Array.from(ctx.declaredVars || []);
|
|
557
|
+
// Model-driven defaults from the spec — these may include FK fields
|
|
558
|
+
// (userId, gameId) that we'd ALSO derive from declaredVars below.
|
|
559
|
+
const defaults = deriveModelDefaults(Model, ctx);
|
|
560
|
+
// Track names already supplied by defaults so we don't double-emit.
|
|
561
|
+
const supplied = new Set<string>();
|
|
562
|
+
for (const entry of defaults) {
|
|
563
|
+
const colonIdx = entry.indexOf(':');
|
|
564
|
+
if (colonIdx > 0) supplied.add(entry.slice(0, colonIdx).trim());
|
|
565
|
+
}
|
|
566
|
+
// FK assignments from prior-declared model vars — only emit those
|
|
567
|
+
// not already provided by spec defaults (avoids TS1117 dup-keys).
|
|
568
|
+
const fkAssignments = declared
|
|
569
|
+
.filter((v) => v !== modelVar && !/^step\d+Result$/.test(v) && v !== 'args')
|
|
570
|
+
.filter((v) => !supplied.has(v + 'Id'))
|
|
571
|
+
.map((v) => {
|
|
572
|
+
supplied.add(v + 'Id');
|
|
573
|
+
return `${v}Id: (${v} as any)?._id ?? (${v} as any)?.id`;
|
|
574
|
+
})
|
|
575
|
+
.join(', ');
|
|
576
|
+
const defaultsBlock = defaults.length > 0 ? defaults.join(', ') + ',' : '';
|
|
577
|
+
ctx.declaredVars?.add(modelVar);
|
|
578
|
+
return ` // Step ${ctx.stepNum}: Otherwise create new ${Model} record
|
|
579
|
+
else {
|
|
580
|
+
const _newRecord = { ${defaultsBlock} ${fkAssignments ? fkAssignments + ',' : ''} ...args, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() };
|
|
581
|
+
const _ins = await (await getCollection('${collection}')).insertOne(_newRecord);
|
|
582
|
+
${wasDeclared ? `${modelVar} = { _id: _ins.insertedId, id: String(_ins.insertedId), ..._newRecord } as any;` : `const ${modelVar} = { _id: _ins.insertedId, id: String(_ins.insertedId), ..._newRecord };
|
|
583
|
+
void ${modelVar};`}
|
|
584
|
+
}`;
|
|
585
|
+
},
|
|
586
|
+
},
|
|
587
|
+
|
|
588
|
+
// --- Send/Emit/Publish event ---
|
|
589
|
+
// Emits an eventBus.publish call. The payload references the controller's
|
|
590
|
+
// primary model variable IF it was declared by a prior matched step;
|
|
591
|
+
// otherwise the payload is just operation + timestamp (the event still
|
|
592
|
+
// fires, just without a record-level id).
|
|
593
|
+
{
|
|
594
|
+
name: 'send-event',
|
|
595
|
+
pattern: /^(?:send|emit|publish)\s+(\w+)\s+event/i,
|
|
596
|
+
generateCall: (m, ctx) => {
|
|
597
|
+
const event = m[1];
|
|
598
|
+
const modelVar = toVar(ctx.modelName);
|
|
599
|
+
const hasModelVar = ctx.declaredVars?.has(modelVar);
|
|
600
|
+
const payload = hasModelVar
|
|
601
|
+
? `{ ${modelVar}Id: (${modelVar} as any)?._id, operation: '${ctx.operationName}', timestamp: new Date().toISOString() }`
|
|
602
|
+
: `{ operation: '${ctx.operationName}', timestamp: new Date().toISOString() }`;
|
|
603
|
+
return ` // Step ${ctx.stepNum}: Emit ${event} event
|
|
604
|
+
await eventBus.publish('${event}', ${payload} as any);`;
|
|
605
|
+
},
|
|
606
|
+
},
|
|
607
|
+
|
|
608
|
+
// --- Call service ---
|
|
609
|
+
{
|
|
610
|
+
name: 'call-service',
|
|
611
|
+
pattern: /^call\s+(\w+)\.(\w+)/i,
|
|
612
|
+
generateCall: (m, ctx) => {
|
|
613
|
+
const service = m[1];
|
|
614
|
+
const method = m[2];
|
|
615
|
+
const args = (ctx.parameterNames || []).join(', ');
|
|
616
|
+
return ` // Step ${ctx.stepNum}: Call ${service}.${method}
|
|
617
|
+
await (${toVar(service)} as any).${method}({ ${args} });`;
|
|
618
|
+
},
|
|
619
|
+
},
|
|
620
|
+
|
|
621
|
+
// --- Return ---
|
|
622
|
+
// Only matches when the returned value is a single declared identifier
|
|
623
|
+
// OR when the natural-language reference resolves to "the model" (e.g.
|
|
624
|
+
// "Return the user", "Return updated game"). Multi-word phrases like
|
|
625
|
+
// "Return state to caller" don't translate to valid JS, so they fall
|
|
626
|
+
// through to the unmatched-step path and emit a `// TODO:` line.
|
|
627
|
+
{
|
|
628
|
+
name: 'return-model',
|
|
629
|
+
pattern: /^return\s+(?:the\s+|updated\s+|created\s+)?(\w+)\s*$/i,
|
|
630
|
+
generateCall: (m, ctx) => {
|
|
631
|
+
const valueRaw = m[1].trim();
|
|
632
|
+
const declared = ctx.declaredVars || new Set();
|
|
633
|
+
const params = ctx.parameterNames || [];
|
|
634
|
+
const modelVar = toVar(ctx.modelName);
|
|
635
|
+
// Resolve to a declared variable, parameter, or the controller's
|
|
636
|
+
// primary model variable. Anything else doesn't have a JS expression
|
|
637
|
+
// — fall through (return unmatched so the TODO path fires).
|
|
638
|
+
if (valueRaw.toLowerCase() === ctx.modelName.toLowerCase() || valueRaw === modelVar) {
|
|
639
|
+
return ` // Step ${ctx.stepNum}: Return ${ctx.modelName}
|
|
640
|
+
return ${modelVar};`;
|
|
641
|
+
}
|
|
642
|
+
if (declared.has(valueRaw)) {
|
|
643
|
+
return ` // Step ${ctx.stepNum}: Return ${valueRaw}
|
|
644
|
+
return ${valueRaw};`;
|
|
645
|
+
}
|
|
646
|
+
if (params.includes(valueRaw)) {
|
|
647
|
+
return ` // Step ${ctx.stepNum}: Return ${valueRaw}
|
|
648
|
+
return args.${valueRaw};`;
|
|
649
|
+
}
|
|
650
|
+
// Unresolvable — emit a TODO comment + neutral return so generated
|
|
651
|
+
// code still compiles. Caller can replace.
|
|
652
|
+
return ` // Step ${ctx.stepNum}: Return ${valueRaw} — TODO: resolve binding
|
|
653
|
+
return null;`;
|
|
654
|
+
},
|
|
655
|
+
},
|
|
656
|
+
];
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Match a step against the mongo-native conventions.
|
|
660
|
+
*
|
|
661
|
+
* Matched: returns the inline native-driver code to emit AND advances the
|
|
662
|
+
* convention's bookkeeping (e.g. find/create add the model variable to
|
|
663
|
+
* declaredVars).
|
|
664
|
+
*
|
|
665
|
+
* Unmatched: returns the function name + inputs + resultVar that an
|
|
666
|
+
* AI-behaviors layer should produce for this step. The shape mirrors
|
|
667
|
+
* prisma's step-conventions matchStep so the orm-agnostic
|
|
668
|
+
* AI-behaviors-generator can drive either side without branching.
|
|
669
|
+
*/
|
|
670
|
+
export function matchMongoStep(
|
|
671
|
+
step: string,
|
|
672
|
+
ctx: MongoStepContext,
|
|
673
|
+
): {
|
|
674
|
+
matched: boolean;
|
|
675
|
+
call: string;
|
|
676
|
+
helperMethod?: string;
|
|
677
|
+
functionName?: string;
|
|
678
|
+
inputs?: string[];
|
|
679
|
+
resultVar?: string;
|
|
680
|
+
} {
|
|
681
|
+
// Mongo's AI-fallback uses `args.X` for parameter inputs because the
|
|
682
|
+
// mongo-native controller-generator wraps the action body with a single
|
|
683
|
+
// `args` parameter (e.g. `register(args)`) and indexes through it
|
|
684
|
+
// rather than destructuring at function entry. Declared variables stay
|
|
685
|
+
// bare (they were declared inside the body by previous steps).
|
|
686
|
+
const aiArgsExpr = (inputs: string[], paramNames: string[]) =>
|
|
687
|
+
inputs.length > 0
|
|
688
|
+
? `{ ${inputs.map((n) => paramNames.includes(n) ? `${n}: args.${n}` : n).join(', ')} }`
|
|
689
|
+
: '{}';
|
|
690
|
+
return matchAgainstConventions(step, ctx, MONGO_STEP_CONVENTIONS, aiArgsExpr);
|
|
691
|
+
}
|