@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/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
- "Environment factory is disabled in production",
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/generated/sql-queries.ts
197
- var POSTGRES_COLUMNS = `SELECT
198
- table_name,
199
- column_name,
200
- data_type,
201
- udt_name,
202
- is_nullable,
203
- column_default
204
- FROM information_schema.columns
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
- columnMaps.set(modelName, colMap);
417
- models.push({ name: modelName, tableName: dbTable, fields });
111
+ for (const v of Object.values(obj)) collectRefs(v, out);
112
+ return;
418
113
  }
419
- const edges = [];
420
- for (const fk of fkRows) {
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 normalizeKeys(rows) {
511
- return rows.map((row) => {
512
- if (!row || typeof row !== "object") return row;
513
- const normalized = {};
514
- for (const [key, val] of Object.entries(row)) {
515
- normalized[key.toLowerCase()] = val;
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
- return normalized;
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
- return results;
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
- await executor.query(
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
- async function insertBatch(executor, dialect, dbTable, colMap, enumTypeMap, fieldsArr, pkFieldName = "id", pkFieldType = "String") {
614
- if (fieldsArr.length === 0) return [];
615
- if (pkFieldName && pkFieldType === "String") {
616
- fieldsArr = fieldsArr.map((fields) => {
617
- if (fields[pkFieldName] === void 0) {
618
- return { ...fields, [pkFieldName]: randomUUID() };
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
- return fields;
621
- });
622
- }
623
- const fieldNameSet = /* @__PURE__ */ new Set();
624
- for (const fields of fieldsArr) {
625
- for (const key of Object.keys(fields)) {
626
- fieldNameSet.add(key);
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 fieldNames = [...fieldNameSet].sort();
630
- if (fieldNames.length === 0) {
631
- const allResults2 = [];
632
- for (const fields of fieldsArr) {
633
- const [record] = await insertOne(executor, dialect, dbTable, colMap, enumTypeMap, fields);
634
- if (record) allResults2.push(record);
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
- return allResults2;
637
- }
638
- const dbCols = fieldNames.map((f) => dialect.quoteId(colMap.get(f) ?? f));
639
- const colList = dbCols.join(", ");
640
- const MAX_PARAMS = 32e3;
641
- const chunkSize = Math.max(1, Math.floor(MAX_PARAMS / fieldNames.length));
642
- const allResults = [];
643
- for (let offset = 0; offset < fieldsArr.length; offset += chunkSize) {
644
- const chunk = fieldsArr.slice(offset, offset + chunkSize);
645
- const params = [];
646
- const valueTuples = [];
647
- let paramIdx = 1;
648
- for (const fields of chunk) {
649
- const placeholders = [];
650
- for (const fieldName of fieldNames) {
651
- placeholders.push(castParam(dialect, paramIdx, enumTypeMap, fieldName));
652
- params.push(serializeValue(fields[fieldName], dialect));
653
- paramIdx++;
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
- return allResults;
669
- }
670
- function mapRowsBack(rows, colMap) {
671
- if (colMap.size === 0) return rows;
672
- const reverse = /* @__PURE__ */ new Map();
673
- for (const [fieldName, dbCol] of colMap) {
674
- reverse.set(dbCol, fieldName);
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
- return placeholder;
692
- }
693
- var MYSQL_DATETIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/;
694
- function serializeValue(value, dialect) {
695
- if (value === null || value === void 0) return null;
696
- if (typeof value === "object" && !(value instanceof Date)) {
697
- if (Array.isArray(value)) return value;
698
- return JSON.stringify(value);
699
- }
700
- if (typeof value === "string" && dialect.name === "mysql") {
701
- if (MYSQL_DATETIME_RE.test(value)) {
702
- return value.replace("T", " ").replace("Z", "").replace(/\.\d+$/, "");
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
- return value;
283
+ if (upOrder.length !== models.length) {
284
+ return [...models].reverse();
285
+ }
286
+ return [...upOrder].reverse();
706
287
  }
707
288
 
708
- // src/teardown.ts
709
- function computeTeardownOrder(schema) {
710
- let scopeRootModel = null;
711
- for (const edge of schema.edges) {
712
- if (edge.localField.toLowerCase() === schema.scopeField.toLowerCase() && edge.to !== edge.from) {
713
- scopeRootModel = edge.to;
714
- break;
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
- const scopeFieldByModel = /* @__PURE__ */ new Map();
718
- if (scopeRootModel) {
719
- for (const edge of schema.edges) {
720
- if (edge.to === scopeRootModel && edge.from !== scopeRootModel) {
721
- scopeFieldByModel.set(edge.from, edge.localField);
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
- const modelNames = schema.models.map((m) => m.name);
726
- const { sorted, cycles } = topoSort(modelNames, schema.edges);
727
- const components = [];
728
- const nodeToComp = /* @__PURE__ */ new Map();
729
- for (const cycle of cycles) {
730
- const idx = components.length;
731
- components.push(cycle);
732
- for (const node of cycle) nodeToComp.set(node, idx);
733
- }
734
- for (const node of sorted) {
735
- nodeToComp.set(node, components.length);
736
- components.push([node]);
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
- const order = [];
769
- for (const compIdx of [...condOrder].reverse()) {
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 { order, scopeRootModel, cycles, scopeFieldByModel };
385
+ return null;
777
386
  }
778
- async function teardown(executor, dialect, tableMap, columnMaps, schema, scopeValue, refs, skipModels) {
779
- const { order, scopeRootModel, cycles, scopeFieldByModel } = computeTeardownOrder(schema);
780
- await executor.transaction(async (tx) => {
781
- for (const cycle of cycles) {
782
- const edge = findDeferrableEdge(cycle, schema.edges);
783
- if (!edge) continue;
784
- const scopeFK = scopeFieldByModel.get(edge.from);
785
- if (!scopeFK) continue;
786
- const dbTable2 = tableMap.get(edge.from);
787
- if (!dbTable2) continue;
788
- const colMap2 = columnMaps.get(edge.from) ?? /* @__PURE__ */ new Map();
789
- const dbFKCol = colMap2.get(edge.localField) ?? edge.localField;
790
- const dbScopeCol = colMap2.get(scopeFK) ?? scopeFK;
791
- await tx.query(
792
- `UPDATE ${dialect.quoteId(dbTable2)} SET ${dialect.quoteId(dbFKCol)} = NULL WHERE ${dialect.quoteId(dbScopeCol)} = ${dialect.param(1)}`,
793
- [scopeValue]
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
- async function deleteModel(tx, dialect, tableMap, columnMaps, model, scopeValue, scopeFieldByModel, refs, schema) {
815
- const dbTable = tableMap.get(model);
816
- if (!dbTable) return;
817
- const colMap = columnMaps.get(model) ?? /* @__PURE__ */ new Map();
818
- const modelInfo = schema.models.find((m) => m.name === model);
819
- const idFields = modelInfo?.fields.filter((f) => f.isId) ?? [];
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(`Unresolved token: {{${token}}}`, "UNRESOLVED_TOKEN", 400);
466
+ throw new AutonomaError(
467
+ `Unresolved token: {{${token}}}`,
468
+ "UNRESOLVED_TOKEN",
469
+ 400
470
+ );
856
471
  });
857
472
  }
858
- if (Array.isArray(value)) return value.map((v) => resolveTokens(v, testRunId, index));
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 introspectionCache = /* @__PURE__ */ new WeakMap();
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) throw Errors.invalidBody("missing 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 { status: err.status, body: { error: err.message, code: err.code } };
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 { schema } = await getIntrospection(config);
937
- return { status: 200, body: { ...buildSdkMeta(config), schema } };
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 ?? crypto.randomUUID();
943
- const { schema, tableMap, columnMaps, enumTypeMaps } = await getIntrospection(config);
944
- const dialect = getDialect(config.dialect);
945
- const tree = resolveTree(create, schema);
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
- await config.executor.transaction(async (tx) => {
949
- let i = 0;
950
- while (i < tree.ops.length) {
951
- const op = tree.ops[i];
952
- const model = op.model;
953
- const batch = [op];
954
- while (i + 1 < tree.ops.length && tree.ops[i + 1].model === model && tree.ops[i + 1].batch === op.batch) {
955
- i++;
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
- for (const deferred of tree.deferredUpdates) {
1031
- const realTargetId = idMap.get(deferred.targetTempId);
1032
- const refTempId = tree.aliases.get(deferred.refAlias);
1033
- const realRefId = refTempId ? idMap.get(refTempId) : void 0;
1034
- if (!realTargetId || !realRefId) {
1035
- throw new Error(
1036
- `_ref "${deferred.refAlias}" could not be resolved. Ensure the referenced node has _alias defined in the scenario.`
1037
- );
1038
- }
1039
- const deferredModelInfo = schema.models.find((m) => m.name === deferred.model);
1040
- const deferredIdFields = deferredModelInfo?.fields.filter((f) => f.isId) ?? [];
1041
- const deferredPkFieldName = (deferredIdFields.find((f) => f.name.toLowerCase() === "id") ?? deferredIdFields[0])?.name ?? "id";
1042
- await updateEntity(tx, dialect, tableMap, columnMaps, deferred.model, String(realTargetId), { [deferred.field]: realRefId }, enumTypeMaps, deferredPkFieldName);
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
- const scopeValue = detectScopeValue(refs, schema.scopeField) ?? testRunId;
1046
- const firstUser = findFirstUser(refs);
1047
- let auth = await config.auth(firstUser, { scopeValue, refs });
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
- { refs, testRunId: scopeValue, environment: "" },
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 { status: 200, body: { ...buildSdkMeta(config), auth, refs, refsToken } };
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 { schema, tableMap, columnMaps } = await getIntrospection(config);
1069
- const dialect = getDialect(config.dialect);
637
+ const refs = payload.refs ?? {};
638
+ const testRunId = payload.testRunId ?? "";
1070
639
  if (config.beforeDown) {
1071
- const hookCtx = { scenarioName: payload.testRunId, refs: payload.refs ?? {} };
640
+ const hookCtx = { scenarioName: testRunId, refs };
1072
641
  await config.beforeDown(hookCtx);
1073
642
  }
1074
- const factoryTeardownModels = /* @__PURE__ */ new Set();
1075
- if (config.factories) {
1076
- for (const [model, factory] of Object.entries(config.factories)) {
1077
- if (factory.teardown) {
1078
- factoryTeardownModels.add(model);
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
- if (factoryTeardownModels.size > 0) {
1083
- const { order, scopeRootModel } = computeTeardownOrder(schema);
1084
- const fullOrder = scopeRootModel ? [...order, scopeRootModel] : order;
1085
- const refs = payload.refs ?? {};
1086
- for (const model of [...fullOrder].reverse()) {
1087
- if (!factoryTeardownModels.has(model)) continue;
1088
- const records = refs[model] ?? [];
1089
- const factoryCtx = {
1090
- refs,
1091
- executor: config.executor,
1092
- scenarioName: payload.testRunId,
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
- await teardown(config.executor, dialect, tableMap, columnMaps, schema, payload.testRunId, payload.refs, factoryTeardownModels);
1101
- return { status: 200, body: { ...buildSdkMeta(config), ok: true } };
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(executor, scenario, options) {
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, fix: suggestFix(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(executor, scenarios, options) {
789
+ async function checkAllScenarios(factories, scenarios, options) {
1197
790
  const results = [];
1198
791
  for (const scenario of scenarios) {
1199
- results.push(await checkScenario(executor, scenario, options));
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
- introspectDatabase,
1240
- resolveTree,
833
+ resolvePayloadTree,
834
+ resolveTokens,
835
+ schemaToWire,
1241
836
  signBody,
1242
837
  signRefs,
1243
- teardown,
1244
838
  topoSort,
1245
839
  verifyRefs,
1246
840
  verifySignature