@autonoma-ai/sdk 0.1.0 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -125,6 +125,7 @@ function resolveTree(create, schema, testRunId) {
125
125
  }
126
126
  const aliases = /* @__PURE__ */ new Map();
127
127
  const ops = [];
128
+ const deferredUpdates = [];
128
129
  let tempCounter = 0;
129
130
  function makeTempId(model) {
130
131
  return `__temp_${model}_${tempCounter++}`;
@@ -137,9 +138,21 @@ function resolveTree(create, schema, testRunId) {
137
138
  const tempId = makeTempId(modelName);
138
139
  for (const [key, value] of Object.entries(node)) {
139
140
  if (RESERVED_KEYS.has(key)) continue;
140
- const relation = relationByParentField.get(`${modelName}.${key}`);
141
+ const exactKey = `${modelName}.${key}`;
142
+ const prefixedKey = `${modelName}.${modelName.charAt(0).toLowerCase()}${modelName.slice(1)}${key.charAt(0).toUpperCase()}${key.slice(1)}`;
143
+ let relation = relationByParentField.get(exactKey) ?? relationByParentField.get(prefixedKey) ?? void 0;
144
+ let matchedKey = relationByParentField.has(exactKey) ? exactKey : prefixedKey;
145
+ if (!relation) {
146
+ for (const [relKey, rel] of relationByParentField) {
147
+ if (relKey.startsWith(`${modelName}.`) && rel.childModel.toLowerCase() === key.toLowerCase()) {
148
+ relation = rel;
149
+ matchedKey = relKey;
150
+ break;
151
+ }
152
+ }
153
+ }
141
154
  if (relation) {
142
- const isOnParent = fkOnParent.has(`${modelName}.${key}`);
155
+ const isOnParent = fkOnParent.has(matchedKey);
143
156
  if (isOnParent) {
144
157
  preChildren.push({ relation, value, fkOnParent: true });
145
158
  } else {
@@ -151,7 +164,8 @@ function resolveTree(create, schema, testRunId) {
151
164
  const refAlias = value._ref;
152
165
  const refTempId = aliases.get(refAlias);
153
166
  if (!refTempId) {
154
- throw new Error(`_ref "${refAlias}" not found. Ensure the referenced node has _alias and is created before this one.`);
167
+ deferredUpdates.push({ targetTempId: tempId, model: modelName, field: key, refAlias });
168
+ continue;
155
169
  }
156
170
  fields[key] = refTempId;
157
171
  continue;
@@ -200,7 +214,7 @@ function resolveTree(create, schema, testRunId) {
200
214
  walkNode(modelName, nodes[i], null, null, false, i);
201
215
  }
202
216
  }
203
- return { ops, aliases };
217
+ return { ops, deferredUpdates, aliases };
204
218
  }
205
219
 
206
220
  // src/errors.ts
@@ -257,7 +271,618 @@ var Errors = {
257
271
  }
258
272
  };
259
273
 
274
+ // src/generated/sql-queries.ts
275
+ var POSTGRES_COLUMNS = `SELECT
276
+ table_name,
277
+ column_name,
278
+ data_type,
279
+ udt_name,
280
+ is_nullable,
281
+ column_default
282
+ FROM information_schema.columns
283
+ WHERE table_schema = '{{schema}}'
284
+ ORDER BY table_name, ordinal_position`;
285
+ var POSTGRES_ENUMS = `SELECT t.typname AS enum_name, e.enumlabel AS enum_value
286
+ FROM pg_type t
287
+ JOIN pg_enum e ON t.oid = e.enumtypid
288
+ JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
289
+ ORDER BY t.typname, e.enumsortorder`;
290
+ var POSTGRES_FOREIGN_KEYS = `SELECT
291
+ kcu.table_name AS from_table,
292
+ kcu.column_name AS from_column,
293
+ ccu.table_name AS to_table,
294
+ ccu.column_name AS to_column,
295
+ c.is_nullable
296
+ FROM information_schema.table_constraints tc
297
+ JOIN information_schema.key_column_usage kcu
298
+ ON tc.constraint_name = kcu.constraint_name
299
+ AND tc.table_schema = kcu.table_schema
300
+ JOIN information_schema.constraint_column_usage ccu
301
+ ON tc.constraint_name = ccu.constraint_name
302
+ AND tc.table_schema = ccu.table_schema
303
+ LEFT JOIN information_schema.columns c
304
+ ON c.table_schema = kcu.table_schema
305
+ AND c.table_name = kcu.table_name
306
+ AND c.column_name = kcu.column_name
307
+ WHERE tc.constraint_type = 'FOREIGN KEY'
308
+ AND tc.table_schema = '{{schema}}'
309
+ ORDER BY kcu.table_name, kcu.ordinal_position`;
310
+ var POSTGRES_PRIMARY_KEYS = `SELECT
311
+ tc.table_name,
312
+ kcu.column_name
313
+ FROM information_schema.table_constraints tc
314
+ JOIN information_schema.key_column_usage kcu
315
+ ON tc.constraint_name = kcu.constraint_name
316
+ AND tc.table_schema = kcu.table_schema
317
+ WHERE tc.constraint_type = 'PRIMARY KEY'
318
+ AND tc.table_schema = '{{schema}}'
319
+ ORDER BY tc.table_name, kcu.ordinal_position`;
320
+ var POSTGRES_TABLES = `SELECT table_name
321
+ FROM information_schema.tables
322
+ WHERE table_schema = '{{schema}}'
323
+ AND table_type = 'BASE TABLE'
324
+ ORDER BY table_name`;
325
+ var MYSQL_COLUMNS = `SELECT
326
+ table_name,
327
+ column_name,
328
+ data_type,
329
+ column_type AS udt_name,
330
+ is_nullable,
331
+ column_default
332
+ FROM information_schema.columns
333
+ WHERE table_schema = '{{schema}}'
334
+ ORDER BY table_name, ordinal_position`;
335
+ var MYSQL_ENUMS = `SELECT NULL AS enum_name, NULL AS enum_value FROM DUAL WHERE 1 = 0`;
336
+ var MYSQL_FOREIGN_KEYS = `SELECT
337
+ kcu.table_name AS from_table,
338
+ kcu.column_name AS from_column,
339
+ kcu.referenced_table_name AS to_table,
340
+ kcu.referenced_column_name AS to_column,
341
+ c.is_nullable
342
+ FROM information_schema.key_column_usage kcu
343
+ JOIN information_schema.columns c
344
+ ON c.table_schema = kcu.table_schema
345
+ AND c.table_name = kcu.table_name
346
+ AND c.column_name = kcu.column_name
347
+ WHERE kcu.referenced_table_name IS NOT NULL
348
+ AND kcu.table_schema = '{{schema}}'
349
+ ORDER BY kcu.table_name, kcu.ordinal_position`;
350
+ var MYSQL_PRIMARY_KEYS = `SELECT
351
+ tc.table_name,
352
+ kcu.column_name
353
+ FROM information_schema.table_constraints tc
354
+ JOIN information_schema.key_column_usage kcu
355
+ ON tc.constraint_name = kcu.constraint_name
356
+ AND tc.table_schema = kcu.table_schema
357
+ AND tc.table_name = kcu.table_name
358
+ WHERE tc.constraint_type = 'PRIMARY KEY'
359
+ AND tc.table_schema = '{{schema}}'
360
+ ORDER BY tc.table_name, kcu.ordinal_position`;
361
+ var MYSQL_TABLES = `SELECT table_name
362
+ FROM information_schema.tables
363
+ WHERE table_schema = '{{schema}}'
364
+ AND table_type = 'BASE TABLE'
365
+ ORDER BY table_name`;
366
+
367
+ // src/dialect.ts
368
+ var replaceSchema = (template, schema) => template.replace("{{schema}}", schema);
369
+ var postgres = {
370
+ name: "postgres",
371
+ param: (i) => `$${i}`,
372
+ quoteId: (name) => `"${name}"`,
373
+ supportsReturning: true,
374
+ tablesSQL: (schema) => replaceSchema(POSTGRES_TABLES, schema),
375
+ columnsSQL: (schema) => replaceSchema(POSTGRES_COLUMNS, schema),
376
+ primaryKeysSQL: (schema) => replaceSchema(POSTGRES_PRIMARY_KEYS, schema),
377
+ foreignKeysSQL: (schema) => replaceSchema(POSTGRES_FOREIGN_KEYS, schema),
378
+ enumsSQL: () => POSTGRES_ENUMS
379
+ };
380
+ var mysql = {
381
+ name: "mysql",
382
+ param: () => "?",
383
+ quoteId: (name) => `\`${name}\``,
384
+ supportsReturning: false,
385
+ tablesSQL: (schema) => replaceSchema(MYSQL_TABLES, schema),
386
+ columnsSQL: (schema) => replaceSchema(MYSQL_COLUMNS, schema),
387
+ primaryKeysSQL: (schema) => replaceSchema(MYSQL_PRIMARY_KEYS, schema),
388
+ foreignKeysSQL: (schema) => replaceSchema(MYSQL_FOREIGN_KEYS, schema),
389
+ enumsSQL: () => MYSQL_ENUMS
390
+ };
391
+ function getDialect(name = "postgres") {
392
+ switch (name) {
393
+ case "postgres":
394
+ return postgres;
395
+ case "mysql":
396
+ return mysql;
397
+ default:
398
+ throw new Error(`Dialect "${name}" is not yet supported. Currently only "postgres" and "mysql" are available.`);
399
+ }
400
+ }
401
+
402
+ // src/introspect.ts
403
+ async function introspectDatabase(executor, dialect, config) {
404
+ const dbSchema = config.schema ?? (dialect.name === "mysql" ? void 0 : "public");
405
+ if (!dbSchema) {
406
+ throw new Error("MySQL requires a schema (database name). Pass it via config.schema or HandlerConfig.dbSchema.");
407
+ }
408
+ const excludeSet = new Set(config.excludeTables ?? ["_prisma_migrations"]);
409
+ const [tableRows, columnRows, pkRows, fkRows, enumRows] = await Promise.all([
410
+ executor.query(dialect.tablesSQL(dbSchema)).then(normalizeKeys),
411
+ executor.query(dialect.columnsSQL(dbSchema)).then(normalizeKeys),
412
+ executor.query(dialect.primaryKeysSQL(dbSchema)).then(normalizeKeys),
413
+ executor.query(dialect.foreignKeysSQL(dbSchema)).then(normalizeKeys),
414
+ executor.query(dialect.enumsSQL(dbSchema)).then(normalizeKeys)
415
+ ]);
416
+ const enumValues = /* @__PURE__ */ new Map();
417
+ for (const row of enumRows) {
418
+ if (!row.enum_name) continue;
419
+ if (!enumValues.has(row.enum_name)) enumValues.set(row.enum_name, []);
420
+ enumValues.get(row.enum_name).push(row.enum_value);
421
+ }
422
+ if (dialect.name === "mysql") {
423
+ for (const col of columnRows) {
424
+ const parsed = parseMySQLEnum(col.udt_name);
425
+ if (parsed) {
426
+ const enumKey = `${col.table_name}.${col.column_name}`;
427
+ enumValues.set(enumKey, parsed);
428
+ }
429
+ }
430
+ }
431
+ const pksByTable = /* @__PURE__ */ new Map();
432
+ for (const row of pkRows) {
433
+ if (!pksByTable.has(row.table_name)) pksByTable.set(row.table_name, /* @__PURE__ */ new Set());
434
+ pksByTable.get(row.table_name).add(row.column_name);
435
+ }
436
+ const userMap = config.tableNameMap ?? {};
437
+ const tableMap = /* @__PURE__ */ new Map();
438
+ const reverseTableMap = /* @__PURE__ */ new Map();
439
+ for (const [model, dbTable] of Object.entries(userMap)) {
440
+ tableMap.set(model, dbTable);
441
+ reverseTableMap.set(dbTable, model);
442
+ }
443
+ const dbTables = tableRows.map((r) => r.table_name).filter((t) => !excludeSet.has(t));
444
+ for (const dbTable of dbTables) {
445
+ if (reverseTableMap.has(dbTable)) continue;
446
+ const modelName = snakeToPascal(dbTable);
447
+ tableMap.set(modelName, dbTable);
448
+ reverseTableMap.set(dbTable, modelName);
449
+ }
450
+ const models = [];
451
+ const columnMaps = /* @__PURE__ */ new Map();
452
+ const enumTypeMaps = /* @__PURE__ */ new Map();
453
+ const columnsByTable = /* @__PURE__ */ new Map();
454
+ for (const row of columnRows) {
455
+ if (!columnsByTable.has(row.table_name)) columnsByTable.set(row.table_name, []);
456
+ columnsByTable.get(row.table_name).push(row);
457
+ }
458
+ for (const [modelName, dbTable] of tableMap) {
459
+ const cols = columnsByTable.get(dbTable) ?? [];
460
+ const pks = pksByTable.get(dbTable) ?? /* @__PURE__ */ new Set();
461
+ const colMap = /* @__PURE__ */ new Map();
462
+ const fields = [];
463
+ for (const col of cols) {
464
+ const fieldName = snakeToCamel(col.column_name);
465
+ colMap.set(fieldName, col.column_name);
466
+ let enumVals;
467
+ if (dialect.name === "mysql") {
468
+ enumVals = enumValues.get(`${col.table_name}.${col.column_name}`);
469
+ } else {
470
+ enumVals = enumValues.get(col.udt_name);
471
+ }
472
+ const type = enumVals ? `enum(${enumVals.join(",")})` : mapDataType(col.data_type, col.udt_name, dialect.name);
473
+ if (dialect.name === "postgres") {
474
+ if (enumVals) {
475
+ if (!enumTypeMaps.has(modelName)) enumTypeMaps.set(modelName, /* @__PURE__ */ new Map());
476
+ enumTypeMaps.get(modelName).set(fieldName, col.udt_name);
477
+ } else if (col.data_type === "jsonb" || col.udt_name === "jsonb" || col.data_type === "json" || col.udt_name === "json") {
478
+ if (!enumTypeMaps.has(modelName)) enumTypeMaps.set(modelName, /* @__PURE__ */ new Map());
479
+ const jsonType = col.data_type === "json" || col.udt_name === "json" ? "json" : "jsonb";
480
+ enumTypeMaps.get(modelName).set(fieldName, jsonType);
481
+ } else if (col.data_type.includes("timestamp") || col.udt_name === "timestamptz" || col.udt_name === "timestamp") {
482
+ if (!enumTypeMaps.has(modelName)) enumTypeMaps.set(modelName, /* @__PURE__ */ new Map());
483
+ enumTypeMaps.get(modelName).set(fieldName, col.udt_name);
484
+ }
485
+ }
486
+ fields.push({
487
+ name: fieldName,
488
+ type,
489
+ isRequired: col.is_nullable === "NO",
490
+ isId: pks.has(col.column_name),
491
+ hasDefault: col.column_default !== null
492
+ });
493
+ }
494
+ columnMaps.set(modelName, colMap);
495
+ models.push({ name: modelName, tableName: dbTable, fields });
496
+ }
497
+ const edges = [];
498
+ for (const fk of fkRows) {
499
+ const fromModel = reverseTableMap.get(fk.from_table);
500
+ const toModel = reverseTableMap.get(fk.to_table);
501
+ if (!fromModel || !toModel) continue;
502
+ const fromColMap = columnMaps.get(fromModel);
503
+ const toColMap = columnMaps.get(toModel);
504
+ const localField = fromColMap ? reverseGet(fromColMap, fk.from_column) ?? fk.from_column : fk.from_column;
505
+ const foreignField = toColMap ? reverseGet(toColMap, fk.to_column) ?? fk.to_column : fk.to_column;
506
+ edges.push({
507
+ from: fromModel,
508
+ to: toModel,
509
+ localField,
510
+ foreignField,
511
+ nullable: fk.is_nullable === "YES"
512
+ });
513
+ }
514
+ const relations = [];
515
+ for (const edge of edges) {
516
+ const fromDbTable = tableMap.get(edge.from);
517
+ const fromColMap = columnMaps.get(edge.from) ?? /* @__PURE__ */ new Map();
518
+ const fkDbCol = fromColMap.get(edge.localField) ?? edge.localField;
519
+ const fromPks = pksByTable.get(fromDbTable);
520
+ const isOneToOne = fromPks !== void 0 && fromPks.size === 1 && fromPks.has(fkDbCol);
521
+ relations.push({
522
+ parentModel: edge.to,
523
+ childModel: edge.from,
524
+ parentField: isOneToOne ? lowerFirst(edge.from) : pluralCamelCase(edge.from),
525
+ childField: edge.localField
526
+ });
527
+ relations.push({
528
+ parentModel: edge.from,
529
+ childModel: edge.to,
530
+ parentField: lowerFirst(edge.to),
531
+ childField: edge.localField
532
+ });
533
+ }
534
+ return {
535
+ schema: { models, edges, relations, scopeField: config.scopeField },
536
+ tableMap,
537
+ columnMaps,
538
+ enumTypeMaps
539
+ };
540
+ }
541
+ function snakeToPascal(str) {
542
+ return str.split("_").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
543
+ }
544
+ function snakeToCamel(str) {
545
+ const pascal = snakeToPascal(str);
546
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
547
+ }
548
+ function parseMySQLEnum(columnType) {
549
+ if (!columnType) return null;
550
+ const match = columnType.match(/^enum\((.+)\)$/i);
551
+ if (!match) return null;
552
+ return match[1].split(",").map((v) => v.trim().replace(/^'|'$/g, ""));
553
+ }
554
+ function mapDataType(dataType, udtName, dialectName) {
555
+ const dt = dataType.toLowerCase();
556
+ if (dt === "integer" || dt === "smallint" || dt === "bigint" || dt === "int" || dt === "mediumint" || dt === "tinyint") return "Int";
557
+ if (dt === "numeric" || dt === "real" || dt === "double precision" || dt === "float" || dt === "double" || dt === "decimal") return "Float";
558
+ if (dt === "boolean" || dt === "tinyint(1)") return "Boolean";
559
+ if (dt === "text" || dt === "character varying" || dt === "character" || dt === "varchar" || dt === "char" || dt === "mediumtext" || dt === "longtext" || dt === "tinytext") return "String";
560
+ if (dt === "timestamp with time zone" || dt === "timestamp without time zone" || dt === "date" || dt === "time" || dt === "datetime" || dt === "timestamp") return "DateTime";
561
+ if (dt === "json" || dt === "jsonb") return "Json";
562
+ if (dt === "uuid") return "String";
563
+ if (dt === "bytea" || dt === "blob" || dt === "mediumblob" || dt === "longblob" || dt === "tinyblob" || dt === "binary" || dt === "varbinary") return "Bytes";
564
+ if (dt === "user-defined" && dialectName === "postgres") return udtName;
565
+ if (dt === "enum" || dt === "set") return udtName;
566
+ return dataType;
567
+ }
568
+ function lowerFirst(str) {
569
+ return str.charAt(0).toLowerCase() + str.slice(1);
570
+ }
571
+ function pluralCamelCase(modelName) {
572
+ const camel = lowerFirst(modelName);
573
+ return pluralize(camel);
574
+ }
575
+ function pluralize(str) {
576
+ if (str.endsWith("s") || str.endsWith("x") || str.endsWith("z") || str.endsWith("ch") || str.endsWith("sh")) {
577
+ return str + "es";
578
+ }
579
+ if (str.endsWith("y") && str.length > 1 && !isVowel(str.charAt(str.length - 2))) {
580
+ return str.slice(0, -1) + "ies";
581
+ }
582
+ return str + "s";
583
+ }
584
+ function isVowel(ch) {
585
+ return "aeiou".includes(ch.toLowerCase());
586
+ }
587
+ function normalizeKeys(rows) {
588
+ return rows.map((row) => {
589
+ if (!row || typeof row !== "object") return row;
590
+ const normalized = {};
591
+ for (const [key, val] of Object.entries(row)) {
592
+ normalized[key.toLowerCase()] = val;
593
+ }
594
+ return normalized;
595
+ });
596
+ }
597
+ function reverseGet(map, dbName) {
598
+ for (const [key, val] of map) {
599
+ if (val === dbName) return key;
600
+ }
601
+ return null;
602
+ }
603
+
604
+ // src/create.ts
605
+ import { randomUUID } from "crypto";
606
+ async function createEntities(executor, dialect, tableMap, columnMaps, spec, _context, enumTypeMaps = /* @__PURE__ */ new Map()) {
607
+ const results = {};
608
+ for (const [model, entitySpec] of Object.entries(spec)) {
609
+ const dbTable = tableMap.get(model);
610
+ if (!dbTable) throw new Error(`Unknown model "${model}". Not found in database tables.`);
611
+ const colMap = columnMaps.get(model) ?? /* @__PURE__ */ new Map();
612
+ const enumTypeMap = enumTypeMaps.get(model) ?? /* @__PURE__ */ new Map();
613
+ if (entitySpec.batch && entitySpec.fields.length > 0) {
614
+ results[model] = await insertBatch(executor, dialect, dbTable, colMap, enumTypeMap, entitySpec.fields);
615
+ } else {
616
+ const created = [];
617
+ for (const fields of entitySpec.fields) {
618
+ const [record] = await insertOne(executor, dialect, dbTable, colMap, enumTypeMap, fields);
619
+ if (record) created.push(record);
620
+ }
621
+ results[model] = created;
622
+ }
623
+ }
624
+ return results;
625
+ }
626
+ async function updateEntity(executor, dialect, tableMap, columnMaps, model, id, fields, enumTypeMaps = /* @__PURE__ */ new Map()) {
627
+ const dbTable = tableMap.get(model);
628
+ if (!dbTable) throw new Error(`Unknown model "${model}" for update.`);
629
+ const colMap = columnMaps.get(model) ?? /* @__PURE__ */ new Map();
630
+ const enumTypeMap = enumTypeMaps.get(model) ?? /* @__PURE__ */ new Map();
631
+ const setClauses = [];
632
+ const params = [];
633
+ let paramIdx = 1;
634
+ for (const [fieldName, value] of Object.entries(fields)) {
635
+ const dbCol = colMap.get(fieldName) ?? fieldName;
636
+ setClauses.push(`${dialect.quoteId(dbCol)} = ${castParam(dialect, paramIdx, enumTypeMap, fieldName)}`);
637
+ params.push(serializeValue(value, dialect));
638
+ paramIdx++;
639
+ }
640
+ const idCol = colMap.get("id") ?? "id";
641
+ params.push(id);
642
+ const sql = `UPDATE ${dialect.quoteId(dbTable)} SET ${setClauses.join(", ")} WHERE ${dialect.quoteId(idCol)} = ${dialect.param(paramIdx)}`;
643
+ await executor.query(sql, params);
644
+ }
645
+ async function insertOne(executor, dialect, dbTable, colMap, enumTypeMap, fields) {
646
+ const idFieldName = reverseGet2(colMap, findIdCol(colMap));
647
+ if (idFieldName && fields[idFieldName] === void 0) {
648
+ fields = { ...fields, [idFieldName]: randomUUID() };
649
+ }
650
+ const entries = Object.entries(fields);
651
+ if (entries.length === 0) {
652
+ const sql = `INSERT INTO ${dialect.quoteId(dbTable)} DEFAULT VALUES RETURNING *`;
653
+ return mapRowsBack(await executor.query(sql), colMap);
654
+ }
655
+ const dbCols = [];
656
+ const params = [];
657
+ const placeholders = [];
658
+ let paramIdx = 1;
659
+ for (const [fieldName, value] of entries) {
660
+ const dbCol = colMap.get(fieldName) ?? fieldName;
661
+ dbCols.push(dialect.quoteId(dbCol));
662
+ placeholders.push(castParam(dialect, paramIdx, enumTypeMap, fieldName));
663
+ params.push(serializeValue(value, dialect));
664
+ paramIdx++;
665
+ }
666
+ const colList = dbCols.join(", ");
667
+ const valList = placeholders.join(", ");
668
+ if (dialect.supportsReturning) {
669
+ const sql = `INSERT INTO ${dialect.quoteId(dbTable)} (${colList}) VALUES (${valList}) RETURNING *`;
670
+ return mapRowsBack(await executor.query(sql, params), colMap);
671
+ }
672
+ await executor.query(
673
+ `INSERT INTO ${dialect.quoteId(dbTable)} (${colList}) VALUES (${valList})`,
674
+ params
675
+ );
676
+ const idCol = findIdCol(colMap);
677
+ const id = fields[idFieldName ?? "id"];
678
+ return mapRowsBack(
679
+ await executor.query(
680
+ `SELECT * FROM ${dialect.quoteId(dbTable)} WHERE ${dialect.quoteId(idCol)} = ${dialect.param(1)}`,
681
+ [id]
682
+ ),
683
+ colMap
684
+ );
685
+ }
686
+ async function insertBatch(executor, dialect, dbTable, colMap, enumTypeMap, fieldsArr) {
687
+ if (fieldsArr.length === 0) return [];
688
+ const idFieldName = reverseGet2(colMap, findIdCol(colMap));
689
+ if (idFieldName) {
690
+ fieldsArr = fieldsArr.map((fields) => {
691
+ if (fields[idFieldName] === void 0) {
692
+ return { ...fields, [idFieldName]: randomUUID() };
693
+ }
694
+ return fields;
695
+ });
696
+ }
697
+ const fieldNames = Object.keys(fieldsArr[0]);
698
+ const dbCols = fieldNames.map((f) => dialect.quoteId(colMap.get(f) ?? f));
699
+ const colList = dbCols.join(", ");
700
+ const MAX_PARAMS = 32e3;
701
+ const chunkSize = Math.max(1, Math.floor(MAX_PARAMS / fieldNames.length));
702
+ const allResults = [];
703
+ for (let offset = 0; offset < fieldsArr.length; offset += chunkSize) {
704
+ const chunk = fieldsArr.slice(offset, offset + chunkSize);
705
+ const params = [];
706
+ const valueTuples = [];
707
+ let paramIdx = 1;
708
+ for (const fields of chunk) {
709
+ const placeholders = [];
710
+ for (const fieldName of fieldNames) {
711
+ placeholders.push(castParam(dialect, paramIdx, enumTypeMap, fieldName));
712
+ params.push(serializeValue(fields[fieldName], dialect));
713
+ paramIdx++;
714
+ }
715
+ valueTuples.push(`(${placeholders.join(", ")})`);
716
+ }
717
+ const valList = valueTuples.join(", ");
718
+ if (dialect.supportsReturning) {
719
+ const sql = `INSERT INTO ${dialect.quoteId(dbTable)} (${colList}) VALUES ${valList} RETURNING *`;
720
+ allResults.push(...mapRowsBack(await executor.query(sql, params), colMap));
721
+ } else {
722
+ await executor.query(
723
+ `INSERT INTO ${dialect.quoteId(dbTable)} (${colList}) VALUES ${valList}`,
724
+ params
725
+ );
726
+ }
727
+ }
728
+ return allResults;
729
+ }
730
+ function mapRowsBack(rows, colMap) {
731
+ if (colMap.size === 0) return rows;
732
+ const reverse = /* @__PURE__ */ new Map();
733
+ for (const [fieldName, dbCol] of colMap) {
734
+ reverse.set(dbCol, fieldName);
735
+ }
736
+ return rows.map((row) => {
737
+ const mapped = {};
738
+ for (const [key, value] of Object.entries(row)) {
739
+ const fieldName = reverse.get(key) ?? key;
740
+ mapped[fieldName] = value;
741
+ }
742
+ return mapped;
743
+ });
744
+ }
745
+ function findIdCol(colMap) {
746
+ return colMap.get("id") ?? "id";
747
+ }
748
+ function reverseGet2(map, dbName) {
749
+ for (const [key, val] of map) {
750
+ if (val === dbName) return key;
751
+ }
752
+ return null;
753
+ }
754
+ function castParam(dialect, paramIdx, enumTypeMap, fieldName) {
755
+ const placeholder = dialect.param(paramIdx);
756
+ if (dialect.name === "postgres") {
757
+ const enumType = enumTypeMap.get(fieldName);
758
+ if (enumType) return `${placeholder}::${dialect.quoteId(enumType)}`;
759
+ }
760
+ return placeholder;
761
+ }
762
+ function serializeValue(value, dialect) {
763
+ if (value === null || value === void 0) return value;
764
+ if (typeof value === "object" && !(value instanceof Date)) {
765
+ return JSON.stringify(value);
766
+ }
767
+ if (typeof value === "string" && dialect.name === "mysql") {
768
+ if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(value)) {
769
+ return value.replace("T", " ").replace("Z", "").replace(/\.\d+$/, "");
770
+ }
771
+ }
772
+ return value;
773
+ }
774
+
775
+ // src/teardown.ts
776
+ async function teardown(executor, dialect, tableMap, columnMaps, schema, scopeValue, refs) {
777
+ let scopeRootModel = null;
778
+ for (const edge of schema.edges) {
779
+ if (edge.localField.toLowerCase() === schema.scopeField.toLowerCase() && edge.to !== edge.from) {
780
+ scopeRootModel = edge.to;
781
+ break;
782
+ }
783
+ }
784
+ const scopeFieldByModel = /* @__PURE__ */ new Map();
785
+ if (scopeRootModel) {
786
+ for (const edge of schema.edges) {
787
+ if (edge.to === scopeRootModel && edge.from !== scopeRootModel) {
788
+ scopeFieldByModel.set(edge.from, edge.localField);
789
+ }
790
+ }
791
+ }
792
+ const modelNames = schema.models.map((m) => m.name);
793
+ const { sorted, cycles } = topoSort(modelNames, schema.edges);
794
+ await executor.transaction(async (tx) => {
795
+ for (const cycle of cycles) {
796
+ const edge = findDeferrableEdge(cycle, schema.edges);
797
+ if (edge) {
798
+ const scopeFK = scopeFieldByModel.get(edge.from);
799
+ if (scopeFK) {
800
+ const dbTable = tableMap.get(edge.from);
801
+ const colMap = columnMaps.get(edge.from) ?? /* @__PURE__ */ new Map();
802
+ if (dbTable) {
803
+ const dbFKCol = colMap.get(edge.localField) ?? edge.localField;
804
+ const dbScopeCol = colMap.get(scopeFK) ?? scopeFK;
805
+ await tx.query(
806
+ `UPDATE ${dialect.quoteId(dbTable)} SET ${dialect.quoteId(dbFKCol)} = NULL WHERE ${dialect.quoteId(dbScopeCol)} = ${dialect.param(1)}`,
807
+ [scopeValue]
808
+ );
809
+ }
810
+ }
811
+ }
812
+ }
813
+ for (const cycle of cycles) {
814
+ for (const model of cycle) {
815
+ await deleteModel(tx, dialect, tableMap, columnMaps, model, scopeValue, scopeFieldByModel, refs);
816
+ }
817
+ }
818
+ const reversed = [...sorted].reverse();
819
+ for (const model of reversed) {
820
+ if (model === scopeRootModel) continue;
821
+ await deleteModel(tx, dialect, tableMap, columnMaps, model, scopeValue, scopeFieldByModel, refs);
822
+ }
823
+ if (scopeRootModel) {
824
+ const dbTable = tableMap.get(scopeRootModel);
825
+ const colMap = columnMaps.get(scopeRootModel) ?? /* @__PURE__ */ new Map();
826
+ if (dbTable) {
827
+ const idCol = colMap.get("id") ?? "id";
828
+ await tx.query(
829
+ `DELETE FROM ${dialect.quoteId(dbTable)} WHERE ${dialect.quoteId(idCol)} = ${dialect.param(1)}`,
830
+ [scopeValue]
831
+ );
832
+ }
833
+ }
834
+ });
835
+ }
836
+ async function deleteModel(tx, dialect, tableMap, columnMaps, model, scopeValue, scopeFieldByModel, refs) {
837
+ const dbTable = tableMap.get(model);
838
+ if (!dbTable) return;
839
+ const colMap = columnMaps.get(model) ?? /* @__PURE__ */ new Map();
840
+ const scopeFK = scopeFieldByModel.get(model);
841
+ if (scopeFK) {
842
+ const dbCol = colMap.get(scopeFK) ?? scopeFK;
843
+ await tx.query(
844
+ `DELETE FROM ${dialect.quoteId(dbTable)} WHERE ${dialect.quoteId(dbCol)} = ${dialect.param(1)}`,
845
+ [scopeValue]
846
+ );
847
+ } else if (refs?.[model]) {
848
+ const ids = refs[model].map((r) => r.id).filter((id) => typeof id === "string");
849
+ if (ids.length > 0) {
850
+ const idCol = colMap.get("id") ?? "id";
851
+ const placeholders = ids.map((_, i) => dialect.param(i + 1)).join(", ");
852
+ await tx.query(
853
+ `DELETE FROM ${dialect.quoteId(dbTable)} WHERE ${dialect.quoteId(idCol)} IN (${placeholders})`,
854
+ ids
855
+ );
856
+ }
857
+ }
858
+ }
859
+
260
860
  // src/handler.ts
861
+ var introspectionCache = /* @__PURE__ */ new WeakMap();
862
+ async function getIntrospection(config) {
863
+ let cached = introspectionCache.get(config);
864
+ if (cached) return cached;
865
+ const dialect = getDialect(config.dialect);
866
+ cached = await introspectDatabase(config.executor, dialect, {
867
+ scopeField: config.scopeField,
868
+ schema: config.dbSchema,
869
+ tableNameMap: config.tableNameMap,
870
+ excludeTables: config.excludeTables
871
+ });
872
+ introspectionCache.set(config, cached);
873
+ return cached;
874
+ }
875
+ var PROTOCOL_VERSION = "1.0";
876
+ function buildSdkMeta(config) {
877
+ return {
878
+ version: PROTOCOL_VERSION,
879
+ sdk: {
880
+ language: "typescript",
881
+ orm: config.sdk?.orm ?? "unknown",
882
+ server: config.sdk?.server ?? "unknown"
883
+ }
884
+ };
885
+ }
261
886
  async function handleRequest(config, req) {
262
887
  try {
263
888
  if (config.sharedSecret === config.signingSecret) {
@@ -284,7 +909,7 @@ async function handleRequest(config, req) {
284
909
  if (!action) throw Errors.invalidBody("missing action");
285
910
  switch (action) {
286
911
  case "discover":
287
- return handleDiscover(config);
912
+ return await handleDiscover(config);
288
913
  case "up":
289
914
  return await handleUp(config, body);
290
915
  case "down":
@@ -300,61 +925,85 @@ async function handleRequest(config, req) {
300
925
  return { status: 500, body: { error: message, code: "INTERNAL_ERROR" } };
301
926
  }
302
927
  }
303
- function handleDiscover(config) {
304
- const schema = config.adapter.getSchema();
305
- return { status: 200, body: { schema } };
928
+ async function handleDiscover(config) {
929
+ const { schema } = await getIntrospection(config);
930
+ return { status: 200, body: { ...buildSdkMeta(config), schema } };
306
931
  }
307
932
  async function handleUp(config, body) {
308
933
  const create = body.create;
309
934
  if (!create) throw Errors.invalidBody('missing "create" in request body');
310
935
  const testRunId = body.testRunId ?? crypto.randomUUID();
311
- const schema = config.adapter.getSchema();
936
+ const { schema, tableMap, columnMaps, enumTypeMaps } = await getIntrospection(config);
937
+ const dialect = getDialect(config.dialect);
312
938
  const tree = resolveTree(create, schema, testRunId);
313
939
  const refs = {};
314
940
  const idMap = /* @__PURE__ */ new Map();
315
- let i = 0;
316
- while (i < tree.ops.length) {
317
- const op = tree.ops[i];
318
- const model = op.model;
319
- const batch = [op];
320
- while (i + 1 < tree.ops.length && tree.ops[i + 1].model === model && tree.ops[i + 1].batch === op.batch) {
321
- i++;
322
- batch.push(tree.ops[i]);
323
- }
324
- const resolvedFields = batch.map((b) => {
325
- const fields = { ...b.fields };
326
- delete fields.id;
327
- for (const [key, value] of Object.entries(fields)) {
328
- if (typeof value === "string" && value.startsWith("__temp_")) {
329
- const realId = idMap.get(value);
330
- if (realId) fields[key] = realId;
331
- }
941
+ await config.executor.transaction(async (tx) => {
942
+ let i = 0;
943
+ while (i < tree.ops.length) {
944
+ const op = tree.ops[i];
945
+ const model = op.model;
946
+ const batch = [op];
947
+ while (i + 1 < tree.ops.length && tree.ops[i + 1].model === model && tree.ops[i + 1].batch === op.batch) {
948
+ i++;
949
+ batch.push(tree.ops[i]);
332
950
  }
333
- const scopeEdge = schema.edges.find(
334
- (e) => e.from === model && e.localField.toLowerCase() === schema.scopeField.toLowerCase() && e.from !== e.to
335
- );
336
- if (scopeEdge && !(scopeEdge.localField in fields)) {
337
- const scopeVal = detectScopeValue(refs, schema.scopeField);
338
- if (scopeVal) fields[scopeEdge.localField] = scopeVal;
951
+ const modelInfo = schema.models.find((m) => m.name === model);
952
+ const resolvedFields = batch.map((b) => {
953
+ const fields = { ...b.fields };
954
+ delete fields.id;
955
+ for (const [key, value] of Object.entries(fields)) {
956
+ if (typeof value === "string" && value.startsWith("__temp_")) {
957
+ const realId = idMap.get(value);
958
+ if (realId) fields[key] = realId;
959
+ }
960
+ }
961
+ const scopeEdge = schema.edges.find(
962
+ (e) => e.from === model && e.localField.toLowerCase() === schema.scopeField.toLowerCase() && e.from !== e.to
963
+ );
964
+ if (scopeEdge && !(scopeEdge.localField in fields)) {
965
+ const scopeVal = detectScopeValue(refs, schema.scopeField);
966
+ if (scopeVal) fields[scopeEdge.localField] = scopeVal;
967
+ }
968
+ if (modelInfo) {
969
+ for (const field of modelInfo.fields) {
970
+ if (field.isRequired && !field.hasDefault && !field.isId && !(field.name in fields)) {
971
+ if (field.type === "DateTime") {
972
+ fields[field.name] = /* @__PURE__ */ new Date();
973
+ }
974
+ }
975
+ }
976
+ }
977
+ return fields;
978
+ });
979
+ const spec = {
980
+ [model]: { count: resolvedFields.length, fields: resolvedFields, batch: op.batch }
981
+ };
982
+ const context = { testRunId, refs };
983
+ const created = await createEntities(tx, dialect, tableMap, columnMaps, spec, context, enumTypeMaps);
984
+ const records = created[model] ?? [];
985
+ if (!refs[model]) refs[model] = [];
986
+ refs[model].push(...records);
987
+ for (let j = 0; j < batch.length; j++) {
988
+ const record = records[j];
989
+ if (record && typeof record.id === "string") {
990
+ idMap.set(batch[j].tempId, record.id);
991
+ }
339
992
  }
340
- return fields;
341
- });
342
- const spec = {
343
- [model]: { count: resolvedFields.length, fields: resolvedFields, batch: op.batch }
344
- };
345
- const context = { testRunId, refs };
346
- const created = await config.adapter.createEntities(spec, context);
347
- const records = created[model] ?? [];
348
- if (!refs[model]) refs[model] = [];
349
- refs[model].push(...records);
350
- for (let j = 0; j < batch.length; j++) {
351
- const record = records[j];
352
- if (record && typeof record.id === "string") {
353
- idMap.set(batch[j].tempId, record.id);
993
+ i++;
994
+ }
995
+ for (const deferred of tree.deferredUpdates) {
996
+ const realTargetId = idMap.get(deferred.targetTempId);
997
+ const refTempId = tree.aliases.get(deferred.refAlias);
998
+ const realRefId = refTempId ? idMap.get(refTempId) : void 0;
999
+ if (!realTargetId || !realRefId) {
1000
+ throw new Error(
1001
+ `_ref "${deferred.refAlias}" could not be resolved. Ensure the referenced node has _alias defined in the scenario.`
1002
+ );
354
1003
  }
1004
+ await updateEntity(tx, dialect, tableMap, columnMaps, deferred.model, realTargetId, { [deferred.field]: realRefId }, enumTypeMaps);
355
1005
  }
356
- i++;
357
- }
1006
+ });
358
1007
  const scopeValue = detectScopeValue(refs, schema.scopeField) ?? testRunId;
359
1008
  const firstUser = findFirstUser(refs);
360
1009
  let auth = { token: "" };
@@ -365,7 +1014,7 @@ async function handleUp(config, body) {
365
1014
  { refs, testRunId: scopeValue, environment: "" },
366
1015
  config.signingSecret
367
1016
  );
368
- return { status: 200, body: { auth, refs, refsToken } };
1017
+ return { status: 200, body: { ...buildSdkMeta(config), auth, refs, refsToken } };
369
1018
  }
370
1019
  async function handleDown(config, body) {
371
1020
  const refsToken = body.refsToken;
@@ -377,8 +1026,10 @@ async function handleDown(config, body) {
377
1026
  const message = err instanceof Error ? err.message : "invalid token";
378
1027
  throw Errors.invalidRefsToken(message);
379
1028
  }
380
- await config.adapter.teardown(payload.testRunId, payload.refs);
381
- return { status: 200, body: { ok: true } };
1029
+ const { schema, tableMap, columnMaps } = await getIntrospection(config);
1030
+ const dialect = getDialect(config.dialect);
1031
+ await teardown(config.executor, dialect, tableMap, columnMaps, schema, payload.testRunId, payload.refs);
1032
+ return { status: 200, body: { ...buildSdkMeta(config), ok: true } };
382
1033
  }
383
1034
  function findFirstUser(refs) {
384
1035
  for (const [model, records] of Object.entries(refs)) {
@@ -422,11 +1073,15 @@ function sortReplacer(_key, value) {
422
1073
  }
423
1074
 
424
1075
  // src/check.ts
425
- async function checkScenario(adapter, scenario, options) {
1076
+ async function checkScenario(executor, scenario, options) {
426
1077
  const sharedSecret = options?.sharedSecret ?? "autonoma-check-shared";
427
1078
  const signingSecret = options?.signingSecret ?? "autonoma-check-signing";
428
1079
  const config = {
429
- adapter,
1080
+ executor,
1081
+ scopeField: options?.scopeField ?? "organizationId",
1082
+ dialect: options?.dialect,
1083
+ dbSchema: options?.dbSchema,
1084
+ tableNameMap: options?.tableNameMap,
430
1085
  sharedSecret,
431
1086
  signingSecret,
432
1087
  auth: options?.auth ?? (async () => ({ token: "check-token" }))
@@ -468,41 +1123,42 @@ async function checkScenario(adapter, scenario, options) {
468
1123
  }
469
1124
  return { valid: true, phase: "ok", errors: [], timing: { upMs, downMs } };
470
1125
  }
471
- async function checkAllScenarios(adapter, scenarios, options) {
1126
+ async function checkAllScenarios(executor, scenarios, options) {
472
1127
  const results = [];
473
1128
  for (const scenario of scenarios) {
474
- results.push(await checkScenario(adapter, scenario, options));
1129
+ results.push(await checkScenario(executor, scenario, options));
475
1130
  }
476
1131
  return results;
477
1132
  }
478
1133
  function suggestFix(errorMsg) {
479
- if (errorMsg.includes("Unique constraint failed")) {
480
- const match = errorMsg.match(/fields: \(`(.+?)`\)/);
1134
+ if (errorMsg.includes("Unique constraint failed") || errorMsg.includes("unique constraint")) {
1135
+ const match = errorMsg.match(/fields: \(`(.+?)`\)/) ?? errorMsg.match(/constraint "(.+?)"/);
481
1136
  if (match) return `Unique constraint on (${match[1]}). Add {{testRunId}} or {{index}} to make values unique.`;
482
1137
  return "Unique constraint violation. Make field values unique across instances.";
483
1138
  }
484
- if (errorMsg.includes("Foreign key constraint")) {
1139
+ if (errorMsg.includes("Foreign key constraint") || errorMsg.includes("foreign key")) {
485
1140
  return "A referenced record does not exist. Check that parent entities are nested correctly.";
486
1141
  }
487
- if (errorMsg.includes("Unknown argument")) {
488
- const match = errorMsg.match(/Unknown argument `(\w+)`/);
489
- if (match) return `Field "${match[1]}" does not exist on this model. Remove it.`;
490
- }
491
- if (errorMsg.includes("must not be null")) {
1142
+ if (errorMsg.includes("null value in column") || errorMsg.includes("must not be null")) {
492
1143
  return "A required field is null. Add it to the node with a value.";
493
1144
  }
494
1145
  return "";
495
1146
  }
496
1147
  export {
1148
+ PROTOCOL_VERSION,
497
1149
  checkAllScenarios,
498
1150
  checkScenario,
1151
+ createEntities,
499
1152
  findDeferrableEdge,
500
1153
  fingerprint,
1154
+ getDialect,
501
1155
  handleRequest,
1156
+ introspectDatabase,
502
1157
  resolveTemplate,
503
1158
  resolveTree,
504
1159
  signBody,
505
1160
  signRefs,
1161
+ teardown,
506
1162
  topoSort,
507
1163
  verifyRefs,
508
1164
  verifySignature