biz-a-cli 2.3.79 → 2.3.80-15224

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.
@@ -0,0 +1,2342 @@
1
+ import axios from "axios";
2
+ import { IDLE_SOCKET_TIMEOUT_MILLISECONDS } from "../../bin/hubEvent.js";
3
+ import { queryData, executeBlock, getGlobalConfig } from "../../db/db.js";
4
+ import * as firebirdDdl from "./firebird-ddl.js";
5
+
6
+ export const TBL_SYS_CONFIG = "SYS$CONFIG";
7
+ export const APP_CONFIG_NAME = "APPLICATION_CONFIG";
8
+ export const DOMAIN_CONFIG_NAME = "DOMAIN_CONFIG";
9
+
10
+ const FINA_METHOD_CLASS = "TFinaMethod";
11
+
12
+ const escapeQuote = (value) => {
13
+ return (value || "").replace(/'/g, "''");
14
+ };
15
+
16
+ const toSingleLine = (value) => {
17
+ return String(value ?? "")
18
+ .replace(/[\r\n]+/g, " ")
19
+ .replace(/\s{2,}/g, " ")
20
+ .trim();
21
+ };
22
+
23
+ const sanitizeSql = (sql) => {
24
+ return toSingleLine(sql ?? "");
25
+ };
26
+
27
+ const toSimpleHash = (input) => {
28
+ let hash = 0;
29
+ for (let index = 0; index < input.length; index++) {
30
+ hash = (hash << 5) - hash + input.charCodeAt(index);
31
+ hash |= 0;
32
+ }
33
+ return Math.abs(hash).toString(16).toUpperCase();
34
+ };
35
+
36
+ const toConstraintIdentifierPart = (identifier) => {
37
+ const normalized = String(identifier ?? "")
38
+ .replace(/^"|"$/g, "")
39
+ .toUpperCase()
40
+ .replace(/[^A-Z0-9_$]+/g, "_")
41
+ .replace(/^_+|_+$/g, "");
42
+ return normalized || "COL";
43
+ };
44
+
45
+ const buildFirebird25NotNullCheckName = (tableIdentifier, columnIdentifier) => {
46
+ const baseName = [
47
+ "CK",
48
+ toConstraintIdentifierPart(tableIdentifier),
49
+ toConstraintIdentifierPart(columnIdentifier),
50
+ "NN",
51
+ ]
52
+ .filter((part) => part.length > 0)
53
+ .join("_");
54
+ if (baseName.length <= 31) {
55
+ return baseName;
56
+ }
57
+
58
+ const suffix = toSimpleHash(baseName).slice(0, 6);
59
+ const prefixLength = Math.max(1, 31 - suffix.length - 1);
60
+ return `${baseName.slice(0, prefixLength)}_${suffix}`;
61
+ };
62
+
63
+ const toDbName = (value) => {
64
+ const source = String(value ?? "");
65
+ // Skip the camelCase->snake_case split for already-uppercase input (e.g. a hash-suffixed
66
+ // name this function itself already produced) -- otherwise a hex hash like "1E8A9B" gets
67
+ // spurious underscores inserted at digit->letter boundaries ("1_E8_A9_B"), pushing an
68
+ // already-valid 31-char identifier over the limit and forcing a second, different hash.
69
+ const withWordBreaks = /[a-z]/.test(source) ? source.replace(/([a-z0-9])([A-Z])/g, "$1_$2") : source;
70
+ const normalized = withWordBreaks
71
+ .replace(/[^A-Za-z0-9]+/g, "_")
72
+ .replace(/_+/g, "_")
73
+ .replace(/^_+|_+$/g, "")
74
+ .toUpperCase();
75
+
76
+ const raw = normalized || "COL";
77
+ if (raw.length <= 31) {
78
+ return raw;
79
+ }
80
+
81
+ const hash = toSimpleHash(raw).slice(0, 6);
82
+ const leadingLength = 31 - hash.length - 1;
83
+ // Strip a trailing underscore left by the slice so we never emit a double underscore before
84
+ // the hash -- a lone trailing "_" here would otherwise get silently collapsed by any later
85
+ // re-normalization pass, producing a different identifier than this function itself stored.
86
+ return `${raw.slice(0, leadingLength).replace(/_+$/, "")}_${hash}`;
87
+ };
88
+
89
+ const buildObjectName = (prefix, ...parts) => {
90
+ const raw = [toDbName(prefix), ...parts.map((part) => toDbName(part))]
91
+ .filter((part) => part.length > 0)
92
+ .join("_");
93
+
94
+ if (raw.length <= 31) {
95
+ return raw;
96
+ }
97
+
98
+ const hash = toSimpleHash(raw).slice(0, 6);
99
+ const leadingLength = 31 - hash.length - 1;
100
+ // Same trailing-underscore guard as toDbName above -- keeps this idempotent under re-normalization.
101
+ return `${raw.slice(0, leadingLength).replace(/_+$/, "")}_${hash}`;
102
+ };
103
+
104
+ const normalizeExpression = (expression) => {
105
+ return String(expression ?? "")
106
+ .replace(/\s+/g, " ")
107
+ .trim()
108
+ .toUpperCase();
109
+ };
110
+
111
+ const toSqlLiteral = (value) => {
112
+ if (value === null) {
113
+ return "NULL";
114
+ }
115
+ if (typeof value === "number") {
116
+ return `${value}`;
117
+ }
118
+ if (typeof value === "boolean") {
119
+ return value ? "1" : "0";
120
+ }
121
+ return `'${String(value).replace(/'/g, "''")}'`;
122
+ };
123
+
124
+ const toPositiveInteger = (value) => {
125
+ const n = Number(value);
126
+ return Number.isInteger(n) && n > 0 ? n : undefined;
127
+ };
128
+
129
+ const toNonNegativeInteger = (value) => {
130
+ const n = Number(value);
131
+ return Number.isInteger(n) && n >= 0 ? n : undefined;
132
+ };
133
+
134
+ const getBaseColumns = () => {
135
+ return [
136
+ { data: `${TBL_SYS_CONFIG}.ID`, key: "id" },
137
+ { data: `${TBL_SYS_CONFIG}.NAME`, key: "name" },
138
+ { data: `${TBL_SYS_CONFIG}.DATA`, key: "data" },
139
+ ];
140
+ };
141
+
142
+ export const getConfigByNameParameters = (name) => {
143
+ return {
144
+ start: 0,
145
+ length: 1,
146
+ order: [],
147
+ filter: [
148
+ {
149
+ junction: "",
150
+ column: "NAME",
151
+ operator: "=",
152
+ value1: `'${escapeQuote(name)}'`,
153
+ },
154
+ ],
155
+ columns: getBaseColumns(),
156
+ };
157
+ };
158
+
159
+ export const getConfigByNameVersionParameters = (name, version) => {
160
+ return {
161
+ start: 0,
162
+ length: 1,
163
+ order: [],
164
+ filter: [
165
+ {
166
+ junction: "",
167
+ column: "NAME",
168
+ operator: "=",
169
+ value1: `'${escapeQuote(name)}'`,
170
+ },
171
+ {
172
+ junction: "AND",
173
+ column: "VERSION",
174
+ operator: "=",
175
+ value1: `'${escapeQuote(version)}'`,
176
+ },
177
+ ],
178
+ columns: [...getBaseColumns(), { data: `${TBL_SYS_CONFIG}.VERSION`, key: "version" }],
179
+ };
180
+ };
181
+
182
+ const buildUpsertBlockSql = (escapedName, escapedVersion, escapedContent, appendOnUpdate) => {
183
+ const updateDataClause = appendOnUpdate
184
+ ? ` SET DATA = COALESCE(DATA, '') || '${escapedContent}' `
185
+ : ` SET DATA = '${escapedContent}' `;
186
+
187
+ return (
188
+ "" +
189
+ "EXECUTE BLOCK " +
190
+ "RETURNS ( " +
191
+ " RESULT VARCHAR(6), " +
192
+ " CONFIG_ID INTEGER " +
193
+ ") AS " +
194
+ " DECLARE VARIABLE EXISTING_ID INTEGER; " +
195
+ "BEGIN " +
196
+ " EXISTING_ID = NULL; " +
197
+ ` SELECT FIRST 1 C.ID FROM ${TBL_SYS_CONFIG} C ` +
198
+ ` WHERE C.NAME = '${escapedName}' AND COALESCE(C.VERSION, '') = '${escapedVersion}' ` +
199
+ " ORDER BY C.ID DESC " +
200
+ " INTO :EXISTING_ID; " +
201
+ " IF (EXISTING_ID IS NULL) THEN BEGIN " +
202
+ ` INSERT INTO ${TBL_SYS_CONFIG} (NAME, VERSION, DATA) VALUES('${escapedName}', '${escapedVersion}', '${escapedContent}') ` +
203
+ " RETURNING ID INTO :CONFIG_ID; " +
204
+ " END ELSE BEGIN " +
205
+ ` UPDATE ${TBL_SYS_CONFIG} ` +
206
+ updateDataClause +
207
+ " WHERE ID = :EXISTING_ID; " +
208
+ " CONFIG_ID = EXISTING_ID; " +
209
+ " END " +
210
+ " RESULT = 'UPSERT'; " +
211
+ " SUSPEND; " +
212
+ "END;"
213
+ );
214
+ };
215
+
216
+ export const getUpsertBlock = (name, version, content) => {
217
+ const escapedName = escapeQuote(toSingleLine(name));
218
+ const escapedVersion = escapeQuote(toSingleLine(version ?? ""));
219
+ const escapedContent = escapeQuote(toSingleLine(content ?? ""));
220
+ return buildUpsertBlockSql(escapedName, escapedVersion, escapedContent, false);
221
+ };
222
+
223
+ const splitContentBySize = (content, chunkSize) => {
224
+ const normalizedChunkSize =
225
+ Number.isFinite(chunkSize) && chunkSize > 0 ? Math.floor(chunkSize) : 2 ** 16;
226
+ if (!content) {
227
+ return [""];
228
+ }
229
+
230
+ const chunks = [];
231
+ for (let index = 0; index < content.length; index += normalizedChunkSize) {
232
+ chunks.push(content.slice(index, index + normalizedChunkSize));
233
+ }
234
+ return chunks;
235
+ };
236
+
237
+ export const getChunkedUpsertBlocks = (name, version, content, chunkSize = 2 ** 16) => {
238
+ const escapedName = escapeQuote(toSingleLine(name));
239
+ const escapedVersion = escapeQuote(toSingleLine(version ?? ""));
240
+ const normalizedContent = toSingleLine(content ?? "");
241
+ const contentChunks = splitContentBySize(normalizedContent, chunkSize);
242
+
243
+ return contentChunks.map((chunk, index) =>
244
+ buildUpsertBlockSql(escapedName, escapedVersion, escapeQuote(chunk), index > 0),
245
+ );
246
+ };
247
+
248
+ const extractObjectLiteral = (source, startIndex) => {
249
+ let depth = 0;
250
+ let quote = "";
251
+ let escaped = false;
252
+
253
+ for (let index = startIndex; index < source.length; index++) {
254
+ const ch = source[index];
255
+ if (quote) {
256
+ if (escaped) {
257
+ escaped = false;
258
+ continue;
259
+ }
260
+ if (ch === "\\") {
261
+ escaped = true;
262
+ continue;
263
+ }
264
+ if (ch === quote) {
265
+ quote = "";
266
+ }
267
+ continue;
268
+ }
269
+
270
+ if (ch === '"' || ch === "'" || ch === "`") {
271
+ quote = ch;
272
+ continue;
273
+ }
274
+ if (ch === "{") {
275
+ depth += 1;
276
+ continue;
277
+ }
278
+ if (ch === "}") {
279
+ depth -= 1;
280
+ if (depth === 0) {
281
+ return source.slice(startIndex, index + 1);
282
+ }
283
+ }
284
+ }
285
+
286
+ return undefined;
287
+ };
288
+
289
+ const extractReturnedObjectLiteral = (source, scopeToken) => {
290
+ const scopeStart = scopeToken ? source.indexOf(scopeToken) : 0;
291
+ const startIndex = scopeStart >= 0 ? scopeStart : 0;
292
+ const returnIndex = source.indexOf("return", startIndex);
293
+ if (returnIndex < 0) {
294
+ return undefined;
295
+ }
296
+
297
+ const braceIndex = source.indexOf("{", returnIndex);
298
+ if (braceIndex < 0) {
299
+ return undefined;
300
+ }
301
+
302
+ return extractObjectLiteral(source, braceIndex);
303
+ };
304
+
305
+ const isLegacyDomainConfigShape = (parsed) => {
306
+ const root = parsed;
307
+ return !!(
308
+ root &&
309
+ typeof root === "object" &&
310
+ (Object.prototype.hasOwnProperty.call(root, "tableName") ||
311
+ Object.prototype.hasOwnProperty.call(root, "fields") ||
312
+ Object.prototype.hasOwnProperty.call(root, "functions"))
313
+ );
314
+ };
315
+
316
+ export const parseConfigForVersion = (configContent) => {
317
+ const raw = (configContent ?? "").trim();
318
+ if (!raw) {
319
+ return {};
320
+ }
321
+
322
+ try {
323
+ return JSON.parse(raw);
324
+ } catch {
325
+ const configObjLiteral = extractReturnedObjectLiteral(raw, "get");
326
+ if (!configObjLiteral) {
327
+ throw new Error("Invalid config JSON/object.");
328
+ }
329
+
330
+ try {
331
+ return Function(`"use strict"; return (${configObjLiteral});`)();
332
+ } catch {
333
+ throw new Error("Invalid config object literal.");
334
+ }
335
+ }
336
+ };
337
+
338
+ export const parseDomainConfig = (domainConfigContent) => {
339
+ const raw = (domainConfigContent ?? "").trim();
340
+ if (!raw) {
341
+ return {};
342
+ }
343
+
344
+ try {
345
+ return JSON.parse(raw);
346
+ } catch {
347
+ const configObjLiteral = extractReturnedObjectLiteral(raw, "get");
348
+ if (!configObjLiteral) {
349
+ throw new Error("Invalid domain config JSON/object.");
350
+ }
351
+
352
+ try {
353
+ const parsed = Function(`"use strict"; return (${configObjLiteral});`)();
354
+ if (isLegacyDomainConfigShape(parsed)) {
355
+ throw new Error(
356
+ "Legacy domain config format is not supported. Use get(){ return { metadata, model, ui } }.",
357
+ );
358
+ }
359
+ return parsed;
360
+ } catch (error) {
361
+ if (String(error?.message ?? "").includes("Legacy domain config format is not supported")) {
362
+ throw error;
363
+ }
364
+ throw new Error("Invalid domain config object literal.");
365
+ }
366
+ }
367
+ };
368
+
369
+ export const extractConfigVersion = (configContent) => {
370
+ const parsed = parseConfigForVersion(configContent);
371
+ const version = parsed?.metadata?.version;
372
+ if (version == null) {
373
+ return null;
374
+ }
375
+
376
+ const trimmedVersion = String(version).trim();
377
+ return trimmedVersion.length > 0 ? trimmedVersion : null;
378
+ };
379
+
380
+ export const isSemanticVersion = (version) => {
381
+ return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?$/.test(String(version ?? "").trim());
382
+ };
383
+
384
+ export const buildTableStructureQueries = (tableName) => {
385
+ return firebirdDdl.buildTableStructureQueries(tableName);
386
+ };
387
+
388
+ export const buildStructureSnapshotFromRows = (input) => {
389
+ return firebirdDdl.buildStructureSnapshotFromRows(input);
390
+ };
391
+
392
+ export const compareTableStructure = (expected, actual) => {
393
+ return firebirdDdl.compareTableStructure(expected, actual);
394
+ };
395
+
396
+ export const generateCreateTableSql = (table, createIfNotExists = true) => {
397
+ return firebirdDdl.generateCreateTableSql(table, createIfNotExists);
398
+ };
399
+
400
+ export const generateCreateSchemaSql = (schema, createIfNotExists = true) => {
401
+ return firebirdDdl.generateCreateSchemaSql(schema, createIfNotExists);
402
+ };
403
+
404
+ export const generateExecuteBlockSql = (schema) => {
405
+ return firebirdDdl.generateExecuteBlockSql(schema);
406
+ };
407
+
408
+ const hasSameColumnDefinitionForRename = (expected, actual) => {
409
+ const expectedType = normalizeTypeForCompare(expected.type, expected.size, expected.scale);
410
+ const actualType = normalizeTypeForCompare(actual.type, actual.size, actual.scale);
411
+ const expectedDefault = normalizeExpression(
412
+ expected.defaultExpression ??
413
+ (expected.defaultValue !== undefined ? toSqlLiteral(expected.defaultValue) : ""),
414
+ );
415
+ const actualDefault = normalizeExpression(actual.defaultExpression ?? "");
416
+ const expectedComputed = normalizeExpression(expected.computedBy ?? "");
417
+ const actualComputed = normalizeExpression(actual.computedBy ?? "");
418
+
419
+ return (
420
+ expectedType === actualType &&
421
+ (expected.nullable !== false) === (actual.nullable !== false) &&
422
+ expectedDefault === actualDefault &&
423
+ expectedComputed === actualComputed
424
+ );
425
+ };
426
+
427
+ const isFirebird25NotNullCheckForColumn = (check, columnName) => {
428
+ const expression = String(check?.expression ?? "")
429
+ .replace(/^CHECK\s*/i, "")
430
+ .replace(/"/g, "")
431
+ .replace(/\s+/g, "")
432
+ .toUpperCase();
433
+ const normalizedColumnName = toConstraintIdentifierPart(columnName);
434
+ return (
435
+ expression === `${normalizedColumnName}ISNOTNULL` ||
436
+ expression === `(${normalizedColumnName}ISNOTNULL)`
437
+ );
438
+ };
439
+
440
+ const resolveFirebird25NotNullCheckState = (tableName, columnName, actual) => {
441
+ const generatedName = buildFirebird25NotNullCheckName(tableName, columnName);
442
+ const checks = actual.checks ?? [];
443
+ const hasGeneratedName = checks.some(
444
+ (check) => check.name.trim().toUpperCase() === generatedName,
445
+ );
446
+ const matches = checks.some((check) => isFirebird25NotNullCheckForColumn(check, columnName));
447
+ return {
448
+ generatedName,
449
+ hasGeneratedName,
450
+ matches,
451
+ };
452
+ };
453
+
454
+ const describeColumnDependency = (dependency) => {
455
+ const dependentName = dependency.dependentName || "unnamed object";
456
+ return `dependency ${dependentName} (type ${dependency.dependentType})`;
457
+ };
458
+
459
+ const getColumnRenameBlockers = (actual, columnName) => {
460
+ const normalizedColumnName = columnName.trim().toUpperCase();
461
+ const blockers = [];
462
+ const hasColumn = (columns) =>
463
+ (columns ?? []).some((column) => column.trim().toUpperCase() === normalizedColumnName);
464
+
465
+ if (hasColumn(actual.primaryKey)) {
466
+ blockers.push("a primary key");
467
+ }
468
+ for (const uniqueKey of actual.uniqueKeys ?? []) {
469
+ if (hasColumn(uniqueKey.columns)) {
470
+ blockers.push(`unique constraint ${uniqueKey.name}`);
471
+ }
472
+ }
473
+ for (const index of actual.indexes ?? []) {
474
+ if (hasColumn(index.columns)) {
475
+ blockers.push(`index ${index.name}`);
476
+ }
477
+ }
478
+ for (const foreignKey of actual.foreignKeys ?? []) {
479
+ if (hasColumn(foreignKey.columns)) {
480
+ blockers.push(`foreign key ${foreignKey.name}`);
481
+ }
482
+ }
483
+ for (const foreignKey of actual.incomingForeignKeys ?? []) {
484
+ if (hasColumn(foreignKey.referenceColumns)) {
485
+ blockers.push(`incoming foreign key ${foreignKey.name} (${foreignKey.tableName})`);
486
+ }
487
+ }
488
+ for (const dependency of actual.columnDependencies ?? []) {
489
+ if (dependency.columnName.trim().toUpperCase() === normalizedColumnName) {
490
+ blockers.push(describeColumnDependency(dependency));
491
+ }
492
+ }
493
+
494
+ return [...new Set(blockers)];
495
+ };
496
+
497
+ const normalizeTypeForCompare = (type, size, scale) => {
498
+ const normalizedType =
499
+ String(type ?? "").trim().toUpperCase() === "BOOLEAN"
500
+ ? "SMALLINT"
501
+ : String(type ?? "").trim().toUpperCase();
502
+
503
+ const supportsLength = ["CHAR", "VARCHAR"].includes(normalizedType);
504
+ const supportsPrecision = ["NUMERIC", "DECIMAL"].includes(normalizedType);
505
+ const hasSize = typeof size === "number" && Number.isFinite(size) && size > 0;
506
+ const hasScale = typeof scale === "number" && Number.isFinite(scale);
507
+
508
+ if (!supportsLength && !supportsPrecision) {
509
+ return normalizedType;
510
+ }
511
+
512
+ if (!hasSize) {
513
+ return normalizedType;
514
+ }
515
+
516
+ if (supportsLength) {
517
+ return `${normalizedType}(${size})`;
518
+ }
519
+
520
+ if (!hasScale || scale === 0) {
521
+ return `${normalizedType}(${size})`;
522
+ }
523
+
524
+ return `${normalizedType}(${size},${scale})`;
525
+ };
526
+
527
+ const resolveColumnTypeForAlter = (column) => {
528
+ const normalizedType =
529
+ String(column.type ?? "").trim().toUpperCase() === "BOOLEAN"
530
+ ? "SMALLINT"
531
+ : String(column.type ?? "").trim().toUpperCase();
532
+
533
+ const supportsLength = ["CHAR", "VARCHAR"].includes(normalizedType);
534
+ const supportsPrecision = ["NUMERIC", "DECIMAL"].includes(normalizedType);
535
+ const hasSize =
536
+ typeof column.size === "number" && Number.isFinite(column.size) && column.size > 0;
537
+ const hasScale = typeof column.scale === "number" && Number.isFinite(column.scale);
538
+
539
+ if (!supportsLength && !supportsPrecision) {
540
+ return normalizedType;
541
+ }
542
+
543
+ if (!hasSize) {
544
+ return normalizedType;
545
+ }
546
+
547
+ if (supportsLength) {
548
+ return `${normalizedType}(${column.size})`;
549
+ }
550
+
551
+ if (!hasScale || column.scale === 0) {
552
+ return `${normalizedType}(${column.size})`;
553
+ }
554
+
555
+ return `${normalizedType}(${column.size},${column.scale})`;
556
+ };
557
+
558
+ const buildColumnDefinitionForAlter = (column) => {
559
+ const parts = [column.name, resolveColumnTypeForAlter(column)];
560
+
561
+ if (column.defaultExpression) {
562
+ parts.push(`DEFAULT ${column.defaultExpression}`);
563
+ } else if (column.defaultValue !== undefined) {
564
+ parts.push(`DEFAULT ${toSqlLiteral(column.defaultValue)}`);
565
+ }
566
+
567
+ if (column.nullable === false) {
568
+ parts.push("NOT NULL");
569
+ }
570
+
571
+ return parts.join(" ");
572
+ };
573
+
574
+ const buildCreateIndexStatementForSync = (tableName, index) => {
575
+ const mode = [index.unique ? "UNIQUE" : "", index.descending ? "DESC" : ""]
576
+ .filter(Boolean)
577
+ .join(" ");
578
+ const modePart = mode.length > 0 ? `${mode} ` : "";
579
+
580
+ if (index.expression) {
581
+ return `CREATE ${modePart}INDEX ${index.name} ON ${tableName} COMPUTED BY (${index.expression});`;
582
+ }
583
+
584
+ return `CREATE ${modePart}INDEX ${index.name} ON ${tableName} (${(index.columns ?? []).join(", ")});`;
585
+ };
586
+
587
+ export const buildTableSyncPlan = (expected, actual, options) => {
588
+ const migrationMode = options?.migrationMode ?? "safe";
589
+ const allowDestructiveChanges = migrationMode === "aggressive";
590
+ const compare = compareTableStructure(expected, actual);
591
+ if (compare.isSame) {
592
+ return { statements: [], warnings: [], differences: [] };
593
+ }
594
+
595
+ const statements = [];
596
+ const warnings = [];
597
+
598
+ const expectedColumns = new Map();
599
+ for (const col of expected.columns ?? []) {
600
+ expectedColumns.set(col.name.trim().toUpperCase(), col);
601
+ }
602
+ const actualColumns = new Map();
603
+ for (const col of actual.columns ?? []) {
604
+ actualColumns.set(col.name.trim().toUpperCase(), col);
605
+ }
606
+
607
+ const unmatchedExpectedColumns = [...expectedColumns.entries()].filter(
608
+ ([columnName]) => !actualColumns.has(columnName),
609
+ );
610
+ const unmatchedActualColumns = [...actualColumns.entries()].filter(
611
+ ([columnName]) => !expectedColumns.has(columnName),
612
+ );
613
+ const handledExpectedColumns = new Set();
614
+ const handledActualColumns = new Set();
615
+ const blockedExpectedColumns = new Set();
616
+ if (
617
+ unmatchedExpectedColumns.length === 1 &&
618
+ unmatchedActualColumns.length === 1 &&
619
+ hasSameColumnDefinitionForRename(unmatchedExpectedColumns[0][1], unmatchedActualColumns[0][1])
620
+ ) {
621
+ const [expectedColumnName, expectedColumn] = unmatchedExpectedColumns[0];
622
+ const [actualColumnName, actualColumn] = unmatchedActualColumns[0];
623
+ const blockers = getColumnRenameBlockers(actual, actualColumnName);
624
+ handledExpectedColumns.add(expectedColumnName);
625
+ handledActualColumns.add(actualColumnName);
626
+ if (blockers.length > 0) {
627
+ blockedExpectedColumns.add(expectedColumnName);
628
+ warnings.push(
629
+ `Column "${actualColumn.name}" cannot be renamed to "${expectedColumn.name}" in table ${expected.name} because it has ${blockers.join(", ")}; requires manual migration.`,
630
+ );
631
+ } else {
632
+ statements.push(
633
+ `ALTER TABLE ${expected.name} ALTER COLUMN ${actualColumn.name} TO ${expectedColumn.name};`,
634
+ );
635
+ }
636
+ } else if (
637
+ unmatchedExpectedColumns.length > 0 &&
638
+ unmatchedActualColumns.length > 0 &&
639
+ (unmatchedExpectedColumns.length > 1 || unmatchedActualColumns.length > 1)
640
+ ) {
641
+ warnings.push(`Ambiguous column rename candidates for table ${expected.name}; requires manual migration.`);
642
+ }
643
+
644
+ for (const [columnName, expectedColumn] of expectedColumns.entries()) {
645
+ const actualColumn = actualColumns.get(columnName);
646
+ if (!actualColumn) {
647
+ if (handledExpectedColumns.has(columnName)) {
648
+ continue;
649
+ }
650
+ statements.push(
651
+ `ALTER TABLE ${expected.name} ADD ${buildColumnDefinitionForAlter(expectedColumn)};`,
652
+ );
653
+ continue;
654
+ }
655
+
656
+ const expectedType = normalizeTypeForCompare(expectedColumn.type, expectedColumn.size, expectedColumn.scale);
657
+ const actualType = normalizeTypeForCompare(actualColumn.type, actualColumn.size, actualColumn.scale);
658
+ if (expectedType !== actualType) {
659
+ statements.push(
660
+ `ALTER TABLE ${expected.name} ALTER COLUMN ${expectedColumn.name} TYPE ${resolveColumnTypeForAlter(expectedColumn)};`,
661
+ );
662
+ }
663
+
664
+ const expectedNullable = expectedColumn.nullable !== false;
665
+ const firebird25NotNullCheck = resolveFirebird25NotNullCheckState(expected.name, expectedColumn.name, actual);
666
+ const actualNullable =
667
+ expectedNullable || !firebird25NotNullCheck.matches
668
+ ? actualColumn.nullable !== false
669
+ : false;
670
+ if (expectedNullable !== actualNullable) {
671
+ if (!expectedNullable && firebird25NotNullCheck.hasGeneratedName && !firebird25NotNullCheck.matches) {
672
+ warnings.push(
673
+ `Constraint "${firebird25NotNullCheck.generatedName}" already exists on table ${expected.name} but does not enforce ${expectedColumn.name} IS NOT NULL; requires manual migration.`,
674
+ );
675
+ } else {
676
+ statements.push(
677
+ expectedNullable
678
+ ? `ALTER TABLE ${expected.name} ALTER COLUMN ${expectedColumn.name} DROP NOT NULL;`
679
+ : `ALTER TABLE ${expected.name} ALTER COLUMN ${expectedColumn.name} SET NOT NULL;`,
680
+ );
681
+ }
682
+ }
683
+
684
+ const expectedDefault = normalizeExpression(
685
+ expectedColumn.defaultExpression ??
686
+ (expectedColumn.defaultValue !== undefined ? toSqlLiteral(expectedColumn.defaultValue) : ""),
687
+ );
688
+ const actualDefault = normalizeExpression(actualColumn.defaultExpression ?? "");
689
+ if (expectedDefault !== actualDefault) {
690
+ if (expectedColumn.defaultExpression) {
691
+ statements.push(
692
+ `ALTER TABLE ${expected.name} ALTER COLUMN ${expectedColumn.name} SET DEFAULT ${expectedColumn.defaultExpression};`,
693
+ );
694
+ } else if (expectedColumn.defaultValue !== undefined) {
695
+ statements.push(
696
+ `ALTER TABLE ${expected.name} ALTER COLUMN ${expectedColumn.name} SET DEFAULT ${toSqlLiteral(expectedColumn.defaultValue)};`,
697
+ );
698
+ } else {
699
+ statements.push(`ALTER TABLE ${expected.name} ALTER COLUMN ${expectedColumn.name} DROP DEFAULT;`);
700
+ }
701
+ }
702
+ }
703
+
704
+ const actualPkColumns = new Set((actual.primaryKey ?? []).map((v) => v.trim().toUpperCase()));
705
+ const incomingForeignKeys = actual.incomingForeignKeys ?? [];
706
+ for (const [columnName, actualColumn] of actualColumns.entries()) {
707
+ if (expectedColumns.has(columnName) || handledActualColumns.has(columnName)) {
708
+ continue;
709
+ }
710
+
711
+ if (!allowDestructiveChanges) {
712
+ warnings.push(`Column "${columnName}" exists in actual table ${expected.name} but is not dropped in safe mode.`);
713
+ continue;
714
+ }
715
+
716
+ if (actualPkColumns.has(columnName)) {
717
+ warnings.push(`Column "${columnName}" is part of primary key in table ${expected.name} requires manual drop.`);
718
+ continue;
719
+ }
720
+
721
+ const incomingReferences = incomingForeignKeys.filter((fk) =>
722
+ (fk.referenceColumns ?? []).some((col) => col.trim().toUpperCase() === columnName),
723
+ );
724
+ if (incomingReferences.length > 0) {
725
+ const incomingReferenceNames = incomingReferences.map((fk) => `${fk.name} (${fk.tableName})`).join(", ");
726
+ warnings.push(
727
+ `Column "${columnName}" is referenced by foreign keys ${incomingReferenceNames} in table ${expected.name} and requires manual migration.`,
728
+ );
729
+ continue;
730
+ }
731
+
732
+ for (const fk of actual.foreignKeys ?? []) {
733
+ if ((fk.columns ?? []).some((col) => col.trim().toUpperCase() === columnName)) {
734
+ statements.push(`ALTER TABLE ${expected.name} DROP CONSTRAINT ${fk.name};`);
735
+ }
736
+ }
737
+
738
+ for (const uniqueKey of actual.uniqueKeys ?? []) {
739
+ if ((uniqueKey.columns ?? []).some((col) => col.trim().toUpperCase() === columnName)) {
740
+ statements.push(`ALTER TABLE ${expected.name} DROP CONSTRAINT ${uniqueKey.name};`);
741
+ }
742
+ }
743
+
744
+ for (const index of actual.indexes ?? []) {
745
+ if ((index.columns ?? []).some((col) => col.trim().toUpperCase() === columnName)) {
746
+ statements.push(`DROP INDEX ${index.name};`);
747
+ }
748
+ }
749
+
750
+ statements.push(`ALTER TABLE ${expected.name} DROP ${actualColumn.name};`);
751
+ }
752
+
753
+ const expectedPk = (expected.primaryKey?.columns ?? []).map((v) => v.trim().toUpperCase());
754
+ const actualPk = (actual.primaryKey ?? []).map((v) => v.trim().toUpperCase());
755
+ const expectedPkIsBlocked = expectedPk.some((columnName) => blockedExpectedColumns.has(columnName));
756
+ if (!expectedPkIsBlocked && expectedPk.length > 0 && actualPk.length === 0) {
757
+ const pkName = expected.primaryKey?.name ?? buildObjectName("PK", expected.name);
758
+ statements.push(`ALTER TABLE ${expected.name} ADD CONSTRAINT ${pkName} PRIMARY KEY (${expectedPk.join(", ")});`);
759
+ } else if (!expectedPkIsBlocked && expectedPk.length > 0 && expectedPk.join("|") !== actualPk.join("|")) {
760
+ warnings.push(`Primary key mismatch for table ${expected.name} requires manual migration.`);
761
+ }
762
+
763
+ const actualUnique = new Set((actual.uniqueKeys ?? []).map((u) => u.name.trim().toUpperCase()));
764
+ for (const uniqueKey of expected.uniqueKeys ?? []) {
765
+ if ((uniqueKey.columns ?? []).some((columnName) => blockedExpectedColumns.has(columnName.trim().toUpperCase()))) {
766
+ continue;
767
+ }
768
+ if (!actualUnique.has(uniqueKey.name.trim().toUpperCase())) {
769
+ statements.push(
770
+ `ALTER TABLE ${expected.name} ADD CONSTRAINT ${uniqueKey.name} UNIQUE (${uniqueKey.columns.join(", ")});`,
771
+ );
772
+ }
773
+ }
774
+
775
+ const actualIndexes = new Map();
776
+ for (const idx of actual.indexes ?? []) {
777
+ actualIndexes.set(idx.name.trim().toUpperCase(), idx);
778
+ }
779
+ for (const index of expected.indexes ?? []) {
780
+ if ((index.columns ?? []).some((columnName) => blockedExpectedColumns.has(columnName.trim().toUpperCase()))) {
781
+ continue;
782
+ }
783
+ const actualIndex = actualIndexes.get(index.name.trim().toUpperCase());
784
+ if (!actualIndex) {
785
+ statements.push(buildCreateIndexStatementForSync(expected.name, index));
786
+ continue;
787
+ }
788
+
789
+ const expectedColumnsKey = (index.columns ?? []).map((v) => v.trim().toUpperCase()).join("|");
790
+ const actualColumnsKey = (actualIndex.columns ?? []).map((v) => v.trim().toUpperCase()).join("|");
791
+ const expectedExpr = normalizeExpression(index.expression ?? "");
792
+ const actualExpr = normalizeExpression(actualIndex.expression ?? "");
793
+ const expectedUnique = index.unique === true;
794
+ const actualUniqueFlag = actualIndex.unique === true;
795
+ const expectedDesc = index.descending === true;
796
+ const actualDesc = actualIndex.descending === true;
797
+ if (
798
+ expectedColumnsKey !== actualColumnsKey ||
799
+ expectedExpr !== actualExpr ||
800
+ expectedUnique !== actualUniqueFlag ||
801
+ expectedDesc !== actualDesc
802
+ ) {
803
+ warnings.push(`Index ${index.name} mismatch on table ${expected.name} requires manual recreate.`);
804
+ }
805
+ }
806
+
807
+ const actualFks = new Map();
808
+ for (const fk of actual.foreignKeys ?? []) {
809
+ actualFks.set(fk.name.trim().toUpperCase(), fk);
810
+ }
811
+ for (const fk of expected.foreignKeys ?? []) {
812
+ if ((fk.columns ?? []).some((columnName) => blockedExpectedColumns.has(columnName.trim().toUpperCase()))) {
813
+ continue;
814
+ }
815
+ if (!actualFks.has(fk.name.trim().toUpperCase())) {
816
+ const onDelete = fk.onDelete ? ` ON DELETE ${fk.onDelete}` : "";
817
+ const onUpdate = fk.onUpdate ? ` ON UPDATE ${fk.onUpdate}` : "";
818
+ statements.push(
819
+ `ALTER TABLE ${expected.name} ADD CONSTRAINT ${fk.name} FOREIGN KEY (${fk.columns.join(", ")}) REFERENCES ${fk.referenceTable} (${fk.referenceColumns.join(", ")})${onDelete}${onUpdate};`,
820
+ );
821
+ }
822
+ }
823
+
824
+ return {
825
+ statements: [...new Set(statements.map((statement) => toSingleLine(statement)))],
826
+ warnings,
827
+ differences: compare.differences,
828
+ };
829
+ };
830
+
831
+ export const buildGeneratorStructureQueries = (table) => {
832
+ const generatorName = toDbName(table.generatorName ?? buildObjectName(table.name, "GEN"));
833
+ const triggerName = toDbName(table.triggerName ?? buildObjectName(table.name, "BI"));
834
+
835
+ return {
836
+ generatorExistsSql: [
837
+ "SELECT COUNT(*) AS GENERATOR_EXISTS",
838
+ "FROM RDB$GENERATORS G",
839
+ `WHERE UPPER(TRIM(G.RDB$GENERATOR_NAME)) = '${generatorName}';`,
840
+ ].join(" "),
841
+ triggerSql: [
842
+ "SELECT COUNT(*) AS TRIGGER_EXISTS",
843
+ "FROM RDB$TRIGGERS T",
844
+ `WHERE UPPER(TRIM(T.RDB$TRIGGER_NAME)) = '${triggerName}'`,
845
+ " AND COALESCE(T.RDB$SYSTEM_FLAG, 0) = 0;",
846
+ ].join(" "),
847
+ };
848
+ };
849
+
850
+ const resolveIdentityColumnForGeneratorSync = (table) => {
851
+ const candidates = [];
852
+ const explicitIdentity = toDbName(table.identityColumnName ?? "");
853
+ const primaryKeyIdentity = toDbName(table.primaryKey?.columns?.[0] ?? "");
854
+
855
+ if (explicitIdentity) {
856
+ candidates.push(explicitIdentity);
857
+ }
858
+ if (primaryKeyIdentity && !candidates.includes(primaryKeyIdentity)) {
859
+ candidates.push(primaryKeyIdentity);
860
+ }
861
+ if (!candidates.includes("ID")) {
862
+ candidates.push("ID");
863
+ }
864
+
865
+ for (const candidate of candidates) {
866
+ const column = (table.columns ?? []).find((item) => toDbName(item.name) === candidate);
867
+ if (column) {
868
+ return candidate;
869
+ }
870
+ }
871
+
872
+ return "";
873
+ };
874
+
875
+ export const shouldSyncGenerator = (table) => {
876
+ return !!resolveIdentityColumnForGeneratorSync(table);
877
+ };
878
+
879
+ const resolveIdentityValueExpression = (table, identityColumn, generatorName) => {
880
+ if (table?.identityGuid) {
881
+ return "'{' || UUID_TO_CHAR(GEN_UUID()) || '}'";
882
+ }
883
+ return `GEN_ID(${generatorName}, 1)`;
884
+ };
885
+
886
+ const buildCreateIdentityTriggerStatementForSync = (tableName, triggerName, identityColumn, valueExpression) => {
887
+ return [
888
+ `CREATE TRIGGER ${triggerName} FOR ${toDbName(tableName)} ACTIVE`,
889
+ "BEFORE INSERT POSITION 0 AS BEGIN",
890
+ `IF (NEW.${identityColumn} IS NULL) THEN NEW.${identityColumn} = ${valueExpression};`,
891
+ "END",
892
+ ].join(" ");
893
+ };
894
+
895
+ const buildAlterIdentityTriggerStatementForSync = (triggerName, identityColumn, valueExpression) => {
896
+ return [
897
+ `ALTER TRIGGER ${triggerName} ACTIVE`,
898
+ "BEFORE INSERT POSITION 0 AS BEGIN",
899
+ `IF (NEW.${identityColumn} IS NULL) THEN NEW.${identityColumn} = ${valueExpression};`,
900
+ "END",
901
+ ].join(" ");
902
+ };
903
+
904
+ const isIdentityTriggerStructureSame = (triggerSource, identityColumn, valueExpression) => {
905
+ const normalized = String(triggerSource ?? "")
906
+ .replace(/\s+/g, "")
907
+ .toUpperCase();
908
+ const expectedColumnToken = `NEW.${identityColumn.toUpperCase()}ISNULL`;
909
+ const expectedValueToken = valueExpression.replace(/\s+/g, "").toUpperCase();
910
+
911
+ return normalized.includes(expectedValueToken) && normalized.includes(expectedColumnToken);
912
+ };
913
+
914
+ export const buildGeneratorSyncPlan = (table, generatorRows, triggerRows) => {
915
+ const identityColumn = resolveIdentityColumnForGeneratorSync(table);
916
+ if (!identityColumn) {
917
+ return { statements: [], differences: [] };
918
+ }
919
+
920
+ const generatorName = toDbName(table.generatorName ?? buildObjectName(table.name, "GEN"));
921
+ const triggerName = toDbName(table.triggerName ?? buildObjectName(table.name, "BI"));
922
+ const identityValueExpression = resolveIdentityValueExpression(table, identityColumn, generatorName);
923
+ const differences = [];
924
+ const statements = [];
925
+
926
+ const generatorExists =
927
+ Number(generatorRows?.[0]?.["GENERATOR_EXISTS"] ?? generatorRows?.[0]?.["generator_exists"] ?? 0) === 1;
928
+
929
+ const triggerExistsRaw = triggerRows?.[0]?.["TRIGGER_EXISTS"] ?? triggerRows?.[0]?.["trigger_exists"];
930
+ const triggerExists = triggerExistsRaw != null ? Number(triggerExistsRaw) === 1 : undefined;
931
+ const triggerSource = String(
932
+ triggerRows?.[0]?.["TRIGGER_SOURCE"] ?? triggerRows?.[0]?.["trigger_source"] ?? "",
933
+ ).trim();
934
+
935
+ if (!table?.identityGuid && !generatorExists) {
936
+ differences.push(`Generator "${generatorName}" is missing.`);
937
+ statements.push(`CREATE GENERATOR ${generatorName};`);
938
+ }
939
+
940
+ if (triggerExists === false || (triggerExists == null && !triggerSource)) {
941
+ differences.push(`Trigger "${triggerName}" is missing.`);
942
+ statements.push(
943
+ buildCreateIdentityTriggerStatementForSync(table.name, triggerName, identityColumn, identityValueExpression),
944
+ );
945
+ } else if (triggerExists === true && !triggerSource) {
946
+ statements.push(buildAlterIdentityTriggerStatementForSync(triggerName, identityColumn, identityValueExpression));
947
+ } else if (triggerSource && !isIdentityTriggerStructureSame(triggerSource, identityColumn, identityValueExpression)) {
948
+ differences.push(`Trigger "${triggerName}" structure mismatch.`);
949
+ statements.push(buildAlterIdentityTriggerStatementForSync(triggerName, identityColumn, identityValueExpression));
950
+ }
951
+
952
+ return {
953
+ statements: [...new Set(statements.map((statement) => toSingleLine(statement)))],
954
+ differences,
955
+ };
956
+ };
957
+
958
+ export const buildDomainDdlPreview = (domainConfigContent, defaultDbIndex) => {
959
+ const buildResult = buildDomainSchemaInternal(domainConfigContent, defaultDbIndex);
960
+ const schema = buildResult.schema;
961
+ return {
962
+ schema,
963
+ ddlSql: schema.tables.length > 0 ? generateCreateSchemaSql(schema, true) : "",
964
+ executeBlockSql: schema.tables.length > 0 ? generateExecuteBlockSql(schema) : "",
965
+ normalizedRelationships: buildResult.normalizedRelationships,
966
+ warnings: buildResult.warnings,
967
+ };
968
+ };
969
+
970
+ export const orderTablesByDependency = (tables) => {
971
+ const entries = (tables ?? []).map((table, index) => ({
972
+ table,
973
+ index,
974
+ key: toDbName(table.name),
975
+ }));
976
+ const entryMap = new Map(entries.map((entry) => [entry.key, entry]));
977
+ const dependencies = new Map();
978
+ const dependents = new Map();
979
+
980
+ for (const entry of entries) {
981
+ const deps = new Set();
982
+ for (const fk of entry.table.foreignKeys ?? []) {
983
+ const refKey = toDbName(fk.referenceTable);
984
+ if (refKey && refKey !== entry.key && entryMap.has(refKey)) {
985
+ deps.add(refKey);
986
+ }
987
+ }
988
+ dependencies.set(entry.key, deps);
989
+ for (const dep of deps) {
990
+ if (!dependents.has(dep)) {
991
+ dependents.set(dep, new Set());
992
+ }
993
+ dependents.get(dep).add(entry.key);
994
+ }
995
+ }
996
+
997
+ const remaining = new Map();
998
+ for (const entry of entries) {
999
+ remaining.set(entry.key, dependencies.get(entry.key)?.size ?? 0);
1000
+ }
1001
+
1002
+ const queue = entries
1003
+ .filter((entry) => (remaining.get(entry.key) ?? 0) === 0)
1004
+ .sort((left, right) => left.index - right.index);
1005
+ const ordered = [];
1006
+ const emitted = new Set();
1007
+
1008
+ while (queue.length > 0) {
1009
+ const current = queue.shift();
1010
+ if (!current || emitted.has(current.key)) {
1011
+ continue;
1012
+ }
1013
+ emitted.add(current.key);
1014
+ ordered.push(current.table);
1015
+
1016
+ for (const dependent of dependents.get(current.key) ?? []) {
1017
+ const nextValue = (remaining.get(dependent) ?? 0) - 1;
1018
+ remaining.set(dependent, nextValue);
1019
+ if (nextValue === 0) {
1020
+ const nextEntry = entryMap.get(dependent);
1021
+ if (nextEntry && !emitted.has(nextEntry.key)) {
1022
+ queue.push(nextEntry);
1023
+ queue.sort((left, right) => left.index - right.index);
1024
+ }
1025
+ }
1026
+ }
1027
+ }
1028
+
1029
+ if (ordered.length === entries.length) {
1030
+ return ordered;
1031
+ }
1032
+
1033
+ for (const entry of entries) {
1034
+ if (!emitted.has(entry.key)) {
1035
+ ordered.push(entry.table);
1036
+ }
1037
+ }
1038
+ return ordered;
1039
+ };
1040
+
1041
+ export const buildDomainSchema = (domainConfigContent, defaultDbIndex) => {
1042
+ return buildDomainSchemaInternal(domainConfigContent, defaultDbIndex).schema;
1043
+ };
1044
+
1045
+ const resolveDefaultDomainDbIndex = (defaultDbIndex) => {
1046
+ const normalized = toNonNegativeInteger(defaultDbIndex);
1047
+ return normalized ?? 2;
1048
+ };
1049
+
1050
+ const normalizeDbIndices = (db, defaultDbIndex) => {
1051
+ const fallbackDbIndex = resolveDefaultDomainDbIndex(defaultDbIndex);
1052
+ if (db == null) {
1053
+ return [fallbackDbIndex];
1054
+ }
1055
+
1056
+ if (Array.isArray(db)) {
1057
+ const normalized = db.map((value) => Number(value)).filter((value) => Number.isInteger(value));
1058
+ return normalized.length > 0 ? [...new Set(normalized)] : [fallbackDbIndex];
1059
+ }
1060
+
1061
+ if (typeof db === "string" && db.trim().length === 0) {
1062
+ return [fallbackDbIndex];
1063
+ }
1064
+
1065
+ const singleValue = Number(db);
1066
+ return Number.isInteger(singleValue) ? [singleValue] : [fallbackDbIndex];
1067
+ };
1068
+
1069
+ const intersectDbIndices = (left, right) => {
1070
+ const set = new Set(right);
1071
+ const result = left.filter((value) => set.has(value));
1072
+ return result.length > 0 ? [...new Set(result)] : [0];
1073
+ };
1074
+
1075
+ const resolveNullable = (attribute) => {
1076
+ if (typeof attribute?.nullable === "boolean") {
1077
+ return attribute.nullable;
1078
+ }
1079
+ return attribute?.required !== true;
1080
+ };
1081
+
1082
+ const resolveColumnType = (attribute) => {
1083
+ const rawType = String(attribute?.type ?? "").toLowerCase();
1084
+ const uiIntent = String(attribute?.uiIntent ?? "").toLowerCase();
1085
+ const precision = toPositiveInteger(attribute?.precision ?? attribute?.size);
1086
+ const scale = toNonNegativeInteger(attribute?.scale);
1087
+
1088
+ if (rawType === "number") {
1089
+ const resolvedPrecision = precision ?? 18;
1090
+ const resolvedScale = scale ?? (uiIntent === "currency" ? 2 : 0);
1091
+ if (resolvedScale > 0) {
1092
+ return { type: "NUMERIC", size: resolvedPrecision, scale: resolvedScale };
1093
+ }
1094
+ if (resolvedPrecision <= 9) {
1095
+ return { type: "INTEGER" };
1096
+ }
1097
+ return { type: "BIGINT" };
1098
+ }
1099
+
1100
+ if (rawType === "date") {
1101
+ return { type: "DATE" };
1102
+ }
1103
+
1104
+ if (rawType === "datetime" || rawType === "timestamp" || uiIntent === "datetime") {
1105
+ return { type: "TIMESTAMP" };
1106
+ }
1107
+
1108
+ if (rawType === "boolean") {
1109
+ return { type: "BOOLEAN" };
1110
+ }
1111
+ if (rawType === "blobref") {
1112
+ return { type: "BLOB" };
1113
+ }
1114
+
1115
+ if (rawType === "string") {
1116
+ if (uiIntent === "longtext" || uiIntent === "textarea") {
1117
+ return { type: "BLOB" };
1118
+ }
1119
+ return { type: "VARCHAR", size: precision ?? 120 };
1120
+ }
1121
+
1122
+ return { type: "BLOB" };
1123
+ };
1124
+
1125
+ const toColumn = (attributeName, rawAttribute) => {
1126
+ const attribute = rawAttribute ?? {};
1127
+ const name = toDbName(attribute?.column ?? attribute?.columnName ?? attributeName);
1128
+ const typeInfo = resolveColumnType(attribute);
1129
+
1130
+ return {
1131
+ name,
1132
+ type: typeInfo.type,
1133
+ ...(typeInfo.size != null && { size: typeInfo.size }),
1134
+ ...(typeInfo.scale != null && { scale: typeInfo.scale }),
1135
+ nullable: resolveNullable(attribute),
1136
+ ...(attribute.defaultExpression && { defaultExpression: String(attribute.defaultExpression) }),
1137
+ ...(attribute.defaultValue !== undefined && { defaultValue: attribute.defaultValue }),
1138
+ };
1139
+ };
1140
+
1141
+ const ensureIdentityPrimaryColumn = (columns, identity) => {
1142
+ const identityPrimaryKey = toDbName(identity?.primaryKey ?? "");
1143
+ if (!identityPrimaryKey) {
1144
+ return;
1145
+ }
1146
+
1147
+ if (columns.some((column) => column.name === identityPrimaryKey)) {
1148
+ return;
1149
+ }
1150
+
1151
+ columns.unshift(
1152
+ identity?.guid
1153
+ ? { name: identityPrimaryKey, type: "VARCHAR", size: 38, nullable: false }
1154
+ : { name: identityPrimaryKey, type: "BIGINT", nullable: false },
1155
+ );
1156
+ };
1157
+
1158
+ const resolvePrimaryKey = (entity, columns) => {
1159
+ const identityKey = entity?.identity?.primaryKey;
1160
+ const identityColumn = identityKey ? toDbName(identityKey) : "";
1161
+
1162
+ if (identityColumn && columns.some((column) => column.name === identityColumn)) {
1163
+ return identityColumn;
1164
+ }
1165
+
1166
+ if (columns.some((column) => column.name === "ID")) {
1167
+ return "ID";
1168
+ }
1169
+
1170
+ return columns[0]?.name;
1171
+ };
1172
+
1173
+ const buildUniqueKeys = (tableName, attributeEntries, primaryKey) => {
1174
+ return attributeEntries
1175
+ .filter((entry) => entry.attribute?.unique === true && entry.column.name !== primaryKey)
1176
+ .map((entry) => ({
1177
+ name: buildObjectName("UQ", tableName, entry.column.name),
1178
+ columns: [entry.column.name],
1179
+ }));
1180
+ };
1181
+
1182
+ const hasSameIndexDefinition = (left, right) => {
1183
+ return (
1184
+ (left.columns ?? []).join("|") === (right.columns ?? []).join("|") &&
1185
+ (left.descending === true) === (right.descending === true) &&
1186
+ (left.expression ?? "") === (right.expression ?? "")
1187
+ );
1188
+ };
1189
+
1190
+ const buildAttributeIndexes = (tableName, attributeEntries, primaryKey, uniqueKeys) => {
1191
+ const indexes = [];
1192
+ const uniqueColumns = new Set(uniqueKeys.map((key) => key.columns[0]));
1193
+
1194
+ for (const entry of attributeEntries) {
1195
+ const columnName = entry.column.name;
1196
+ const uiIntent = String(entry.attribute?.uiIntent ?? "").toLowerCase();
1197
+ const shouldIndex =
1198
+ entry.attribute?.index === true ||
1199
+ uiIntent === "lookup" ||
1200
+ uiIntent === "date" ||
1201
+ uiIntent === "datetime" ||
1202
+ columnName.endsWith("_ID");
1203
+
1204
+ if (!shouldIndex || columnName === primaryKey || uniqueColumns.has(columnName)) {
1205
+ continue;
1206
+ }
1207
+
1208
+ const nextIndex = {
1209
+ name: buildObjectName("IDX", tableName, columnName),
1210
+ columns: [columnName],
1211
+ ...(uiIntent === "date" || uiIntent === "datetime" ? { descending: true } : {}),
1212
+ };
1213
+
1214
+ if (!indexes.some((idx) => hasSameIndexDefinition(idx, nextIndex))) {
1215
+ indexes.push(nextIndex);
1216
+ }
1217
+ }
1218
+
1219
+ return indexes;
1220
+ };
1221
+
1222
+ const buildEntityTable = (entityName, entity, defaultDbIndex) => {
1223
+ const tableName = toDbName(entity?.table ?? entity?.tableName ?? entityName);
1224
+ const dbIndices = normalizeDbIndices(entity?.db, defaultDbIndex);
1225
+ const attributes = entity?.attributes ?? {};
1226
+ const attributeEntries = Object.entries(attributes).map(([attributeName, attribute]) => ({
1227
+ attributeName,
1228
+ attribute: attribute ?? {},
1229
+ column: toColumn(attributeName, attribute),
1230
+ }));
1231
+ const attributeColumns = new Map(
1232
+ attributeEntries.map((entry) => [toDbName(entry.attributeName), entry.column.name]),
1233
+ );
1234
+
1235
+ const identityIsGuid = !!entity?.identity?.guid;
1236
+ const columns = attributeEntries.map((entry) => entry.column);
1237
+ ensureIdentityPrimaryColumn(columns, entity?.identity);
1238
+ const primaryKey = resolvePrimaryKey(entity, columns);
1239
+ const uniqueKeys = buildUniqueKeys(tableName, attributeEntries, primaryKey);
1240
+ const indexes = buildAttributeIndexes(tableName, attributeEntries, primaryKey, uniqueKeys);
1241
+
1242
+ const table = {
1243
+ name: tableName,
1244
+ dbIndex: dbIndices[0],
1245
+ columns,
1246
+ ...(primaryKey && { primaryKey: { columns: [primaryKey] } }),
1247
+ ...(uniqueKeys.length > 0 && { uniqueKeys }),
1248
+ ...(indexes.length > 0 && { indexes }),
1249
+ };
1250
+ const shouldCreateIdentityObjects = !!entity?.identity;
1251
+ if (shouldCreateIdentityObjects && primaryKey) {
1252
+ table.identityColumnName = primaryKey;
1253
+ table.identityGuid = identityIsGuid;
1254
+ if (!identityIsGuid) {
1255
+ table.generatorName = buildObjectName(tableName, "GEN");
1256
+ }
1257
+ table.triggerName = buildObjectName(tableName, "BI");
1258
+ }
1259
+
1260
+ return {
1261
+ entityName,
1262
+ tableName,
1263
+ dbIndices,
1264
+ primaryKey,
1265
+ columns: new Set(columns.map((column) => column.name)),
1266
+ attributeColumns,
1267
+ table,
1268
+ };
1269
+ };
1270
+
1271
+ const normalizeRelationshipType = (value) => {
1272
+ if (value === "one-to-one" || value === "many-to-many") {
1273
+ return value;
1274
+ }
1275
+ return "one-to-many";
1276
+ };
1277
+
1278
+ const normalizeOwnership = (value) => {
1279
+ if (value === "parent" || value === "shared" || value === "independent") {
1280
+ return value;
1281
+ }
1282
+ return "shared";
1283
+ };
1284
+
1285
+ const normalizeJoinStrategy = (explicit, fromDb, toDb) => {
1286
+ if (explicit === "sql" || explicit === "extract" || explicit === "script") {
1287
+ return explicit;
1288
+ }
1289
+
1290
+ return fromDb.some((idx) => toDb.includes(idx)) ? "sql" : "extract";
1291
+ };
1292
+
1293
+ const normalizeRelationship = (relationshipName, relationship, entityInfos, warnings) => {
1294
+ const fromName = relationship?.from;
1295
+ const toName = relationship?.to;
1296
+ if (!fromName || !toName) {
1297
+ warnings.push(`Relationship "${relationshipName}" ignored: "from" and "to" are required.`);
1298
+ return undefined;
1299
+ }
1300
+
1301
+ const fromInfo = entityInfos.get(fromName);
1302
+ const toInfo = entityInfos.get(toName);
1303
+ if (!fromInfo || !toInfo) {
1304
+ warnings.push(`Relationship "${relationshipName}" ignored: entity "${fromName}" or "${toName}" not found.`);
1305
+ return undefined;
1306
+ }
1307
+
1308
+ const type = normalizeRelationshipType(relationship?.type);
1309
+ const joinType = relationship?.joinType === "outer" ? "outer" : "inner";
1310
+ const ownership = normalizeOwnership(relationship?.ownership);
1311
+ const joinStrategy = normalizeJoinStrategy(relationship?.joinStrategy, fromInfo.dbIndices, toInfo.dbIndices);
1312
+ const parentKey = toDbName(relationship?.parentKey ?? fromInfo.primaryKey ?? "");
1313
+
1314
+ if (type === "many-to-many") {
1315
+ const through = relationship?.through;
1316
+ if (!through?.table || !through?.fromKey || !through?.toKey) {
1317
+ warnings.push(
1318
+ `Relationship "${relationshipName}" ignored: many-to-many requires "through.table/fromKey/toKey".`,
1319
+ );
1320
+ return undefined;
1321
+ }
1322
+
1323
+ return {
1324
+ type,
1325
+ from: fromName,
1326
+ to: toName,
1327
+ joinType,
1328
+ joinStrategy,
1329
+ ownership,
1330
+ parentKey,
1331
+ through: {
1332
+ table: toDbName(through.table),
1333
+ fromKey: toDbName(through.fromKey),
1334
+ toKey: toDbName(through.toKey),
1335
+ },
1336
+ uiIntent: relationship?.uiIntent,
1337
+ };
1338
+ }
1339
+
1340
+ if (!relationship?.foreignKey) {
1341
+ warnings.push(`Relationship "${relationshipName}" ignored: "${type}" requires "foreignKey".`);
1342
+ return undefined;
1343
+ }
1344
+
1345
+ return {
1346
+ type,
1347
+ from: fromName,
1348
+ to: toName,
1349
+ joinType,
1350
+ joinStrategy,
1351
+ ownership,
1352
+ parentKey,
1353
+ foreignKey: toDbName(relationship.foreignKey),
1354
+ uiIntent: relationship?.uiIntent,
1355
+ };
1356
+ };
1357
+
1358
+ const findColumn = (table, columnName) => {
1359
+ return table.columns.find((column) => column.name === columnName);
1360
+ };
1361
+
1362
+ const cloneColumn = (source, targetName, nullable) => {
1363
+ return {
1364
+ name: targetName,
1365
+ type: source.type,
1366
+ ...(source.size != null && { size: source.size }),
1367
+ ...(source.scale != null && { scale: source.scale }),
1368
+ nullable,
1369
+ };
1370
+ };
1371
+
1372
+ const ensureColumn = (info, column) => {
1373
+ if (info.columns.has(column.name)) {
1374
+ return;
1375
+ }
1376
+ info.columns.add(column.name);
1377
+ info.table.columns = [...info.table.columns, column];
1378
+ };
1379
+
1380
+ const ensureForeignKey = (table, foreignKey) => {
1381
+ const existing = table.foreignKeys ?? [];
1382
+ if (existing.some((fk) => fk.name === foreignKey.name)) {
1383
+ return;
1384
+ }
1385
+ table.foreignKeys = [...existing, foreignKey];
1386
+ };
1387
+
1388
+ const ensureIndex = (table, index) => {
1389
+ const existingColumnNames = new Set((table.columns ?? []).map((column) => column.name));
1390
+ const safeColumns = (index.columns ?? []).filter((column) => existingColumnNames.has(column));
1391
+ if (safeColumns.length === 0) {
1392
+ return;
1393
+ }
1394
+
1395
+ const safeIndex = {
1396
+ ...index,
1397
+ columns: safeColumns,
1398
+ };
1399
+ const existing = table.indexes ?? [];
1400
+ if (existing.some((idx) => hasSameIndexDefinition(idx, safeIndex))) {
1401
+ return;
1402
+ }
1403
+ table.indexes = [...existing, safeIndex];
1404
+ };
1405
+
1406
+ const resolveEntityColumnName = (info, key) => {
1407
+ const normalizedKey = toDbName(key);
1408
+ if (info.columns.has(normalizedKey)) {
1409
+ return normalizedKey;
1410
+ }
1411
+ return info.attributeColumns.get(normalizedKey);
1412
+ };
1413
+
1414
+ const applyDirectRelationship = (relationship, entityInfos, warnings) => {
1415
+ const fromInfo = entityInfos.get(relationship.from);
1416
+ const toInfo = entityInfos.get(relationship.to);
1417
+ if (!fromInfo || !toInfo || !relationship.foreignKey) {
1418
+ return;
1419
+ }
1420
+
1421
+ const toForeignKey = resolveEntityColumnName(toInfo, relationship.foreignKey);
1422
+ const fromForeignKey = resolveEntityColumnName(fromInfo, relationship.foreignKey);
1423
+
1424
+ let ownerInfo = toForeignKey ? toInfo : fromForeignKey ? fromInfo : toInfo;
1425
+ const foreignKey =
1426
+ ownerInfo === toInfo
1427
+ ? (toForeignKey ?? toDbName(relationship.foreignKey))
1428
+ : (fromForeignKey ?? toDbName(relationship.foreignKey));
1429
+
1430
+ const referenceInfo = ownerInfo === toInfo ? fromInfo : toInfo;
1431
+ const refPrimaryKey = toDbName(relationship.parentKey ?? referenceInfo.primaryKey ?? "");
1432
+ if (!refPrimaryKey || !referenceInfo.primaryKey) {
1433
+ warnings.push(`Relationship "${relationship.from}->${relationship.to}" ignored: missing parent key.`);
1434
+ return;
1435
+ }
1436
+
1437
+ const refColumn = findColumn(referenceInfo.table, refPrimaryKey);
1438
+ if (!refColumn) {
1439
+ warnings.push(
1440
+ `Relationship "${relationship.from}->${relationship.to}" ignored: parent key column "${refPrimaryKey}" not found.`,
1441
+ );
1442
+ return;
1443
+ }
1444
+
1445
+ if (!ownerInfo.columns.has(foreignKey)) {
1446
+ ensureColumn(ownerInfo, cloneColumn(refColumn, foreignKey, true));
1447
+ }
1448
+
1449
+ const onDelete = relationship.ownership === "parent" ? "CASCADE" : "NO ACTION";
1450
+ const fk = {
1451
+ name: buildObjectName("FK", ownerInfo.tableName, referenceInfo.tableName, foreignKey),
1452
+ columns: [foreignKey],
1453
+ referenceTable: referenceInfo.tableName,
1454
+ referenceColumns: [refPrimaryKey],
1455
+ onDelete,
1456
+ onUpdate: "NO ACTION",
1457
+ };
1458
+ ensureForeignKey(ownerInfo.table, fk);
1459
+
1460
+ const relationIndexColumns = [foreignKey];
1461
+ if (
1462
+ ownerInfo.primaryKey &&
1463
+ ownerInfo.primaryKey !== foreignKey &&
1464
+ ownerInfo.columns.has(ownerInfo.primaryKey)
1465
+ ) {
1466
+ relationIndexColumns.push(ownerInfo.primaryKey);
1467
+ }
1468
+ ensureIndex(ownerInfo.table, {
1469
+ name: buildObjectName("IDX", ownerInfo.tableName, ...relationIndexColumns),
1470
+ columns: relationIndexColumns,
1471
+ });
1472
+ };
1473
+
1474
+ const applyManyToManyRelationship = (relationship, entityInfos, tableInfos, tables, warnings) => {
1475
+ const fromInfo = entityInfos.get(relationship.from);
1476
+ const toInfo = entityInfos.get(relationship.to);
1477
+ const through = relationship.through;
1478
+ if (!fromInfo || !toInfo || !through) {
1479
+ return;
1480
+ }
1481
+
1482
+ const fromPk = toDbName(relationship.parentKey ?? fromInfo.primaryKey ?? "");
1483
+ const toPk = toDbName(toInfo.primaryKey ?? "");
1484
+ if (!fromPk || !toPk) {
1485
+ warnings.push(`Relationship "${relationship.from}<->${relationship.to}" ignored: missing primary key.`);
1486
+ return;
1487
+ }
1488
+
1489
+ const fromPkColumn = findColumn(fromInfo.table, fromPk);
1490
+ const toPkColumn = findColumn(toInfo.table, toPk);
1491
+ if (!fromPkColumn || !toPkColumn) {
1492
+ warnings.push(`Relationship "${relationship.from}<->${relationship.to}" ignored: parent/child key column not found.`);
1493
+ return;
1494
+ }
1495
+
1496
+ let throughInfo = tableInfos.get(through.table);
1497
+ if (!throughInfo) {
1498
+ const throughDbIndices = intersectDbIndices(fromInfo.dbIndices, toInfo.dbIndices);
1499
+ const throughTable = {
1500
+ name: through.table,
1501
+ dbIndex: throughDbIndices[0],
1502
+ columns: [
1503
+ cloneColumn(fromPkColumn, through.fromKey, false),
1504
+ cloneColumn(toPkColumn, through.toKey, false),
1505
+ ],
1506
+ primaryKey: { columns: [through.fromKey, through.toKey] },
1507
+ };
1508
+ throughInfo = {
1509
+ entityName: `through:${through.table}`,
1510
+ tableName: through.table,
1511
+ dbIndices: throughDbIndices,
1512
+ primaryKey: undefined,
1513
+ columns: new Set([through.fromKey, through.toKey]),
1514
+ attributeColumns: new Map([
1515
+ [through.fromKey, through.fromKey],
1516
+ [through.toKey, through.toKey],
1517
+ ]),
1518
+ table: throughTable,
1519
+ };
1520
+ tableInfos.set(through.table, throughInfo);
1521
+ tables.push(throughTable);
1522
+ } else {
1523
+ if (!throughInfo.columns.has(through.fromKey)) {
1524
+ ensureColumn(throughInfo, cloneColumn(fromPkColumn, through.fromKey, false));
1525
+ }
1526
+ if (!throughInfo.columns.has(through.toKey)) {
1527
+ ensureColumn(throughInfo, cloneColumn(toPkColumn, through.toKey, false));
1528
+ }
1529
+ }
1530
+
1531
+ const onDelete = relationship.ownership === "parent" ? "CASCADE" : "NO ACTION";
1532
+ ensureForeignKey(throughInfo.table, {
1533
+ name: buildObjectName("FK", throughInfo.tableName, fromInfo.tableName, through.fromKey),
1534
+ columns: [through.fromKey],
1535
+ referenceTable: fromInfo.tableName,
1536
+ referenceColumns: [fromPk],
1537
+ onDelete,
1538
+ onUpdate: "NO ACTION",
1539
+ });
1540
+ ensureForeignKey(throughInfo.table, {
1541
+ name: buildObjectName("FK", throughInfo.tableName, toInfo.tableName, through.toKey),
1542
+ columns: [through.toKey],
1543
+ referenceTable: toInfo.tableName,
1544
+ referenceColumns: [toPk],
1545
+ onDelete,
1546
+ onUpdate: "NO ACTION",
1547
+ });
1548
+
1549
+ ensureIndex(throughInfo.table, {
1550
+ name: buildObjectName("IDX", throughInfo.tableName, through.fromKey),
1551
+ columns: [through.fromKey],
1552
+ });
1553
+ ensureIndex(throughInfo.table, {
1554
+ name: buildObjectName("IDX", throughInfo.tableName, through.toKey),
1555
+ columns: [through.toKey],
1556
+ });
1557
+ };
1558
+
1559
+ const buildDomainSchemaInternal = (domainConfigContent, defaultDbIndex) => {
1560
+ const parsed = parseDomainConfig(domainConfigContent);
1561
+ const model = parsed?.model ?? {};
1562
+ const entities = model?.entities ?? {};
1563
+ const relationships = model?.relationships ?? {};
1564
+ const warnings = [];
1565
+ const normalizedRelationships = {};
1566
+
1567
+ const entityInfos = new Map();
1568
+ const tableInfos = new Map();
1569
+ const tables = [];
1570
+
1571
+ for (const [entityName, entity] of Object.entries(entities)) {
1572
+ const runtime = buildEntityTable(entityName, entity, defaultDbIndex);
1573
+ entityInfos.set(entityName, runtime);
1574
+ tableInfos.set(runtime.tableName, runtime);
1575
+ tables.push(runtime.table);
1576
+ }
1577
+
1578
+ for (const [relationshipName, relationship] of Object.entries(relationships)) {
1579
+ const normalized = normalizeRelationship(relationshipName, relationship, entityInfos, warnings);
1580
+ if (!normalized) {
1581
+ continue;
1582
+ }
1583
+
1584
+ normalizedRelationships[relationshipName] = normalized;
1585
+ if (normalized.type === "many-to-many") {
1586
+ applyManyToManyRelationship(normalized, entityInfos, tableInfos, tables, warnings);
1587
+ } else {
1588
+ applyDirectRelationship(normalized, entityInfos, warnings);
1589
+ }
1590
+ }
1591
+
1592
+ return {
1593
+ schema: { tables },
1594
+ normalizedRelationships,
1595
+ warnings,
1596
+ };
1597
+ };
1598
+
1599
+ const normalizeDbNameNoTruncate = (value) => {
1600
+ const raw = String(value ?? "").trim();
1601
+ if (!raw) {
1602
+ return "";
1603
+ }
1604
+ return raw
1605
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
1606
+ .replace(/[^A-Za-z0-9_]+/g, "_")
1607
+ .replace(/^_+|_+$/g, "")
1608
+ .replace(/_+/g, "_")
1609
+ .toUpperCase();
1610
+ };
1611
+
1612
+ const toBooleanFlag = (value) => {
1613
+ if (typeof value === "boolean") {
1614
+ return value;
1615
+ }
1616
+ if (typeof value === "number") {
1617
+ return value === 1;
1618
+ }
1619
+ const raw = String(value ?? "").trim().toLowerCase();
1620
+ if (!raw) {
1621
+ return false;
1622
+ }
1623
+ return raw === "1" || raw === "true" || raw === "yes";
1624
+ };
1625
+
1626
+ const hashIdentifierBase36 = (input) => {
1627
+ let hash = 0;
1628
+ for (let index = 0; index < input.length; index += 1) {
1629
+ hash = (hash << 5) - hash + input.charCodeAt(index);
1630
+ hash |= 0;
1631
+ }
1632
+ return Math.abs(hash).toString(36).toUpperCase().slice(0, 6);
1633
+ };
1634
+
1635
+ const buildEntityIndexObjectName = (...parts) => {
1636
+ const joined = parts
1637
+ .map((part) => normalizeDbNameNoTruncate(part))
1638
+ .filter(Boolean)
1639
+ .join("_");
1640
+ if (joined.length <= 31) {
1641
+ return joined;
1642
+ }
1643
+ const hash = hashIdentifierBase36(joined);
1644
+ const keep = Math.max(1, 31 - hash.length - 1);
1645
+ return `${joined.slice(0, keep)}_${hash}`;
1646
+ };
1647
+
1648
+ const buildIndexDefinitionKey = (index) => {
1649
+ const columns = (index.columns ?? []).map((column) => normalizeDbNameNoTruncate(column));
1650
+ return [
1651
+ columns.join("|"),
1652
+ index.unique === true ? "U1" : "U0",
1653
+ index.descending === true ? "D1" : "D0",
1654
+ String(index.expression ?? "").trim().toUpperCase(),
1655
+ ].join("::");
1656
+ };
1657
+
1658
+ const getNextIndexName = (input) => {
1659
+ const base = buildEntityIndexObjectName("IDX", input.tableName, input.indexKey);
1660
+ if (!input.usedNames.has(normalizeDbNameNoTruncate(base))) {
1661
+ return base;
1662
+ }
1663
+ let suffix = 2;
1664
+ while (suffix < 1000) {
1665
+ const candidate = buildEntityIndexObjectName("IDX", input.tableName, input.indexKey, String(suffix));
1666
+ if (!input.usedNames.has(normalizeDbNameNoTruncate(candidate))) {
1667
+ return candidate;
1668
+ }
1669
+ suffix += 1;
1670
+ }
1671
+ return buildEntityIndexObjectName("IDX", input.tableName, input.indexKey, String(Date.now()));
1672
+ };
1673
+
1674
+ const buildAttributeColumnMap = (entity) => {
1675
+ const attributes = entity?.attributes && typeof entity.attributes === "object" ? entity.attributes : {};
1676
+ return new Map(
1677
+ Object.entries(attributes).map(([attributeName, rawAttribute]) => {
1678
+ const attribute = rawAttribute && typeof rawAttribute === "object" ? rawAttribute : {};
1679
+ const columnName = normalizeDbNameNoTruncate(attribute.column ?? attribute.columnName ?? attributeName);
1680
+ return [normalizeDbNameNoTruncate(attributeName), columnName];
1681
+ }),
1682
+ );
1683
+ };
1684
+
1685
+ const mergeEntityIndices = (input) => {
1686
+ const existing = Array.isArray(input.table?.indexes) ? [...(input.table.indexes ?? [])] : [];
1687
+ const seenByDefinition = new Set();
1688
+ for (const idx of existing) {
1689
+ seenByDefinition.add(buildIndexDefinitionKey(idx));
1690
+ }
1691
+ const usedNames = new Set(existing.map((idx) => normalizeDbNameNoTruncate(idx?.name ?? "")).filter(Boolean));
1692
+
1693
+ for (const [indexKey, rawIndex] of Object.entries(input.indexConfig)) {
1694
+ const cfg = rawIndex && typeof rawIndex === "object" ? rawIndex : null;
1695
+ if (!cfg) {
1696
+ continue;
1697
+ }
1698
+ const requestedColumns = Array.isArray(cfg.columns) ? cfg.columns : [];
1699
+ const columns = requestedColumns
1700
+ .map((attributeName) => {
1701
+ const normalizedAttributeName = normalizeDbNameNoTruncate(attributeName);
1702
+ return input.attributeColumnMap.get(normalizedAttributeName) ?? "";
1703
+ })
1704
+ .filter(Boolean);
1705
+ if (columns.length === 0) {
1706
+ continue;
1707
+ }
1708
+ const orders = Array.isArray(cfg.orders)
1709
+ ? cfg.orders.map((order) => String(order ?? "").trim().toLowerCase())
1710
+ : [];
1711
+ const hasAnyDesc = orders.some((order) => order === "desc");
1712
+ const allDesc = orders.length > 0 && orders.every((order) => order === "desc");
1713
+ const unique = toBooleanFlag(cfg.unique);
1714
+ const nextIndex = {
1715
+ name: getNextIndexName({ tableName: input.table.name, indexKey, usedNames }),
1716
+ columns,
1717
+ unique,
1718
+ ...(hasAnyDesc && allDesc ? { descending: true } : {}),
1719
+ };
1720
+ const definitionKey = buildIndexDefinitionKey(nextIndex);
1721
+ if (seenByDefinition.has(definitionKey)) {
1722
+ continue;
1723
+ }
1724
+ seenByDefinition.add(definitionKey);
1725
+ usedNames.add(normalizeDbNameNoTruncate(nextIndex.name));
1726
+ existing.push(nextIndex);
1727
+ }
1728
+
1729
+ return existing;
1730
+ };
1731
+
1732
+ const parseDomainConfigContent = (content) => {
1733
+ const raw = String(content ?? "").trim();
1734
+ if (!raw) {
1735
+ return {};
1736
+ }
1737
+
1738
+ try {
1739
+ return JSON.parse(raw);
1740
+ } catch {}
1741
+
1742
+ try {
1743
+ const evaluator = new Function(
1744
+ `
1745
+ let get;
1746
+ ${raw}
1747
+ if (typeof get === 'function') {
1748
+ return get();
1749
+ }
1750
+ return {};
1751
+ `,
1752
+ );
1753
+ return evaluator();
1754
+ } catch {
1755
+ return {};
1756
+ }
1757
+ };
1758
+
1759
+ const enrichSchemaWithEntityIndices = (schema, domainConfigContent) => {
1760
+ const parsed = parseDomainConfigContent(domainConfigContent);
1761
+ const entities = parsed && typeof parsed === "object" ? parsed?.model?.entities : null;
1762
+ if (!entities || typeof entities !== "object") {
1763
+ return;
1764
+ }
1765
+
1766
+ const tablesByName = new Map();
1767
+ for (const table of schema?.tables ?? []) {
1768
+ const normalizedName = normalizeDbNameNoTruncate(table?.name ?? "");
1769
+ if (normalizedName) {
1770
+ tablesByName.set(normalizedName, table);
1771
+ }
1772
+ }
1773
+
1774
+ for (const [entityName, rawEntity] of Object.entries(entities)) {
1775
+ const entity = rawEntity && typeof rawEntity === "object" ? rawEntity : null;
1776
+ if (!entity) {
1777
+ continue;
1778
+ }
1779
+ const tableName = normalizeDbNameNoTruncate(entity.table ?? entity.tableName ?? entityName);
1780
+ if (!tableName) {
1781
+ continue;
1782
+ }
1783
+ const table = tablesByName.get(tableName);
1784
+ if (!table) {
1785
+ continue;
1786
+ }
1787
+ const attributeColumnMap = buildAttributeColumnMap(entity);
1788
+ const indexConfig = entity.indices && typeof entity.indices === "object" ? entity.indices : null;
1789
+ if (!indexConfig) {
1790
+ continue;
1791
+ }
1792
+ table.indexes = mergeEntityIndices({ table, indexConfig, attributeColumnMap });
1793
+ }
1794
+ };
1795
+
1796
+ const resolveDsBaseConfig = (config = {}) => {
1797
+ const globalConfig = getGlobalConfig() || {};
1798
+ const isSecure = config.secure ?? globalConfig.secure ?? false;
1799
+ const protocol = isSecure ? "https://" : "http://";
1800
+ const hostname = config.hostname ?? globalConfig.hostname ?? "127.0.0.1";
1801
+ const port = config.port ?? globalConfig.port ?? 212;
1802
+ return {
1803
+ url: config.url || `${protocol}${hostname}:${port}`,
1804
+ dbindex: config.dbindex ?? config.dbIndex ?? globalConfig.dbindex ?? 2,
1805
+ timeout: config.timeout ?? IDLE_SOCKET_TIMEOUT_MILLISECONDS,
1806
+ };
1807
+ };
1808
+
1809
+ const resolveExecutionDbIndex = (dbIndex, config) => {
1810
+ const explicitDbIndex = Number(dbIndex);
1811
+ if (Number.isInteger(explicitDbIndex) && explicitDbIndex > 0) {
1812
+ return explicitDbIndex;
1813
+ }
1814
+
1815
+ const defaultDbIndex = Number(resolveDsBaseConfig(config).dbindex);
1816
+ return Number.isInteger(defaultDbIndex) && defaultDbIndex > 0 ? defaultDbIndex : 1;
1817
+ };
1818
+
1819
+ const extractFinaHttpErrorMessage = (error, sql) => {
1820
+ const responseData = error?.response?.data;
1821
+ let parsedBody = responseData;
1822
+ if (typeof responseData === "string") {
1823
+ try {
1824
+ parsedBody = JSON.parse(responseData);
1825
+ } catch {
1826
+ parsedBody = responseData;
1827
+ }
1828
+ }
1829
+
1830
+ const serverMessage =
1831
+ (parsedBody && typeof parsedBody === "object" && (parsedBody.error || parsedBody.message)) ||
1832
+ (typeof parsedBody === "string" && parsedBody) ||
1833
+ null;
1834
+
1835
+ const status = error?.response?.status;
1836
+ const statusPart = status ? `HTTP ${status}` : error?.message || "request failed";
1837
+ const messagePart = serverMessage ? `: ${serverMessage}` : "";
1838
+ return `ExecuteSQL ${statusPart}${messagePart} (sql: ${String(sql).slice(0, 200)})`;
1839
+ };
1840
+
1841
+ const runFinaExecuteSql = async (sql, dbIndex, config) => {
1842
+ const cfg = resolveDsBaseConfig(config);
1843
+ const resolvedDbIndex = Number.isInteger(dbIndex) && dbIndex > 0 ? dbIndex : cfg.dbindex;
1844
+ const url = `${cfg.url}/fina/rest/${FINA_METHOD_CLASS}/%22ExecuteSQL%22`;
1845
+ const body = JSON.stringify({ _parameters: [sql, String(resolvedDbIndex)] });
1846
+
1847
+ let res;
1848
+ try {
1849
+ res = await axios.request({
1850
+ method: "POST",
1851
+ url,
1852
+ data: body,
1853
+ timeout: cfg.timeout,
1854
+ headers: { "content-type": "text/plain" },
1855
+ });
1856
+ } catch (error) {
1857
+ throw new Error(extractFinaHttpErrorMessage(error, sql));
1858
+ }
1859
+
1860
+ const parsedRes = typeof res.data === "string" ? JSON.parse(res.data) : res.data;
1861
+ if (parsedRes && parsedRes.error) {
1862
+ throw new Error(parsedRes.error);
1863
+ }
1864
+
1865
+ if (parsedRes && Array.isArray(parsedRes.result) && parsedRes.result.length > 0) {
1866
+ if (parsedRes.result[0] && parsedRes.result[0].error) {
1867
+ throw new Error(parsedRes.result[0].error);
1868
+ }
1869
+
1870
+ try {
1871
+ const parsed = JSON.parse(parsedRes.result[0]);
1872
+ const rows = Array.isArray(parsed?.data) ? parsed.data : [];
1873
+ return rows.map((row) =>
1874
+ Object.keys(row).reduce((acc, key) => {
1875
+ acc[key.toLowerCase()] = row[key];
1876
+ return acc;
1877
+ }, {}),
1878
+ );
1879
+ } catch {
1880
+ return [];
1881
+ }
1882
+ }
1883
+
1884
+ return [];
1885
+ };
1886
+
1887
+ const execRawSql = async (sql, dbIndex, config) => {
1888
+ return runFinaExecuteSql(sanitizeSql(sql), resolveExecutionDbIndex(dbIndex, config), config);
1889
+ };
1890
+
1891
+ const loadTableExists = async (tableName, dbIndex, config) => {
1892
+ const queries = buildTableStructureQueries(tableName);
1893
+ const rows = await execRawSql(queries.tableExistsSql, dbIndex, config);
1894
+ return Number(rows?.[0]?.["TABLE_EXISTS"] ?? rows?.[0]?.["table_exists"] ?? 0) === 1;
1895
+ };
1896
+
1897
+ const loadTableSnapshot = async (tableName, knownExists, dbIndex, config) => {
1898
+ const queries = buildTableStructureQueries(tableName);
1899
+ const tableExistsRows =
1900
+ knownExists == null
1901
+ ? await execRawSql(queries.tableExistsSql, dbIndex, config)
1902
+ : [{ TABLE_EXISTS: knownExists ? 1 : 0 }];
1903
+
1904
+ const [columnRows, primaryKeyRows, uniqueKeyRows, checkConstraintRows, indexRows, foreignKeyRows, incomingForeignKeyRows, columnDependencyRows] =
1905
+ await Promise.all([
1906
+ execRawSql(queries.columnsSql, dbIndex, config),
1907
+ execRawSql(queries.primaryKeySql, dbIndex, config),
1908
+ execRawSql(queries.uniqueKeysSql, dbIndex, config),
1909
+ execRawSql(queries.checkConstraintsSql, dbIndex, config),
1910
+ execRawSql(queries.indexesSql, dbIndex, config),
1911
+ execRawSql(queries.foreignKeysSql, dbIndex, config),
1912
+ execRawSql(queries.incomingForeignKeysSql, dbIndex, config),
1913
+ execRawSql(queries.columnDependenciesSql, dbIndex, config),
1914
+ ]);
1915
+
1916
+ return buildStructureSnapshotFromRows({
1917
+ tableName,
1918
+ tableExistsRows,
1919
+ columnRows,
1920
+ primaryKeyRows,
1921
+ uniqueKeyRows,
1922
+ checkConstraintRows,
1923
+ indexRows,
1924
+ foreignKeyRows,
1925
+ incomingForeignKeyRows,
1926
+ columnDependencyRows,
1927
+ });
1928
+ };
1929
+
1930
+ const loadGeneratorSyncPlan = async (table, dbIndex, config) => {
1931
+ if (!shouldSyncGenerator(table)) {
1932
+ return { statements: [], differences: [] };
1933
+ }
1934
+
1935
+ const queries = buildGeneratorStructureQueries(table);
1936
+ const [generatorRows, triggerRows] = await Promise.all([
1937
+ execRawSql(queries.generatorExistsSql, dbIndex, config),
1938
+ execRawSql(queries.triggerSql, dbIndex, config),
1939
+ ]);
1940
+
1941
+ return buildGeneratorSyncPlan(table, generatorRows, triggerRows);
1942
+ };
1943
+
1944
+ const toFirebird25CompatibleDdl = (statement) => {
1945
+ const sanitizedStatement = sanitizeSql(statement);
1946
+ const notNullMatch = sanitizedStatement.match(
1947
+ /^ALTER\s+TABLE\s+("[^"]+"|[A-Z0-9_$]+)\s+ALTER\s+(?:COLUMN\s+)?("[^"]+"|[A-Z0-9_$]+)\s+SET\s+NOT\s+NULL;?$/i,
1948
+ );
1949
+ if (!notNullMatch) {
1950
+ return sanitizedStatement;
1951
+ }
1952
+
1953
+ const tableIdentifier = notNullMatch[1];
1954
+ const columnIdentifier = notNullMatch[2];
1955
+ const constraintName = buildFirebird25NotNullCheckName(tableIdentifier, columnIdentifier);
1956
+ return `ALTER TABLE ${tableIdentifier} ADD CONSTRAINT ${constraintName} CHECK (${columnIdentifier} IS NOT NULL)`;
1957
+ };
1958
+
1959
+ const executeStatementsBySql = async (statements, dbIndex, config, ctx) => {
1960
+ if (statements.length === 0) {
1961
+ return null;
1962
+ }
1963
+
1964
+ for (const statement of statements) {
1965
+ const signature = toSimpleHash(statement);
1966
+ if (ctx?.executedBlockSignatures?.has(signature)) {
1967
+ continue;
1968
+ }
1969
+ let lastError;
1970
+ let succeeded = false;
1971
+ for (let attempt = 0; attempt < 3; attempt++) {
1972
+ try {
1973
+ await execRawSql(statement, dbIndex, config);
1974
+ succeeded = true;
1975
+ break;
1976
+ } catch (error) {
1977
+ lastError = error;
1978
+ }
1979
+ }
1980
+ if (!succeeded) {
1981
+ throw lastError;
1982
+ }
1983
+ ctx?.executedBlockSignatures?.add(signature);
1984
+ }
1985
+ return null;
1986
+ };
1987
+
1988
+ const executeSqlStatements = async (statements, dbIndex, config, ctx) => {
1989
+ const normalizedStatements = (statements ?? [])
1990
+ .map((statement) => toFirebird25CompatibleDdl(statement))
1991
+ .map((statement) => sanitizeSql(statement))
1992
+ .filter((statement) => statement.length > 0);
1993
+ if (normalizedStatements.length === 0) {
1994
+ return null;
1995
+ }
1996
+
1997
+ // Every DDL statement (CREATE TABLE, CREATE GENERATOR, CREATE TRIGGER, ALTER TABLE, ...) is
1998
+ // submitted one at a time via the FINA ExecuteSQL endpoint, never batched via EXECUTE STATEMENT
1999
+ // wrapped inside an EXECUTE BLOCK -- Firebird does not correctly resolve NEW/OLD trigger
2000
+ // pseudo-columns when a CREATE TRIGGER body is compiled that way.
2001
+ await executeStatementsBySql(normalizedStatements, dbIndex, config, ctx);
2002
+ return null;
2003
+ };
2004
+
2005
+ const logGeneratorCreateStatements = (tableName, statements) => {
2006
+ if (statements.some((statement) => /^\s*CREATE\s+GENERATOR\b/i.test(statement))) {
2007
+ console.log(`[AppConfig Publish] Table ${tableName} generator not found. Creating...`);
2008
+ }
2009
+ };
2010
+
2011
+ const logTriggerCreateStatements = (tableName, statements) => {
2012
+ for (const statement of statements) {
2013
+ const match = statement.match(/^\s*CREATE\s+TRIGGER\s+([A-Z0-9_$"]+)/i);
2014
+ if (match) {
2015
+ const triggerName = String(match[1] ?? "").replace(/"/g, "").trim();
2016
+ if (triggerName) {
2017
+ console.log(`[AppConfig Publish] Table ${tableName} trigger ${triggerName} not found. Creating...`);
2018
+ }
2019
+ }
2020
+ }
2021
+ };
2022
+
2023
+ const logIndexCreateStatements = (tableName, statements) => {
2024
+ for (const statement of statements) {
2025
+ const match = statement.match(/^\s*CREATE\s+(?:UNIQUE\s+|DESC\s+|UNIQUE\s+DESC\s+)?INDEX\s+([A-Z0-9_$"]+)/i);
2026
+ if (match) {
2027
+ const indexName = String(match[1] ?? "").replace(/"/g, "").trim();
2028
+ if (indexName) {
2029
+ console.log(`[AppConfig Publish] Table ${tableName} index ${indexName} not found. Creating...`);
2030
+ }
2031
+ }
2032
+ }
2033
+ };
2034
+
2035
+ const appendDryRunReport = (report, migrationMode, tableName, phase, data) => {
2036
+ const warnings = data.warnings ?? [];
2037
+ const differences = data.differences ?? [];
2038
+ const blockedActions = warnings.filter((warning) => /safe mode|requires manual/i.test(String(warning)));
2039
+ report.push({
2040
+ tableName,
2041
+ phase,
2042
+ migrationMode,
2043
+ statements: data.statements ?? [],
2044
+ warnings,
2045
+ differences,
2046
+ blockedActions,
2047
+ });
2048
+ };
2049
+
2050
+ const applyGeneratorSyncPlan = async (table, generatorSyncPlan, dbIndex, config, ctx) => {
2051
+ if (generatorSyncPlan.differences.length > 0) {
2052
+ console.log(`[AppConfig Publish] Table ${table.name} generator discrepancies:`, generatorSyncPlan.differences);
2053
+ }
2054
+ appendDryRunReport(ctx.report, ctx.migrationMode, table.name, "generator-sync", {
2055
+ statements: generatorSyncPlan.statements,
2056
+ warnings: [],
2057
+ differences: generatorSyncPlan.differences,
2058
+ });
2059
+ if (generatorSyncPlan.statements.length === 0) {
2060
+ if (generatorSyncPlan.differences.length > 0) {
2061
+ throw new Error(
2062
+ `[AppConfig Publish] Table ${table.name} unresolved generator/trigger discrepancies: ${generatorSyncPlan.differences.join(" | ")}`,
2063
+ );
2064
+ }
2065
+ return null;
2066
+ }
2067
+ logGeneratorCreateStatements(table.name, generatorSyncPlan.statements);
2068
+ logTriggerCreateStatements(table.name, generatorSyncPlan.statements);
2069
+
2070
+ await executeSqlStatements(generatorSyncPlan.statements, dbIndex, config, ctx);
2071
+ return null;
2072
+ };
2073
+
2074
+ const splitSqlStatements = (sqlBatch) => {
2075
+ const source = sqlBatch ?? "";
2076
+ const statements = [];
2077
+ let current = "";
2078
+ let quote = "";
2079
+ let escaped = false;
2080
+
2081
+ for (let i = 0; i < source.length; i++) {
2082
+ const ch = source[i];
2083
+ current += ch;
2084
+
2085
+ if (quote) {
2086
+ if (escaped) {
2087
+ escaped = false;
2088
+ continue;
2089
+ }
2090
+ if (quote === "'" && ch === "\\") {
2091
+ escaped = true;
2092
+ continue;
2093
+ }
2094
+ if (ch === quote) {
2095
+ quote = "";
2096
+ }
2097
+ continue;
2098
+ }
2099
+
2100
+ if (ch === '"' || ch === "'") {
2101
+ quote = ch;
2102
+ continue;
2103
+ }
2104
+
2105
+ if (ch === ";") {
2106
+ const statement = current.trim();
2107
+ if (statement.length > 0) {
2108
+ statements.push(statement);
2109
+ }
2110
+ current = "";
2111
+ }
2112
+ }
2113
+
2114
+ const tail = current.trim();
2115
+ if (tail.length > 0) {
2116
+ statements.push(tail);
2117
+ }
2118
+
2119
+ return statements.map((statement) => sanitizeSql(statement));
2120
+ };
2121
+
2122
+ const syncTable = async (table, ctx) => {
2123
+ const tableDbIndex = resolveExecutionDbIndex(table?.dbIndex, ctx.config);
2124
+ const exists = await loadTableExists(table.name, tableDbIndex, ctx.config);
2125
+
2126
+ if (!exists) {
2127
+ console.log(`[AppConfig Publish] Table ${table.name} not found. Creating...`);
2128
+ const createSqlBatch = generateCreateSchemaSql({ tables: [table] }, false);
2129
+ const createStatements = splitSqlStatements(createSqlBatch);
2130
+ appendDryRunReport(ctx.report, ctx.migrationMode, table.name, "create-missing-table", {
2131
+ statements: createStatements,
2132
+ warnings: [],
2133
+ differences: [`Table "${table.name}" does not exist.`],
2134
+ });
2135
+ await executeSqlStatements(createStatements, tableDbIndex, ctx.config, ctx);
2136
+
2137
+ const generatorSyncPlan = await loadGeneratorSyncPlan(table, tableDbIndex, ctx.config);
2138
+ await applyGeneratorSyncPlan(table, generatorSyncPlan, tableDbIndex, ctx.config, ctx);
2139
+
2140
+ const latestSnapshot = await loadTableSnapshot(table.name, true, tableDbIndex, ctx.config);
2141
+ const postCreateSyncPlan = buildTableSyncPlan(table, latestSnapshot, { migrationMode: ctx.migrationMode });
2142
+ if (postCreateSyncPlan.warnings.length > 0) {
2143
+ console.log(`[AppConfig Publish] Table ${table.name} post-create warnings:`, postCreateSyncPlan.warnings);
2144
+ }
2145
+ appendDryRunReport(ctx.report, ctx.migrationMode, table.name, "post-create-sync", postCreateSyncPlan);
2146
+ if (postCreateSyncPlan.statements.length === 0) {
2147
+ return null;
2148
+ }
2149
+
2150
+ console.log(`[AppConfig Publish] Table ${table.name} post-create discrepancies:`, postCreateSyncPlan.differences);
2151
+ logIndexCreateStatements(table.name, postCreateSyncPlan.statements);
2152
+ return executeSqlStatements(postCreateSyncPlan.statements, tableDbIndex, ctx.config, ctx);
2153
+ }
2154
+
2155
+ const snapshot = await loadTableSnapshot(table.name, true, tableDbIndex, ctx.config);
2156
+ const compareResult = compareTableStructure(table, snapshot);
2157
+ const generatorSyncPlan = await loadGeneratorSyncPlan(table, tableDbIndex, ctx.config);
2158
+ await applyGeneratorSyncPlan(table, generatorSyncPlan, tableDbIndex, ctx.config, ctx);
2159
+
2160
+ if (compareResult.isSame) {
2161
+ return null;
2162
+ }
2163
+
2164
+ console.log(`[AppConfig Publish] Table ${table.name} discrepancies:`, compareResult.differences);
2165
+ const syncPlan = buildTableSyncPlan(table, snapshot, { migrationMode: ctx.migrationMode });
2166
+ if (syncPlan.warnings.length > 0) {
2167
+ console.log(`[AppConfig Publish] Table ${table.name} sync warnings:`, syncPlan.warnings);
2168
+ }
2169
+ appendDryRunReport(ctx.report, ctx.migrationMode, table.name, "sync-existing-table", syncPlan);
2170
+ if (syncPlan.statements.length === 0) {
2171
+ return null;
2172
+ }
2173
+ logIndexCreateStatements(table.name, syncPlan.statements);
2174
+
2175
+ return executeSqlStatements(syncPlan.statements, tableDbIndex, ctx.config, ctx);
2176
+ };
2177
+
2178
+ const resolveConfigFromPayload = (payload = {}, argv = {}) => {
2179
+ const resolvedDbIndex = toNonNegativeInteger(payload.dbIndex) ?? toNonNegativeInteger(argv.dbindex) ?? 2;
2180
+ return {
2181
+ dbindex: resolvedDbIndex,
2182
+ dbIndex: resolvedDbIndex,
2183
+ ...(payload.url ? { url: payload.url } : {}),
2184
+ ...(payload.hostname ? { hostname: payload.hostname } : {}),
2185
+ ...(payload.port ? { port: payload.port } : {}),
2186
+ ...(payload.secure != null ? { secure: payload.secure } : {}),
2187
+ ...(payload.subdomain || argv.subdomain ? { subdomain: payload.subdomain ?? argv.subdomain } : {}),
2188
+ ...(payload.timeout ? { timeout: payload.timeout } : {}),
2189
+ };
2190
+ };
2191
+
2192
+ const buildPublishStatusConfigKey = (domainName, domainVersion) => ({
2193
+ name: `${domainName}__PUBLISH_STATUS`,
2194
+ version: domainVersion,
2195
+ });
2196
+
2197
+ const writePublishStatus = async (name, version, statusPayload, config) => {
2198
+ const upsertSql = getUpsertBlock(name, version, JSON.stringify(statusPayload));
2199
+ await executeBlock(upsertSql, config);
2200
+ };
2201
+
2202
+ export const getPublishStatus = async (payload = {}, argv = {}) => {
2203
+ const domainName = String(payload?.domainName ?? "").trim();
2204
+ const domainVersion = String(payload?.domainVersion ?? "").trim();
2205
+ if (!domainName || !domainVersion) {
2206
+ return { status: "UNKNOWN", error: "payload.domainName and payload.domainVersion are required." };
2207
+ }
2208
+
2209
+ const config = resolveConfigFromPayload(payload, argv);
2210
+ const { name, version } = buildPublishStatusConfigKey(domainName, domainVersion);
2211
+ const rows = await queryData(getConfigByNameVersionParameters(name, version), config);
2212
+ const row = rows?.[0];
2213
+ if (!row?.data) {
2214
+ return { status: "NOT_STARTED" };
2215
+ }
2216
+
2217
+ try {
2218
+ return JSON.parse(String(row.data));
2219
+ } catch {
2220
+ return { status: "UNKNOWN" };
2221
+ }
2222
+ };
2223
+
2224
+ export const publishConfigsAsync = async (payload = {}, argv = {}) => {
2225
+ const domainName = String(payload?.domainName ?? "").trim();
2226
+ const domainVersion = String(payload?.domainVersion ?? "").trim();
2227
+ if (!domainName) {
2228
+ return { success: false, error: "payload.domainName is required." };
2229
+ }
2230
+ if (!domainVersion) {
2231
+ return { success: false, error: "payload.domainVersion is required." };
2232
+ }
2233
+
2234
+ const config = resolveConfigFromPayload(payload, argv);
2235
+ const { name, version } = buildPublishStatusConfigKey(domainName, domainVersion);
2236
+ const startedAt = new Date().toISOString();
2237
+
2238
+ await writePublishStatus(name, version, { status: "IN_PROGRESS", domainName, domainVersion, startedAt }, config);
2239
+
2240
+ publishConfigs(payload, argv)
2241
+ .then((result) => {
2242
+ const { report, ...resultWithoutReport } = result ?? {};
2243
+ return writePublishStatus(
2244
+ name,
2245
+ version,
2246
+ {
2247
+ ...resultWithoutReport,
2248
+ status: result?.success ? "SUCCESS" : "FAILED",
2249
+ domainName,
2250
+ domainVersion,
2251
+ startedAt,
2252
+ finishedAt: new Date().toISOString(),
2253
+ },
2254
+ config,
2255
+ );
2256
+ })
2257
+ .catch((error) =>
2258
+ writePublishStatus(
2259
+ name,
2260
+ version,
2261
+ {
2262
+ success: false,
2263
+ status: "FAILED",
2264
+ domainName,
2265
+ domainVersion,
2266
+ startedAt,
2267
+ finishedAt: new Date().toISOString(),
2268
+ error: error?.message ?? String(error),
2269
+ },
2270
+ config,
2271
+ ),
2272
+ )
2273
+ .catch((error) => {
2274
+ console.error(`[AppConfig Publish] Failed to record publish status for ${domainName}@${domainVersion}:`, error);
2275
+ });
2276
+
2277
+ return { success: true, status: "IN_PROGRESS", domainName, domainVersion, startedAt };
2278
+ };
2279
+
2280
+ export const publishConfigs = async (payload = {}, argv = {}) => {
2281
+ try {
2282
+ const domainName = payload.domainName;
2283
+ const domainVersion = payload.domainVersion;
2284
+ if (!domainName) {
2285
+ throw new Error("payload.domainName is required.");
2286
+ }
2287
+ if (!domainVersion) {
2288
+ throw new Error("payload.domainVersion is required.");
2289
+ }
2290
+
2291
+ const migrationMode = payload.migrationMode === "aggressive" ? "aggressive" : "safe";
2292
+ const config = resolveConfigFromPayload(payload, argv);
2293
+
2294
+ const configRows = await queryData(getConfigByNameVersionParameters(domainName, domainVersion), config);
2295
+ const domainRecord = configRows?.[0];
2296
+ if (!domainRecord?.data) {
2297
+ throw new Error(`SYS$CONFIG record ${domainName}@${domainVersion} was not found.`);
2298
+ }
2299
+
2300
+ const domainConfigContent = String(domainRecord.data);
2301
+
2302
+ const preview = buildDomainDdlPreview(domainConfigContent, config.dbIndex);
2303
+ enrichSchemaWithEntityIndices(preview.schema, domainConfigContent);
2304
+ if (preview.warnings.length > 0) {
2305
+ throw new Error(`Domain model warning (${domainName}): ${preview.warnings.join(" | ")}`);
2306
+ }
2307
+
2308
+ const report = [];
2309
+ if (preview.schema.tables.length === 0) {
2310
+ return { success: true, domainName, domainVersion, tablesProcessed: 0, report };
2311
+ }
2312
+
2313
+ const orderedTables = orderTablesByDependency(preview.schema.tables);
2314
+ const ctx = {
2315
+ config,
2316
+ migrationMode,
2317
+ executedBlockSignatures: new Set(),
2318
+ report,
2319
+ };
2320
+
2321
+ for (const table of orderedTables) {
2322
+ await syncTable(table, ctx);
2323
+ }
2324
+
2325
+ if (report.length > 0) {
2326
+ console.log(`[AppConfig Publish] Publish report (${domainName}@${domainVersion}):`, report);
2327
+ }
2328
+
2329
+ return {
2330
+ success: true,
2331
+ domainName,
2332
+ domainVersion,
2333
+ tablesProcessed: orderedTables.length,
2334
+ report,
2335
+ };
2336
+ } catch (error) {
2337
+ return {
2338
+ success: false,
2339
+ error: error?.message || error,
2340
+ };
2341
+ }
2342
+ };