@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,515 @@
|
|
|
1
|
+
import {
|
|
2
|
+
toVar,
|
|
3
|
+
matchAgainstConventions
|
|
4
|
+
} from "../_shared/step-matching.js";
|
|
5
|
+
function deriveModelDefaults(modelName, ctx) {
|
|
6
|
+
if (!ctx.models) return [];
|
|
7
|
+
const model = ctx.models[modelName] || ctx.models[modelName.charAt(0).toUpperCase() + modelName.slice(1)];
|
|
8
|
+
if (!model) return [];
|
|
9
|
+
const attrs = model.attributes;
|
|
10
|
+
if (!attrs) return [];
|
|
11
|
+
const list = Array.isArray(attrs) ? attrs.map((a) => [a.name, a]) : Object.entries(attrs);
|
|
12
|
+
const declaredVars = ctx.declaredVars || /* @__PURE__ */ new Set();
|
|
13
|
+
const out = [];
|
|
14
|
+
const conventionManaged = /* @__PURE__ */ new Set(["createdAt", "updatedAt", "id"]);
|
|
15
|
+
for (const [name, attr] of list) {
|
|
16
|
+
if (!name) continue;
|
|
17
|
+
if (conventionManaged.has(name)) continue;
|
|
18
|
+
const required = !!attr.required;
|
|
19
|
+
const hasDefault = attr.default !== void 0;
|
|
20
|
+
if (!required && !hasDefault) continue;
|
|
21
|
+
if (hasDefault) {
|
|
22
|
+
out.push(`${name}: ${formatDefault(attr.default, attr.type)}`);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const type = (attr.type || "String").toLowerCase();
|
|
26
|
+
if (type === "integer" || type === "int" || type === "number" || type === "float") {
|
|
27
|
+
const min = attr.min ?? 0;
|
|
28
|
+
out.push(`${name}: ${min}`);
|
|
29
|
+
} else if (type === "boolean") {
|
|
30
|
+
out.push(`${name}: false`);
|
|
31
|
+
} else if (type === "datetime" || type === "date") {
|
|
32
|
+
if (attr.auto === "now") {
|
|
33
|
+
out.push(`${name}: new Date().toISOString()`);
|
|
34
|
+
} else {
|
|
35
|
+
out.push(`${name}: new Date().toISOString()`);
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
const selfVar = modelName.charAt(0).toLowerCase() + modelName.slice(1);
|
|
39
|
+
const fkMatch = name.match(/^(.+)Id$/);
|
|
40
|
+
if (fkMatch && fkMatch[1] !== selfVar && declaredVars.has(fkMatch[1])) {
|
|
41
|
+
out.push(`${name}: (${fkMatch[1]} as any)?._id ?? (${fkMatch[1]} as any)?.id`);
|
|
42
|
+
} else {
|
|
43
|
+
out.push(`${name}: ''`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
function formatDefault(value, type) {
|
|
50
|
+
if (value === null || value === void 0) return "null";
|
|
51
|
+
if (typeof value === "boolean") return String(value);
|
|
52
|
+
if (typeof value === "number") return String(value);
|
|
53
|
+
if (typeof value === "string") {
|
|
54
|
+
if (/^-?\d+(\.\d+)?$/.test(value)) return value;
|
|
55
|
+
if (value === "true" || value === "false") return value;
|
|
56
|
+
if (value === "now" && (type === "DateTime" || type === "Date")) {
|
|
57
|
+
return "new Date().toISOString()";
|
|
58
|
+
}
|
|
59
|
+
return JSON.stringify(value);
|
|
60
|
+
}
|
|
61
|
+
return JSON.stringify(value);
|
|
62
|
+
}
|
|
63
|
+
function toCollection(modelName) {
|
|
64
|
+
return modelName.toLowerCase() + "s";
|
|
65
|
+
}
|
|
66
|
+
function resolveValue(rawValue, ctx) {
|
|
67
|
+
const value = rawValue.trim().replace(/^['"]|['"]$/g, "");
|
|
68
|
+
if (/^(current\s*time|now|timestamp)$/i.test(value)) return "new Date().toISOString()";
|
|
69
|
+
if (value === "true" || value === "false") return value;
|
|
70
|
+
if (/^-?\d+(\.\d+)?$/.test(value)) return value;
|
|
71
|
+
const declared = ctx.declaredVars || /* @__PURE__ */ new Set();
|
|
72
|
+
const params = ctx.parameterNames || [];
|
|
73
|
+
const rootMatch = value.match(/^([a-zA-Z_][a-zA-Z0-9_]*)(\.[a-zA-Z0-9_.]+)?$/);
|
|
74
|
+
if (rootMatch) {
|
|
75
|
+
const root = rootMatch[1];
|
|
76
|
+
if (declared.has(root) || params.includes(root)) return value;
|
|
77
|
+
if (params.length > 0) return `args.${value}`;
|
|
78
|
+
}
|
|
79
|
+
if (/\s/.test(value)) return `/* TODO: resolve "${value}" */ null`;
|
|
80
|
+
return `'${value.replace(/'/g, "\\'")}'`;
|
|
81
|
+
}
|
|
82
|
+
function mostRecentStepResult(ctx) {
|
|
83
|
+
const declared = Array.from(ctx.declaredVars || []);
|
|
84
|
+
const stepResults = declared.filter((v) => /^step\d+Result$/.test(v)).sort((a, b) => parseInt(b.slice(4), 10) - parseInt(a.slice(4), 10));
|
|
85
|
+
return stepResults[0] ?? null;
|
|
86
|
+
}
|
|
87
|
+
function pascal(model) {
|
|
88
|
+
return model.charAt(0).toUpperCase() + model.slice(1);
|
|
89
|
+
}
|
|
90
|
+
const MONGO_STEP_CONVENTIONS = [
|
|
91
|
+
// --- Find / Lookup by single field ---
|
|
92
|
+
// Matches: "Look up X by Y", "Find X by Y", "Find X by Y or fail with 404"
|
|
93
|
+
{
|
|
94
|
+
name: "find-by-field",
|
|
95
|
+
pattern: /^(?:look\s+up|find|fetch|get)\s+(\w+)\s+by\s+(\w+)(?:\s+or\s+fail.*)?$/i,
|
|
96
|
+
generateCall: (m, ctx) => {
|
|
97
|
+
const model = m[1];
|
|
98
|
+
const field = m[2];
|
|
99
|
+
const modelVar = toVar(model);
|
|
100
|
+
const collection = toCollection(model);
|
|
101
|
+
const params = ctx.parameterNames || [];
|
|
102
|
+
const declared = ctx.declaredVars || /* @__PURE__ */ new Set();
|
|
103
|
+
const idVal = field === "id" ? params.includes(`${modelVar}Id`) ? `args.${modelVar}Id` : "args.id" : params.includes(field) ? `args.${field}` : `args.${field}`;
|
|
104
|
+
if (declared.has(modelVar)) {
|
|
105
|
+
return ` // Step ${ctx.stepNum}: Find ${model} by ${field} (already loaded)`;
|
|
106
|
+
}
|
|
107
|
+
declared.add(modelVar);
|
|
108
|
+
const failOnMissing = /or\s+fail/i.test(m[0]);
|
|
109
|
+
return ` // Step ${ctx.stepNum}: Find ${model} by ${field}
|
|
110
|
+
const ${modelVar}_collection = await getCollection('${collection}');
|
|
111
|
+
let ${modelVar} = await ${modelVar}_collection.findOne({ ${field}: ${idVal} });
|
|
112
|
+
if (${modelVar} && !(${modelVar} as any).id && (${modelVar} as any)._id) (${modelVar} as any).id = String((${modelVar} as any)._id);${failOnMissing ? `
|
|
113
|
+
if (!${modelVar}) throw new Error('${model} not found');` : ""}`;
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
// --- Find by composite (two fields) ---
|
|
117
|
+
// Matches: "Look up X by Y and Z", "Find X by Y and Z"
|
|
118
|
+
{
|
|
119
|
+
name: "find-by-composite",
|
|
120
|
+
pattern: /^(?:look\s+up|find|fetch|get)(?:\s+existing)?\s+(\w+)\s+by\s+(\w+)\s+and\s+(\w+)$/i,
|
|
121
|
+
generateCall: (m, ctx) => {
|
|
122
|
+
const model = m[1];
|
|
123
|
+
const f1 = m[2];
|
|
124
|
+
const f2 = m[3];
|
|
125
|
+
const modelVar = toVar(model);
|
|
126
|
+
const collection = toCollection(model);
|
|
127
|
+
const declared = ctx.declaredVars || /* @__PURE__ */ new Set();
|
|
128
|
+
if (declared.has(modelVar)) {
|
|
129
|
+
return ` // Step ${ctx.stepNum}: Find ${model} by ${f1} and ${f2} (already loaded)`;
|
|
130
|
+
}
|
|
131
|
+
declared.add(modelVar);
|
|
132
|
+
const resolveFieldSource = (f) => {
|
|
133
|
+
const stripIdMatch = f.match(/^(.+)Id$/);
|
|
134
|
+
if (stripIdMatch) {
|
|
135
|
+
const candidate = stripIdMatch[1];
|
|
136
|
+
if (candidate !== modelVar && declared.has(candidate)) {
|
|
137
|
+
return `(${candidate} as any)?._id ?? (${candidate} as any)?.id`;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return `args.${f}`;
|
|
141
|
+
};
|
|
142
|
+
const f1Src = resolveFieldSource(f1);
|
|
143
|
+
const f2Src = resolveFieldSource(f2);
|
|
144
|
+
return ` // Step ${ctx.stepNum}: Find ${model} by ${f1} and ${f2}
|
|
145
|
+
const ${modelVar}_collection = await getCollection('${collection}');
|
|
146
|
+
let ${modelVar} = await ${modelVar}_collection.findOne({ ${f1}: ${f1Src}, ${f2}: ${f2Src} });
|
|
147
|
+
if (${modelVar} && !(${modelVar} as any).id && (${modelVar} as any)._id) (${modelVar} as any).id = String((${modelVar} as any)._id);`;
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
// --- Create model record ---
|
|
151
|
+
// Matches: "Create X", "Create new X with ..."
|
|
152
|
+
{
|
|
153
|
+
name: "create",
|
|
154
|
+
pattern: /^create\s+(?:new\s+)?(\w+)(?:\s+(?:with\s+)?(.+))?/i,
|
|
155
|
+
generateCall: (m, ctx) => {
|
|
156
|
+
const model = m[1];
|
|
157
|
+
const modelVar = toVar(model);
|
|
158
|
+
const collection = toCollection(model);
|
|
159
|
+
const params = ctx.parameterNames || [];
|
|
160
|
+
const declared = ctx.declaredVars || /* @__PURE__ */ new Set();
|
|
161
|
+
declared.add(modelVar);
|
|
162
|
+
const dataExpr = params.length > 0 ? `{ ${params.join(", ")} }` : "args";
|
|
163
|
+
return ` // Step ${ctx.stepNum}: Create ${model}
|
|
164
|
+
const ${modelVar}_collection = await getCollection('${collection}');
|
|
165
|
+
const ${modelVar}_inserted = await ${modelVar}_collection.insertOne(${dataExpr});
|
|
166
|
+
const ${modelVar} = { _id: ${modelVar}_inserted.insertedId, ...${dataExpr} };`;
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
// --- Update specific field on previously-loaded model ---
|
|
170
|
+
// Matches: "Update X field to value". Only fires when X has been
|
|
171
|
+
// declared by a prior matched find — otherwise the model var doesn't
|
|
172
|
+
// exist in scope and tsc fails. Falling through to the unmatched path
|
|
173
|
+
// lets the AI layer handle it.
|
|
174
|
+
{
|
|
175
|
+
name: "update-field",
|
|
176
|
+
pattern: /^update\s+(\w+)\s+(\w+)\s+to\s+(.+)/i,
|
|
177
|
+
generateCall: (m, ctx) => {
|
|
178
|
+
const model = m[1];
|
|
179
|
+
const field = m[2];
|
|
180
|
+
const rawValue = m[3];
|
|
181
|
+
const modelVar = toVar(model);
|
|
182
|
+
if (!ctx.declaredVars?.has(modelVar)) return "";
|
|
183
|
+
const collection = toCollection(model);
|
|
184
|
+
const val = resolveValue(rawValue, ctx);
|
|
185
|
+
return ` // Step ${ctx.stepNum}: Update ${model}.${field} to ${rawValue.trim()}
|
|
186
|
+
{
|
|
187
|
+
const ${modelVar}_collection = await getCollection('${collection}');
|
|
188
|
+
await ${modelVar}_collection.updateOne({ _id: ${modelVar}._id }, { $set: { ${field}: ${val} } });
|
|
189
|
+
}`;
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
// --- Update field timestamp (e.g. "Update device lastLoginAt timestamp") ---
|
|
193
|
+
{
|
|
194
|
+
name: "update-field-timestamp",
|
|
195
|
+
pattern: /^update\s+(\w+)\s+(\w+)\s+timestamp$/i,
|
|
196
|
+
generateCall: (m, ctx) => {
|
|
197
|
+
const model = m[1];
|
|
198
|
+
const field = m[2];
|
|
199
|
+
const modelVar = toVar(model);
|
|
200
|
+
if (!ctx.declaredVars?.has(modelVar)) return "";
|
|
201
|
+
const collection = toCollection(model);
|
|
202
|
+
return ` // Step ${ctx.stepNum}: Update ${model}.${field} timestamp
|
|
203
|
+
{
|
|
204
|
+
const ${modelVar}_collection = await getCollection('${collection}');
|
|
205
|
+
await ${modelVar}_collection.updateOne({ _id: ${modelVar}._id }, { $set: { ${field}: new Date().toISOString() } });
|
|
206
|
+
}`;
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
// --- Generic "Update X" (writes args back to record) ---
|
|
210
|
+
{
|
|
211
|
+
name: "update",
|
|
212
|
+
pattern: /^update\s+(\w+)(?:\s+(.+))?$/i,
|
|
213
|
+
generateCall: (m, ctx) => {
|
|
214
|
+
const model = m[1];
|
|
215
|
+
const modelVar = toVar(model);
|
|
216
|
+
if (!ctx.declaredVars?.has(modelVar)) return "";
|
|
217
|
+
const collection = toCollection(model);
|
|
218
|
+
return ` // Step ${ctx.stepNum}: Update ${model}
|
|
219
|
+
{
|
|
220
|
+
const ${modelVar}_collection = await getCollection('${collection}');
|
|
221
|
+
await ${modelVar}_collection.updateOne({ _id: ${modelVar}._id }, { $set: args });
|
|
222
|
+
}`;
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
// --- Delete model record ---
|
|
226
|
+
{
|
|
227
|
+
name: "delete",
|
|
228
|
+
pattern: /^delete\s+(\w+)/i,
|
|
229
|
+
generateCall: (m, ctx) => {
|
|
230
|
+
const model = m[1];
|
|
231
|
+
const modelVar = toVar(model);
|
|
232
|
+
if (!ctx.declaredVars?.has(modelVar)) return "";
|
|
233
|
+
const collection = toCollection(model);
|
|
234
|
+
return ` // Step ${ctx.stepNum}: Delete ${model}
|
|
235
|
+
{
|
|
236
|
+
const ${modelVar}_collection = await getCollection('${collection}');
|
|
237
|
+
await ${modelVar}_collection.deleteOne({ _id: ${modelVar}._id });
|
|
238
|
+
}`;
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
// --- Transition to lifecycle state ---
|
|
242
|
+
// Matches: "Transition X to state"
|
|
243
|
+
{
|
|
244
|
+
name: "transition",
|
|
245
|
+
pattern: /^transition\s+(\w+)\s+to\s+(\w+)/i,
|
|
246
|
+
generateCall: (m, ctx) => {
|
|
247
|
+
const model = m[1];
|
|
248
|
+
const state = m[2];
|
|
249
|
+
const modelVar = toVar(model);
|
|
250
|
+
if (!ctx.declaredVars?.has(modelVar)) return "";
|
|
251
|
+
const collection = toCollection(model);
|
|
252
|
+
return ` // Step ${ctx.stepNum}: Transition ${model} to ${state}
|
|
253
|
+
if ((${modelVar} as any).status === '${state}') throw new Error('${model} is already ${state}');
|
|
254
|
+
{
|
|
255
|
+
const ${modelVar}_collection = await getCollection('${collection}');
|
|
256
|
+
await ${modelVar}_collection.updateOne({ _id: ${modelVar}._id }, { $set: { status: '${state}' } });
|
|
257
|
+
}`;
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
// --- Set field to value (on the controller's primary model) ---
|
|
261
|
+
{
|
|
262
|
+
name: "set",
|
|
263
|
+
pattern: /^set\s+(\w+)\s+to\s+(.+)/i,
|
|
264
|
+
generateCall: (m, ctx) => {
|
|
265
|
+
const field = m[1];
|
|
266
|
+
const rawValue = m[2];
|
|
267
|
+
const modelVar = toVar(ctx.modelName);
|
|
268
|
+
const val = resolveValue(rawValue, ctx);
|
|
269
|
+
return ` // Step ${ctx.stepNum}: Set ${field} to ${rawValue.trim()}
|
|
270
|
+
{
|
|
271
|
+
const ${modelVar}_collection = await getCollection(COLLECTION_NAME);
|
|
272
|
+
await ${modelVar}_collection.updateOne({ _id: ${modelVar}._id }, { $set: { ${field}: ${val} } });
|
|
273
|
+
}`;
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
// --- Increment / Decrement ---
|
|
277
|
+
{
|
|
278
|
+
name: "increment",
|
|
279
|
+
pattern: /^increment\s+(\w+)\s+by\s+(\w+)/i,
|
|
280
|
+
generateCall: (m, ctx) => {
|
|
281
|
+
const field = m[1];
|
|
282
|
+
const amount = m[2];
|
|
283
|
+
const modelVar = toVar(ctx.modelName);
|
|
284
|
+
return ` // Step ${ctx.stepNum}: Increment ${field} by ${amount}
|
|
285
|
+
{
|
|
286
|
+
const ${modelVar}_collection = await getCollection(COLLECTION_NAME);
|
|
287
|
+
await ${modelVar}_collection.updateOne({ _id: ${modelVar}._id }, { $inc: { ${field}: ${amount} } });
|
|
288
|
+
}`;
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
name: "decrement",
|
|
293
|
+
pattern: /^decrement\s+(\w+)\s+by\s+(\w+)/i,
|
|
294
|
+
generateCall: (m, ctx) => {
|
|
295
|
+
const field = m[1];
|
|
296
|
+
const amount = m[2];
|
|
297
|
+
const modelVar = toVar(ctx.modelName);
|
|
298
|
+
return ` // Step ${ctx.stepNum}: Decrement ${field} by ${amount}
|
|
299
|
+
{
|
|
300
|
+
const ${modelVar}_collection = await getCollection(COLLECTION_NAME);
|
|
301
|
+
await ${modelVar}_collection.updateOne({ _id: ${modelVar}._id }, { $inc: { ${field}: -${amount} } });
|
|
302
|
+
}`;
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
// ──────────────────────────────────────────────────────────────────
|
|
306
|
+
// SIDE-EFFECT CONVENTIONS — patterns that the generator handles
|
|
307
|
+
// mechanically. The LLM never sees these; persistence happens
|
|
308
|
+
// deterministically from the spec text.
|
|
309
|
+
// ──────────────────────────────────────────────────────────────────
|
|
310
|
+
// --- Persist / Save / Store {Model} record ---
|
|
311
|
+
// Matches: "Persist refresh token for revocation tracking",
|
|
312
|
+
// "Save user record", "Store session"
|
|
313
|
+
// Source of the record: most-recent step{N}Result if any (typical case
|
|
314
|
+
// — the prior AI step computed what to persist), else `args`.
|
|
315
|
+
{
|
|
316
|
+
name: "persist",
|
|
317
|
+
pattern: /^(?:persist|save|store)\s+(\w+(?:\s+\w+)?)(?:\s+(?:for|to|record).*)?$/i,
|
|
318
|
+
generateCall: (m, ctx) => {
|
|
319
|
+
const target = toVar(m[1].replace(/\s+(.)/g, (_, c) => c.toUpperCase()));
|
|
320
|
+
const collection = toCollection(target);
|
|
321
|
+
const recordSrc = mostRecentStepResult(ctx) ?? "args";
|
|
322
|
+
return ` // Step ${ctx.stepNum}: Persist ${m[1]}
|
|
323
|
+
{
|
|
324
|
+
const _coll = await getCollection('${collection}');
|
|
325
|
+
await _coll.insertOne(${recordSrc} && typeof ${recordSrc} === 'object' && !Array.isArray(${recordSrc}) ? ${recordSrc} as any : { value: ${recordSrc} });
|
|
326
|
+
}`;
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
// --- Conditional create: "If X does not exist, create new X with ..." ---
|
|
330
|
+
// Reuses the model variable already declared by a prior find; mutates
|
|
331
|
+
// it to point at the newly-created record so subsequent steps see it.
|
|
332
|
+
{
|
|
333
|
+
name: "conditional-create",
|
|
334
|
+
pattern: /^if\s+(\w+)\s+does\s+not\s+exist,?\s+create\s+new\s+(\w+)(?:\s+with\s+.+)?$/i,
|
|
335
|
+
generateCall: (m, ctx) => {
|
|
336
|
+
const modelVar = toVar(m[1]);
|
|
337
|
+
const Model = pascal(m[2]);
|
|
338
|
+
const collection = toCollection(Model);
|
|
339
|
+
if (!ctx.declaredVars?.has(modelVar)) return "";
|
|
340
|
+
const defaults = deriveModelDefaults(Model, ctx);
|
|
341
|
+
const defaultsBlock = defaults.length > 0 ? defaults.join(", ") + "," : "";
|
|
342
|
+
return ` // Step ${ctx.stepNum}: If ${modelVar} does not exist, create new ${Model}
|
|
343
|
+
if (!${modelVar}) {
|
|
344
|
+
const _newRecord = { ${defaultsBlock} ...args, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() };
|
|
345
|
+
const _ins = await (await getCollection('${collection}')).insertOne(_newRecord);
|
|
346
|
+
${modelVar} = { _id: _ins.insertedId, id: String(_ins.insertedId), ..._newRecord } as any;
|
|
347
|
+
} else if (${modelVar} && !(${modelVar} as any).id && (${modelVar} as any)._id) {
|
|
348
|
+
(${modelVar} as any).id = String((${modelVar} as any)._id);
|
|
349
|
+
}`;
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
// --- Conditional update: "If X exists, update Y" ---
|
|
353
|
+
// Updates a single field on the previously-loaded record. Field defaults
|
|
354
|
+
// to lastLoginAt-style timestamp when the step says "update Y" without
|
|
355
|
+
// a "to <value>" clause.
|
|
356
|
+
{
|
|
357
|
+
name: "conditional-update",
|
|
358
|
+
pattern: /^if\s+(\w+)\s+exists,?\s+update\s+(\w+)(?:\s+(.+))?$/i,
|
|
359
|
+
generateCall: (m, ctx) => {
|
|
360
|
+
const modelVar = toVar(m[1]);
|
|
361
|
+
const field = m[2];
|
|
362
|
+
const collection = toCollection(m[1]);
|
|
363
|
+
if (!ctx.declaredVars?.has(modelVar)) return "";
|
|
364
|
+
return ` // Step ${ctx.stepNum}: If ${modelVar} exists, update ${field}
|
|
365
|
+
if (${modelVar}) {
|
|
366
|
+
await (await getCollection('${collection}')).updateOne(
|
|
367
|
+
{ _id: ${modelVar}._id },
|
|
368
|
+
{ $set: { ${field}: new Date().toISOString() } }
|
|
369
|
+
);
|
|
370
|
+
}`;
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
// --- Auto-create / Bulk fan-out create ---
|
|
374
|
+
// Matches: "Auto-create player profiles for all available games"
|
|
375
|
+
// "Create X records for all Y"
|
|
376
|
+
// Pure-mechanical: enumerate the source collection, build a record per
|
|
377
|
+
// item linking it to the most-recently-loaded model var (typically
|
|
378
|
+
// `user`), insertMany. No AI hop — domain-specific field defaults are
|
|
379
|
+
// limited to the link fields (userId, gameId) plus createdAt/updatedAt.
|
|
380
|
+
// Anything richer should be done in a follow-up step (e.g. "Set displayName...").
|
|
381
|
+
{
|
|
382
|
+
name: "auto-create-loop",
|
|
383
|
+
pattern: /^(?:auto-create|bulk\s+create)\s+(\w+)\s+(?:profile|record|entry|entries)?s?\s+for\s+(?:all\s+)?(?:available\s+)?(\w+)$/i,
|
|
384
|
+
generateCall: (m, ctx) => {
|
|
385
|
+
const Model = pascal(m[1]);
|
|
386
|
+
const targetCollection = toCollection(Model);
|
|
387
|
+
const sourceCollection = m[2].toLowerCase();
|
|
388
|
+
const declared = Array.from(ctx.declaredVars || []);
|
|
389
|
+
const ownerVar = declared.find((v) => !/^step\d+Result$/.test(v) && v !== "args");
|
|
390
|
+
const sourceSingular = sourceCollection.replace(/s$/, "");
|
|
391
|
+
const linkField = sourceSingular + "Id";
|
|
392
|
+
const ownerLinkField = ownerVar ? ownerVar + "Id" : "ownerId";
|
|
393
|
+
const modelDefaults = deriveModelDefaults(Model, ctx);
|
|
394
|
+
const defaultsBlock = modelDefaults.length > 0 ? modelDefaults.filter((d) => !d.startsWith(`${ownerLinkField}:`) && !d.startsWith(`${linkField}:`)).join(", ") : "";
|
|
395
|
+
return ` // Step ${ctx.stepNum}: Auto-create ${m[1]} ${m[2]} for all ${m[2]}
|
|
396
|
+
{
|
|
397
|
+
const _allItems = await (await getCollection('${sourceCollection}')).find({}).toArray();
|
|
398
|
+
const _ownerId = ${ownerVar ? `(${ownerVar} as any)?.id ?? (${ownerVar} as any)?._id` : "null"};
|
|
399
|
+
const _records = _allItems.map((_item: any) => ({
|
|
400
|
+
${defaultsBlock ? defaultsBlock + "," : ""}
|
|
401
|
+
${ownerLinkField}: _ownerId,
|
|
402
|
+
${linkField}: (_item as any).${sourceSingular}Id ?? (_item as any).id ?? String((_item as any)._id),
|
|
403
|
+
createdAt: new Date().toISOString(),
|
|
404
|
+
updatedAt: new Date().toISOString(),
|
|
405
|
+
}));
|
|
406
|
+
if (_records.length > 0) {
|
|
407
|
+
await (await getCollection('${targetCollection}')).insertMany(_records as any);
|
|
408
|
+
}
|
|
409
|
+
}`;
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
// --- "Otherwise create new X record" — pairs with prior conditional ---
|
|
413
|
+
// Emitted as an `else` branch attached to the conditional that came
|
|
414
|
+
// before. We don't enforce ordering at the convention level; if the
|
|
415
|
+
// author writes "Otherwise" without a prior "If ... does not exist,
|
|
416
|
+
// create" the emitted else lands without an if and tsc catches it.
|
|
417
|
+
{
|
|
418
|
+
name: "otherwise-create",
|
|
419
|
+
pattern: /^otherwise\s+create\s+(?:new\s+)?(\w+)\s+record$/i,
|
|
420
|
+
generateCall: (m, ctx) => {
|
|
421
|
+
const Model = pascal(m[1]);
|
|
422
|
+
const modelVar = toVar(Model);
|
|
423
|
+
const collection = toCollection(Model);
|
|
424
|
+
const wasDeclared = ctx.declaredVars?.has(modelVar);
|
|
425
|
+
const declared = Array.from(ctx.declaredVars || []);
|
|
426
|
+
const defaults = deriveModelDefaults(Model, ctx);
|
|
427
|
+
const supplied = /* @__PURE__ */ new Set();
|
|
428
|
+
for (const entry of defaults) {
|
|
429
|
+
const colonIdx = entry.indexOf(":");
|
|
430
|
+
if (colonIdx > 0) supplied.add(entry.slice(0, colonIdx).trim());
|
|
431
|
+
}
|
|
432
|
+
const fkAssignments = declared.filter((v) => v !== modelVar && !/^step\d+Result$/.test(v) && v !== "args").filter((v) => !supplied.has(v + "Id")).map((v) => {
|
|
433
|
+
supplied.add(v + "Id");
|
|
434
|
+
return `${v}Id: (${v} as any)?._id ?? (${v} as any)?.id`;
|
|
435
|
+
}).join(", ");
|
|
436
|
+
const defaultsBlock = defaults.length > 0 ? defaults.join(", ") + "," : "";
|
|
437
|
+
ctx.declaredVars?.add(modelVar);
|
|
438
|
+
return ` // Step ${ctx.stepNum}: Otherwise create new ${Model} record
|
|
439
|
+
else {
|
|
440
|
+
const _newRecord = { ${defaultsBlock} ${fkAssignments ? fkAssignments + "," : ""} ...args, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() };
|
|
441
|
+
const _ins = await (await getCollection('${collection}')).insertOne(_newRecord);
|
|
442
|
+
${wasDeclared ? `${modelVar} = { _id: _ins.insertedId, id: String(_ins.insertedId), ..._newRecord } as any;` : `const ${modelVar} = { _id: _ins.insertedId, id: String(_ins.insertedId), ..._newRecord };
|
|
443
|
+
void ${modelVar};`}
|
|
444
|
+
}`;
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
// --- Send/Emit/Publish event ---
|
|
448
|
+
// Emits an eventBus.publish call. The payload references the controller's
|
|
449
|
+
// primary model variable IF it was declared by a prior matched step;
|
|
450
|
+
// otherwise the payload is just operation + timestamp (the event still
|
|
451
|
+
// fires, just without a record-level id).
|
|
452
|
+
{
|
|
453
|
+
name: "send-event",
|
|
454
|
+
pattern: /^(?:send|emit|publish)\s+(\w+)\s+event/i,
|
|
455
|
+
generateCall: (m, ctx) => {
|
|
456
|
+
const event = m[1];
|
|
457
|
+
const modelVar = toVar(ctx.modelName);
|
|
458
|
+
const hasModelVar = ctx.declaredVars?.has(modelVar);
|
|
459
|
+
const payload = hasModelVar ? `{ ${modelVar}Id: (${modelVar} as any)?._id, operation: '${ctx.operationName}', timestamp: new Date().toISOString() }` : `{ operation: '${ctx.operationName}', timestamp: new Date().toISOString() }`;
|
|
460
|
+
return ` // Step ${ctx.stepNum}: Emit ${event} event
|
|
461
|
+
await eventBus.publish('${event}', ${payload} as any);`;
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
// --- Call service ---
|
|
465
|
+
{
|
|
466
|
+
name: "call-service",
|
|
467
|
+
pattern: /^call\s+(\w+)\.(\w+)/i,
|
|
468
|
+
generateCall: (m, ctx) => {
|
|
469
|
+
const service = m[1];
|
|
470
|
+
const method = m[2];
|
|
471
|
+
const args = (ctx.parameterNames || []).join(", ");
|
|
472
|
+
return ` // Step ${ctx.stepNum}: Call ${service}.${method}
|
|
473
|
+
await (${toVar(service)} as any).${method}({ ${args} });`;
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
// --- Return ---
|
|
477
|
+
// Only matches when the returned value is a single declared identifier
|
|
478
|
+
// OR when the natural-language reference resolves to "the model" (e.g.
|
|
479
|
+
// "Return the user", "Return updated game"). Multi-word phrases like
|
|
480
|
+
// "Return state to caller" don't translate to valid JS, so they fall
|
|
481
|
+
// through to the unmatched-step path and emit a `// TODO:` line.
|
|
482
|
+
{
|
|
483
|
+
name: "return-model",
|
|
484
|
+
pattern: /^return\s+(?:the\s+|updated\s+|created\s+)?(\w+)\s*$/i,
|
|
485
|
+
generateCall: (m, ctx) => {
|
|
486
|
+
const valueRaw = m[1].trim();
|
|
487
|
+
const declared = ctx.declaredVars || /* @__PURE__ */ new Set();
|
|
488
|
+
const params = ctx.parameterNames || [];
|
|
489
|
+
const modelVar = toVar(ctx.modelName);
|
|
490
|
+
if (valueRaw.toLowerCase() === ctx.modelName.toLowerCase() || valueRaw === modelVar) {
|
|
491
|
+
return ` // Step ${ctx.stepNum}: Return ${ctx.modelName}
|
|
492
|
+
return ${modelVar};`;
|
|
493
|
+
}
|
|
494
|
+
if (declared.has(valueRaw)) {
|
|
495
|
+
return ` // Step ${ctx.stepNum}: Return ${valueRaw}
|
|
496
|
+
return ${valueRaw};`;
|
|
497
|
+
}
|
|
498
|
+
if (params.includes(valueRaw)) {
|
|
499
|
+
return ` // Step ${ctx.stepNum}: Return ${valueRaw}
|
|
500
|
+
return args.${valueRaw};`;
|
|
501
|
+
}
|
|
502
|
+
return ` // Step ${ctx.stepNum}: Return ${valueRaw} \u2014 TODO: resolve binding
|
|
503
|
+
return null;`;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
];
|
|
507
|
+
function matchMongoStep(step, ctx) {
|
|
508
|
+
const aiArgsExpr = (inputs, paramNames) => inputs.length > 0 ? `{ ${inputs.map((n) => paramNames.includes(n) ? `${n}: args.${n}` : n).join(", ")} }` : "{}";
|
|
509
|
+
return matchAgainstConventions(step, ctx, MONGO_STEP_CONVENTIONS, aiArgsExpr);
|
|
510
|
+
}
|
|
511
|
+
export {
|
|
512
|
+
MONGO_STEP_CONVENTIONS,
|
|
513
|
+
deriveModelDefaults,
|
|
514
|
+
matchMongoStep
|
|
515
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
function generatePgClient(_context) {
|
|
2
|
+
return `/**
|
|
3
|
+
* Postgres native (pg) \u2014 singleton pool + helpers.
|
|
4
|
+
*
|
|
5
|
+
* Picks up POSTGRES_URL or DATABASE_URL from the environment. The pool is
|
|
6
|
+
* lazily initialised on first use and reused across requests; \`disconnect\`
|
|
7
|
+
* is exposed for graceful-shutdown wiring. \`withTx\` runs a callback in a
|
|
8
|
+
* single connection inside BEGIN/COMMIT (rolled back on throw).
|
|
9
|
+
*/
|
|
10
|
+
import { Pool, type PoolClient, type QueryResult, type QueryResultRow } from 'pg';
|
|
11
|
+
|
|
12
|
+
const connectionString =
|
|
13
|
+
process.env.POSTGRES_URL ||
|
|
14
|
+
process.env.DATABASE_URL ||
|
|
15
|
+
'postgres://postgres:postgres@localhost:5432/specverse';
|
|
16
|
+
|
|
17
|
+
let pool: Pool | null = null;
|
|
18
|
+
|
|
19
|
+
export function getPool(): Pool {
|
|
20
|
+
if (pool) return pool;
|
|
21
|
+
pool = new Pool({ connectionString });
|
|
22
|
+
return pool;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Run a parameterised query against the pool.
|
|
26
|
+
* T defaults to QueryResultRow so callers can pass a row interface. */
|
|
27
|
+
export async function query<T extends QueryResultRow = QueryResultRow>(
|
|
28
|
+
text: string,
|
|
29
|
+
params: ReadonlyArray<unknown> = [],
|
|
30
|
+
): Promise<QueryResult<T>> {
|
|
31
|
+
return getPool().query<T>(text, params as unknown[]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Run a callback inside a transaction. The callback receives a dedicated
|
|
35
|
+
* client; commit on resolve, rollback on throw. */
|
|
36
|
+
export async function withTx<T>(
|
|
37
|
+
fn: (client: PoolClient) => Promise<T>,
|
|
38
|
+
): Promise<T> {
|
|
39
|
+
const client = await getPool().connect();
|
|
40
|
+
try {
|
|
41
|
+
await client.query('BEGIN');
|
|
42
|
+
const result = await fn(client);
|
|
43
|
+
await client.query('COMMIT');
|
|
44
|
+
return result;
|
|
45
|
+
} catch (err) {
|
|
46
|
+
await client.query('ROLLBACK');
|
|
47
|
+
throw err;
|
|
48
|
+
} finally {
|
|
49
|
+
client.release();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Build a comma-separated list of \`"col"\` from an object's keys, plus
|
|
54
|
+
* matching positional placeholders \`$1, $2, ...\`. Quoted to preserve
|
|
55
|
+
* case-sensitive identifiers (camelCase columns work without folding). */
|
|
56
|
+
function buildPositionals(keys: string[], offset = 0): string {
|
|
57
|
+
return keys.map((_k, i) => '$' + (i + 1 + offset)).join(', ');
|
|
58
|
+
}
|
|
59
|
+
function quoteCols(keys: string[]): string {
|
|
60
|
+
return keys.map((k) => '"' + k.replace(/"/g, '""') + '"').join(', ');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** INSERT a record into \`table\` and return the inserted row (with any
|
|
64
|
+
* database-supplied defaults like generated id / createdAt populated). */
|
|
65
|
+
export async function insertOne<T extends QueryResultRow = QueryResultRow>(
|
|
66
|
+
table: string,
|
|
67
|
+
record: Record<string, unknown>,
|
|
68
|
+
): Promise<T> {
|
|
69
|
+
const keys = Object.keys(record);
|
|
70
|
+
const values = keys.map((k) => record[k]);
|
|
71
|
+
const sql = keys.length === 0
|
|
72
|
+
? \`INSERT INTO "\${table}" DEFAULT VALUES RETURNING *\`
|
|
73
|
+
: \`INSERT INTO "\${table}" (\${quoteCols(keys)}) VALUES (\${buildPositionals(keys)}) RETURNING *\`;
|
|
74
|
+
const result = await query<T>(sql, values);
|
|
75
|
+
return result.rows[0] as T;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Bulk-insert many records into \`table\` in one round trip. Returns the
|
|
79
|
+
* inserted rows. All records must share the same shape. */
|
|
80
|
+
export async function insertMany<T extends QueryResultRow = QueryResultRow>(
|
|
81
|
+
table: string,
|
|
82
|
+
records: ReadonlyArray<Record<string, unknown>>,
|
|
83
|
+
): Promise<T[]> {
|
|
84
|
+
if (records.length === 0) return [];
|
|
85
|
+
const firstKeys = Object.keys(records[0]!);
|
|
86
|
+
const valuesClauses: string[] = [];
|
|
87
|
+
const flatValues: unknown[] = [];
|
|
88
|
+
for (let i = 0; i < records.length; i++) {
|
|
89
|
+
const row = records[i] as Record<string, unknown>;
|
|
90
|
+
const placeholders = firstKeys.map((_k, j) => '$' + (i * firstKeys.length + j + 1)).join(', ');
|
|
91
|
+
valuesClauses.push('(' + placeholders + ')');
|
|
92
|
+
for (const k of firstKeys) flatValues.push(row[k]);
|
|
93
|
+
}
|
|
94
|
+
const sql = \`INSERT INTO "\${table}" (\${quoteCols(firstKeys)}) VALUES \${valuesClauses.join(', ')} RETURNING *\`;
|
|
95
|
+
const result = await query<T>(sql, flatValues);
|
|
96
|
+
return result.rows;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** SELECT a single row by a field equality. Returns null if not found. */
|
|
100
|
+
export async function findOneByField<T extends QueryResultRow = QueryResultRow>(
|
|
101
|
+
table: string,
|
|
102
|
+
field: string,
|
|
103
|
+
value: unknown,
|
|
104
|
+
): Promise<T | null> {
|
|
105
|
+
const sql = \`SELECT * FROM "\${table}" WHERE "\${field.replace(/"/g, '""')}" = $1 LIMIT 1\`;
|
|
106
|
+
const result = await query<T>(sql, [value]);
|
|
107
|
+
return result.rows[0] ?? null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** SELECT a single row matching all (field, value) pairs (AND-ed). */
|
|
111
|
+
export async function findOneByFields<T extends QueryResultRow = QueryResultRow>(
|
|
112
|
+
table: string,
|
|
113
|
+
fields: Record<string, unknown>,
|
|
114
|
+
): Promise<T | null> {
|
|
115
|
+
const keys = Object.keys(fields);
|
|
116
|
+
if (keys.length === 0) {
|
|
117
|
+
const result = await query<T>(\`SELECT * FROM "\${table}" LIMIT 1\`);
|
|
118
|
+
return result.rows[0] ?? null;
|
|
119
|
+
}
|
|
120
|
+
const where = keys.map((k, i) => '"' + k.replace(/"/g, '""') + '" = $' + (i + 1)).join(' AND ');
|
|
121
|
+
const sql = \`SELECT * FROM "\${table}" WHERE \${where} LIMIT 1\`;
|
|
122
|
+
const result = await query<T>(sql, keys.map((k) => fields[k]));
|
|
123
|
+
return result.rows[0] ?? null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** SELECT all rows from a table \u2014 equivalent to a Mongo
|
|
127
|
+
* \`collection.find({}).toArray()\` for the bulk-fan-out auto-create
|
|
128
|
+
* convention. */
|
|
129
|
+
export async function findAll<T extends QueryResultRow = QueryResultRow>(
|
|
130
|
+
table: string,
|
|
131
|
+
): Promise<T[]> {
|
|
132
|
+
const result = await query<T>(\`SELECT * FROM "\${table}"\`);
|
|
133
|
+
return result.rows;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** UPDATE columns of the row identified by id. Quotes column names so
|
|
137
|
+
* camelCase identifiers survive postgres' default lower-case folding. */
|
|
138
|
+
export async function updateOneById(
|
|
139
|
+
table: string,
|
|
140
|
+
id: unknown,
|
|
141
|
+
patch: Record<string, unknown>,
|
|
142
|
+
): Promise<void> {
|
|
143
|
+
const keys = Object.keys(patch);
|
|
144
|
+
if (keys.length === 0) return;
|
|
145
|
+
const setClause = keys.map((k, i) => '"' + k.replace(/"/g, '""') + '" = $' + (i + 1)).join(', ');
|
|
146
|
+
const sql = \`UPDATE "\${table}" SET \${setClause} WHERE "id" = $\${keys.length + 1}\`;
|
|
147
|
+
await query(sql, [...keys.map((k) => patch[k]), id]);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** DELETE the row identified by id. */
|
|
151
|
+
export async function deleteOneById(table: string, id: unknown): Promise<void> {
|
|
152
|
+
await query(\`DELETE FROM "\${table}" WHERE "id" = $1\`, [id]);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function disconnect(): Promise<void> {
|
|
156
|
+
if (pool) {
|
|
157
|
+
await pool.end();
|
|
158
|
+
pool = null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
`;
|
|
162
|
+
}
|
|
163
|
+
export {
|
|
164
|
+
generatePgClient as default
|
|
165
|
+
};
|