arkormx 2.0.0-next.3 → 2.0.0-next.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -3,6 +3,7 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync
3
3
  import { dirname, extname, join, resolve } from "node:path";
4
4
  import { spawnSync } from "node:child_process";
5
5
  import { str } from "@h3ravel/support";
6
+ import { createHash } from "node:crypto";
6
7
  import path, { dirname as dirname$1, extname as extname$1, join as join$1, relative } from "path";
7
8
  import { copyFileSync, existsSync as existsSync$1, mkdirSync as mkdirSync$1, readFileSync as readFileSync$1, readdirSync as readdirSync$1, rmSync as rmSync$1, writeFileSync as writeFileSync$1 } from "fs";
8
9
  import { AsyncLocalStorage } from "async_hooks";
@@ -12,7 +13,6 @@ import { createRequire } from "module";
12
13
  import { fileURLToPath } from "url";
13
14
  import { Logger } from "@h3ravel/shared";
14
15
  import { Command, Kernel } from "@h3ravel/musket";
15
- import { createHash } from "node:crypto";
16
16
 
17
17
  //#region src/Exceptions/ArkormException.ts
18
18
  var ArkormException = class extends Error {
@@ -1436,6 +1436,377 @@ const applyMigrationRollbackToPrismaSchema = async (migration, options = {}) =>
1436
1436
  };
1437
1437
  };
1438
1438
 
1439
+ //#endregion
1440
+ //#region src/helpers/migration-history.ts
1441
+ const createEmptyAppliedMigrationsState = () => ({
1442
+ version: 1,
1443
+ migrations: [],
1444
+ runs: []
1445
+ });
1446
+ const supportsDatabaseMigrationState = (adapter) => {
1447
+ return typeof adapter?.readAppliedMigrationsState === "function" && typeof adapter?.writeAppliedMigrationsState === "function";
1448
+ };
1449
+ const resolveMigrationStateFilePath = (cwd, configuredPath) => {
1450
+ if (configuredPath && configuredPath.trim().length > 0) return resolve(configuredPath);
1451
+ return join(cwd, ".arkormx", "migrations.applied.json");
1452
+ };
1453
+ const buildMigrationIdentity = (filePath, className) => {
1454
+ const fileName = filePath.split("/").pop()?.split("\\").pop() ?? filePath;
1455
+ return `${fileName.slice(0, fileName.length - extname(fileName).length)}:${className}`;
1456
+ };
1457
+ const computeMigrationChecksum = (filePath) => {
1458
+ const source = readFileSync(filePath, "utf-8");
1459
+ return createHash("sha256").update(source).digest("hex");
1460
+ };
1461
+ const readAppliedMigrationsState = (stateFilePath) => {
1462
+ if (!existsSync(stateFilePath)) return createEmptyAppliedMigrationsState();
1463
+ try {
1464
+ const parsed = JSON.parse(readFileSync(stateFilePath, "utf-8"));
1465
+ if (!Array.isArray(parsed.migrations)) return createEmptyAppliedMigrationsState();
1466
+ return {
1467
+ version: 1,
1468
+ migrations: parsed.migrations.filter((migration) => {
1469
+ return typeof migration?.id === "string" && typeof migration?.file === "string" && typeof migration?.className === "string" && typeof migration?.appliedAt === "string" && (migration?.checksum === void 0 || typeof migration?.checksum === "string");
1470
+ }),
1471
+ runs: Array.isArray(parsed.runs) ? parsed.runs.filter((run) => {
1472
+ return typeof run?.id === "string" && typeof run?.appliedAt === "string" && Array.isArray(run?.migrationIds) && run.migrationIds.every((item) => typeof item === "string");
1473
+ }) : []
1474
+ };
1475
+ } catch {
1476
+ return createEmptyAppliedMigrationsState();
1477
+ }
1478
+ };
1479
+ const readAppliedMigrationsStateFromStore = async (adapter, stateFilePath) => {
1480
+ if (supportsDatabaseMigrationState(adapter)) return await adapter.readAppliedMigrationsState();
1481
+ return readAppliedMigrationsState(stateFilePath);
1482
+ };
1483
+ const writeAppliedMigrationsState = (stateFilePath, state) => {
1484
+ const directory = dirname(stateFilePath);
1485
+ if (!existsSync(directory)) mkdirSync(directory, { recursive: true });
1486
+ writeFileSync(stateFilePath, JSON.stringify(state, null, 2));
1487
+ };
1488
+ const writeAppliedMigrationsStateToStore = async (adapter, stateFilePath, state) => {
1489
+ if (supportsDatabaseMigrationState(adapter)) {
1490
+ await adapter.writeAppliedMigrationsState(state);
1491
+ return;
1492
+ }
1493
+ writeAppliedMigrationsState(stateFilePath, state);
1494
+ };
1495
+ const isMigrationApplied = (state, identity, checksum) => {
1496
+ const matched = state.migrations.find((migration) => migration.id === identity);
1497
+ if (!matched) return false;
1498
+ if (checksum && matched.checksum) return matched.checksum === checksum;
1499
+ if (checksum && !matched.checksum) return false;
1500
+ return true;
1501
+ };
1502
+ const findAppliedMigration = (state, identity) => {
1503
+ return state.migrations.find((migration) => migration.id === identity);
1504
+ };
1505
+ const markMigrationApplied = (state, entry) => {
1506
+ const next = state.migrations.filter((migration) => migration.id !== entry.id);
1507
+ next.push(entry);
1508
+ return {
1509
+ version: 1,
1510
+ migrations: next,
1511
+ runs: state.runs ?? []
1512
+ };
1513
+ };
1514
+ const removeAppliedMigration = (state, identity) => {
1515
+ return {
1516
+ version: 1,
1517
+ migrations: state.migrations.filter((migration) => migration.id !== identity),
1518
+ runs: (state.runs ?? []).map((run) => ({
1519
+ ...run,
1520
+ migrationIds: run.migrationIds.filter((id) => id !== identity)
1521
+ })).filter((run) => run.migrationIds.length > 0)
1522
+ };
1523
+ };
1524
+ const buildMigrationRunId = () => {
1525
+ return `run_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
1526
+ };
1527
+ const markMigrationRun = (state, run) => {
1528
+ const nextRuns = (state.runs ?? []).filter((existing) => existing.id !== run.id);
1529
+ nextRuns.push(run);
1530
+ return {
1531
+ version: 1,
1532
+ migrations: state.migrations,
1533
+ runs: nextRuns
1534
+ };
1535
+ };
1536
+ const getLastMigrationRun = (state) => {
1537
+ const runs = state.runs ?? [];
1538
+ if (runs.length === 0) return void 0;
1539
+ return runs.map((run, index) => ({
1540
+ run,
1541
+ index
1542
+ })).sort((left, right) => {
1543
+ const appliedAtOrder = right.run.appliedAt.localeCompare(left.run.appliedAt);
1544
+ if (appliedAtOrder !== 0) return appliedAtOrder;
1545
+ return right.index - left.index;
1546
+ })[0]?.run;
1547
+ };
1548
+ const getLatestAppliedMigrations = (state, steps) => {
1549
+ return state.migrations.map((migration, index) => ({
1550
+ migration,
1551
+ index
1552
+ })).sort((left, right) => {
1553
+ const appliedAtOrder = right.migration.appliedAt.localeCompare(left.migration.appliedAt);
1554
+ if (appliedAtOrder !== 0) return appliedAtOrder;
1555
+ return right.index - left.index;
1556
+ }).slice(0, Math.max(0, steps)).map((entry) => entry.migration);
1557
+ };
1558
+
1559
+ //#endregion
1560
+ //#region src/helpers/column-mappings.ts
1561
+ let cachedColumnMappingsPath;
1562
+ let cachedColumnMappingsState;
1563
+ const resolvePersistedMetadataFeatures = (features) => {
1564
+ return {
1565
+ persistedColumnMappings: features?.persistedColumnMappings !== false,
1566
+ persistedEnums: features?.persistedEnums !== false
1567
+ };
1568
+ };
1569
+ const createEmptyPersistedColumnMappingsState = () => ({
1570
+ version: 1,
1571
+ tables: {}
1572
+ });
1573
+ const resolveColumnMappingsFilePath = (cwd, configuredPath) => {
1574
+ if (configuredPath && configuredPath.trim().length > 0) return resolve(configuredPath);
1575
+ return join(cwd, ".arkormx", "column-mappings.json");
1576
+ };
1577
+ const normalizePersistedEnumValues = (values) => {
1578
+ if (!Array.isArray(values)) return [];
1579
+ return values.filter((value) => typeof value === "string" && value.trim().length > 0);
1580
+ };
1581
+ const normalizeLegacyTableColumns = (columns) => {
1582
+ return Object.entries(columns).reduce((mapped, [attribute, column]) => {
1583
+ if (attribute.trim().length === 0) return mapped;
1584
+ if (typeof column !== "string" || column.trim().length === 0) return mapped;
1585
+ mapped[attribute] = column;
1586
+ return mapped;
1587
+ }, {});
1588
+ };
1589
+ const normalizePersistedTableMetadata = (table) => {
1590
+ if (!table || typeof table !== "object" || Array.isArray(table)) return {
1591
+ columns: {},
1592
+ enums: {}
1593
+ };
1594
+ const candidate = table;
1595
+ if (!(Object.prototype.hasOwnProperty.call(candidate, "columns") || Object.prototype.hasOwnProperty.call(candidate, "enums"))) return {
1596
+ columns: normalizeLegacyTableColumns(candidate),
1597
+ enums: {}
1598
+ };
1599
+ return {
1600
+ columns: normalizeLegacyTableColumns(candidate.columns ?? {}),
1601
+ enums: Object.entries(candidate.enums ?? {}).reduce((all, [columnName, values]) => {
1602
+ if (columnName.trim().length === 0) return all;
1603
+ const normalizedValues = normalizePersistedEnumValues(values);
1604
+ if (normalizedValues.length > 0) all[columnName] = normalizedValues;
1605
+ return all;
1606
+ }, {})
1607
+ };
1608
+ };
1609
+ const normalizePersistedColumnMappingsState = (state) => {
1610
+ return {
1611
+ version: 1,
1612
+ tables: Object.entries(state?.tables ?? {}).reduce((all, [tableName, tableMetadata]) => {
1613
+ if (tableName.trim().length === 0) return all;
1614
+ const normalized = normalizePersistedTableMetadata(tableMetadata);
1615
+ if (Object.keys(normalized.columns).length > 0 || Object.keys(normalized.enums).length > 0) all[tableName] = normalized;
1616
+ return all;
1617
+ }, {})
1618
+ };
1619
+ };
1620
+ const buildPersistedFeatureDisabledError = (feature, table) => {
1621
+ return new ArkormException(`Table [${table}] requires ${feature === "persistedColumnMappings" ? "persisted column mappings" : "persisted enum metadata"}, but ${feature === "persistedColumnMappings" ? "features.persistedColumnMappings" : "features.persistedEnums"} is disabled in arkormx.config.*.`, {
1622
+ operation: "metadata.persisted",
1623
+ meta: {
1624
+ feature,
1625
+ table
1626
+ }
1627
+ });
1628
+ };
1629
+ const assertPersistedTableMetadataEnabled = (table, metadata, features, strict) => {
1630
+ if (!strict) return;
1631
+ if (!features.persistedColumnMappings && Object.keys(metadata.columns).length > 0) throw buildPersistedFeatureDisabledError("persistedColumnMappings", table);
1632
+ if (!features.persistedEnums && Object.keys(metadata.enums).length > 0) throw buildPersistedFeatureDisabledError("persistedEnums", table);
1633
+ };
1634
+ const buildEnumUnionType = (values) => {
1635
+ return values.map((value) => {
1636
+ return `'${value.replace(/'/g, String.raw`\'`)}'`;
1637
+ }).join(" | ");
1638
+ };
1639
+ const resetPersistedColumnMappingsCache = () => {
1640
+ cachedColumnMappingsPath = void 0;
1641
+ cachedColumnMappingsState = void 0;
1642
+ };
1643
+ const readPersistedColumnMappingsState = (filePath) => {
1644
+ if (cachedColumnMappingsPath === filePath && cachedColumnMappingsState) return cachedColumnMappingsState;
1645
+ if (!existsSync(filePath)) {
1646
+ const empty = createEmptyPersistedColumnMappingsState();
1647
+ cachedColumnMappingsPath = filePath;
1648
+ cachedColumnMappingsState = empty;
1649
+ return empty;
1650
+ }
1651
+ try {
1652
+ const normalized = normalizePersistedColumnMappingsState(JSON.parse(readFileSync(filePath, "utf-8")));
1653
+ cachedColumnMappingsPath = filePath;
1654
+ cachedColumnMappingsState = normalized;
1655
+ return normalized;
1656
+ } catch {
1657
+ const empty = createEmptyPersistedColumnMappingsState();
1658
+ cachedColumnMappingsPath = filePath;
1659
+ cachedColumnMappingsState = empty;
1660
+ return empty;
1661
+ }
1662
+ };
1663
+ const writePersistedColumnMappingsState = (filePath, state) => {
1664
+ const normalized = normalizePersistedColumnMappingsState(state);
1665
+ const directory = dirname(filePath);
1666
+ if (!existsSync(directory)) mkdirSync(directory, { recursive: true });
1667
+ writeFileSync(filePath, JSON.stringify(normalized, null, 2));
1668
+ cachedColumnMappingsPath = filePath;
1669
+ cachedColumnMappingsState = normalized;
1670
+ };
1671
+ const deletePersistedColumnMappingsState = (filePath) => {
1672
+ if (existsSync(filePath)) rmSync(filePath, { force: true });
1673
+ resetPersistedColumnMappingsCache();
1674
+ };
1675
+ const getPersistedTableMetadata = (table, options = {}) => {
1676
+ const metadata = readPersistedColumnMappingsState(resolveColumnMappingsFilePath(options.cwd ?? process.cwd(), options.configuredPath)).tables[table] ?? {
1677
+ columns: {},
1678
+ enums: {}
1679
+ };
1680
+ assertPersistedTableMetadataEnabled(table, metadata, options.features ?? resolvePersistedMetadataFeatures(), options.strict ?? false);
1681
+ return {
1682
+ columns: { ...metadata.columns },
1683
+ enums: Object.entries(metadata.enums).reduce((all, [columnName, values]) => {
1684
+ all[columnName] = [...values];
1685
+ return all;
1686
+ }, {})
1687
+ };
1688
+ };
1689
+ const getPersistedEnumMap = (table, options = {}) => {
1690
+ return getPersistedTableMetadata(table, options).enums;
1691
+ };
1692
+ const applyMappedColumn = (tableColumns, column, features, table) => {
1693
+ if (typeof column.map === "string" && column.map.trim().length > 0 && column.map !== column.name) {
1694
+ if (!features.persistedColumnMappings) throw buildPersistedFeatureDisabledError("persistedColumnMappings", table);
1695
+ tableColumns[column.name] = column.map;
1696
+ return;
1697
+ }
1698
+ delete tableColumns[column.name];
1699
+ };
1700
+ const applyEnumColumn = (tableEnums, column, features, table) => {
1701
+ const values = column.enumValues ?? [];
1702
+ if (column.type === "enum" && values.length > 0) {
1703
+ if (!features.persistedEnums) throw buildPersistedFeatureDisabledError("persistedEnums", table);
1704
+ tableEnums[column.name] = [...values];
1705
+ return;
1706
+ }
1707
+ delete tableEnums[column.name];
1708
+ };
1709
+ const removePersistedColumnMetadata = (tableMetadata, columnName) => {
1710
+ delete tableMetadata.columns[columnName];
1711
+ delete tableMetadata.enums[columnName];
1712
+ Object.entries(tableMetadata.columns).forEach(([attribute, mappedColumn]) => {
1713
+ if (mappedColumn === columnName) delete tableMetadata.columns[attribute];
1714
+ });
1715
+ };
1716
+ const applyOperationsToPersistedColumnMappingsState = (state, operations, features = resolvePersistedMetadataFeatures()) => {
1717
+ const nextTables = Object.entries(state.tables).reduce((all, [table, metadata]) => {
1718
+ all[table] = {
1719
+ columns: { ...metadata.columns },
1720
+ enums: Object.entries(metadata.enums).reduce((nextEnums, [columnName, values]) => {
1721
+ nextEnums[columnName] = [...values];
1722
+ return nextEnums;
1723
+ }, {})
1724
+ };
1725
+ return all;
1726
+ }, {});
1727
+ operations.forEach((operation) => {
1728
+ if (operation.type === "createTable") {
1729
+ const tableMetadata = nextTables[operation.table] ?? {
1730
+ columns: {},
1731
+ enums: {}
1732
+ };
1733
+ operation.columns.forEach((column) => {
1734
+ applyMappedColumn(tableMetadata.columns, column, features, operation.table);
1735
+ applyEnumColumn(tableMetadata.enums, column, features, operation.table);
1736
+ });
1737
+ if (Object.keys(tableMetadata.columns).length > 0 || Object.keys(tableMetadata.enums).length > 0) nextTables[operation.table] = tableMetadata;
1738
+ else delete nextTables[operation.table];
1739
+ return;
1740
+ }
1741
+ if (operation.type === "alterTable") {
1742
+ const tableMetadata = nextTables[operation.table] ?? {
1743
+ columns: {},
1744
+ enums: {}
1745
+ };
1746
+ operation.addColumns.forEach((column) => {
1747
+ applyMappedColumn(tableMetadata.columns, column, features, operation.table);
1748
+ applyEnumColumn(tableMetadata.enums, column, features, operation.table);
1749
+ });
1750
+ operation.dropColumns.forEach((columnName) => {
1751
+ removePersistedColumnMetadata(tableMetadata, columnName);
1752
+ });
1753
+ if (Object.keys(tableMetadata.columns).length > 0 || Object.keys(tableMetadata.enums).length > 0) nextTables[operation.table] = tableMetadata;
1754
+ else delete nextTables[operation.table];
1755
+ return;
1756
+ }
1757
+ delete nextTables[operation.table];
1758
+ });
1759
+ return {
1760
+ version: 1,
1761
+ tables: nextTables
1762
+ };
1763
+ };
1764
+ const rebuildPersistedColumnMappingsState = async (state, availableMigrations, features = resolvePersistedMetadataFeatures()) => {
1765
+ const availableByIdentity = new Map(availableMigrations.map(([migrationClass, file]) => [buildMigrationIdentity(file, migrationClass.name), migrationClass]));
1766
+ let nextState = createEmptyPersistedColumnMappingsState();
1767
+ const orderedMigrations = state.migrations.map((migration, index) => ({
1768
+ migration,
1769
+ index
1770
+ })).sort((left, right) => {
1771
+ const appliedAtOrder = left.migration.appliedAt.localeCompare(right.migration.appliedAt);
1772
+ if (appliedAtOrder !== 0) return appliedAtOrder;
1773
+ return left.index - right.index;
1774
+ });
1775
+ for (const { migration } of orderedMigrations) {
1776
+ const migrationClass = availableByIdentity.get(migration.id);
1777
+ if (!migrationClass) throw new ArkormException(`Unable to rebuild persisted column mappings because migration [${migration.id}] could not be resolved from the current migration files.`, {
1778
+ operation: "migration.columnMappings",
1779
+ meta: {
1780
+ migrationId: migration.id,
1781
+ file: migration.file,
1782
+ className: migration.className
1783
+ }
1784
+ });
1785
+ const operations = await getMigrationPlan(migrationClass, "up");
1786
+ nextState = applyOperationsToPersistedColumnMappingsState(nextState, operations, features);
1787
+ }
1788
+ return nextState;
1789
+ };
1790
+ const syncPersistedColumnMappingsFromState = async (cwd, state, availableMigrations, features = resolvePersistedMetadataFeatures()) => {
1791
+ const filePath = resolveColumnMappingsFilePath(cwd);
1792
+ const nextState = await rebuildPersistedColumnMappingsState(state, availableMigrations, features);
1793
+ if (Object.keys(nextState.tables).length === 0) {
1794
+ deletePersistedColumnMappingsState(filePath);
1795
+ return;
1796
+ }
1797
+ writePersistedColumnMappingsState(filePath, nextState);
1798
+ };
1799
+ const validatePersistedMetadataFeaturesForMigrations = async (migrations, features = resolvePersistedMetadataFeatures()) => {
1800
+ let nextState = createEmptyPersistedColumnMappingsState();
1801
+ for (const [migrationClass] of migrations) {
1802
+ const operations = await getMigrationPlan(migrationClass, "up");
1803
+ nextState = applyOperationsToPersistedColumnMappingsState(nextState, operations, features);
1804
+ }
1805
+ };
1806
+ const getPersistedEnumTsType = (values) => {
1807
+ return buildEnumUnionType(values);
1808
+ };
1809
+
1439
1810
  //#endregion
1440
1811
  //#region src/helpers/runtime-module-loader.ts
1441
1812
  var RuntimeModuleLoader = class {
@@ -1463,6 +1834,10 @@ const resolveDefaultStubsPath = () => {
1463
1834
  return path.join(process.cwd(), "stubs");
1464
1835
  };
1465
1836
  const baseConfig = {
1837
+ features: {
1838
+ persistedColumnMappings: true,
1839
+ persistedEnums: true
1840
+ },
1466
1841
  paths: {
1467
1842
  stubs: resolveDefaultStubsPath(),
1468
1843
  seeders: path.join(process.cwd(), "database", "seeders"),
@@ -1475,6 +1850,7 @@ const baseConfig = {
1475
1850
  };
1476
1851
  const userConfig = {
1477
1852
  ...baseConfig,
1853
+ features: { ...baseConfig.features ?? {} },
1478
1854
  paths: { ...baseConfig.paths ?? {} }
1479
1855
  };
1480
1856
  let runtimeConfigLoaded = false;
@@ -1497,6 +1873,15 @@ const mergePathConfig = (paths) => {
1497
1873
  ...incoming
1498
1874
  };
1499
1875
  };
1876
+ const mergeFeatureConfig = (features) => {
1877
+ const defaults = baseConfig.features ?? {};
1878
+ const current = userConfig.features ?? {};
1879
+ return {
1880
+ ...defaults,
1881
+ ...current,
1882
+ ...features ?? {}
1883
+ };
1884
+ };
1500
1885
  const bindAdapterToModels = (adapter, models) => {
1501
1886
  models.forEach((model) => {
1502
1887
  model.setAdapter(adapter);
@@ -1522,6 +1907,7 @@ const getUserConfig = (key) => {
1522
1907
  const configureArkormRuntime = (prisma, options = {}) => {
1523
1908
  const nextConfig = {
1524
1909
  ...userConfig,
1910
+ features: mergeFeatureConfig(options.features),
1525
1911
  paths: mergePathConfig(options.paths)
1526
1912
  };
1527
1913
  nextConfig.prisma = prisma;
@@ -1566,6 +1952,7 @@ const resolveAndApplyConfig = (imported) => {
1566
1952
  configureArkormRuntime(config.prisma, {
1567
1953
  adapter: config.adapter,
1568
1954
  boot: config.boot,
1955
+ features: config.features,
1569
1956
  pagination: config.pagination,
1570
1957
  paths: config.paths,
1571
1958
  outputExt: config.outputExt
@@ -2157,6 +2544,26 @@ var CliApp = class {
2157
2544
  skipped
2158
2545
  };
2159
2546
  }
2547
+ applyPersistedEnumMetadata(structure) {
2548
+ const persistedEnums = getPersistedEnumMap(structure.table, {
2549
+ features: resolvePersistedMetadataFeatures(this.getConfig("features")),
2550
+ strict: true
2551
+ });
2552
+ if (Object.keys(persistedEnums).length === 0) return structure;
2553
+ return {
2554
+ ...structure,
2555
+ fields: structure.fields.map((field) => {
2556
+ const enumValues = persistedEnums[field.name];
2557
+ if (!enumValues || enumValues.length === 0) return field;
2558
+ const enumType = getPersistedEnumTsType(enumValues);
2559
+ const isArray = /^Array<.+>$/.test(field.type);
2560
+ return {
2561
+ ...field,
2562
+ type: isArray ? `Array<${enumType}>` : enumType
2563
+ };
2564
+ })
2565
+ };
2566
+ }
2160
2567
  /**
2161
2568
  * Parse Prisma enum definitions from a schema and return their member names.
2162
2569
  *
@@ -2315,7 +2722,10 @@ var CliApp = class {
2315
2722
  return all;
2316
2723
  }, /* @__PURE__ */ new Map());
2317
2724
  const discovered = await adapter.introspectModels({ tables: [...new Set([...sources.values()].map((source) => source.table))] });
2318
- const structuresByTable = new Map(discovered.map((model) => [model.table, model]));
2725
+ const structuresByTable = new Map(discovered.map((model) => {
2726
+ const enriched = this.applyPersistedEnumMetadata(model);
2727
+ return [enriched.table, enriched];
2728
+ }));
2319
2729
  const result = this.syncModelFiles(modelFiles, (filePath) => {
2320
2730
  const parsed = sources.get(filePath);
2321
2731
  return parsed ? structuresByTable.get(parsed.table) : void 0;
@@ -2529,126 +2939,6 @@ var MakeSeederCommand = class extends Command {
2529
2939
  }
2530
2940
  };
2531
2941
 
2532
- //#endregion
2533
- //#region src/helpers/migration-history.ts
2534
- const createEmptyAppliedMigrationsState = () => ({
2535
- version: 1,
2536
- migrations: [],
2537
- runs: []
2538
- });
2539
- const supportsDatabaseMigrationState = (adapter) => {
2540
- return typeof adapter?.readAppliedMigrationsState === "function" && typeof adapter?.writeAppliedMigrationsState === "function";
2541
- };
2542
- const resolveMigrationStateFilePath = (cwd, configuredPath) => {
2543
- if (configuredPath && configuredPath.trim().length > 0) return resolve(configuredPath);
2544
- return join(cwd, ".arkormx", "migrations.applied.json");
2545
- };
2546
- const buildMigrationIdentity = (filePath, className) => {
2547
- const fileName = filePath.split("/").pop()?.split("\\").pop() ?? filePath;
2548
- return `${fileName.slice(0, fileName.length - extname(fileName).length)}:${className}`;
2549
- };
2550
- const computeMigrationChecksum = (filePath) => {
2551
- const source = readFileSync(filePath, "utf-8");
2552
- return createHash("sha256").update(source).digest("hex");
2553
- };
2554
- const readAppliedMigrationsState = (stateFilePath) => {
2555
- if (!existsSync(stateFilePath)) return createEmptyAppliedMigrationsState();
2556
- try {
2557
- const parsed = JSON.parse(readFileSync(stateFilePath, "utf-8"));
2558
- if (!Array.isArray(parsed.migrations)) return createEmptyAppliedMigrationsState();
2559
- return {
2560
- version: 1,
2561
- migrations: parsed.migrations.filter((migration) => {
2562
- return typeof migration?.id === "string" && typeof migration?.file === "string" && typeof migration?.className === "string" && typeof migration?.appliedAt === "string" && (migration?.checksum === void 0 || typeof migration?.checksum === "string");
2563
- }),
2564
- runs: Array.isArray(parsed.runs) ? parsed.runs.filter((run) => {
2565
- return typeof run?.id === "string" && typeof run?.appliedAt === "string" && Array.isArray(run?.migrationIds) && run.migrationIds.every((item) => typeof item === "string");
2566
- }) : []
2567
- };
2568
- } catch {
2569
- return createEmptyAppliedMigrationsState();
2570
- }
2571
- };
2572
- const readAppliedMigrationsStateFromStore = async (adapter, stateFilePath) => {
2573
- if (supportsDatabaseMigrationState(adapter)) return await adapter.readAppliedMigrationsState();
2574
- return readAppliedMigrationsState(stateFilePath);
2575
- };
2576
- const writeAppliedMigrationsState = (stateFilePath, state) => {
2577
- const directory = dirname(stateFilePath);
2578
- if (!existsSync(directory)) mkdirSync(directory, { recursive: true });
2579
- writeFileSync(stateFilePath, JSON.stringify(state, null, 2));
2580
- };
2581
- const writeAppliedMigrationsStateToStore = async (adapter, stateFilePath, state) => {
2582
- if (supportsDatabaseMigrationState(adapter)) {
2583
- await adapter.writeAppliedMigrationsState(state);
2584
- return;
2585
- }
2586
- writeAppliedMigrationsState(stateFilePath, state);
2587
- };
2588
- const isMigrationApplied = (state, identity, checksum) => {
2589
- const matched = state.migrations.find((migration) => migration.id === identity);
2590
- if (!matched) return false;
2591
- if (checksum && matched.checksum) return matched.checksum === checksum;
2592
- if (checksum && !matched.checksum) return false;
2593
- return true;
2594
- };
2595
- const findAppliedMigration = (state, identity) => {
2596
- return state.migrations.find((migration) => migration.id === identity);
2597
- };
2598
- const markMigrationApplied = (state, entry) => {
2599
- const next = state.migrations.filter((migration) => migration.id !== entry.id);
2600
- next.push(entry);
2601
- return {
2602
- version: 1,
2603
- migrations: next,
2604
- runs: state.runs ?? []
2605
- };
2606
- };
2607
- const removeAppliedMigration = (state, identity) => {
2608
- return {
2609
- version: 1,
2610
- migrations: state.migrations.filter((migration) => migration.id !== identity),
2611
- runs: (state.runs ?? []).map((run) => ({
2612
- ...run,
2613
- migrationIds: run.migrationIds.filter((id) => id !== identity)
2614
- })).filter((run) => run.migrationIds.length > 0)
2615
- };
2616
- };
2617
- const buildMigrationRunId = () => {
2618
- return `run_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
2619
- };
2620
- const markMigrationRun = (state, run) => {
2621
- const nextRuns = (state.runs ?? []).filter((existing) => existing.id !== run.id);
2622
- nextRuns.push(run);
2623
- return {
2624
- version: 1,
2625
- migrations: state.migrations,
2626
- runs: nextRuns
2627
- };
2628
- };
2629
- const getLastMigrationRun = (state) => {
2630
- const runs = state.runs ?? [];
2631
- if (runs.length === 0) return void 0;
2632
- return runs.map((run, index) => ({
2633
- run,
2634
- index
2635
- })).sort((left, right) => {
2636
- const appliedAtOrder = right.run.appliedAt.localeCompare(left.run.appliedAt);
2637
- if (appliedAtOrder !== 0) return appliedAtOrder;
2638
- return right.index - left.index;
2639
- })[0]?.run;
2640
- };
2641
- const getLatestAppliedMigrations = (state, steps) => {
2642
- return state.migrations.map((migration, index) => ({
2643
- migration,
2644
- index
2645
- })).sort((left, right) => {
2646
- const appliedAtOrder = right.migration.appliedAt.localeCompare(left.migration.appliedAt);
2647
- if (appliedAtOrder !== 0) return appliedAtOrder;
2648
- return right.index - left.index;
2649
- }).slice(0, Math.max(0, steps)).map((entry) => entry.migration);
2650
- };
2651
-
2652
2942
  //#endregion
2653
2943
  //#region src/database/Migration.ts
2654
2944
  const MIGRATION_BRAND = Symbol.for("arkormx.migration");
@@ -2706,6 +2996,7 @@ var MigrateCommand = class extends Command {
2706
2996
  let appliedState = await readAppliedMigrationsStateFromStore(this.app.getConfig("adapter"), stateFilePath);
2707
2997
  const adapter = this.app.getConfig("adapter");
2708
2998
  const useDatabaseMigrations = supportsDatabaseMigrationExecution(adapter);
2999
+ const persistedFeatures = resolvePersistedMetadataFeatures(this.app.getConfig("features"));
2709
3000
  const skipped = [];
2710
3001
  const changed = [];
2711
3002
  const pending = classes.filter(([migrationClass, file]) => {
@@ -2724,9 +3015,21 @@ var MigrateCommand = class extends Command {
2724
3015
  this.success(this.app.splitLogger("Changed", `${file} (${migrationClass.name})`));
2725
3016
  });
2726
3017
  if (pending.length === 0) {
3018
+ if (appliedState) try {
3019
+ await syncPersistedColumnMappingsFromState(process.cwd(), appliedState, await this.loadAllMigrations(migrationsDir), persistedFeatures);
3020
+ } catch (error) {
3021
+ this.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
3022
+ return;
3023
+ }
2727
3024
  this.success("No pending migration classes to apply.");
2728
3025
  return;
2729
3026
  }
3027
+ if (useDatabaseMigrations) try {
3028
+ await validatePersistedMetadataFeaturesForMigrations(pending, persistedFeatures);
3029
+ } catch (error) {
3030
+ this.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
3031
+ return;
3032
+ }
2730
3033
  for (const [MigrationClassItem] of pending) {
2731
3034
  if (useDatabaseMigrations) {
2732
3035
  await applyMigrationToDatabase(adapter, MigrationClassItem);
@@ -2756,6 +3059,12 @@ var MigrateCommand = class extends Command {
2756
3059
  migrationIds: runAppliedIds
2757
3060
  });
2758
3061
  await writeAppliedMigrationsStateToStore(adapter, stateFilePath, appliedState);
3062
+ try {
3063
+ await syncPersistedColumnMappingsFromState(process.cwd(), appliedState, await this.loadAllMigrations(migrationsDir), persistedFeatures);
3064
+ } catch (error) {
3065
+ this.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
3066
+ return;
3067
+ }
2759
3068
  }
2760
3069
  if (!useDatabaseMigrations && !this.option("skip-generate")) runPrismaCommand(["generate"], process.cwd());
2761
3070
  if (!useDatabaseMigrations && !this.option("skip-migrate")) if (this.option("deploy")) runPrismaCommand(["migrate", "deploy"], process.cwd());
@@ -2835,12 +3144,24 @@ var MigrateFreshCommand = class extends Command {
2835
3144
  if (!existsSync(migrationsDir)) return void this.error(`Error: Migrations directory not found: ${this.app.formatPathForLog(configuredMigrationsDir)}`);
2836
3145
  const adapter = this.app.getConfig("adapter");
2837
3146
  const useDatabaseMigrations = supportsDatabaseMigrationExecution(adapter);
3147
+ const persistedFeatures = resolvePersistedMetadataFeatures(this.app.getConfig("features"));
2838
3148
  const schemaPath = this.option("schema") ? resolve(String(this.option("schema"))) : join(process.cwd(), "prisma", "schema.prisma");
2839
3149
  const stateFilePath = resolveMigrationStateFilePath(process.cwd(), this.option("state-file") ? String(this.option("state-file")) : void 0);
2840
3150
  const migrations = await this.loadAllMigrations(migrationsDir);
2841
3151
  if (migrations.length === 0) return void this.error("Error: No migration classes found to run.");
2842
- if (supportsDatabaseReset(adapter)) await adapter.resetDatabase();
2843
- else {
3152
+ if (useDatabaseMigrations) try {
3153
+ await validatePersistedMetadataFeaturesForMigrations(migrations, persistedFeatures);
3154
+ } catch (error) {
3155
+ this.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
3156
+ return;
3157
+ }
3158
+ if (useDatabaseMigrations) {
3159
+ if (!supportsDatabaseReset(adapter)) {
3160
+ this.error("Error: Your current database adapter does not support database reset.");
3161
+ return;
3162
+ }
3163
+ await adapter.resetDatabase();
3164
+ } else {
2844
3165
  if (!existsSync(schemaPath)) return void this.error(`Error: Prisma schema file not found: ${this.app.formatPathForLog(schemaPath)}`);
2845
3166
  writeFileSync(schemaPath, stripPrismaSchemaModelsAndEnums(readFileSync(schemaPath, "utf-8")));
2846
3167
  }
@@ -2869,6 +3190,12 @@ var MigrateFreshCommand = class extends Command {
2869
3190
  migrationIds: appliedState.migrations.map((migration) => migration.id)
2870
3191
  });
2871
3192
  await writeAppliedMigrationsStateToStore(adapter, stateFilePath, appliedState);
3193
+ try {
3194
+ await syncPersistedColumnMappingsFromState(process.cwd(), appliedState, migrations, persistedFeatures);
3195
+ } catch (error) {
3196
+ this.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
3197
+ return;
3198
+ }
2872
3199
  if (!useDatabaseMigrations) {
2873
3200
  const schemaArgs = this.option("schema") ? ["--schema", schemaPath] : [];
2874
3201
  if (!this.option("skip-generate")) runPrismaCommand(["generate", ...schemaArgs], process.cwd());
@@ -2927,6 +3254,7 @@ var MigrateRollbackCommand = class extends Command {
2927
3254
  const stateFilePath = resolveMigrationStateFilePath(process.cwd(), this.option("state-file") ? String(this.option("state-file")) : void 0);
2928
3255
  const adapter = this.app.getConfig("adapter");
2929
3256
  const useDatabaseMigrations = supportsDatabaseMigrationExecution(adapter);
3257
+ const persistedFeatures = resolvePersistedMetadataFeatures(this.app.getConfig("features"));
2930
3258
  let appliedState = await readAppliedMigrationsStateFromStore(adapter, stateFilePath);
2931
3259
  const stepOption = this.option("step");
2932
3260
  const stepCount = stepOption == null ? void 0 : Number(stepOption);
@@ -2964,6 +3292,12 @@ var MigrateRollbackCommand = class extends Command {
2964
3292
  appliedState = removeAppliedMigration(appliedState, identity);
2965
3293
  }
2966
3294
  await writeAppliedMigrationsStateToStore(adapter, stateFilePath, appliedState);
3295
+ try {
3296
+ await syncPersistedColumnMappingsFromState(process.cwd(), appliedState, available, persistedFeatures);
3297
+ } catch (error) {
3298
+ this.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
3299
+ return;
3300
+ }
2967
3301
  if (!useDatabaseMigrations && !this.option("skip-generate")) runPrismaCommand(["generate"], process.cwd());
2968
3302
  if (!useDatabaseMigrations && !this.option("skip-migrate")) if (this.option("deploy")) runPrismaCommand(["migrate", "deploy"], process.cwd());
2969
3303
  else runPrismaCommand([
@@ -3014,6 +3348,7 @@ var MigrationHistoryCommand = class extends Command {
3014
3348
  if (this.option("delete")) {
3015
3349
  if (usesDatabaseState) {
3016
3350
  await adapter.writeAppliedMigrationsState(createEmptyAppliedMigrationsState());
3351
+ deletePersistedColumnMappingsState(resolveColumnMappingsFilePath(process.cwd()));
3017
3352
  this.success("Deleted tracked migration state from database.");
3018
3353
  return;
3019
3354
  }
@@ -3022,11 +3357,13 @@ var MigrationHistoryCommand = class extends Command {
3022
3357
  return;
3023
3358
  }
3024
3359
  rmSync(stateFilePath);
3360
+ deletePersistedColumnMappingsState(resolveColumnMappingsFilePath(process.cwd()));
3025
3361
  this.success(`Deleted migration state file: ${this.app.formatPathForLog(stateFilePath)}`);
3026
3362
  return;
3027
3363
  }
3028
3364
  if (this.option("reset")) {
3029
3365
  await writeAppliedMigrationsStateToStore(adapter, stateFilePath, createEmptyAppliedMigrationsState());
3366
+ deletePersistedColumnMappingsState(resolveColumnMappingsFilePath(process.cwd()));
3030
3367
  this.success(usesDatabaseState ? "Reset migration state in database." : `Reset migration state: ${this.app.formatPathForLog(stateFilePath)}`);
3031
3368
  return;
3032
3369
  }
@@ -3060,10 +3397,16 @@ var ModelsSyncCommand = class extends Command {
3060
3397
  description = "Sync model declare attributes from the active adapter when supported, otherwise fall back to the Prisma schema";
3061
3398
  async handle() {
3062
3399
  this.app.command = this;
3063
- const result = await this.app.syncModels({
3064
- schemaPath: this.option("schema") ? resolve(String(this.option("schema"))) : void 0,
3065
- modelsDir: this.option("models") ? resolve(String(this.option("models"))) : void 0
3066
- });
3400
+ let result;
3401
+ try {
3402
+ result = await this.app.syncModels({
3403
+ schemaPath: this.option("schema") ? resolve(String(this.option("schema"))) : void 0,
3404
+ modelsDir: this.option("models") ? resolve(String(this.option("models"))) : void 0
3405
+ });
3406
+ } catch (error) {
3407
+ this.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
3408
+ return;
3409
+ }
3067
3410
  const updatedLines = result.updated.length === 0 ? [this.app.splitLogger("Updated", "none")] : result.updated.map((path) => this.app.splitLogger("Updated", path));
3068
3411
  this.success("SUCCESS: Model sync completed with the following results:");
3069
3412
  [