@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/README.md +88 -0
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/{graph-DpqVvKaD.d.ts → graph-gkFzydIb.d.ts} +35 -10
- package/dist/graph.d.ts +1 -1
- package/dist/index.d.ts +102 -5
- package/dist/index.js +719 -63
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
i
|
|
322
|
-
|
|
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
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const
|
|
338
|
-
|
|
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
|
-
|
|
341
|
-
}
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
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
|
|
381
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
1126
|
+
async function checkAllScenarios(executor, scenarios, options) {
|
|
472
1127
|
const results = [];
|
|
473
1128
|
for (const scenario of scenarios) {
|
|
474
|
-
results.push(await checkScenario(
|
|
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("
|
|
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
|