@autonoma-ai/sdk 0.2.1-canary.bbfed39 → 0.2.2
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/{graph-OB0ohy_v.d.ts → graph-Bh_9SEer.d.ts} +61 -53
- package/dist/graph.d.ts +2 -1
- package/dist/index.d.ts +125 -134
- package/dist/index.js +479 -885
- package/dist/index.js.map +1 -1
- package/package.json +6 -2
package/dist/index.js
CHANGED
|
@@ -44,101 +44,6 @@ function hmac(data, secret) {
|
|
|
44
44
|
return createHmac2("sha256", secret).update(data).digest("base64url");
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
// src/tree.ts
|
|
48
|
-
var RESERVED_KEYS = /* @__PURE__ */ new Set(["_alias", "_ref"]);
|
|
49
|
-
function resolveTree(create, schema) {
|
|
50
|
-
const relationByParentField = /* @__PURE__ */ new Map();
|
|
51
|
-
for (const rel of schema.relations) {
|
|
52
|
-
relationByParentField.set(`${rel.parentModel}.${rel.parentField}`, rel);
|
|
53
|
-
}
|
|
54
|
-
const fkOnParent = /* @__PURE__ */ new Set();
|
|
55
|
-
for (const rel of schema.relations) {
|
|
56
|
-
const edge = schema.edges.find(
|
|
57
|
-
(e) => e.localField === rel.childField && (e.from === rel.parentModel || e.from === rel.childModel)
|
|
58
|
-
);
|
|
59
|
-
if (edge && edge.from === rel.parentModel) {
|
|
60
|
-
fkOnParent.add(`${rel.parentModel}.${rel.parentField}`);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
const aliases = /* @__PURE__ */ new Map();
|
|
64
|
-
const ops = [];
|
|
65
|
-
const deferredUpdates = [];
|
|
66
|
-
let tempCounter = 0;
|
|
67
|
-
function makeTempId(model) {
|
|
68
|
-
return `__temp_${model}_${tempCounter++}`;
|
|
69
|
-
}
|
|
70
|
-
function walkNode(modelName, node, parentTempId, parentRelation, parentFkOnParent) {
|
|
71
|
-
const fields = {};
|
|
72
|
-
const preChildren = [];
|
|
73
|
-
const postChildren = [];
|
|
74
|
-
const alias = node._alias;
|
|
75
|
-
const tempId = makeTempId(modelName);
|
|
76
|
-
for (const [key, value] of Object.entries(node)) {
|
|
77
|
-
if (RESERVED_KEYS.has(key)) continue;
|
|
78
|
-
const exactKey = `${modelName}.${key}`;
|
|
79
|
-
const prefixedKey = `${modelName}.${modelName.charAt(0).toLowerCase()}${modelName.slice(1)}${key.charAt(0).toUpperCase()}${key.slice(1)}`;
|
|
80
|
-
let relation = relationByParentField.get(exactKey) ?? relationByParentField.get(prefixedKey) ?? void 0;
|
|
81
|
-
let matchedKey = relationByParentField.has(exactKey) ? exactKey : prefixedKey;
|
|
82
|
-
if (!relation) {
|
|
83
|
-
for (const [relKey, rel] of relationByParentField) {
|
|
84
|
-
if (relKey.startsWith(`${modelName}.`) && rel.childModel.toLowerCase() === key.toLowerCase()) {
|
|
85
|
-
relation = rel;
|
|
86
|
-
matchedKey = relKey;
|
|
87
|
-
break;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
if (relation) {
|
|
92
|
-
const isOnParent = fkOnParent.has(matchedKey);
|
|
93
|
-
if (isOnParent) {
|
|
94
|
-
preChildren.push({ relation, value, fkOnParent: true });
|
|
95
|
-
} else {
|
|
96
|
-
postChildren.push({ relation, value, fkOnParent: false });
|
|
97
|
-
}
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
if (value && typeof value === "object" && "_ref" in value) {
|
|
101
|
-
const refAlias = value._ref;
|
|
102
|
-
const refTempId = aliases.get(refAlias);
|
|
103
|
-
if (!refTempId) {
|
|
104
|
-
deferredUpdates.push({ targetTempId: tempId, model: modelName, field: key, refAlias });
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
fields[key] = refTempId;
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
fields[key] = value;
|
|
111
|
-
}
|
|
112
|
-
if (parentRelation && parentTempId && !parentFkOnParent) {
|
|
113
|
-
fields[parentRelation.childField] = parentTempId;
|
|
114
|
-
}
|
|
115
|
-
for (const { relation, value } of preChildren) {
|
|
116
|
-
if (Array.isArray(value)) {
|
|
117
|
-
for (const item of value) {
|
|
118
|
-
const childTempId = walkNode(relation.childModel, item, tempId, relation, true);
|
|
119
|
-
fields[relation.childField] = childTempId;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
ops.push({ model: modelName, fields, tempId, batch: false });
|
|
124
|
-
if (alias) aliases.set(alias, tempId);
|
|
125
|
-
for (const { relation, value } of postChildren) {
|
|
126
|
-
if (Array.isArray(value)) {
|
|
127
|
-
for (const item of value) {
|
|
128
|
-
walkNode(relation.childModel, item, tempId, relation, false);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
return tempId;
|
|
133
|
-
}
|
|
134
|
-
for (const [modelName, nodes] of Object.entries(create)) {
|
|
135
|
-
for (const node of nodes) {
|
|
136
|
-
walkNode(modelName, node, null, null, false);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
return { ops, deferredUpdates, aliases };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
47
|
// src/errors.ts
|
|
143
48
|
var AutonomaError = class extends Error {
|
|
144
49
|
constructor(message, code, status) {
|
|
@@ -177,9 +82,9 @@ var Errors = {
|
|
|
177
82
|
403
|
|
178
83
|
);
|
|
179
84
|
},
|
|
180
|
-
productionBlocked() {
|
|
85
|
+
productionBlocked(detail) {
|
|
181
86
|
return new AutonomaError(
|
|
182
|
-
|
|
87
|
+
`Environment factory is disabled in production${detail != null ? `. ${detail}` : ""}`,
|
|
183
88
|
"PRODUCTION_BLOCKED",
|
|
184
89
|
404
|
|
185
90
|
);
|
|
@@ -193,649 +98,355 @@ var Errors = {
|
|
|
193
98
|
}
|
|
194
99
|
};
|
|
195
100
|
|
|
196
|
-
// src/
|
|
197
|
-
var
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
WHERE table_schema = '{{schema}}'
|
|
206
|
-
ORDER BY table_name, ordinal_position`;
|
|
207
|
-
var POSTGRES_ENUMS = `SELECT t.typname AS enum_name, e.enumlabel AS enum_value
|
|
208
|
-
FROM pg_type t
|
|
209
|
-
JOIN pg_enum e ON t.oid = e.enumtypid
|
|
210
|
-
JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
|
|
211
|
-
ORDER BY t.typname, e.enumsortorder`;
|
|
212
|
-
var POSTGRES_FOREIGN_KEYS = `SELECT
|
|
213
|
-
kcu.table_name AS from_table,
|
|
214
|
-
kcu.column_name AS from_column,
|
|
215
|
-
ccu.table_name AS to_table,
|
|
216
|
-
ccu.column_name AS to_column,
|
|
217
|
-
c.is_nullable
|
|
218
|
-
FROM information_schema.table_constraints tc
|
|
219
|
-
JOIN information_schema.key_column_usage kcu
|
|
220
|
-
ON tc.constraint_name = kcu.constraint_name
|
|
221
|
-
AND tc.table_schema = kcu.table_schema
|
|
222
|
-
JOIN information_schema.constraint_column_usage ccu
|
|
223
|
-
ON tc.constraint_name = ccu.constraint_name
|
|
224
|
-
AND tc.table_schema = ccu.table_schema
|
|
225
|
-
LEFT JOIN information_schema.columns c
|
|
226
|
-
ON c.table_schema = kcu.table_schema
|
|
227
|
-
AND c.table_name = kcu.table_name
|
|
228
|
-
AND c.column_name = kcu.column_name
|
|
229
|
-
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
230
|
-
AND tc.table_schema = '{{schema}}'
|
|
231
|
-
ORDER BY kcu.table_name, kcu.ordinal_position`;
|
|
232
|
-
var POSTGRES_PRIMARY_KEYS = `SELECT
|
|
233
|
-
tc.table_name,
|
|
234
|
-
kcu.column_name
|
|
235
|
-
FROM information_schema.table_constraints tc
|
|
236
|
-
JOIN information_schema.key_column_usage kcu
|
|
237
|
-
ON tc.constraint_name = kcu.constraint_name
|
|
238
|
-
AND tc.table_schema = kcu.table_schema
|
|
239
|
-
WHERE tc.constraint_type = 'PRIMARY KEY'
|
|
240
|
-
AND tc.table_schema = '{{schema}}'
|
|
241
|
-
ORDER BY tc.table_name, kcu.ordinal_position`;
|
|
242
|
-
var POSTGRES_TABLES = `SELECT table_name
|
|
243
|
-
FROM information_schema.tables
|
|
244
|
-
WHERE table_schema = '{{schema}}'
|
|
245
|
-
AND table_type = 'BASE TABLE'
|
|
246
|
-
ORDER BY table_name`;
|
|
247
|
-
var MYSQL_COLUMNS = `SELECT
|
|
248
|
-
table_name,
|
|
249
|
-
column_name,
|
|
250
|
-
data_type,
|
|
251
|
-
column_type AS udt_name,
|
|
252
|
-
is_nullable,
|
|
253
|
-
column_default
|
|
254
|
-
FROM information_schema.columns
|
|
255
|
-
WHERE table_schema = '{{schema}}'
|
|
256
|
-
ORDER BY table_name, ordinal_position`;
|
|
257
|
-
var MYSQL_ENUMS = `SELECT NULL AS enum_name, NULL AS enum_value FROM DUAL WHERE 1 = 0`;
|
|
258
|
-
var MYSQL_FOREIGN_KEYS = `SELECT
|
|
259
|
-
kcu.table_name AS from_table,
|
|
260
|
-
kcu.column_name AS from_column,
|
|
261
|
-
kcu.referenced_table_name AS to_table,
|
|
262
|
-
kcu.referenced_column_name AS to_column,
|
|
263
|
-
c.is_nullable
|
|
264
|
-
FROM information_schema.key_column_usage kcu
|
|
265
|
-
JOIN information_schema.columns c
|
|
266
|
-
ON c.table_schema = kcu.table_schema
|
|
267
|
-
AND c.table_name = kcu.table_name
|
|
268
|
-
AND c.column_name = kcu.column_name
|
|
269
|
-
WHERE kcu.referenced_table_name IS NOT NULL
|
|
270
|
-
AND kcu.table_schema = '{{schema}}'
|
|
271
|
-
ORDER BY kcu.table_name, kcu.ordinal_position`;
|
|
272
|
-
var MYSQL_PRIMARY_KEYS = `SELECT
|
|
273
|
-
tc.table_name,
|
|
274
|
-
kcu.column_name
|
|
275
|
-
FROM information_schema.table_constraints tc
|
|
276
|
-
JOIN information_schema.key_column_usage kcu
|
|
277
|
-
ON tc.constraint_name = kcu.constraint_name
|
|
278
|
-
AND tc.table_schema = kcu.table_schema
|
|
279
|
-
AND tc.table_name = kcu.table_name
|
|
280
|
-
WHERE tc.constraint_type = 'PRIMARY KEY'
|
|
281
|
-
AND tc.table_schema = '{{schema}}'
|
|
282
|
-
ORDER BY tc.table_name, kcu.ordinal_position`;
|
|
283
|
-
var MYSQL_TABLES = `SELECT table_name
|
|
284
|
-
FROM information_schema.tables
|
|
285
|
-
WHERE table_schema = '{{schema}}'
|
|
286
|
-
AND table_type = 'BASE TABLE'
|
|
287
|
-
ORDER BY table_name`;
|
|
288
|
-
|
|
289
|
-
// src/dialect.ts
|
|
290
|
-
var replaceSchema = (template, schema) => template.replace("{{schema}}", schema);
|
|
291
|
-
var postgres = {
|
|
292
|
-
name: "postgres",
|
|
293
|
-
param: (i) => `$${i}`,
|
|
294
|
-
quoteId: (name) => `"${name}"`,
|
|
295
|
-
supportsReturning: true,
|
|
296
|
-
tablesSQL: (schema) => replaceSchema(POSTGRES_TABLES, schema),
|
|
297
|
-
columnsSQL: (schema) => replaceSchema(POSTGRES_COLUMNS, schema),
|
|
298
|
-
primaryKeysSQL: (schema) => replaceSchema(POSTGRES_PRIMARY_KEYS, schema),
|
|
299
|
-
foreignKeysSQL: (schema) => replaceSchema(POSTGRES_FOREIGN_KEYS, schema),
|
|
300
|
-
enumsSQL: () => POSTGRES_ENUMS
|
|
301
|
-
};
|
|
302
|
-
var mysql = {
|
|
303
|
-
name: "mysql",
|
|
304
|
-
param: () => "?",
|
|
305
|
-
quoteId: (name) => `\`${name}\``,
|
|
306
|
-
supportsReturning: false,
|
|
307
|
-
tablesSQL: (schema) => replaceSchema(MYSQL_TABLES, schema),
|
|
308
|
-
columnsSQL: (schema) => replaceSchema(MYSQL_COLUMNS, schema),
|
|
309
|
-
primaryKeysSQL: (schema) => replaceSchema(MYSQL_PRIMARY_KEYS, schema),
|
|
310
|
-
foreignKeysSQL: (schema) => replaceSchema(MYSQL_FOREIGN_KEYS, schema),
|
|
311
|
-
enumsSQL: () => MYSQL_ENUMS
|
|
312
|
-
};
|
|
313
|
-
function getDialect(name = "postgres") {
|
|
314
|
-
switch (name) {
|
|
315
|
-
case "postgres":
|
|
316
|
-
return postgres;
|
|
317
|
-
case "mysql":
|
|
318
|
-
return mysql;
|
|
319
|
-
default:
|
|
320
|
-
throw new Error(`Dialect "${name}" is not yet supported. Currently only "postgres" and "mysql" are available.`);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// src/introspect.ts
|
|
325
|
-
async function introspectDatabase(executor, dialect, config) {
|
|
326
|
-
const dbSchema = config.schema ?? (dialect.name === "mysql" ? void 0 : "public");
|
|
327
|
-
if (!dbSchema) {
|
|
328
|
-
throw new Error("MySQL requires a schema (database name). Pass it via config.schema or HandlerConfig.dbSchema.");
|
|
329
|
-
}
|
|
330
|
-
const excludeSet = new Set(config.excludeTables ?? ["_prisma_migrations"]);
|
|
331
|
-
const [tableRows, columnRows, pkRows, fkRows, enumRows] = await Promise.all([
|
|
332
|
-
executor.query(dialect.tablesSQL(dbSchema)).then(normalizeKeys),
|
|
333
|
-
executor.query(dialect.columnsSQL(dbSchema)).then(normalizeKeys),
|
|
334
|
-
executor.query(dialect.primaryKeysSQL(dbSchema)).then(normalizeKeys),
|
|
335
|
-
executor.query(dialect.foreignKeysSQL(dbSchema)).then(normalizeKeys),
|
|
336
|
-
executor.query(dialect.enumsSQL(dbSchema)).then(normalizeKeys)
|
|
337
|
-
]);
|
|
338
|
-
const enumValues = /* @__PURE__ */ new Map();
|
|
339
|
-
for (const row of enumRows) {
|
|
340
|
-
if (!row.enum_name) continue;
|
|
341
|
-
if (!enumValues.has(row.enum_name)) enumValues.set(row.enum_name, []);
|
|
342
|
-
enumValues.get(row.enum_name).push(row.enum_value);
|
|
343
|
-
}
|
|
344
|
-
if (dialect.name === "mysql") {
|
|
345
|
-
for (const col of columnRows) {
|
|
346
|
-
const parsed = parseMySQLEnum(col.udt_name);
|
|
347
|
-
if (parsed) {
|
|
348
|
-
const enumKey = `${col.table_name}.${col.column_name}`;
|
|
349
|
-
enumValues.set(enumKey, parsed);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
const pksByTable = /* @__PURE__ */ new Map();
|
|
354
|
-
for (const row of pkRows) {
|
|
355
|
-
if (!pksByTable.has(row.table_name)) pksByTable.set(row.table_name, /* @__PURE__ */ new Set());
|
|
356
|
-
pksByTable.get(row.table_name).add(row.column_name);
|
|
357
|
-
}
|
|
358
|
-
const userMap = config.tableNameMap ?? {};
|
|
359
|
-
const tableMap = /* @__PURE__ */ new Map();
|
|
360
|
-
const reverseTableMap = /* @__PURE__ */ new Map();
|
|
361
|
-
for (const [model, dbTable] of Object.entries(userMap)) {
|
|
362
|
-
tableMap.set(model, dbTable);
|
|
363
|
-
reverseTableMap.set(dbTable, model);
|
|
364
|
-
}
|
|
365
|
-
const dbTables = tableRows.map((r) => r.table_name).filter((t) => !excludeSet.has(t));
|
|
366
|
-
for (const dbTable of dbTables) {
|
|
367
|
-
if (reverseTableMap.has(dbTable)) continue;
|
|
368
|
-
const modelName = snakeToPascal(dbTable);
|
|
369
|
-
tableMap.set(modelName, dbTable);
|
|
370
|
-
reverseTableMap.set(dbTable, modelName);
|
|
371
|
-
}
|
|
372
|
-
const models = [];
|
|
373
|
-
const columnMaps = /* @__PURE__ */ new Map();
|
|
374
|
-
const enumTypeMaps = /* @__PURE__ */ new Map();
|
|
375
|
-
const columnsByTable = /* @__PURE__ */ new Map();
|
|
376
|
-
for (const row of columnRows) {
|
|
377
|
-
if (!columnsByTable.has(row.table_name)) columnsByTable.set(row.table_name, []);
|
|
378
|
-
columnsByTable.get(row.table_name).push(row);
|
|
379
|
-
}
|
|
380
|
-
for (const [modelName, dbTable] of tableMap) {
|
|
381
|
-
const cols = columnsByTable.get(dbTable) ?? [];
|
|
382
|
-
const pks = pksByTable.get(dbTable) ?? /* @__PURE__ */ new Set();
|
|
383
|
-
const colMap = /* @__PURE__ */ new Map();
|
|
384
|
-
const fields = [];
|
|
385
|
-
for (const col of cols) {
|
|
386
|
-
const fieldName = snakeToCamel(col.column_name);
|
|
387
|
-
colMap.set(fieldName, col.column_name);
|
|
388
|
-
let enumVals;
|
|
389
|
-
if (dialect.name === "mysql") {
|
|
390
|
-
enumVals = enumValues.get(`${col.table_name}.${col.column_name}`);
|
|
391
|
-
} else {
|
|
392
|
-
enumVals = enumValues.get(col.udt_name);
|
|
393
|
-
}
|
|
394
|
-
const type = enumVals ? `enum(${enumVals.join(",")})` : mapDataType(col.data_type, col.udt_name, dialect.name);
|
|
395
|
-
if (dialect.name === "postgres") {
|
|
396
|
-
if (enumVals) {
|
|
397
|
-
if (!enumTypeMaps.has(modelName)) enumTypeMaps.set(modelName, /* @__PURE__ */ new Map());
|
|
398
|
-
enumTypeMaps.get(modelName).set(fieldName, col.udt_name);
|
|
399
|
-
} else if (col.data_type === "jsonb" || col.udt_name === "jsonb" || col.data_type === "json" || col.udt_name === "json") {
|
|
400
|
-
if (!enumTypeMaps.has(modelName)) enumTypeMaps.set(modelName, /* @__PURE__ */ new Map());
|
|
401
|
-
const jsonType = col.data_type === "json" || col.udt_name === "json" ? "json" : "jsonb";
|
|
402
|
-
enumTypeMaps.get(modelName).set(fieldName, jsonType);
|
|
403
|
-
} else if (col.data_type.includes("timestamp") || col.udt_name === "timestamptz" || col.udt_name === "timestamp") {
|
|
404
|
-
if (!enumTypeMaps.has(modelName)) enumTypeMaps.set(modelName, /* @__PURE__ */ new Map());
|
|
405
|
-
enumTypeMaps.get(modelName).set(fieldName, col.udt_name);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
fields.push({
|
|
409
|
-
name: fieldName,
|
|
410
|
-
type,
|
|
411
|
-
isRequired: col.is_nullable === "NO",
|
|
412
|
-
isId: pks.has(col.column_name),
|
|
413
|
-
hasDefault: col.column_default !== null
|
|
414
|
-
});
|
|
101
|
+
// src/payload-topo.ts
|
|
102
|
+
var RESERVED_KEYS = /* @__PURE__ */ new Set(["_alias", "_ref"]);
|
|
103
|
+
function collectRefs(value, out) {
|
|
104
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
105
|
+
const obj = value;
|
|
106
|
+
const ref = obj._ref;
|
|
107
|
+
if (typeof ref === "string") {
|
|
108
|
+
out.push(ref);
|
|
109
|
+
return;
|
|
415
110
|
}
|
|
416
|
-
|
|
417
|
-
|
|
111
|
+
for (const v of Object.values(obj)) collectRefs(v, out);
|
|
112
|
+
return;
|
|
418
113
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
const fromModel = reverseTableMap.get(fk.from_table);
|
|
422
|
-
const toModel = reverseTableMap.get(fk.to_table);
|
|
423
|
-
if (!fromModel || !toModel) continue;
|
|
424
|
-
const fromColMap = columnMaps.get(fromModel);
|
|
425
|
-
const toColMap = columnMaps.get(toModel);
|
|
426
|
-
const localField = fromColMap ? reverseGet(fromColMap, fk.from_column) ?? fk.from_column : fk.from_column;
|
|
427
|
-
const foreignField = toColMap ? reverseGet(toColMap, fk.to_column) ?? fk.to_column : fk.to_column;
|
|
428
|
-
edges.push({
|
|
429
|
-
from: fromModel,
|
|
430
|
-
to: toModel,
|
|
431
|
-
localField,
|
|
432
|
-
foreignField,
|
|
433
|
-
nullable: fk.is_nullable === "YES"
|
|
434
|
-
});
|
|
114
|
+
if (Array.isArray(value)) {
|
|
115
|
+
for (const v of value) collectRefs(v, out);
|
|
435
116
|
}
|
|
436
|
-
const relations = [];
|
|
437
|
-
for (const edge of edges) {
|
|
438
|
-
const fromDbTable = tableMap.get(edge.from);
|
|
439
|
-
const fromColMap = columnMaps.get(edge.from) ?? /* @__PURE__ */ new Map();
|
|
440
|
-
const fkDbCol = fromColMap.get(edge.localField) ?? edge.localField;
|
|
441
|
-
const fromPks = pksByTable.get(fromDbTable);
|
|
442
|
-
const isOneToOne = fromPks !== void 0 && fromPks.size === 1 && fromPks.has(fkDbCol);
|
|
443
|
-
relations.push({
|
|
444
|
-
parentModel: edge.to,
|
|
445
|
-
childModel: edge.from,
|
|
446
|
-
parentField: isOneToOne ? lowerFirst(edge.from) : pluralCamelCase(edge.from),
|
|
447
|
-
childField: edge.localField
|
|
448
|
-
});
|
|
449
|
-
relations.push({
|
|
450
|
-
parentModel: edge.from,
|
|
451
|
-
childModel: edge.to,
|
|
452
|
-
parentField: lowerFirst(edge.to),
|
|
453
|
-
childField: edge.localField
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
return {
|
|
457
|
-
schema: { models, edges, relations, scopeField: config.scopeField },
|
|
458
|
-
tableMap,
|
|
459
|
-
columnMaps,
|
|
460
|
-
enumTypeMaps
|
|
461
|
-
};
|
|
462
|
-
}
|
|
463
|
-
function snakeToPascal(str) {
|
|
464
|
-
return str.split("_").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
|
|
465
|
-
}
|
|
466
|
-
function snakeToCamel(str) {
|
|
467
|
-
const pascal = snakeToPascal(str);
|
|
468
|
-
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
469
|
-
}
|
|
470
|
-
function parseMySQLEnum(columnType) {
|
|
471
|
-
if (!columnType) return null;
|
|
472
|
-
const match = columnType.match(/^enum\((.+)\)$/i);
|
|
473
|
-
if (!match) return null;
|
|
474
|
-
return match[1].split(",").map((v) => v.trim().replace(/^'|'$/g, ""));
|
|
475
|
-
}
|
|
476
|
-
function mapDataType(dataType, udtName, dialectName) {
|
|
477
|
-
const dt = dataType.toLowerCase();
|
|
478
|
-
if (dialectName === "mysql" && dt === "tinyint" && udtName.toLowerCase().startsWith("tinyint(1)")) return "Boolean";
|
|
479
|
-
if (dt === "integer" || dt === "smallint" || dt === "bigint" || dt === "int" || dt === "mediumint" || dt === "tinyint") return "Int";
|
|
480
|
-
if (dt === "numeric" || dt === "real" || dt === "double precision" || dt === "float" || dt === "double" || dt === "decimal") return "Float";
|
|
481
|
-
if (dt === "boolean" || dt === "tinyint(1)") return "Boolean";
|
|
482
|
-
if (dt === "text" || dt === "character varying" || dt === "character" || dt === "varchar" || dt === "char" || dt === "mediumtext" || dt === "longtext" || dt === "tinytext") return "String";
|
|
483
|
-
if (dt === "timestamp with time zone" || dt === "timestamp without time zone" || dt === "date" || dt === "time" || dt === "datetime" || dt === "timestamp") return "DateTime";
|
|
484
|
-
if (dt === "json" || dt === "jsonb") return "Json";
|
|
485
|
-
if (dt === "uuid") return "String";
|
|
486
|
-
if (dt === "bytea" || dt === "blob" || dt === "mediumblob" || dt === "longblob" || dt === "tinyblob" || dt === "binary" || dt === "varbinary") return "Bytes";
|
|
487
|
-
if (dt === "user-defined" && dialectName === "postgres") return udtName;
|
|
488
|
-
if (dt === "enum" || dt === "set") return udtName;
|
|
489
|
-
return dataType;
|
|
490
|
-
}
|
|
491
|
-
function lowerFirst(str) {
|
|
492
|
-
return str.charAt(0).toLowerCase() + str.slice(1);
|
|
493
|
-
}
|
|
494
|
-
function pluralCamelCase(modelName) {
|
|
495
|
-
const camel = lowerFirst(modelName);
|
|
496
|
-
return pluralize(camel);
|
|
497
|
-
}
|
|
498
|
-
function pluralize(str) {
|
|
499
|
-
if (str.endsWith("s") || str.endsWith("x") || str.endsWith("z") || str.endsWith("ch") || str.endsWith("sh")) {
|
|
500
|
-
return str + "es";
|
|
501
|
-
}
|
|
502
|
-
if (str.endsWith("y") && str.length > 1 && !isVowel(str.charAt(str.length - 2))) {
|
|
503
|
-
return str.slice(0, -1) + "ies";
|
|
504
|
-
}
|
|
505
|
-
return str + "s";
|
|
506
|
-
}
|
|
507
|
-
function isVowel(ch) {
|
|
508
|
-
return "aeiou".includes(ch.toLowerCase());
|
|
509
117
|
}
|
|
510
|
-
function
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
|
|
118
|
+
function resolveRefs(value, aliasToTempId) {
|
|
119
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
120
|
+
const obj = value;
|
|
121
|
+
const ref = obj._ref;
|
|
122
|
+
if (typeof ref === "string") {
|
|
123
|
+
const real = aliasToTempId[ref];
|
|
124
|
+
return real !== void 0 ? real : value;
|
|
516
125
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
function reverseGet(map, dbName) {
|
|
521
|
-
for (const [key, val] of map) {
|
|
522
|
-
if (val === dbName) return key;
|
|
523
|
-
}
|
|
524
|
-
return null;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
// src/create.ts
|
|
528
|
-
import { randomUUID } from "crypto";
|
|
529
|
-
async function createEntities(executor, dialect, tableMap, columnMaps, spec, _context, enumTypeMaps = /* @__PURE__ */ new Map(), schemaModels = []) {
|
|
530
|
-
const results = {};
|
|
531
|
-
for (const [model, entitySpec] of Object.entries(spec)) {
|
|
532
|
-
const dbTable = tableMap.get(model);
|
|
533
|
-
if (!dbTable) throw new Error(`Unknown model "${model}". Not found in database tables.`);
|
|
534
|
-
const colMap = columnMaps.get(model) ?? /* @__PURE__ */ new Map();
|
|
535
|
-
const enumTypeMap = enumTypeMaps.get(model) ?? /* @__PURE__ */ new Map();
|
|
536
|
-
const modelInfo = schemaModels.find((m) => m.name === model);
|
|
537
|
-
const idFields = modelInfo?.fields.filter((f) => f.isId) ?? [];
|
|
538
|
-
const pkField = idFields.find((f) => f.name.toLowerCase() === "id") ?? idFields[0];
|
|
539
|
-
const pkFieldName = pkField?.name ?? "id";
|
|
540
|
-
const pkFieldType = pkField?.type ?? "String";
|
|
541
|
-
if (entitySpec.batch && entitySpec.fields.length > 0) {
|
|
542
|
-
results[model] = await insertBatch(executor, dialect, dbTable, colMap, enumTypeMap, entitySpec.fields, pkFieldName, pkFieldType);
|
|
543
|
-
} else {
|
|
544
|
-
const created = [];
|
|
545
|
-
for (const fields of entitySpec.fields) {
|
|
546
|
-
const [record] = await insertOne(executor, dialect, dbTable, colMap, enumTypeMap, fields, pkFieldName, pkFieldType);
|
|
547
|
-
if (record) created.push(record);
|
|
548
|
-
}
|
|
549
|
-
results[model] = created;
|
|
126
|
+
const out = {};
|
|
127
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
128
|
+
out[k] = resolveRefs(v, aliasToTempId);
|
|
550
129
|
}
|
|
130
|
+
return out;
|
|
551
131
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
async function updateEntity(executor, dialect, tableMap, columnMaps, model, id, fields, enumTypeMaps = /* @__PURE__ */ new Map(), pkFieldName = "id") {
|
|
555
|
-
const dbTable = tableMap.get(model);
|
|
556
|
-
if (!dbTable) throw new Error(`Unknown model "${model}" for update.`);
|
|
557
|
-
const colMap = columnMaps.get(model) ?? /* @__PURE__ */ new Map();
|
|
558
|
-
const enumTypeMap = enumTypeMaps.get(model) ?? /* @__PURE__ */ new Map();
|
|
559
|
-
const setClauses = [];
|
|
560
|
-
const params = [];
|
|
561
|
-
let paramIdx = 1;
|
|
562
|
-
for (const [fieldName, value] of Object.entries(fields)) {
|
|
563
|
-
const dbCol = colMap.get(fieldName) ?? fieldName;
|
|
564
|
-
setClauses.push(`${dialect.quoteId(dbCol)} = ${castParam(dialect, paramIdx, enumTypeMap, fieldName)}`);
|
|
565
|
-
params.push(serializeValue(value, dialect));
|
|
566
|
-
paramIdx++;
|
|
567
|
-
}
|
|
568
|
-
const idCol = colMap.get(pkFieldName) ?? pkFieldName;
|
|
569
|
-
params.push(id);
|
|
570
|
-
const sql = `UPDATE ${dialect.quoteId(dbTable)} SET ${setClauses.join(", ")} WHERE ${dialect.quoteId(idCol)} = ${dialect.param(paramIdx)}`;
|
|
571
|
-
await executor.query(sql, params);
|
|
572
|
-
}
|
|
573
|
-
async function insertOne(executor, dialect, dbTable, colMap, enumTypeMap, fields, pkFieldName = "id", pkFieldType = "String") {
|
|
574
|
-
if (pkFieldName && fields[pkFieldName] === void 0 && pkFieldType === "String") {
|
|
575
|
-
fields = { ...fields, [pkFieldName]: randomUUID() };
|
|
576
|
-
}
|
|
577
|
-
const entries = Object.entries(fields);
|
|
578
|
-
if (entries.length === 0) {
|
|
579
|
-
const sql = `INSERT INTO ${dialect.quoteId(dbTable)} DEFAULT VALUES RETURNING *`;
|
|
580
|
-
return mapRowsBack(await executor.query(sql), colMap);
|
|
581
|
-
}
|
|
582
|
-
const dbCols = [];
|
|
583
|
-
const params = [];
|
|
584
|
-
const placeholders = [];
|
|
585
|
-
let paramIdx = 1;
|
|
586
|
-
for (const [fieldName, value] of entries) {
|
|
587
|
-
const dbCol = colMap.get(fieldName) ?? fieldName;
|
|
588
|
-
dbCols.push(dialect.quoteId(dbCol));
|
|
589
|
-
placeholders.push(castParam(dialect, paramIdx, enumTypeMap, fieldName));
|
|
590
|
-
params.push(serializeValue(value, dialect));
|
|
591
|
-
paramIdx++;
|
|
592
|
-
}
|
|
593
|
-
const colList = dbCols.join(", ");
|
|
594
|
-
const valList = placeholders.join(", ");
|
|
595
|
-
if (dialect.supportsReturning) {
|
|
596
|
-
const sql = `INSERT INTO ${dialect.quoteId(dbTable)} (${colList}) VALUES (${valList}) RETURNING *`;
|
|
597
|
-
return mapRowsBack(await executor.query(sql, params), colMap);
|
|
132
|
+
if (Array.isArray(value)) {
|
|
133
|
+
return value.map((v) => resolveRefs(v, aliasToTempId));
|
|
598
134
|
}
|
|
599
|
-
|
|
600
|
-
`INSERT INTO ${dialect.quoteId(dbTable)} (${colList}) VALUES (${valList})`,
|
|
601
|
-
params
|
|
602
|
-
);
|
|
603
|
-
const idCol = colMap.get(pkFieldName) ?? pkFieldName;
|
|
604
|
-
const id = fields[pkFieldName];
|
|
605
|
-
return mapRowsBack(
|
|
606
|
-
await executor.query(
|
|
607
|
-
`SELECT * FROM ${dialect.quoteId(dbTable)} WHERE ${dialect.quoteId(idCol)} = ${dialect.param(1)}`,
|
|
608
|
-
[id]
|
|
609
|
-
),
|
|
610
|
-
colMap
|
|
611
|
-
);
|
|
135
|
+
return value;
|
|
612
136
|
}
|
|
613
|
-
|
|
614
|
-
if (
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
137
|
+
function resolvePayloadTree(create) {
|
|
138
|
+
if (!create || typeof create !== "object" || Array.isArray(create)) {
|
|
139
|
+
throw Errors.invalidBody("`create` must be an object keyed by model name");
|
|
140
|
+
}
|
|
141
|
+
const rawEntries = [];
|
|
142
|
+
let counter = 0;
|
|
143
|
+
const aliases = {};
|
|
144
|
+
const aliasOwnerModel = {};
|
|
145
|
+
for (const [model, entities] of Object.entries(create)) {
|
|
146
|
+
if (!Array.isArray(entities)) {
|
|
147
|
+
throw Errors.invalidBody(
|
|
148
|
+
`\`create.${model}\` must be a list of entity objects, got ${typeof entities}`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
for (const entity of entities) {
|
|
152
|
+
if (!entity || typeof entity !== "object" || Array.isArray(entity)) {
|
|
153
|
+
throw Errors.invalidBody(
|
|
154
|
+
`\`create.${model}\` entries must be objects, got ${Array.isArray(entity) ? "array" : typeof entity}`
|
|
155
|
+
);
|
|
619
156
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
157
|
+
const tempId = `__temp_${model}_${counter++}`;
|
|
158
|
+
const obj = entity;
|
|
159
|
+
const aliasRaw = obj._alias;
|
|
160
|
+
let alias = null;
|
|
161
|
+
if (typeof aliasRaw === "string") {
|
|
162
|
+
if (aliases[aliasRaw] !== void 0) {
|
|
163
|
+
throw Errors.invalidBody(`duplicate _alias "${aliasRaw}"`);
|
|
164
|
+
}
|
|
165
|
+
aliases[aliasRaw] = tempId;
|
|
166
|
+
aliasOwnerModel[aliasRaw] = model;
|
|
167
|
+
alias = aliasRaw;
|
|
168
|
+
} else if (aliasRaw !== void 0 && aliasRaw !== null) {
|
|
169
|
+
throw Errors.invalidBody('"_alias" must be a string');
|
|
170
|
+
}
|
|
171
|
+
rawEntries.push({ model, tempId, entity: obj, alias });
|
|
627
172
|
}
|
|
628
173
|
}
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
174
|
+
const depsByTempId = {};
|
|
175
|
+
const fieldsByTempId = {};
|
|
176
|
+
const modelByTempId = {};
|
|
177
|
+
for (const { model, tempId, entity } of rawEntries) {
|
|
178
|
+
const deps = [];
|
|
179
|
+
const cleaned = {};
|
|
180
|
+
for (const [key, value] of Object.entries(entity)) {
|
|
181
|
+
if (RESERVED_KEYS.has(key)) continue;
|
|
182
|
+
collectRefs(value, deps);
|
|
183
|
+
cleaned[key] = resolveRefs(value, aliases);
|
|
184
|
+
}
|
|
185
|
+
const unknowns = deps.filter((a) => aliases[a] === void 0);
|
|
186
|
+
if (unknowns.length > 0) {
|
|
187
|
+
const sorted = Array.from(new Set(unknowns)).sort();
|
|
188
|
+
throw Errors.invalidBody(
|
|
189
|
+
`\`create.${model}\` references unknown alias(es): ${sorted.join(", ")}`
|
|
190
|
+
);
|
|
635
191
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
const
|
|
641
|
-
const
|
|
642
|
-
const
|
|
643
|
-
for (
|
|
644
|
-
const
|
|
645
|
-
const
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
192
|
+
depsByTempId[tempId] = deps;
|
|
193
|
+
fieldsByTempId[tempId] = cleaned;
|
|
194
|
+
modelByTempId[tempId] = model;
|
|
195
|
+
}
|
|
196
|
+
const inDegree = {};
|
|
197
|
+
for (const { tempId } of rawEntries) inDegree[tempId] = 0;
|
|
198
|
+
const edges = {};
|
|
199
|
+
for (const [tempId, deps] of Object.entries(depsByTempId)) {
|
|
200
|
+
const seen = /* @__PURE__ */ new Set();
|
|
201
|
+
for (const depAlias of deps) {
|
|
202
|
+
const depTempId = aliases[depAlias];
|
|
203
|
+
if (depTempId === tempId || seen.has(depTempId)) continue;
|
|
204
|
+
seen.add(depTempId);
|
|
205
|
+
(edges[depTempId] ??= []).push(tempId);
|
|
206
|
+
inDegree[tempId] = (inDegree[tempId] ?? 0) + 1;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const payloadOrder = {};
|
|
210
|
+
rawEntries.forEach((e, i) => {
|
|
211
|
+
payloadOrder[e.tempId] = i;
|
|
212
|
+
});
|
|
213
|
+
const ready = Object.keys(inDegree).filter((t) => inDegree[t] === 0).sort((a, b) => payloadOrder[a] - payloadOrder[b]);
|
|
214
|
+
const sortedTempIds = [];
|
|
215
|
+
while (ready.length > 0) {
|
|
216
|
+
const tid = ready.shift();
|
|
217
|
+
sortedTempIds.push(tid);
|
|
218
|
+
for (const next of edges[tid] ?? []) {
|
|
219
|
+
inDegree[next] = (inDegree[next] ?? 0) - 1;
|
|
220
|
+
if (inDegree[next] === 0) ready.push(next);
|
|
221
|
+
}
|
|
222
|
+
ready.sort((a, b) => payloadOrder[a] - payloadOrder[b]);
|
|
223
|
+
}
|
|
224
|
+
if (sortedTempIds.length !== rawEntries.length) {
|
|
225
|
+
const cycle = Object.entries(inDegree).filter(([, deg]) => deg > 0).map(([tid]) => tid).sort((a, b) => payloadOrder[a] - payloadOrder[b]);
|
|
226
|
+
const cycleModels = cycle.map((t) => modelByTempId[t]).join(", ");
|
|
227
|
+
throw Errors.invalidBody(`cycle detected in _alias/_ref graph: ${cycleModels}`);
|
|
228
|
+
}
|
|
229
|
+
const aliasDependencies = {};
|
|
230
|
+
for (const [alias, tempId] of Object.entries(aliases)) {
|
|
231
|
+
aliasDependencies[alias] = [...depsByTempId[tempId] ?? []];
|
|
232
|
+
}
|
|
233
|
+
const ops = sortedTempIds.map((tid) => ({
|
|
234
|
+
model: modelByTempId[tid],
|
|
235
|
+
fields: fieldsByTempId[tid],
|
|
236
|
+
tempId: tid
|
|
237
|
+
}));
|
|
238
|
+
return { ops, aliases, aliasOwnerModel, aliasDependencies };
|
|
239
|
+
}
|
|
240
|
+
function computeTeardownOrder(refs, aliasDependencies, aliasOwnerModel) {
|
|
241
|
+
const models = Object.keys(refs);
|
|
242
|
+
if (!aliasDependencies || !aliasOwnerModel || Object.keys(aliasDependencies).length === 0) {
|
|
243
|
+
return [...models].reverse();
|
|
244
|
+
}
|
|
245
|
+
const modelDeps = {};
|
|
246
|
+
for (const m of models) modelDeps[m] = /* @__PURE__ */ new Set();
|
|
247
|
+
for (const [alias, deps] of Object.entries(aliasDependencies)) {
|
|
248
|
+
const owner = aliasOwnerModel[alias];
|
|
249
|
+
if (!owner || !(owner in modelDeps)) continue;
|
|
250
|
+
for (const depAlias of deps) {
|
|
251
|
+
const depModel = aliasOwnerModel[depAlias];
|
|
252
|
+
if (!depModel || depModel === owner) continue;
|
|
253
|
+
if (depModel in modelDeps) {
|
|
254
|
+
modelDeps[owner].add(depModel);
|
|
654
255
|
}
|
|
655
|
-
valueTuples.push(`(${placeholders.join(", ")})`);
|
|
656
|
-
}
|
|
657
|
-
const valList = valueTuples.join(", ");
|
|
658
|
-
if (dialect.supportsReturning) {
|
|
659
|
-
const sql = `INSERT INTO ${dialect.quoteId(dbTable)} (${colList}) VALUES ${valList} RETURNING *`;
|
|
660
|
-
allResults.push(...mapRowsBack(await executor.query(sql, params), colMap));
|
|
661
|
-
} else {
|
|
662
|
-
await executor.query(
|
|
663
|
-
`INSERT INTO ${dialect.quoteId(dbTable)} (${colList}) VALUES ${valList}`,
|
|
664
|
-
params
|
|
665
|
-
);
|
|
666
256
|
}
|
|
667
257
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
return rows.map((row) => {
|
|
677
|
-
const mapped = {};
|
|
678
|
-
for (const [key, value] of Object.entries(row)) {
|
|
679
|
-
const fieldName = reverse.get(key) ?? key;
|
|
680
|
-
mapped[fieldName] = value;
|
|
258
|
+
const inDegree = {};
|
|
259
|
+
for (const m of models) inDegree[m] = 0;
|
|
260
|
+
const adj = {};
|
|
261
|
+
for (const [owner, deps] of Object.entries(modelDeps)) {
|
|
262
|
+
for (const depModel of deps) {
|
|
263
|
+
;
|
|
264
|
+
(adj[depModel] ??= []).push(owner);
|
|
265
|
+
inDegree[owner] = (inDegree[owner] ?? 0) + 1;
|
|
681
266
|
}
|
|
682
|
-
return mapped;
|
|
683
|
-
});
|
|
684
|
-
}
|
|
685
|
-
function castParam(dialect, paramIdx, enumTypeMap, fieldName) {
|
|
686
|
-
const placeholder = dialect.param(paramIdx);
|
|
687
|
-
if (dialect.name === "postgres") {
|
|
688
|
-
const enumType = enumTypeMap.get(fieldName);
|
|
689
|
-
if (enumType) return `${placeholder}::${dialect.quoteId(enumType)}`;
|
|
690
267
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
268
|
+
const payloadOrder = {};
|
|
269
|
+
models.forEach((m, i) => {
|
|
270
|
+
payloadOrder[m] = i;
|
|
271
|
+
});
|
|
272
|
+
const ready = models.filter((m) => inDegree[m] === 0).sort((a, b) => payloadOrder[a] - payloadOrder[b]);
|
|
273
|
+
const upOrder = [];
|
|
274
|
+
while (ready.length > 0) {
|
|
275
|
+
const m = ready.shift();
|
|
276
|
+
upOrder.push(m);
|
|
277
|
+
for (const next of adj[m] ?? []) {
|
|
278
|
+
inDegree[next] = (inDegree[next] ?? 0) - 1;
|
|
279
|
+
if (inDegree[next] === 0) ready.push(next);
|
|
703
280
|
}
|
|
281
|
+
ready.sort((a, b) => payloadOrder[a] - payloadOrder[b]);
|
|
704
282
|
}
|
|
705
|
-
|
|
283
|
+
if (upOrder.length !== models.length) {
|
|
284
|
+
return [...models].reverse();
|
|
285
|
+
}
|
|
286
|
+
return [...upOrder].reverse();
|
|
706
287
|
}
|
|
707
288
|
|
|
708
|
-
// src/
|
|
709
|
-
function
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
289
|
+
// src/schema.ts
|
|
290
|
+
function fieldTypeFromZod(schema) {
|
|
291
|
+
return classifyZod(unwrap(schema));
|
|
292
|
+
}
|
|
293
|
+
function getDef(schema) {
|
|
294
|
+
const def = schema._def;
|
|
295
|
+
return def && typeof def === "object" ? def : {};
|
|
296
|
+
}
|
|
297
|
+
function unwrap(schema) {
|
|
298
|
+
let current = schema;
|
|
299
|
+
for (let i = 0; i < 16; i++) {
|
|
300
|
+
const def = getDef(current);
|
|
301
|
+
const wrapped = wrappedSchemaFrom(def);
|
|
302
|
+
if (!wrapped) return current;
|
|
303
|
+
current = wrapped;
|
|
304
|
+
}
|
|
305
|
+
return current;
|
|
306
|
+
}
|
|
307
|
+
function wrappedSchemaFrom(def) {
|
|
308
|
+
const candidate = def.innerType ?? def.schema;
|
|
309
|
+
if (!candidate) return null;
|
|
310
|
+
const tn = def.typeName;
|
|
311
|
+
if (tn === "ZodOptional" || tn === "ZodNullable" || tn === "ZodDefault" || tn === "ZodCatch" || tn === "ZodBranded" || tn === "ZodReadonly" || tn === "ZodEffects" || tn === "ZodPipeline" || tn === "ZodLazy" || // Zod v4 uses `type` instead of `typeName`.
|
|
312
|
+
def.type === "optional" || def.type === "nullable" || def.type === "default" || def.type === "catch" || def.type === "readonly" || def.type === "pipe" || def.type === "lazy") {
|
|
313
|
+
return candidate;
|
|
716
314
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
function classifyZod(schema) {
|
|
318
|
+
const def = getDef(schema);
|
|
319
|
+
const name = (def.typeName ?? def.type ?? "").toString();
|
|
320
|
+
if (name === "ZodString" || name === "string") return "string";
|
|
321
|
+
if (name === "ZodNumber" || name === "number") {
|
|
322
|
+
return "number";
|
|
323
|
+
}
|
|
324
|
+
if (name === "ZodBigInt" || name === "bigint") return "integer";
|
|
325
|
+
if (name === "ZodBoolean" || name === "boolean") return "boolean";
|
|
326
|
+
if (name === "ZodDate" || name === "date") return "timestamp";
|
|
327
|
+
if (name === "ZodEnum" || name === "enum") return "string";
|
|
328
|
+
if (name === "ZodNativeEnum" || name === "nativeEnum") return "string";
|
|
329
|
+
if (name === "ZodLiteral" || name === "literal") return "string";
|
|
330
|
+
if (name === "ZodArray" || name === "array" || name === "ZodObject" || name === "object" || name === "ZodRecord" || name === "record" || name === "ZodTuple" || name === "tuple" || name === "ZodMap" || name === "map" || name === "ZodSet" || name === "set" || name === "ZodAny" || name === "any" || name === "ZodUnknown" || name === "unknown") {
|
|
331
|
+
return "json";
|
|
332
|
+
}
|
|
333
|
+
return "string";
|
|
334
|
+
}
|
|
335
|
+
function isOptional(schema) {
|
|
336
|
+
const def = getDef(schema);
|
|
337
|
+
const tn = def.typeName ?? def.type;
|
|
338
|
+
if (tn === "ZodOptional" || tn === "optional") return true;
|
|
339
|
+
if (tn === "ZodDefault" || tn === "default") return true;
|
|
340
|
+
if (tn === "ZodNullable" || tn === "nullable") {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
const inner = wrappedSchemaFrom(def);
|
|
344
|
+
if (inner) return isOptional(inner);
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
function hasDefault(schema) {
|
|
348
|
+
const def = getDef(schema);
|
|
349
|
+
const tn = def.typeName ?? def.type;
|
|
350
|
+
if (tn === "ZodDefault" || tn === "default") return true;
|
|
351
|
+
const inner = wrappedSchemaFrom(def);
|
|
352
|
+
if (inner) return hasDefault(inner);
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
function camelToSnake(name) {
|
|
356
|
+
let out = "";
|
|
357
|
+
for (let i = 0; i < name.length; i++) {
|
|
358
|
+
const ch = name.charAt(i);
|
|
359
|
+
if (ch >= "A" && ch <= "Z" && i > 0) {
|
|
360
|
+
const prev = name.charAt(i - 1);
|
|
361
|
+
if (!(prev >= "A" && prev <= "Z")) {
|
|
362
|
+
out += "_";
|
|
722
363
|
}
|
|
723
364
|
}
|
|
365
|
+
out += ch.toLowerCase();
|
|
724
366
|
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
const
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
const condAdj = /* @__PURE__ */ new Map();
|
|
739
|
-
const condInDeg = /* @__PURE__ */ new Map();
|
|
740
|
-
for (let i = 0; i < components.length; i++) {
|
|
741
|
-
condAdj.set(i, /* @__PURE__ */ new Set());
|
|
742
|
-
condInDeg.set(i, 0);
|
|
743
|
-
}
|
|
744
|
-
for (const edge of schema.edges) {
|
|
745
|
-
if (edge.from === edge.to) continue;
|
|
746
|
-
const fc = nodeToComp.get(edge.from);
|
|
747
|
-
const tc = nodeToComp.get(edge.to);
|
|
748
|
-
if (fc !== void 0 && tc !== void 0 && fc !== tc && !condAdj.get(tc).has(fc)) {
|
|
749
|
-
condAdj.get(tc).add(fc);
|
|
750
|
-
condInDeg.set(fc, (condInDeg.get(fc) ?? 0) + 1);
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
const condQueue = [];
|
|
754
|
-
for (const [idx, deg] of condInDeg) {
|
|
755
|
-
if (deg === 0) condQueue.push(idx);
|
|
756
|
-
}
|
|
757
|
-
const condOrder = [];
|
|
758
|
-
while (condQueue.length > 0) {
|
|
759
|
-
condQueue.sort();
|
|
760
|
-
const idx = condQueue.shift();
|
|
761
|
-
condOrder.push(idx);
|
|
762
|
-
for (const neighbor of condAdj.get(idx)) {
|
|
763
|
-
const nd = (condInDeg.get(neighbor) ?? 1) - 1;
|
|
764
|
-
condInDeg.set(neighbor, nd);
|
|
765
|
-
if (nd === 0) condQueue.push(neighbor);
|
|
367
|
+
return out;
|
|
368
|
+
}
|
|
369
|
+
function objectShape(schema) {
|
|
370
|
+
const def = getDef(schema);
|
|
371
|
+
const tn = def.typeName ?? def.type;
|
|
372
|
+
if (tn !== "ZodObject" && tn !== "object") return null;
|
|
373
|
+
const shape = def.shape;
|
|
374
|
+
if (typeof shape === "function") {
|
|
375
|
+
try {
|
|
376
|
+
const evaluated = shape();
|
|
377
|
+
if (evaluated && typeof evaluated === "object") return evaluated;
|
|
378
|
+
} catch {
|
|
379
|
+
return null;
|
|
766
380
|
}
|
|
767
381
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
for (const model of components[compIdx]) {
|
|
771
|
-
if (model !== scopeRootModel) {
|
|
772
|
-
order.push(model);
|
|
773
|
-
}
|
|
774
|
-
}
|
|
382
|
+
if (shape && typeof shape === "object") {
|
|
383
|
+
return shape;
|
|
775
384
|
}
|
|
776
|
-
return
|
|
385
|
+
return null;
|
|
777
386
|
}
|
|
778
|
-
|
|
779
|
-
const
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
for (const model of order) {
|
|
797
|
-
if (skipModels?.has(model)) continue;
|
|
798
|
-
await deleteModel(tx, dialect, tableMap, columnMaps, model, scopeValue, scopeFieldByModel, refs, schema);
|
|
799
|
-
}
|
|
800
|
-
if (!scopeRootModel || skipModels?.has(scopeRootModel)) return;
|
|
801
|
-
const dbTable = tableMap.get(scopeRootModel);
|
|
802
|
-
if (!dbTable) return;
|
|
803
|
-
const colMap = columnMaps.get(scopeRootModel) ?? /* @__PURE__ */ new Map();
|
|
804
|
-
const rootModelInfo = schema.models.find((m) => m.name === scopeRootModel);
|
|
805
|
-
const rootIdFields = rootModelInfo?.fields.filter((f) => f.isId) ?? [];
|
|
806
|
-
const rootPkFieldName = (rootIdFields.find((f) => f.name.toLowerCase() === "id") ?? rootIdFields[0])?.name ?? "id";
|
|
807
|
-
const idCol = colMap.get(rootPkFieldName) ?? rootPkFieldName;
|
|
808
|
-
await tx.query(
|
|
809
|
-
`DELETE FROM ${dialect.quoteId(dbTable)} WHERE ${dialect.quoteId(idCol)} = ${dialect.param(1)}`,
|
|
810
|
-
[scopeValue]
|
|
811
|
-
);
|
|
812
|
-
});
|
|
387
|
+
function modelToFields(inputSchema) {
|
|
388
|
+
const fields = [
|
|
389
|
+
{ name: "id", type: "string", isRequired: false, isId: true, hasDefault: true }
|
|
390
|
+
];
|
|
391
|
+
const shape = objectShape(unwrap(inputSchema));
|
|
392
|
+
if (!shape) return fields;
|
|
393
|
+
for (const [name, value] of Object.entries(shape)) {
|
|
394
|
+
const optional = isOptional(value);
|
|
395
|
+
const defaulted = hasDefault(value);
|
|
396
|
+
fields.push({
|
|
397
|
+
name,
|
|
398
|
+
type: fieldTypeFromZod(value),
|
|
399
|
+
isRequired: !optional && !defaulted,
|
|
400
|
+
isId: false,
|
|
401
|
+
hasDefault: defaulted
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
return fields;
|
|
813
405
|
}
|
|
814
|
-
|
|
815
|
-
const
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
const pkFieldName = (idFields.find((f) => f.name.toLowerCase() === "id") ?? idFields[0])?.name ?? "id";
|
|
821
|
-
const scopeFK = scopeFieldByModel.get(model);
|
|
822
|
-
if (scopeFK) {
|
|
823
|
-
const dbCol = colMap.get(scopeFK) ?? scopeFK;
|
|
824
|
-
await tx.query(
|
|
825
|
-
`DELETE FROM ${dialect.quoteId(dbTable)} WHERE ${dialect.quoteId(dbCol)} = ${dialect.param(1)}`,
|
|
826
|
-
[scopeValue]
|
|
827
|
-
);
|
|
828
|
-
} else if (refs?.[model]) {
|
|
829
|
-
const ids = refs[model].map((r) => r[pkFieldName]).filter((id) => id != null);
|
|
830
|
-
if (ids.length > 0) {
|
|
831
|
-
const idCol = colMap.get(pkFieldName) ?? pkFieldName;
|
|
832
|
-
const placeholders = ids.map((_, i) => dialect.param(i + 1)).join(", ");
|
|
833
|
-
await tx.query(
|
|
834
|
-
`DELETE FROM ${dialect.quoteId(dbTable)} WHERE ${dialect.quoteId(idCol)} IN (${placeholders})`,
|
|
835
|
-
ids
|
|
406
|
+
function buildSchemaFromFactories(factories, scopeField) {
|
|
407
|
+
const models = [];
|
|
408
|
+
for (const [entity, factory] of Object.entries(factories)) {
|
|
409
|
+
if (!factory.inputSchema) {
|
|
410
|
+
throw new Error(
|
|
411
|
+
`Factory "${entity}" has no inputSchema. Every factory must declare a Zod schema in defineFactory({ ..., inputSchema }).`
|
|
836
412
|
);
|
|
837
413
|
}
|
|
414
|
+
models.push({
|
|
415
|
+
name: entity,
|
|
416
|
+
tableName: camelToSnake(entity),
|
|
417
|
+
fields: modelToFields(factory.inputSchema)
|
|
418
|
+
});
|
|
838
419
|
}
|
|
420
|
+
return { models, edges: [], relations: [], scopeField };
|
|
421
|
+
}
|
|
422
|
+
function schemaToWire(schema) {
|
|
423
|
+
return {
|
|
424
|
+
models: schema.models.map((m) => ({
|
|
425
|
+
name: m.name,
|
|
426
|
+
tableName: m.tableName,
|
|
427
|
+
fields: m.fields.map((f) => ({
|
|
428
|
+
name: f.name,
|
|
429
|
+
type: f.type,
|
|
430
|
+
isRequired: f.isRequired,
|
|
431
|
+
isId: f.isId,
|
|
432
|
+
hasDefault: f.hasDefault
|
|
433
|
+
}))
|
|
434
|
+
})),
|
|
435
|
+
edges: schema.edges.map((e) => ({
|
|
436
|
+
from: e.from,
|
|
437
|
+
to: e.to,
|
|
438
|
+
localField: e.localField,
|
|
439
|
+
foreignField: e.foreignField,
|
|
440
|
+
nullable: e.nullable
|
|
441
|
+
})),
|
|
442
|
+
relations: schema.relations.map((r) => ({
|
|
443
|
+
parentModel: r.parentModel,
|
|
444
|
+
childModel: r.childModel,
|
|
445
|
+
parentField: r.parentField,
|
|
446
|
+
childField: r.childField
|
|
447
|
+
})),
|
|
448
|
+
scopeField: schema.scopeField
|
|
449
|
+
};
|
|
839
450
|
}
|
|
840
451
|
|
|
841
452
|
// src/handler.ts
|
|
@@ -852,10 +463,15 @@ function resolveTokens(value, testRunId, index) {
|
|
|
852
463
|
const parts = cycle[1].split(",").map((p) => p.trim().replace(/^['"]|['"]$/g, ""));
|
|
853
464
|
return parts.length ? parts[index % parts.length] : "";
|
|
854
465
|
}
|
|
855
|
-
throw new AutonomaError(
|
|
466
|
+
throw new AutonomaError(
|
|
467
|
+
`Unresolved token: {{${token}}}`,
|
|
468
|
+
"UNRESOLVED_TOKEN",
|
|
469
|
+
400
|
|
470
|
+
);
|
|
856
471
|
});
|
|
857
472
|
}
|
|
858
|
-
if (Array.isArray(value))
|
|
473
|
+
if (Array.isArray(value))
|
|
474
|
+
return value.map((v) => resolveTokens(v, testRunId, index));
|
|
859
475
|
if (value && typeof value === "object") {
|
|
860
476
|
const out = {};
|
|
861
477
|
for (const [k, v] of Object.entries(value)) {
|
|
@@ -865,21 +481,7 @@ function resolveTokens(value, testRunId, index) {
|
|
|
865
481
|
}
|
|
866
482
|
return value;
|
|
867
483
|
}
|
|
868
|
-
var
|
|
869
|
-
async function getIntrospection(config) {
|
|
870
|
-
let cached = introspectionCache.get(config);
|
|
871
|
-
if (cached) return cached;
|
|
872
|
-
const dialect = getDialect(config.dialect);
|
|
873
|
-
cached = await introspectDatabase(config.executor, dialect, {
|
|
874
|
-
scopeField: config.scopeField,
|
|
875
|
-
schema: config.dbSchema,
|
|
876
|
-
tableNameMap: config.tableNameMap,
|
|
877
|
-
excludeTables: config.excludeTables
|
|
878
|
-
});
|
|
879
|
-
introspectionCache.set(config, cached);
|
|
880
|
-
return cached;
|
|
881
|
-
}
|
|
882
|
-
var PROTOCOL_VERSION = "1.0";
|
|
484
|
+
var PROTOCOL_VERSION = true ? "1.0" : "1.0";
|
|
883
485
|
function buildSdkMeta(config) {
|
|
884
486
|
return {
|
|
885
487
|
version: PROTOCOL_VERSION,
|
|
@@ -900,7 +502,9 @@ async function handleRequest(config, req) {
|
|
|
900
502
|
);
|
|
901
503
|
}
|
|
902
504
|
if (!config.allowProduction && process.env.NODE_ENV === "production") {
|
|
903
|
-
throw Errors.productionBlocked(
|
|
505
|
+
throw Errors.productionBlocked(
|
|
506
|
+
"allowProduction not set and NODE_ENV === 'production'. if you want to change this, set allowProduction explicitly."
|
|
507
|
+
);
|
|
904
508
|
}
|
|
905
509
|
const signature = req.headers["x-signature"] ?? req.headers["X-Signature"] ?? "";
|
|
906
510
|
if (!verifySignature(req.body, signature, config.sharedSecret)) {
|
|
@@ -913,7 +517,10 @@ async function handleRequest(config, req) {
|
|
|
913
517
|
throw Errors.invalidBody("invalid JSON");
|
|
914
518
|
}
|
|
915
519
|
const action = body.action;
|
|
916
|
-
if (!action)
|
|
520
|
+
if (!action)
|
|
521
|
+
throw Errors.invalidBody(
|
|
522
|
+
'missing action. expected one of "discover", "up" or "down"'
|
|
523
|
+
);
|
|
917
524
|
switch (action) {
|
|
918
525
|
case "discover":
|
|
919
526
|
return await handleDiscover(config);
|
|
@@ -926,134 +533,96 @@ async function handleRequest(config, req) {
|
|
|
926
533
|
}
|
|
927
534
|
} catch (err) {
|
|
928
535
|
if (err instanceof AutonomaError) {
|
|
929
|
-
return {
|
|
536
|
+
return {
|
|
537
|
+
status: err.status,
|
|
538
|
+
body: { error: err.message, code: err.code }
|
|
539
|
+
};
|
|
930
540
|
}
|
|
931
541
|
const message = err instanceof Error ? err.message : "Internal error";
|
|
932
542
|
return { status: 500, body: { error: message, code: "INTERNAL_ERROR" } };
|
|
933
543
|
}
|
|
934
544
|
}
|
|
935
545
|
async function handleDiscover(config) {
|
|
936
|
-
const
|
|
937
|
-
|
|
546
|
+
const schema = buildSchemaFromFactories(
|
|
547
|
+
config.factories ?? {},
|
|
548
|
+
config.scopeField
|
|
549
|
+
);
|
|
550
|
+
return {
|
|
551
|
+
status: 200,
|
|
552
|
+
body: { ...buildSdkMeta(config), schema: schemaToWire(schema) }
|
|
553
|
+
};
|
|
938
554
|
}
|
|
939
555
|
async function handleUp(config, body) {
|
|
940
556
|
const create = body.create;
|
|
941
557
|
if (!create) throw Errors.invalidBody('missing "create" in request body');
|
|
942
|
-
const testRunId = body.testRunId ??
|
|
943
|
-
const
|
|
944
|
-
|
|
945
|
-
|
|
558
|
+
const testRunId = body.testRunId ?? randomUUID();
|
|
559
|
+
const factories = config.factories ?? {};
|
|
560
|
+
if (Object.keys(factories).length === 0) {
|
|
561
|
+
throw Errors.invalidBody(
|
|
562
|
+
"no factories registered \u2014 every model in `create` must have a factory."
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
const tree = resolvePayloadTree(create);
|
|
946
566
|
const refs = {};
|
|
947
567
|
const idMap = /* @__PURE__ */ new Map();
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
batch.push(tree.ops[i]);
|
|
957
|
-
}
|
|
958
|
-
const modelInfo = schema.models.find((m) => m.name === model);
|
|
959
|
-
const idFields = modelInfo?.fields.filter((f) => f.isId) ?? [];
|
|
960
|
-
const pkField = idFields.find((f) => f.name.toLowerCase() === "id") ?? idFields[0];
|
|
961
|
-
const pkFieldName = pkField?.name ?? "id";
|
|
962
|
-
const resolvedFields = batch.map((b, batchIndex) => {
|
|
963
|
-
const fields = resolveTokens({ ...b.fields }, testRunId, batchIndex);
|
|
964
|
-
for (const [key, value] of Object.entries(fields)) {
|
|
965
|
-
if (typeof value === "string" && value.startsWith("__temp_")) {
|
|
966
|
-
const realId = idMap.get(value);
|
|
967
|
-
if (realId) fields[key] = realId;
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
const scopeEdge = schema.edges.find(
|
|
971
|
-
(e) => e.from === model && e.localField.replace(/_/g, "").toLowerCase() === schema.scopeField.replace(/_/g, "").toLowerCase() && e.from !== e.to
|
|
972
|
-
);
|
|
973
|
-
if (scopeEdge && !(scopeEdge.localField in fields)) {
|
|
974
|
-
const scopeVal = detectScopeValue(refs, schema.scopeField);
|
|
975
|
-
if (scopeVal) fields[scopeEdge.localField] = scopeVal;
|
|
976
|
-
}
|
|
977
|
-
if (modelInfo) {
|
|
978
|
-
for (const field of modelInfo.fields) {
|
|
979
|
-
if (field.isRequired && !field.hasDefault && !field.isId && !(field.name in fields)) {
|
|
980
|
-
if (field.type === "DateTime") {
|
|
981
|
-
fields[field.name] = /* @__PURE__ */ new Date();
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
return fields;
|
|
987
|
-
});
|
|
988
|
-
let records;
|
|
989
|
-
const factory = config.factories?.[model];
|
|
990
|
-
if (factory) {
|
|
991
|
-
records = [];
|
|
992
|
-
for (const fields of resolvedFields) {
|
|
993
|
-
const factoryCtx = {
|
|
994
|
-
refs,
|
|
995
|
-
executor: tx,
|
|
996
|
-
scenarioName: testRunId,
|
|
997
|
-
testRunId
|
|
998
|
-
};
|
|
999
|
-
const record = await factory.create(fields, factoryCtx);
|
|
1000
|
-
if (record[pkFieldName] == null) {
|
|
1001
|
-
throw new AutonomaError(
|
|
1002
|
-
`Factory for "${model}" must return a record with "${pkFieldName}"`,
|
|
1003
|
-
"FACTORY_MISSING_PK",
|
|
1004
|
-
500
|
|
1005
|
-
);
|
|
1006
|
-
}
|
|
1007
|
-
records.push(record);
|
|
1008
|
-
}
|
|
1009
|
-
} else {
|
|
1010
|
-
const spec = {
|
|
1011
|
-
[model]: { count: resolvedFields.length, fields: resolvedFields, batch: op.batch }
|
|
1012
|
-
};
|
|
1013
|
-
const context = { testRunId, refs };
|
|
1014
|
-
const created = await createEntities(tx, dialect, tableMap, columnMaps, spec, context, enumTypeMaps, schema.models);
|
|
1015
|
-
records = created[model] ?? [];
|
|
1016
|
-
}
|
|
1017
|
-
if (!refs[model]) refs[model] = [];
|
|
1018
|
-
refs[model].push(...records);
|
|
1019
|
-
for (let j = 0; j < batch.length; j++) {
|
|
1020
|
-
const record = records[j];
|
|
1021
|
-
if (record) {
|
|
1022
|
-
const recordId = record[pkFieldName];
|
|
1023
|
-
if (recordId != null) {
|
|
1024
|
-
idMap.set(batch[j].tempId, recordId);
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
i++;
|
|
568
|
+
const modelIndex = {};
|
|
569
|
+
for (const op of tree.ops) {
|
|
570
|
+
const model = op.model;
|
|
571
|
+
const factory = factories[model];
|
|
572
|
+
if (!factory) {
|
|
573
|
+
throw Errors.invalidBody(
|
|
574
|
+
`no factory registered for model "${model}". Register one with \`defineFactory(...)\` and add it to HandlerConfig.factories.`
|
|
575
|
+
);
|
|
1029
576
|
}
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
)
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
577
|
+
const idx = modelIndex[model] ?? 0;
|
|
578
|
+
modelIndex[model] = idx + 1;
|
|
579
|
+
const tokenResolved = resolveTokens(op.fields, testRunId, idx);
|
|
580
|
+
const swapped = swapTempIds(tokenResolved, idMap);
|
|
581
|
+
const parsed = factory.inputSchema.safeParse(swapped);
|
|
582
|
+
if (!parsed.success) {
|
|
583
|
+
const formatted = parsed.error.issues.map(
|
|
584
|
+
(i) => `${i.path.join(".") || "<root>"}: ${i.message}`
|
|
585
|
+
).join("; ");
|
|
586
|
+
throw new AutonomaError(
|
|
587
|
+
`Invalid input for "${model}": ${formatted}`,
|
|
588
|
+
"INTERNAL_ERROR",
|
|
589
|
+
500
|
|
590
|
+
);
|
|
1043
591
|
}
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
592
|
+
const ctx = { refs, scenarioName: testRunId, testRunId };
|
|
593
|
+
const recordRaw = await factory.create(parsed.data, ctx);
|
|
594
|
+
const record = normaliseRecord(recordRaw);
|
|
595
|
+
if (!record || record.id == null) {
|
|
596
|
+
throw new AutonomaError(
|
|
597
|
+
`Factory for "${model}" must return a record with "id"`,
|
|
598
|
+
"FACTORY_MISSING_PK",
|
|
599
|
+
500
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
(refs[model] ??= []).push(record);
|
|
603
|
+
idMap.set(op.tempId, record.id);
|
|
604
|
+
}
|
|
605
|
+
const authUser = findFirstUser(refs);
|
|
606
|
+
const scopeValue = detectScopeValue(refs, config.scopeField) ?? testRunId;
|
|
607
|
+
let auth = await config.auth(authUser, { scopeValue, refs });
|
|
1048
608
|
if (config.afterUp) {
|
|
1049
609
|
const hookCtx = { scenarioName: scopeValue, refs };
|
|
1050
610
|
auth = await config.afterUp(hookCtx, auth);
|
|
1051
611
|
}
|
|
1052
612
|
const refsToken = signRefs(
|
|
1053
|
-
{
|
|
613
|
+
{
|
|
614
|
+
refs,
|
|
615
|
+
testRunId: scopeValue,
|
|
616
|
+
environment: "",
|
|
617
|
+
aliasDependencies: tree.aliasDependencies,
|
|
618
|
+
aliasOwnerModel: tree.aliasOwnerModel
|
|
619
|
+
},
|
|
1054
620
|
config.signingSecret
|
|
1055
621
|
);
|
|
1056
|
-
return {
|
|
622
|
+
return {
|
|
623
|
+
status: 200,
|
|
624
|
+
body: { ...buildSdkMeta(config), auth, refs, refsToken }
|
|
625
|
+
};
|
|
1057
626
|
}
|
|
1058
627
|
async function handleDown(config, body) {
|
|
1059
628
|
const refsToken = body.refsToken;
|
|
@@ -1065,40 +634,61 @@ async function handleDown(config, body) {
|
|
|
1065
634
|
const message = err instanceof Error ? err.message : "invalid token";
|
|
1066
635
|
throw Errors.invalidRefsToken(message);
|
|
1067
636
|
}
|
|
1068
|
-
const
|
|
1069
|
-
const
|
|
637
|
+
const refs = payload.refs ?? {};
|
|
638
|
+
const testRunId = payload.testRunId ?? "";
|
|
1070
639
|
if (config.beforeDown) {
|
|
1071
|
-
const hookCtx = { scenarioName:
|
|
640
|
+
const hookCtx = { scenarioName: testRunId, refs };
|
|
1072
641
|
await config.beforeDown(hookCtx);
|
|
1073
642
|
}
|
|
1074
|
-
const
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
643
|
+
const factories = config.factories ?? {};
|
|
644
|
+
const teardownOrder = computeTeardownOrder(
|
|
645
|
+
refs,
|
|
646
|
+
payload.aliasDependencies,
|
|
647
|
+
payload.aliasOwnerModel
|
|
648
|
+
);
|
|
649
|
+
for (const model of teardownOrder) {
|
|
650
|
+
const factory = factories[model];
|
|
651
|
+
if (!factory || !factory.teardown) continue;
|
|
652
|
+
const records = refs[model] ?? [];
|
|
653
|
+
const ctx = { refs, scenarioName: testRunId, testRunId };
|
|
654
|
+
for (const record of [...records].reverse()) {
|
|
655
|
+
let teardownInput = record;
|
|
656
|
+
if (factory.refSchema) {
|
|
657
|
+
const parsed = factory.refSchema.safeParse(record);
|
|
658
|
+
if (!parsed.success) {
|
|
659
|
+
const formatted = parsed.error.issues.map(
|
|
660
|
+
(i) => `${i.path.join(".") || "<root>"}: ${i.message}`
|
|
661
|
+
).join("; ");
|
|
662
|
+
throw new AutonomaError(
|
|
663
|
+
`Invalid teardown record for "${model}": ${formatted}`,
|
|
664
|
+
"INTERNAL_ERROR",
|
|
665
|
+
500
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
teardownInput = parsed.data;
|
|
1079
669
|
}
|
|
670
|
+
await factory.teardown(teardownInput, ctx);
|
|
1080
671
|
}
|
|
1081
672
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
testRunId: payload.testRunId
|
|
1094
|
-
};
|
|
1095
|
-
for (const record of [...records].reverse()) {
|
|
1096
|
-
await config.factories[model].teardown(record, factoryCtx);
|
|
1097
|
-
}
|
|
673
|
+
return { status: 200, body: { ...buildSdkMeta(config), ok: true } };
|
|
674
|
+
}
|
|
675
|
+
function swapTempIds(value, idMap) {
|
|
676
|
+
if (typeof value === "string" && value.startsWith("__temp_")) {
|
|
677
|
+
return idMap.get(value) ?? value;
|
|
678
|
+
}
|
|
679
|
+
if (Array.isArray(value)) return value.map((v) => swapTempIds(v, idMap));
|
|
680
|
+
if (value && typeof value === "object") {
|
|
681
|
+
const out = {};
|
|
682
|
+
for (const [k, v] of Object.entries(value)) {
|
|
683
|
+
out[k] = swapTempIds(v, idMap);
|
|
1098
684
|
}
|
|
685
|
+
return out;
|
|
1099
686
|
}
|
|
1100
|
-
|
|
1101
|
-
|
|
687
|
+
return value;
|
|
688
|
+
}
|
|
689
|
+
function normaliseRecord(value) {
|
|
690
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
691
|
+
return value;
|
|
1102
692
|
}
|
|
1103
693
|
function findFirstUser(refs) {
|
|
1104
694
|
for (const [model, records] of Object.entries(refs)) {
|
|
@@ -1122,6 +712,12 @@ function detectScopeValue(refs, scopeField) {
|
|
|
1122
712
|
}
|
|
1123
713
|
return null;
|
|
1124
714
|
}
|
|
715
|
+
function randomUUID() {
|
|
716
|
+
if (typeof globalThis.crypto?.randomUUID === "function") {
|
|
717
|
+
return globalThis.crypto.randomUUID();
|
|
718
|
+
}
|
|
719
|
+
return `run-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
720
|
+
}
|
|
1125
721
|
|
|
1126
722
|
// src/fingerprint.ts
|
|
1127
723
|
import { createHash } from "crypto";
|
|
@@ -1143,17 +739,14 @@ function sortReplacer(_key, value) {
|
|
|
1143
739
|
}
|
|
1144
740
|
|
|
1145
741
|
// src/check.ts
|
|
1146
|
-
async function checkScenario(
|
|
742
|
+
async function checkScenario(factories, scenario, options) {
|
|
1147
743
|
const sharedSecret = options?.sharedSecret ?? "autonoma-check-shared";
|
|
1148
744
|
const signingSecret = options?.signingSecret ?? "autonoma-check-signing";
|
|
1149
745
|
const config = {
|
|
1150
|
-
executor,
|
|
1151
746
|
scopeField: options?.scopeField ?? "organizationId",
|
|
1152
|
-
dialect: options?.dialect,
|
|
1153
|
-
dbSchema: options?.dbSchema,
|
|
1154
|
-
tableNameMap: options?.tableNameMap,
|
|
1155
747
|
sharedSecret,
|
|
1156
748
|
signingSecret,
|
|
749
|
+
factories,
|
|
1157
750
|
auth: options?.auth ?? (async () => ({ headers: { Authorization: "Bearer check-token" } }))
|
|
1158
751
|
};
|
|
1159
752
|
const upBody = JSON.stringify({ action: "up", create: scenario.create });
|
|
@@ -1169,7 +762,7 @@ async function checkScenario(executor, scenario, options) {
|
|
|
1169
762
|
return {
|
|
1170
763
|
valid: false,
|
|
1171
764
|
phase: "up",
|
|
1172
|
-
errors: [{ phase: "up", message: errorMsg
|
|
765
|
+
errors: [{ phase: "up", message: errorMsg }],
|
|
1173
766
|
timing: { upMs, downMs: 0 }
|
|
1174
767
|
};
|
|
1175
768
|
}
|
|
@@ -1193,27 +786,13 @@ async function checkScenario(executor, scenario, options) {
|
|
|
1193
786
|
}
|
|
1194
787
|
return { valid: true, phase: "ok", errors: [], timing: { upMs, downMs } };
|
|
1195
788
|
}
|
|
1196
|
-
async function checkAllScenarios(
|
|
789
|
+
async function checkAllScenarios(factories, scenarios, options) {
|
|
1197
790
|
const results = [];
|
|
1198
791
|
for (const scenario of scenarios) {
|
|
1199
|
-
results.push(await checkScenario(
|
|
792
|
+
results.push(await checkScenario(factories, scenario, options));
|
|
1200
793
|
}
|
|
1201
794
|
return results;
|
|
1202
795
|
}
|
|
1203
|
-
function suggestFix(errorMsg) {
|
|
1204
|
-
if (errorMsg.includes("Unique constraint failed") || errorMsg.includes("unique constraint")) {
|
|
1205
|
-
const match = errorMsg.match(/fields: \(`(.+?)`\)/) ?? errorMsg.match(/constraint "(.+?)"/);
|
|
1206
|
-
if (match) return `Unique constraint on (${match[1]}). Ensure field values are unique across instances.`;
|
|
1207
|
-
return "Unique constraint violation. Make field values unique across instances.";
|
|
1208
|
-
}
|
|
1209
|
-
if (errorMsg.includes("Foreign key constraint") || errorMsg.includes("foreign key")) {
|
|
1210
|
-
return "A referenced record does not exist. Check that parent entities are nested correctly.";
|
|
1211
|
-
}
|
|
1212
|
-
if (errorMsg.includes("null value in column") || errorMsg.includes("must not be null")) {
|
|
1213
|
-
return "A required field is null. Add it to the node with a value.";
|
|
1214
|
-
}
|
|
1215
|
-
return "";
|
|
1216
|
-
}
|
|
1217
796
|
|
|
1218
797
|
// src/factory.ts
|
|
1219
798
|
function defineFactory(definition) {
|
|
@@ -1223,24 +802,39 @@ function defineFactory(definition) {
|
|
|
1223
802
|
if (definition.teardown !== void 0 && typeof definition.teardown !== "function") {
|
|
1224
803
|
throw new Error('Factory "teardown" must be a function if provided');
|
|
1225
804
|
}
|
|
805
|
+
if (!definition.inputSchema || !isZodSchema(definition.inputSchema)) {
|
|
806
|
+
throw new Error(
|
|
807
|
+
'Factory "inputSchema" must be a Zod schema (e.g. z.object({...})). Discover relies on it to describe the model to the dashboard.'
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
if (definition.refSchema !== void 0 && !isZodSchema(definition.refSchema)) {
|
|
811
|
+
throw new Error('Factory "refSchema" must be a Zod schema if provided');
|
|
812
|
+
}
|
|
1226
813
|
return definition;
|
|
1227
814
|
}
|
|
815
|
+
function isZodSchema(value) {
|
|
816
|
+
if (!value || typeof value !== "object") return false;
|
|
817
|
+
const candidate = value;
|
|
818
|
+
return typeof candidate.parse === "function" && typeof candidate.safeParse === "function";
|
|
819
|
+
}
|
|
1228
820
|
export {
|
|
821
|
+
AutonomaError,
|
|
822
|
+
Errors,
|
|
1229
823
|
PROTOCOL_VERSION,
|
|
824
|
+
buildSchemaFromFactories,
|
|
1230
825
|
checkAllScenarios,
|
|
1231
826
|
checkScenario,
|
|
1232
827
|
computeTeardownOrder,
|
|
1233
|
-
createEntities,
|
|
1234
828
|
defineFactory,
|
|
829
|
+
fieldTypeFromZod,
|
|
1235
830
|
findDeferrableEdge,
|
|
1236
831
|
fingerprint,
|
|
1237
|
-
getDialect,
|
|
1238
832
|
handleRequest,
|
|
1239
|
-
|
|
1240
|
-
|
|
833
|
+
resolvePayloadTree,
|
|
834
|
+
resolveTokens,
|
|
835
|
+
schemaToWire,
|
|
1241
836
|
signBody,
|
|
1242
837
|
signRefs,
|
|
1243
|
-
teardown,
|
|
1244
838
|
topoSort,
|
|
1245
839
|
verifyRefs,
|
|
1246
840
|
verifySignature
|