arkormx 2.0.0-next.2 → 2.0.0-next.21
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 +1287 -155
- package/dist/index.cjs +7584 -5685
- package/dist/index.d.cts +620 -105
- package/dist/index.d.mts +620 -105
- package/dist/index.mjs +7573 -5708
- package/package.json +1 -1
- package/stubs/model.js.stub +1 -1
- package/stubs/model.stub +2 -2
- package/stubs/pivot-model.stub +4 -0
package/dist/cli.mjs
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
3
3
|
import { dirname, extname, join, resolve } from "node:path";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
6
|
import { spawnSync } from "node:child_process";
|
|
5
7
|
import { str } from "@h3ravel/support";
|
|
6
|
-
import path, { dirname as dirname$1, extname as extname$1, join as join$1, relative } from "path";
|
|
7
|
-
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
|
-
import { AsyncLocalStorage } from "async_hooks";
|
|
9
8
|
import { createJiti } from "@rexxars/jiti";
|
|
10
9
|
import { pathToFileURL } from "node:url";
|
|
11
10
|
import { createRequire } from "module";
|
|
11
|
+
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";
|
|
12
12
|
import { fileURLToPath } from "url";
|
|
13
|
+
import path, { dirname as dirname$1, extname as extname$1, join as join$1, relative } from "path";
|
|
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 {
|
|
@@ -55,6 +55,188 @@ var ArkormException = class extends Error {
|
|
|
55
55
|
}
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region src/Exceptions/MissingDelegateException.ts
|
|
60
|
+
var MissingDelegateException = class extends ArkormException {
|
|
61
|
+
constructor(message, context = {}) {
|
|
62
|
+
super(message, {
|
|
63
|
+
code: "MISSING_DELEGATE",
|
|
64
|
+
...context
|
|
65
|
+
});
|
|
66
|
+
this.name = "MissingDelegateException";
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region src/Exceptions/QueryExecutionException.ts
|
|
72
|
+
var QueryExecutionException = class extends ArkormException {
|
|
73
|
+
inspection;
|
|
74
|
+
constructor(message = "Database query execution failed.", context = {}) {
|
|
75
|
+
super(message, {
|
|
76
|
+
code: "QUERY_EXECUTION_FAILED",
|
|
77
|
+
...context,
|
|
78
|
+
meta: {
|
|
79
|
+
...context.meta ?? {},
|
|
80
|
+
...context.inspection ? { inspection: context.inspection } : {}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
this.name = "QueryExecutionException";
|
|
84
|
+
this.inspection = context.inspection;
|
|
85
|
+
}
|
|
86
|
+
getInspection() {
|
|
87
|
+
return this.inspection;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/Exceptions/UnsupportedAdapterFeatureException.ts
|
|
93
|
+
var UnsupportedAdapterFeatureException = class extends ArkormException {
|
|
94
|
+
constructor(message, context = {}) {
|
|
95
|
+
super(message, {
|
|
96
|
+
code: "UNSUPPORTED_ADAPTER_FEATURE",
|
|
97
|
+
...context
|
|
98
|
+
});
|
|
99
|
+
this.name = "UnsupportedAdapterFeatureException";
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region src/helpers/migration-history.ts
|
|
105
|
+
const createEmptyAppliedMigrationsState = () => ({
|
|
106
|
+
version: 1,
|
|
107
|
+
migrations: [],
|
|
108
|
+
runs: []
|
|
109
|
+
});
|
|
110
|
+
const supportsDatabaseMigrationState = (adapter) => {
|
|
111
|
+
return typeof adapter?.readAppliedMigrationsState === "function" && typeof adapter?.writeAppliedMigrationsState === "function";
|
|
112
|
+
};
|
|
113
|
+
const resolveMigrationStateFilePath = (cwd, configuredPath) => {
|
|
114
|
+
if (configuredPath && configuredPath.trim().length > 0) return resolve(configuredPath);
|
|
115
|
+
return join(cwd, ".arkormx", "migrations.applied.json");
|
|
116
|
+
};
|
|
117
|
+
const buildMigrationIdentity = (filePath, className) => {
|
|
118
|
+
const fileName = filePath.split("/").pop()?.split("\\").pop() ?? filePath;
|
|
119
|
+
return `${fileName.slice(0, fileName.length - extname(fileName).length)}:${className}`;
|
|
120
|
+
};
|
|
121
|
+
const computeMigrationChecksum = (filePath) => {
|
|
122
|
+
const source = readFileSync(filePath, "utf-8");
|
|
123
|
+
return createHash("sha256").update(source).digest("hex");
|
|
124
|
+
};
|
|
125
|
+
const readAppliedMigrationsState = (stateFilePath) => {
|
|
126
|
+
if (!existsSync(stateFilePath)) return createEmptyAppliedMigrationsState();
|
|
127
|
+
try {
|
|
128
|
+
const parsed = JSON.parse(readFileSync(stateFilePath, "utf-8"));
|
|
129
|
+
if (!Array.isArray(parsed.migrations)) return createEmptyAppliedMigrationsState();
|
|
130
|
+
return {
|
|
131
|
+
version: 1,
|
|
132
|
+
migrations: parsed.migrations.filter((migration) => {
|
|
133
|
+
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");
|
|
134
|
+
}),
|
|
135
|
+
runs: Array.isArray(parsed.runs) ? parsed.runs.filter((run) => {
|
|
136
|
+
return typeof run?.id === "string" && typeof run?.appliedAt === "string" && Array.isArray(run?.migrationIds) && run.migrationIds.every((item) => typeof item === "string");
|
|
137
|
+
}) : []
|
|
138
|
+
};
|
|
139
|
+
} catch {
|
|
140
|
+
return createEmptyAppliedMigrationsState();
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
const readAppliedMigrationsStateFromStore = async (adapter, stateFilePath) => {
|
|
144
|
+
if (supportsDatabaseMigrationState(adapter)) return await adapter.readAppliedMigrationsState();
|
|
145
|
+
return readAppliedMigrationsState(stateFilePath);
|
|
146
|
+
};
|
|
147
|
+
const writeAppliedMigrationsState = (stateFilePath, state) => {
|
|
148
|
+
const directory = dirname(stateFilePath);
|
|
149
|
+
if (!existsSync(directory)) mkdirSync(directory, { recursive: true });
|
|
150
|
+
writeFileSync(stateFilePath, JSON.stringify(state, null, 2));
|
|
151
|
+
};
|
|
152
|
+
const writeAppliedMigrationsStateToStore = async (adapter, stateFilePath, state) => {
|
|
153
|
+
if (supportsDatabaseMigrationState(adapter)) {
|
|
154
|
+
await adapter.writeAppliedMigrationsState(state);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
writeAppliedMigrationsState(stateFilePath, state);
|
|
158
|
+
};
|
|
159
|
+
const isMigrationApplied = (state, identity, checksum) => {
|
|
160
|
+
const matched = state.migrations.find((migration) => migration.id === identity);
|
|
161
|
+
if (!matched) return false;
|
|
162
|
+
if (checksum && matched.checksum) return matched.checksum === checksum;
|
|
163
|
+
if (checksum && !matched.checksum) return false;
|
|
164
|
+
return true;
|
|
165
|
+
};
|
|
166
|
+
const findAppliedMigration = (state, identity) => {
|
|
167
|
+
return state.migrations.find((migration) => migration.id === identity);
|
|
168
|
+
};
|
|
169
|
+
const markMigrationApplied = (state, entry) => {
|
|
170
|
+
const next = state.migrations.filter((migration) => migration.id !== entry.id);
|
|
171
|
+
next.push(entry);
|
|
172
|
+
return {
|
|
173
|
+
version: 1,
|
|
174
|
+
migrations: next,
|
|
175
|
+
runs: state.runs ?? []
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
const removeAppliedMigration = (state, identity) => {
|
|
179
|
+
return {
|
|
180
|
+
version: 1,
|
|
181
|
+
migrations: state.migrations.filter((migration) => migration.id !== identity),
|
|
182
|
+
runs: (state.runs ?? []).map((run) => ({
|
|
183
|
+
...run,
|
|
184
|
+
migrationIds: run.migrationIds.filter((id) => id !== identity)
|
|
185
|
+
})).filter((run) => run.migrationIds.length > 0)
|
|
186
|
+
};
|
|
187
|
+
};
|
|
188
|
+
const buildMigrationRunId = () => {
|
|
189
|
+
return `run_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
190
|
+
};
|
|
191
|
+
const markMigrationRun = (state, run) => {
|
|
192
|
+
const nextRuns = (state.runs ?? []).filter((existing) => existing.id !== run.id);
|
|
193
|
+
nextRuns.push(run);
|
|
194
|
+
return {
|
|
195
|
+
version: 1,
|
|
196
|
+
migrations: state.migrations,
|
|
197
|
+
runs: nextRuns
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
const getLastMigrationRun = (state) => {
|
|
201
|
+
const runs = state.runs ?? [];
|
|
202
|
+
if (runs.length === 0) return void 0;
|
|
203
|
+
return runs.map((run, index) => ({
|
|
204
|
+
run,
|
|
205
|
+
index
|
|
206
|
+
})).sort((left, right) => {
|
|
207
|
+
const appliedAtOrder = right.run.appliedAt.localeCompare(left.run.appliedAt);
|
|
208
|
+
if (appliedAtOrder !== 0) return appliedAtOrder;
|
|
209
|
+
return right.index - left.index;
|
|
210
|
+
})[0]?.run;
|
|
211
|
+
};
|
|
212
|
+
const getLatestAppliedMigrations = (state, steps) => {
|
|
213
|
+
return state.migrations.map((migration, index) => ({
|
|
214
|
+
migration,
|
|
215
|
+
index
|
|
216
|
+
})).sort((left, right) => {
|
|
217
|
+
const appliedAtOrder = right.migration.appliedAt.localeCompare(left.migration.appliedAt);
|
|
218
|
+
if (appliedAtOrder !== 0) return appliedAtOrder;
|
|
219
|
+
return right.index - left.index;
|
|
220
|
+
}).slice(0, Math.max(0, steps)).map((entry) => entry.migration);
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
//#endregion
|
|
224
|
+
//#region src/helpers/PrimaryKeyGenerationPlanner.ts
|
|
225
|
+
var PrimaryKeyGenerationPlanner = class {
|
|
226
|
+
static plan(column) {
|
|
227
|
+
if (!column.primary || column.default !== void 0) return void 0;
|
|
228
|
+
if (column.type === "uuid" || column.type === "string") return {
|
|
229
|
+
strategy: "uuid",
|
|
230
|
+
prismaDefault: "@default(uuid())",
|
|
231
|
+
databaseDefault: column.type === "uuid" ? "gen_random_uuid()" : "gen_random_uuid()::text",
|
|
232
|
+
runtimeFactory: "uuid"
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
static generate(generation) {
|
|
236
|
+
if (generation?.runtimeFactory === "uuid") return randomUUID();
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
|
|
58
240
|
//#endregion
|
|
59
241
|
//#region src/database/ForeignKeyBuilder.ts
|
|
60
242
|
/**
|
|
@@ -248,6 +430,7 @@ var TableBuilder = class {
|
|
|
248
430
|
column.primary = true;
|
|
249
431
|
if (typeof config.autoIncrement === "boolean") column.autoIncrement = config.autoIncrement;
|
|
250
432
|
if (Object.prototype.hasOwnProperty.call(config, "default")) column.default = config.default;
|
|
433
|
+
column.primaryKeyGeneration = PrimaryKeyGenerationPlanner.plan(column);
|
|
251
434
|
return this;
|
|
252
435
|
}
|
|
253
436
|
/**
|
|
@@ -611,8 +794,11 @@ var TableBuilder = class {
|
|
|
611
794
|
autoIncrement: options.autoIncrement,
|
|
612
795
|
after: options.after,
|
|
613
796
|
default: options.default,
|
|
614
|
-
updatedAt: options.updatedAt
|
|
797
|
+
updatedAt: options.updatedAt,
|
|
798
|
+
primaryKeyGeneration: options.primaryKeyGeneration
|
|
615
799
|
});
|
|
800
|
+
const column = this.columns[this.columns.length - 1];
|
|
801
|
+
column.primaryKeyGeneration = PrimaryKeyGenerationPlanner.plan(column);
|
|
616
802
|
this.latestColumnName = name;
|
|
617
803
|
return this;
|
|
618
804
|
}
|
|
@@ -867,7 +1053,7 @@ const buildFieldLine = (column) => {
|
|
|
867
1053
|
const primary = column.primary ? " @id" : "";
|
|
868
1054
|
const mapped = typeof column.map === "string" && column.map.trim().length > 0 ? ` @map("${column.map.replace(/"/g, "\\\"")}")` : "";
|
|
869
1055
|
const updatedAt = column.updatedAt ? " @updatedAt" : "";
|
|
870
|
-
const defaultValue = column.type === "enum" ? formatEnumDefaultValue(column.default) :
|
|
1056
|
+
const defaultValue = column.type === "enum" ? formatEnumDefaultValue(column.default) : column.primaryKeyGeneration?.prismaDefault ?? formatDefaultValue(column.default);
|
|
871
1057
|
const defaultSuffix = defaultValue ? ` ${defaultValue}` : "";
|
|
872
1058
|
return ` ${column.name} ${scalar}${nullable}${primary}${unique}${defaultSuffix}${updatedAt}${mapped}`;
|
|
873
1059
|
};
|
|
@@ -1371,6 +1557,28 @@ const getMigrationPlan = async (migration, direction = "up") => {
|
|
|
1371
1557
|
else await instance.down(schema);
|
|
1372
1558
|
return schema.getOperations();
|
|
1373
1559
|
};
|
|
1560
|
+
const supportsDatabaseMigrationExecution = (adapter) => {
|
|
1561
|
+
return typeof adapter?.executeSchemaOperations === "function";
|
|
1562
|
+
};
|
|
1563
|
+
const supportsDatabaseReset = (adapter) => {
|
|
1564
|
+
return typeof adapter?.resetDatabase === "function";
|
|
1565
|
+
};
|
|
1566
|
+
const stripPrismaSchemaModelsAndEnums = (schema) => {
|
|
1567
|
+
const stripped = schema.replace(PRISMA_MODEL_REGEX, "").replace(PRISMA_ENUM_REGEX, "").replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
1568
|
+
return stripped.length > 0 ? `${stripped}\n` : "";
|
|
1569
|
+
};
|
|
1570
|
+
const applyMigrationToDatabase = async (adapter, migration) => {
|
|
1571
|
+
if (!supportsDatabaseMigrationExecution(adapter)) throw new ArkormException("The configured adapter does not support database-backed migration execution.");
|
|
1572
|
+
const operations = await getMigrationPlan(migration, "up");
|
|
1573
|
+
await adapter.executeSchemaOperations(operations);
|
|
1574
|
+
return { operations };
|
|
1575
|
+
};
|
|
1576
|
+
const applyMigrationRollbackToDatabase = async (adapter, migration) => {
|
|
1577
|
+
if (!supportsDatabaseMigrationExecution(adapter)) throw new ArkormException("The configured adapter does not support database-backed migration execution.");
|
|
1578
|
+
const operations = await getMigrationPlan(migration, "down");
|
|
1579
|
+
await adapter.executeSchemaOperations(operations);
|
|
1580
|
+
return { operations };
|
|
1581
|
+
};
|
|
1374
1582
|
/**
|
|
1375
1583
|
* Apply the schema operations defined in a migration to a Prisma schema
|
|
1376
1584
|
* file, updating the file on disk if specified, and return the updated
|
|
@@ -1414,6 +1622,321 @@ const applyMigrationRollbackToPrismaSchema = async (migration, options = {}) =>
|
|
|
1414
1622
|
};
|
|
1415
1623
|
};
|
|
1416
1624
|
|
|
1625
|
+
//#endregion
|
|
1626
|
+
//#region src/helpers/column-mappings.ts
|
|
1627
|
+
let cachedColumnMappingsPath;
|
|
1628
|
+
let cachedColumnMappingsState;
|
|
1629
|
+
const resolvePersistedMetadataFeatures = (features) => {
|
|
1630
|
+
return {
|
|
1631
|
+
persistedColumnMappings: features?.persistedColumnMappings !== false,
|
|
1632
|
+
persistedEnums: features?.persistedEnums !== false
|
|
1633
|
+
};
|
|
1634
|
+
};
|
|
1635
|
+
const createEmptyPersistedColumnMappingsState = () => ({
|
|
1636
|
+
version: 1,
|
|
1637
|
+
tables: {}
|
|
1638
|
+
});
|
|
1639
|
+
const resolveColumnMappingsFilePath = (cwd, configuredPath) => {
|
|
1640
|
+
if (configuredPath && configuredPath.trim().length > 0) return resolve(configuredPath);
|
|
1641
|
+
return join(cwd, ".arkormx", "column-mappings.json");
|
|
1642
|
+
};
|
|
1643
|
+
const normalizePersistedEnumValues = (values) => {
|
|
1644
|
+
if (!Array.isArray(values)) return [];
|
|
1645
|
+
return values.filter((value) => typeof value === "string" && value.trim().length > 0);
|
|
1646
|
+
};
|
|
1647
|
+
const normalizeLegacyTableColumns = (columns) => {
|
|
1648
|
+
return Object.entries(columns).reduce((mapped, [attribute, column]) => {
|
|
1649
|
+
if (attribute.trim().length === 0) return mapped;
|
|
1650
|
+
if (typeof column !== "string" || column.trim().length === 0) return mapped;
|
|
1651
|
+
mapped[attribute] = column;
|
|
1652
|
+
return mapped;
|
|
1653
|
+
}, {});
|
|
1654
|
+
};
|
|
1655
|
+
const normalizePersistedTableMetadata = (table) => {
|
|
1656
|
+
if (!table || typeof table !== "object" || Array.isArray(table)) return {
|
|
1657
|
+
columns: {},
|
|
1658
|
+
enums: {}
|
|
1659
|
+
};
|
|
1660
|
+
const candidate = table;
|
|
1661
|
+
if (!(Object.prototype.hasOwnProperty.call(candidate, "columns") || Object.prototype.hasOwnProperty.call(candidate, "enums") || Object.prototype.hasOwnProperty.call(candidate, "primaryKeyGeneration") || Object.prototype.hasOwnProperty.call(candidate, "timestampColumns"))) return {
|
|
1662
|
+
columns: normalizeLegacyTableColumns(candidate),
|
|
1663
|
+
enums: {}
|
|
1664
|
+
};
|
|
1665
|
+
return {
|
|
1666
|
+
columns: normalizeLegacyTableColumns(candidate.columns ?? {}),
|
|
1667
|
+
enums: Object.entries(candidate.enums ?? {}).reduce((all, [columnName, values]) => {
|
|
1668
|
+
if (columnName.trim().length === 0) return all;
|
|
1669
|
+
const normalizedValues = normalizePersistedEnumValues(values);
|
|
1670
|
+
if (normalizedValues.length > 0) all[columnName] = normalizedValues;
|
|
1671
|
+
return all;
|
|
1672
|
+
}, {}),
|
|
1673
|
+
primaryKeyGeneration: normalizePersistedPrimaryKeyGeneration(candidate.primaryKeyGeneration),
|
|
1674
|
+
timestampColumns: normalizePersistedTimestampColumns(candidate.timestampColumns)
|
|
1675
|
+
};
|
|
1676
|
+
};
|
|
1677
|
+
const normalizePersistedPrimaryKeyGeneration = (value) => {
|
|
1678
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
1679
|
+
const candidate = value;
|
|
1680
|
+
if (candidate.strategy !== "uuid" || typeof candidate.column !== "string" || candidate.column.trim().length === 0) return void 0;
|
|
1681
|
+
return {
|
|
1682
|
+
column: candidate.column,
|
|
1683
|
+
strategy: "uuid",
|
|
1684
|
+
prismaDefault: typeof candidate.prismaDefault === "string" && candidate.prismaDefault.trim().length > 0 ? candidate.prismaDefault : void 0,
|
|
1685
|
+
databaseDefault: typeof candidate.databaseDefault === "string" && candidate.databaseDefault.trim().length > 0 ? candidate.databaseDefault : void 0,
|
|
1686
|
+
runtimeFactory: candidate.runtimeFactory === "uuid" ? "uuid" : void 0
|
|
1687
|
+
};
|
|
1688
|
+
};
|
|
1689
|
+
const normalizePersistedTimestampColumns = (value) => {
|
|
1690
|
+
if (!Array.isArray(value)) return void 0;
|
|
1691
|
+
const columns = value.reduce((all, entry) => {
|
|
1692
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return all;
|
|
1693
|
+
const candidate = entry;
|
|
1694
|
+
if (typeof candidate.column !== "string" || candidate.column.trim().length === 0) return all;
|
|
1695
|
+
const normalized = { column: candidate.column };
|
|
1696
|
+
if (candidate.default === "now()") normalized.default = "now()";
|
|
1697
|
+
if (candidate.updatedAt === true) normalized.updatedAt = true;
|
|
1698
|
+
if (!normalized.default && !normalized.updatedAt) return all;
|
|
1699
|
+
all.push(normalized);
|
|
1700
|
+
return all;
|
|
1701
|
+
}, []);
|
|
1702
|
+
return columns.length > 0 ? columns : void 0;
|
|
1703
|
+
};
|
|
1704
|
+
const normalizePersistedColumnMappingsState = (state) => {
|
|
1705
|
+
return {
|
|
1706
|
+
version: 1,
|
|
1707
|
+
tables: Object.entries(state?.tables ?? {}).reduce((all, [tableName, tableMetadata]) => {
|
|
1708
|
+
if (tableName.trim().length === 0) return all;
|
|
1709
|
+
const normalized = normalizePersistedTableMetadata(tableMetadata);
|
|
1710
|
+
if (Object.keys(normalized.columns).length > 0 || Object.keys(normalized.enums).length > 0 || normalized.primaryKeyGeneration || normalized.timestampColumns?.length) all[tableName] = normalized;
|
|
1711
|
+
return all;
|
|
1712
|
+
}, {})
|
|
1713
|
+
};
|
|
1714
|
+
};
|
|
1715
|
+
const buildPersistedFeatureDisabledError = (feature, table) => {
|
|
1716
|
+
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.*.`, {
|
|
1717
|
+
operation: "metadata.persisted",
|
|
1718
|
+
meta: {
|
|
1719
|
+
feature,
|
|
1720
|
+
table
|
|
1721
|
+
}
|
|
1722
|
+
});
|
|
1723
|
+
};
|
|
1724
|
+
const assertPersistedTableMetadataEnabled = (table, metadata, features, strict) => {
|
|
1725
|
+
if (!strict) return;
|
|
1726
|
+
if (!features.persistedColumnMappings && Object.keys(metadata.columns).length > 0) throw buildPersistedFeatureDisabledError("persistedColumnMappings", table);
|
|
1727
|
+
if (!features.persistedEnums && Object.keys(metadata.enums).length > 0) throw buildPersistedFeatureDisabledError("persistedEnums", table);
|
|
1728
|
+
};
|
|
1729
|
+
const buildEnumUnionType = (values) => {
|
|
1730
|
+
return values.map((value) => {
|
|
1731
|
+
return `'${value.replace(/'/g, String.raw`\'`)}'`;
|
|
1732
|
+
}).join(" | ");
|
|
1733
|
+
};
|
|
1734
|
+
const resetPersistedColumnMappingsCache = () => {
|
|
1735
|
+
cachedColumnMappingsPath = void 0;
|
|
1736
|
+
cachedColumnMappingsState = void 0;
|
|
1737
|
+
};
|
|
1738
|
+
const readPersistedColumnMappingsState = (filePath) => {
|
|
1739
|
+
if (cachedColumnMappingsPath === filePath && cachedColumnMappingsState) return cachedColumnMappingsState;
|
|
1740
|
+
if (!existsSync(filePath)) {
|
|
1741
|
+
const empty = createEmptyPersistedColumnMappingsState();
|
|
1742
|
+
cachedColumnMappingsPath = filePath;
|
|
1743
|
+
cachedColumnMappingsState = empty;
|
|
1744
|
+
return empty;
|
|
1745
|
+
}
|
|
1746
|
+
try {
|
|
1747
|
+
const normalized = normalizePersistedColumnMappingsState(JSON.parse(readFileSync(filePath, "utf-8")));
|
|
1748
|
+
cachedColumnMappingsPath = filePath;
|
|
1749
|
+
cachedColumnMappingsState = normalized;
|
|
1750
|
+
return normalized;
|
|
1751
|
+
} catch {
|
|
1752
|
+
const empty = createEmptyPersistedColumnMappingsState();
|
|
1753
|
+
cachedColumnMappingsPath = filePath;
|
|
1754
|
+
cachedColumnMappingsState = empty;
|
|
1755
|
+
return empty;
|
|
1756
|
+
}
|
|
1757
|
+
};
|
|
1758
|
+
const writePersistedColumnMappingsState = (filePath, state) => {
|
|
1759
|
+
const normalized = normalizePersistedColumnMappingsState(state);
|
|
1760
|
+
const directory = dirname(filePath);
|
|
1761
|
+
if (!existsSync(directory)) mkdirSync(directory, { recursive: true });
|
|
1762
|
+
writeFileSync(filePath, JSON.stringify(normalized, null, 2));
|
|
1763
|
+
cachedColumnMappingsPath = filePath;
|
|
1764
|
+
cachedColumnMappingsState = normalized;
|
|
1765
|
+
};
|
|
1766
|
+
const deletePersistedColumnMappingsState = (filePath) => {
|
|
1767
|
+
if (existsSync(filePath)) rmSync(filePath, { force: true });
|
|
1768
|
+
resetPersistedColumnMappingsCache();
|
|
1769
|
+
};
|
|
1770
|
+
const getPersistedTableMetadata = (table, options = {}) => {
|
|
1771
|
+
const metadata = readPersistedColumnMappingsState(resolveColumnMappingsFilePath(options.cwd ?? process.cwd(), options.configuredPath)).tables[table] ?? {
|
|
1772
|
+
columns: {},
|
|
1773
|
+
enums: {}
|
|
1774
|
+
};
|
|
1775
|
+
assertPersistedTableMetadataEnabled(table, metadata, options.features ?? resolvePersistedMetadataFeatures(), options.strict ?? false);
|
|
1776
|
+
return {
|
|
1777
|
+
columns: { ...metadata.columns },
|
|
1778
|
+
enums: Object.entries(metadata.enums).reduce((all, [columnName, values]) => {
|
|
1779
|
+
all[columnName] = [...values];
|
|
1780
|
+
return all;
|
|
1781
|
+
}, {}),
|
|
1782
|
+
primaryKeyGeneration: metadata.primaryKeyGeneration ? { ...metadata.primaryKeyGeneration } : void 0,
|
|
1783
|
+
timestampColumns: metadata.timestampColumns?.map((column) => ({ ...column }))
|
|
1784
|
+
};
|
|
1785
|
+
};
|
|
1786
|
+
const applyMappedColumn = (tableColumns, column, features, table) => {
|
|
1787
|
+
if (typeof column.map === "string" && column.map.trim().length > 0 && column.map !== column.name) {
|
|
1788
|
+
if (!features.persistedColumnMappings) throw buildPersistedFeatureDisabledError("persistedColumnMappings", table);
|
|
1789
|
+
tableColumns[column.name] = column.map;
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
delete tableColumns[column.name];
|
|
1793
|
+
};
|
|
1794
|
+
const applyEnumColumn = (tableEnums, column, features, table) => {
|
|
1795
|
+
const values = column.enumValues ?? [];
|
|
1796
|
+
if (column.type === "enum" && values.length > 0) {
|
|
1797
|
+
if (!features.persistedEnums) throw buildPersistedFeatureDisabledError("persistedEnums", table);
|
|
1798
|
+
tableEnums[column.name] = [...values];
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
delete tableEnums[column.name];
|
|
1802
|
+
};
|
|
1803
|
+
const removePersistedColumnMetadata = (tableMetadata, columnName) => {
|
|
1804
|
+
delete tableMetadata.columns[columnName];
|
|
1805
|
+
delete tableMetadata.enums[columnName];
|
|
1806
|
+
Object.entries(tableMetadata.columns).forEach(([attribute, mappedColumn]) => {
|
|
1807
|
+
if (mappedColumn === columnName) delete tableMetadata.columns[attribute];
|
|
1808
|
+
});
|
|
1809
|
+
if (tableMetadata.primaryKeyGeneration?.column === columnName) delete tableMetadata.primaryKeyGeneration;
|
|
1810
|
+
if (tableMetadata.timestampColumns) {
|
|
1811
|
+
tableMetadata.timestampColumns = tableMetadata.timestampColumns.filter((column) => column.column !== columnName);
|
|
1812
|
+
if (tableMetadata.timestampColumns.length === 0) delete tableMetadata.timestampColumns;
|
|
1813
|
+
}
|
|
1814
|
+
};
|
|
1815
|
+
const applyPrimaryKeyGeneration = (tableMetadata, column) => {
|
|
1816
|
+
if (!column.primary || !column.primaryKeyGeneration) {
|
|
1817
|
+
if (tableMetadata.primaryKeyGeneration?.column === column.name) delete tableMetadata.primaryKeyGeneration;
|
|
1818
|
+
return;
|
|
1819
|
+
}
|
|
1820
|
+
tableMetadata.primaryKeyGeneration = {
|
|
1821
|
+
column: column.name,
|
|
1822
|
+
...column.primaryKeyGeneration
|
|
1823
|
+
};
|
|
1824
|
+
};
|
|
1825
|
+
const applyTimestampColumn = (tableMetadata, column) => {
|
|
1826
|
+
if (column.type !== "timestamp" || column.default !== "now()" && column.updatedAt !== true) {
|
|
1827
|
+
if (tableMetadata.timestampColumns) {
|
|
1828
|
+
tableMetadata.timestampColumns = tableMetadata.timestampColumns.filter((entry) => entry.column !== column.name);
|
|
1829
|
+
if (tableMetadata.timestampColumns.length === 0) delete tableMetadata.timestampColumns;
|
|
1830
|
+
}
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
const nextColumn = {
|
|
1834
|
+
column: column.name,
|
|
1835
|
+
...column.default === "now()" ? { default: "now()" } : {},
|
|
1836
|
+
...column.updatedAt ? { updatedAt: true } : {}
|
|
1837
|
+
};
|
|
1838
|
+
tableMetadata.timestampColumns = [...(tableMetadata.timestampColumns ?? []).filter((entry) => entry.column !== column.name), nextColumn];
|
|
1839
|
+
};
|
|
1840
|
+
const applyOperationsToPersistedColumnMappingsState = (state, operations, features = resolvePersistedMetadataFeatures()) => {
|
|
1841
|
+
const nextTables = Object.entries(state.tables).reduce((all, [table, metadata]) => {
|
|
1842
|
+
all[table] = {
|
|
1843
|
+
columns: { ...metadata.columns },
|
|
1844
|
+
enums: Object.entries(metadata.enums).reduce((nextEnums, [columnName, values]) => {
|
|
1845
|
+
nextEnums[columnName] = [...values];
|
|
1846
|
+
return nextEnums;
|
|
1847
|
+
}, {}),
|
|
1848
|
+
primaryKeyGeneration: metadata.primaryKeyGeneration ? { ...metadata.primaryKeyGeneration } : void 0,
|
|
1849
|
+
timestampColumns: metadata.timestampColumns?.map((column) => ({ ...column }))
|
|
1850
|
+
};
|
|
1851
|
+
return all;
|
|
1852
|
+
}, {});
|
|
1853
|
+
operations.forEach((operation) => {
|
|
1854
|
+
if (operation.type === "createTable") {
|
|
1855
|
+
const tableMetadata = nextTables[operation.table] ?? {
|
|
1856
|
+
columns: {},
|
|
1857
|
+
enums: {}
|
|
1858
|
+
};
|
|
1859
|
+
operation.columns.forEach((column) => {
|
|
1860
|
+
applyMappedColumn(tableMetadata.columns, column, features, operation.table);
|
|
1861
|
+
applyEnumColumn(tableMetadata.enums, column, features, operation.table);
|
|
1862
|
+
applyPrimaryKeyGeneration(tableMetadata, column);
|
|
1863
|
+
applyTimestampColumn(tableMetadata, column);
|
|
1864
|
+
});
|
|
1865
|
+
if (Object.keys(tableMetadata.columns).length > 0 || Object.keys(tableMetadata.enums).length > 0 || tableMetadata.primaryKeyGeneration || tableMetadata.timestampColumns?.length) nextTables[operation.table] = tableMetadata;
|
|
1866
|
+
else delete nextTables[operation.table];
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
if (operation.type === "alterTable") {
|
|
1870
|
+
const tableMetadata = nextTables[operation.table] ?? {
|
|
1871
|
+
columns: {},
|
|
1872
|
+
enums: {}
|
|
1873
|
+
};
|
|
1874
|
+
operation.addColumns.forEach((column) => {
|
|
1875
|
+
applyMappedColumn(tableMetadata.columns, column, features, operation.table);
|
|
1876
|
+
applyEnumColumn(tableMetadata.enums, column, features, operation.table);
|
|
1877
|
+
applyPrimaryKeyGeneration(tableMetadata, column);
|
|
1878
|
+
applyTimestampColumn(tableMetadata, column);
|
|
1879
|
+
});
|
|
1880
|
+
operation.dropColumns.forEach((columnName) => {
|
|
1881
|
+
removePersistedColumnMetadata(tableMetadata, columnName);
|
|
1882
|
+
});
|
|
1883
|
+
if (Object.keys(tableMetadata.columns).length > 0 || Object.keys(tableMetadata.enums).length > 0 || tableMetadata.primaryKeyGeneration || tableMetadata.timestampColumns?.length) nextTables[operation.table] = tableMetadata;
|
|
1884
|
+
else delete nextTables[operation.table];
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
delete nextTables[operation.table];
|
|
1888
|
+
});
|
|
1889
|
+
return {
|
|
1890
|
+
version: 1,
|
|
1891
|
+
tables: nextTables
|
|
1892
|
+
};
|
|
1893
|
+
};
|
|
1894
|
+
const rebuildPersistedColumnMappingsState = async (state, availableMigrations, features = resolvePersistedMetadataFeatures()) => {
|
|
1895
|
+
const availableByIdentity = new Map(availableMigrations.map(([migrationClass, file]) => [buildMigrationIdentity(file, migrationClass.name), migrationClass]));
|
|
1896
|
+
let nextState = createEmptyPersistedColumnMappingsState();
|
|
1897
|
+
const orderedMigrations = state.migrations.map((migration, index) => ({
|
|
1898
|
+
migration,
|
|
1899
|
+
index
|
|
1900
|
+
})).sort((left, right) => {
|
|
1901
|
+
const appliedAtOrder = left.migration.appliedAt.localeCompare(right.migration.appliedAt);
|
|
1902
|
+
if (appliedAtOrder !== 0) return appliedAtOrder;
|
|
1903
|
+
return left.index - right.index;
|
|
1904
|
+
});
|
|
1905
|
+
for (const { migration } of orderedMigrations) {
|
|
1906
|
+
const migrationClass = availableByIdentity.get(migration.id);
|
|
1907
|
+
if (!migrationClass) throw new ArkormException(`Unable to rebuild persisted column mappings because migration [${migration.id}] could not be resolved from the current migration files.`, {
|
|
1908
|
+
operation: "migration.columnMappings",
|
|
1909
|
+
meta: {
|
|
1910
|
+
migrationId: migration.id,
|
|
1911
|
+
file: migration.file,
|
|
1912
|
+
className: migration.className
|
|
1913
|
+
}
|
|
1914
|
+
});
|
|
1915
|
+
const operations = await getMigrationPlan(migrationClass, "up");
|
|
1916
|
+
nextState = applyOperationsToPersistedColumnMappingsState(nextState, operations, features);
|
|
1917
|
+
}
|
|
1918
|
+
return nextState;
|
|
1919
|
+
};
|
|
1920
|
+
const syncPersistedColumnMappingsFromState = async (cwd, state, availableMigrations, features = resolvePersistedMetadataFeatures()) => {
|
|
1921
|
+
const filePath = resolveColumnMappingsFilePath(cwd);
|
|
1922
|
+
const nextState = await rebuildPersistedColumnMappingsState(state, availableMigrations, features);
|
|
1923
|
+
if (Object.keys(nextState.tables).length === 0) {
|
|
1924
|
+
deletePersistedColumnMappingsState(filePath);
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
writePersistedColumnMappingsState(filePath, nextState);
|
|
1928
|
+
};
|
|
1929
|
+
const validatePersistedMetadataFeaturesForMigrations = async (migrations, features = resolvePersistedMetadataFeatures()) => {
|
|
1930
|
+
let nextState = createEmptyPersistedColumnMappingsState();
|
|
1931
|
+
for (const [migrationClass] of migrations) {
|
|
1932
|
+
const operations = await getMigrationPlan(migrationClass, "up");
|
|
1933
|
+
nextState = applyOperationsToPersistedColumnMappingsState(nextState, operations, features);
|
|
1934
|
+
}
|
|
1935
|
+
};
|
|
1936
|
+
const getPersistedEnumTsType = (values) => {
|
|
1937
|
+
return buildEnumUnionType(values);
|
|
1938
|
+
};
|
|
1939
|
+
|
|
1417
1940
|
//#endregion
|
|
1418
1941
|
//#region src/helpers/runtime-module-loader.ts
|
|
1419
1942
|
var RuntimeModuleLoader = class {
|
|
@@ -1441,6 +1964,10 @@ const resolveDefaultStubsPath = () => {
|
|
|
1441
1964
|
return path.join(process.cwd(), "stubs");
|
|
1442
1965
|
};
|
|
1443
1966
|
const baseConfig = {
|
|
1967
|
+
features: {
|
|
1968
|
+
persistedColumnMappings: true,
|
|
1969
|
+
persistedEnums: true
|
|
1970
|
+
},
|
|
1444
1971
|
paths: {
|
|
1445
1972
|
stubs: resolveDefaultStubsPath(),
|
|
1446
1973
|
seeders: path.join(process.cwd(), "database", "seeders"),
|
|
@@ -1453,6 +1980,7 @@ const baseConfig = {
|
|
|
1453
1980
|
};
|
|
1454
1981
|
const userConfig = {
|
|
1455
1982
|
...baseConfig,
|
|
1983
|
+
features: { ...baseConfig.features ?? {} },
|
|
1456
1984
|
paths: { ...baseConfig.paths ?? {} }
|
|
1457
1985
|
};
|
|
1458
1986
|
let runtimeConfigLoaded = false;
|
|
@@ -1461,7 +1989,27 @@ let runtimeClientResolver;
|
|
|
1461
1989
|
let runtimeAdapter;
|
|
1462
1990
|
let runtimePaginationURLDriverFactory;
|
|
1463
1991
|
let runtimePaginationCurrentPageResolver;
|
|
1992
|
+
let runtimeDebugHandler;
|
|
1464
1993
|
const transactionClientStorage = new AsyncLocalStorage();
|
|
1994
|
+
const defaultDebugHandler = (event) => {
|
|
1995
|
+
const prefix = `[arkorm:${event.adapter}] ${event.operation}${event.target ? ` [${event.target}]` : ""}`;
|
|
1996
|
+
const payload = {
|
|
1997
|
+
phase: event.phase,
|
|
1998
|
+
durationMs: event.durationMs,
|
|
1999
|
+
inspection: event.inspection ?? void 0,
|
|
2000
|
+
meta: event.meta,
|
|
2001
|
+
error: event.error
|
|
2002
|
+
};
|
|
2003
|
+
if (event.phase === "error") {
|
|
2004
|
+
console.error(prefix, payload);
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
2007
|
+
console.debug(prefix, payload);
|
|
2008
|
+
};
|
|
2009
|
+
const resolveDebugHandler = (debug) => {
|
|
2010
|
+
if (debug === true) return defaultDebugHandler;
|
|
2011
|
+
return typeof debug === "function" ? debug : void 0;
|
|
2012
|
+
};
|
|
1465
2013
|
const mergePathConfig = (paths) => {
|
|
1466
2014
|
const defaults = baseConfig.paths ?? {};
|
|
1467
2015
|
const current = userConfig.paths ?? {};
|
|
@@ -1475,6 +2023,15 @@ const mergePathConfig = (paths) => {
|
|
|
1475
2023
|
...incoming
|
|
1476
2024
|
};
|
|
1477
2025
|
};
|
|
2026
|
+
const mergeFeatureConfig = (features) => {
|
|
2027
|
+
const defaults = baseConfig.features ?? {};
|
|
2028
|
+
const current = userConfig.features ?? {};
|
|
2029
|
+
return {
|
|
2030
|
+
...defaults,
|
|
2031
|
+
...current,
|
|
2032
|
+
...features ?? {}
|
|
2033
|
+
};
|
|
2034
|
+
};
|
|
1478
2035
|
const bindAdapterToModels = (adapter, models) => {
|
|
1479
2036
|
models.forEach((model) => {
|
|
1480
2037
|
model.setAdapter(adapter);
|
|
@@ -1500,18 +2057,21 @@ const getUserConfig = (key) => {
|
|
|
1500
2057
|
const configureArkormRuntime = (prisma, options = {}) => {
|
|
1501
2058
|
const nextConfig = {
|
|
1502
2059
|
...userConfig,
|
|
2060
|
+
features: mergeFeatureConfig(options.features),
|
|
1503
2061
|
paths: mergePathConfig(options.paths)
|
|
1504
2062
|
};
|
|
1505
2063
|
nextConfig.prisma = prisma;
|
|
1506
2064
|
if (options.pagination !== void 0) nextConfig.pagination = options.pagination;
|
|
1507
2065
|
if (options.adapter !== void 0) nextConfig.adapter = options.adapter;
|
|
1508
2066
|
if (options.boot !== void 0) nextConfig.boot = options.boot;
|
|
2067
|
+
if (options.debug !== void 0) nextConfig.debug = options.debug;
|
|
1509
2068
|
if (options.outputExt !== void 0) nextConfig.outputExt = options.outputExt;
|
|
1510
2069
|
Object.assign(userConfig, { ...nextConfig });
|
|
1511
2070
|
runtimeClientResolver = prisma;
|
|
1512
2071
|
runtimeAdapter = options.adapter;
|
|
1513
2072
|
runtimePaginationURLDriverFactory = nextConfig.pagination?.urlDriver;
|
|
1514
2073
|
runtimePaginationCurrentPageResolver = nextConfig.pagination?.resolveCurrentPage;
|
|
2074
|
+
runtimeDebugHandler = resolveDebugHandler(nextConfig.debug);
|
|
1515
2075
|
options.boot?.({
|
|
1516
2076
|
prisma: resolveClient(prisma),
|
|
1517
2077
|
bindAdapter: bindAdapterToModels
|
|
@@ -1544,6 +2104,8 @@ const resolveAndApplyConfig = (imported) => {
|
|
|
1544
2104
|
configureArkormRuntime(config.prisma, {
|
|
1545
2105
|
adapter: config.adapter,
|
|
1546
2106
|
boot: config.boot,
|
|
2107
|
+
debug: config.debug,
|
|
2108
|
+
features: config.features,
|
|
1547
2109
|
pagination: config.pagination,
|
|
1548
2110
|
paths: config.paths,
|
|
1549
2111
|
outputExt: config.outputExt
|
|
@@ -1601,8 +2163,497 @@ const loadArkormConfig = async () => {
|
|
|
1601
2163
|
const getDefaultStubsPath = () => {
|
|
1602
2164
|
return resolveDefaultStubsPath();
|
|
1603
2165
|
};
|
|
2166
|
+
const getRuntimeDebugHandler = () => {
|
|
2167
|
+
if (!runtimeConfigLoaded) loadRuntimeConfigSync();
|
|
2168
|
+
return runtimeDebugHandler;
|
|
2169
|
+
};
|
|
2170
|
+
const emitRuntimeDebugEvent = (event) => {
|
|
2171
|
+
getRuntimeDebugHandler()?.(event);
|
|
2172
|
+
};
|
|
2173
|
+
/**
|
|
2174
|
+
* Check if a given value is a Prisma delegate-like object
|
|
2175
|
+
* by verifying the presence of common delegate methods.
|
|
2176
|
+
*
|
|
2177
|
+
* @param value The value to check.
|
|
2178
|
+
* @returns True if the value is a Prisma delegate-like object, false otherwise.
|
|
2179
|
+
*/
|
|
2180
|
+
const isDelegateLike = (value) => {
|
|
2181
|
+
if (!value || typeof value !== "object") return false;
|
|
2182
|
+
const candidate = value;
|
|
2183
|
+
return [
|
|
2184
|
+
"findMany",
|
|
2185
|
+
"findFirst",
|
|
2186
|
+
"create",
|
|
2187
|
+
"update",
|
|
2188
|
+
"delete",
|
|
2189
|
+
"count"
|
|
2190
|
+
].every((method) => typeof candidate[method] === "function");
|
|
2191
|
+
};
|
|
1604
2192
|
loadArkormConfig();
|
|
1605
2193
|
|
|
2194
|
+
//#endregion
|
|
2195
|
+
//#region src/helpers/prisma.ts
|
|
2196
|
+
/**
|
|
2197
|
+
* Infer the Prisma delegate name for a given model name using a simple convention.
|
|
2198
|
+
*
|
|
2199
|
+
* @param modelName The name of the model to infer the delegate name for.
|
|
2200
|
+
* @returns The inferred Prisma delegate name.
|
|
2201
|
+
*/
|
|
2202
|
+
function inferDelegateName(modelName) {
|
|
2203
|
+
return `${modelName.charAt(0).toLowerCase()}${modelName.slice(1)}s`;
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
//#endregion
|
|
2207
|
+
//#region src/adapters/PrismaDatabaseAdapter.ts
|
|
2208
|
+
/**
|
|
2209
|
+
* Database adapter implementation for Prisma, allowing Arkorm to execute queries using Prisma
|
|
2210
|
+
* as the underlying query builder and executor.
|
|
2211
|
+
*
|
|
2212
|
+
* @author Legacy (3m1n3nc3)
|
|
2213
|
+
* @since 2.0.0-next.0
|
|
2214
|
+
*/
|
|
2215
|
+
var PrismaDatabaseAdapter = class PrismaDatabaseAdapter {
|
|
2216
|
+
capabilities;
|
|
2217
|
+
delegates;
|
|
2218
|
+
constructor(prisma, mapping = {}) {
|
|
2219
|
+
this.prisma = prisma;
|
|
2220
|
+
this.mapping = mapping;
|
|
2221
|
+
this.delegates = Object.entries(prisma).reduce((accumulator, [key, value]) => {
|
|
2222
|
+
if (!isDelegateLike(value)) return accumulator;
|
|
2223
|
+
accumulator[key] = value;
|
|
2224
|
+
return accumulator;
|
|
2225
|
+
}, {});
|
|
2226
|
+
this.capabilities = {
|
|
2227
|
+
transactions: this.hasTransactionSupport(prisma),
|
|
2228
|
+
insertMany: Object.values(this.delegates).some((delegate) => typeof delegate.createMany === "function"),
|
|
2229
|
+
upsert: false,
|
|
2230
|
+
updateMany: Object.values(this.delegates).some((delegate) => typeof delegate.updateMany === "function"),
|
|
2231
|
+
deleteMany: false,
|
|
2232
|
+
exists: true,
|
|
2233
|
+
relationLoads: false,
|
|
2234
|
+
relationAggregates: false,
|
|
2235
|
+
relationFilters: false,
|
|
2236
|
+
rawWhere: false,
|
|
2237
|
+
returning: false
|
|
2238
|
+
};
|
|
2239
|
+
}
|
|
2240
|
+
hasTransactionSupport(client) {
|
|
2241
|
+
return Boolean(client) && typeof client.$transaction === "function";
|
|
2242
|
+
}
|
|
2243
|
+
normalizeCandidate(value) {
|
|
2244
|
+
return value.trim();
|
|
2245
|
+
}
|
|
2246
|
+
unique(values) {
|
|
2247
|
+
return [...new Set(values.filter(Boolean))];
|
|
2248
|
+
}
|
|
2249
|
+
runtimeModelTypeToTs(typeName, kind, enumValues) {
|
|
2250
|
+
if (kind === "enum" && enumValues && enumValues.length > 0) return enumValues.map((value) => `'${value.replace(/'/g, "\\'")}'`).join(" | ");
|
|
2251
|
+
switch (typeName) {
|
|
2252
|
+
case "Int":
|
|
2253
|
+
case "Float":
|
|
2254
|
+
case "Decimal":
|
|
2255
|
+
case "BigInt": return "number";
|
|
2256
|
+
case "Boolean": return "boolean";
|
|
2257
|
+
case "DateTime": return "Date";
|
|
2258
|
+
case "Json": return "Record<string, unknown> | unknown[]";
|
|
2259
|
+
case "Bytes": return "Uint8Array";
|
|
2260
|
+
case "String":
|
|
2261
|
+
case "UUID": return "string";
|
|
2262
|
+
default: return "string";
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
getRuntimeDataModel() {
|
|
2266
|
+
const runtimeDataModel = this.prisma._runtimeDataModel;
|
|
2267
|
+
if (runtimeDataModel && typeof runtimeDataModel === "object") return runtimeDataModel;
|
|
2268
|
+
return null;
|
|
2269
|
+
}
|
|
2270
|
+
toQuerySelect(columns) {
|
|
2271
|
+
if (!columns || columns.length === 0) return void 0;
|
|
2272
|
+
return columns.reduce((select, column) => {
|
|
2273
|
+
select[column.column] = true;
|
|
2274
|
+
return select;
|
|
2275
|
+
}, {});
|
|
2276
|
+
}
|
|
2277
|
+
toQueryOrderBy(orderBy) {
|
|
2278
|
+
if (!orderBy || orderBy.length === 0) return void 0;
|
|
2279
|
+
return orderBy.map((entry) => ({ [entry.column]: entry.direction }));
|
|
2280
|
+
}
|
|
2281
|
+
toComparisonWhere(condition) {
|
|
2282
|
+
if (condition.operator === "is-null") return { [condition.column]: null };
|
|
2283
|
+
if (condition.operator === "is-not-null") return { [condition.column]: { not: null } };
|
|
2284
|
+
if (condition.operator === "=") return { [condition.column]: condition.value };
|
|
2285
|
+
if (condition.operator === "!=") return { [condition.column]: { not: condition.value } };
|
|
2286
|
+
if (condition.operator === ">") return { [condition.column]: { gt: condition.value } };
|
|
2287
|
+
if (condition.operator === ">=") return { [condition.column]: { gte: condition.value } };
|
|
2288
|
+
if (condition.operator === "<") return { [condition.column]: { lt: condition.value } };
|
|
2289
|
+
if (condition.operator === "<=") return { [condition.column]: { lte: condition.value } };
|
|
2290
|
+
if (condition.operator === "in") return { [condition.column]: { in: Array.isArray(condition.value) ? condition.value : [condition.value] } };
|
|
2291
|
+
if (condition.operator === "not-in") return { [condition.column]: { notIn: Array.isArray(condition.value) ? condition.value : [condition.value] } };
|
|
2292
|
+
if (condition.operator === "contains") return { [condition.column]: { contains: condition.value } };
|
|
2293
|
+
if (condition.operator === "starts-with") return { [condition.column]: { startsWith: condition.value } };
|
|
2294
|
+
return { [condition.column]: { endsWith: condition.value } };
|
|
2295
|
+
}
|
|
2296
|
+
toQueryWhere(condition) {
|
|
2297
|
+
if (!condition) return void 0;
|
|
2298
|
+
if (condition.type === "comparison") return this.toComparisonWhere(condition);
|
|
2299
|
+
if (condition.type === "group") {
|
|
2300
|
+
const group = condition;
|
|
2301
|
+
const grouped = group.conditions.map((entry) => this.toQueryWhere(entry)).filter((entry) => Boolean(entry));
|
|
2302
|
+
if (grouped.length === 0) return void 0;
|
|
2303
|
+
return group.operator === "and" ? { AND: grouped } : { OR: grouped };
|
|
2304
|
+
}
|
|
2305
|
+
if (condition.type === "not") {
|
|
2306
|
+
const notCondition = condition;
|
|
2307
|
+
const nested = this.toQueryWhere(notCondition.condition);
|
|
2308
|
+
if (!nested) return void 0;
|
|
2309
|
+
return { NOT: nested };
|
|
2310
|
+
}
|
|
2311
|
+
throw new UnsupportedAdapterFeatureException("Raw where clauses are not supported by the Prisma compatibility adapter.", {
|
|
2312
|
+
operation: "adapter.where",
|
|
2313
|
+
meta: {
|
|
2314
|
+
feature: "rawWhere",
|
|
2315
|
+
sql: condition.sql
|
|
2316
|
+
}
|
|
2317
|
+
});
|
|
2318
|
+
}
|
|
2319
|
+
buildFindArgs(spec) {
|
|
2320
|
+
return {
|
|
2321
|
+
include: this.toQueryInclude(spec.relationLoads),
|
|
2322
|
+
where: this.toQueryWhere(spec.where),
|
|
2323
|
+
orderBy: this.toQueryOrderBy(spec.orderBy),
|
|
2324
|
+
select: this.toQuerySelect(spec.columns),
|
|
2325
|
+
skip: spec.offset,
|
|
2326
|
+
take: spec.limit
|
|
2327
|
+
};
|
|
2328
|
+
}
|
|
2329
|
+
emitDebugQuery(phase, operation, target, meta, durationMs, error, inspection = null) {
|
|
2330
|
+
emitRuntimeDebugEvent({
|
|
2331
|
+
type: "query",
|
|
2332
|
+
phase,
|
|
2333
|
+
adapter: "prisma",
|
|
2334
|
+
operation,
|
|
2335
|
+
target: target.table,
|
|
2336
|
+
inspection,
|
|
2337
|
+
meta,
|
|
2338
|
+
durationMs,
|
|
2339
|
+
error
|
|
2340
|
+
});
|
|
2341
|
+
}
|
|
2342
|
+
wrapExecutionError(error, operation, target, meta) {
|
|
2343
|
+
if (error instanceof ArkormException) return error;
|
|
2344
|
+
return new QueryExecutionException(`Failed to execute ${operation} query.`, {
|
|
2345
|
+
operation: `adapter.${operation}`,
|
|
2346
|
+
model: target.modelName,
|
|
2347
|
+
delegate: target.table,
|
|
2348
|
+
meta,
|
|
2349
|
+
cause: error
|
|
2350
|
+
});
|
|
2351
|
+
}
|
|
2352
|
+
async runWithDebug(operation, target, executor, meta) {
|
|
2353
|
+
const startedAt = Date.now();
|
|
2354
|
+
this.emitDebugQuery("before", operation, target, meta);
|
|
2355
|
+
try {
|
|
2356
|
+
const result = await executor();
|
|
2357
|
+
this.emitDebugQuery("after", operation, target, meta, Date.now() - startedAt);
|
|
2358
|
+
return result;
|
|
2359
|
+
} catch (error) {
|
|
2360
|
+
const wrapped = this.wrapExecutionError(error, operation, target, meta);
|
|
2361
|
+
this.emitDebugQuery("error", operation, target, meta, Date.now() - startedAt, wrapped);
|
|
2362
|
+
throw wrapped;
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
inspectQuery(_request) {
|
|
2366
|
+
return null;
|
|
2367
|
+
}
|
|
2368
|
+
toQueryInclude(relationLoads) {
|
|
2369
|
+
if (!relationLoads || relationLoads.length === 0) return void 0;
|
|
2370
|
+
return relationLoads.reduce((include, plan) => {
|
|
2371
|
+
const nestedInclude = this.toQueryInclude(plan.relationLoads);
|
|
2372
|
+
const nestedSelect = this.toQuerySelect(plan.columns);
|
|
2373
|
+
const nestedWhere = this.toQueryWhere(plan.constraint);
|
|
2374
|
+
const nestedOrderBy = this.toQueryOrderBy(plan.orderBy);
|
|
2375
|
+
if (!nestedInclude && !nestedSelect && !nestedWhere && !nestedOrderBy && plan.offset === void 0 && plan.limit === void 0) {
|
|
2376
|
+
include[plan.relation] = true;
|
|
2377
|
+
return include;
|
|
2378
|
+
}
|
|
2379
|
+
include[plan.relation] = {
|
|
2380
|
+
where: nestedWhere,
|
|
2381
|
+
orderBy: nestedOrderBy,
|
|
2382
|
+
select: nestedSelect,
|
|
2383
|
+
include: nestedInclude,
|
|
2384
|
+
skip: plan.offset,
|
|
2385
|
+
take: plan.limit
|
|
2386
|
+
};
|
|
2387
|
+
return include;
|
|
2388
|
+
}, {});
|
|
2389
|
+
}
|
|
2390
|
+
async introspectModels(options = {}) {
|
|
2391
|
+
const runtimeDataModel = this.getRuntimeDataModel();
|
|
2392
|
+
if (!runtimeDataModel?.models) return [];
|
|
2393
|
+
const requestedTables = new Set(options.tables?.filter(Boolean) ?? []);
|
|
2394
|
+
const enums = runtimeDataModel.enums ?? {};
|
|
2395
|
+
return Object.entries(runtimeDataModel.models).flatMap(([name, model]) => {
|
|
2396
|
+
const table = model.dbName ?? `${str(name).camel().plural()}`;
|
|
2397
|
+
if (requestedTables.size > 0 && !requestedTables.has(table)) return [];
|
|
2398
|
+
return [{
|
|
2399
|
+
name,
|
|
2400
|
+
table,
|
|
2401
|
+
fields: (model.fields ?? []).filter((field) => field.kind !== "object").map((field) => {
|
|
2402
|
+
const enumValues = field.kind === "enum" ? (enums[field.type]?.values ?? []).map((value) => typeof value === "string" ? value : value.name ?? "").filter(Boolean) : null;
|
|
2403
|
+
const baseType = this.runtimeModelTypeToTs(field.type, field.kind, enumValues);
|
|
2404
|
+
return {
|
|
2405
|
+
name: field.name,
|
|
2406
|
+
type: field.isList ? `Array<${baseType}>` : baseType,
|
|
2407
|
+
nullable: field.isRequired === false
|
|
2408
|
+
};
|
|
2409
|
+
})
|
|
2410
|
+
}];
|
|
2411
|
+
});
|
|
2412
|
+
}
|
|
2413
|
+
resolveDelegate(target) {
|
|
2414
|
+
const tableName = target.table ? this.normalizeCandidate(target.table) : "";
|
|
2415
|
+
const singularTableName = tableName ? `${str(tableName).singular()}` : "";
|
|
2416
|
+
const camelTableName = tableName ? `${str(tableName).camel()}` : "";
|
|
2417
|
+
const camelSingularTableName = tableName ? `${str(tableName).camel().singular()}` : "";
|
|
2418
|
+
const candidates = this.unique([
|
|
2419
|
+
target.table ? this.mapping[target.table] : "",
|
|
2420
|
+
tableName,
|
|
2421
|
+
singularTableName ? this.mapping[singularTableName] : "",
|
|
2422
|
+
singularTableName,
|
|
2423
|
+
camelTableName ? this.mapping[camelTableName] : "",
|
|
2424
|
+
camelTableName,
|
|
2425
|
+
camelSingularTableName ? this.mapping[camelSingularTableName] : "",
|
|
2426
|
+
camelSingularTableName,
|
|
2427
|
+
target.modelName ? this.mapping[target.modelName] : "",
|
|
2428
|
+
target.modelName ? this.normalizeCandidate(target.modelName) : "",
|
|
2429
|
+
target.modelName ? inferDelegateName(target.modelName) : "",
|
|
2430
|
+
target.modelName ? this.mapping[inferDelegateName(target.modelName)] : ""
|
|
2431
|
+
]);
|
|
2432
|
+
const resolved = candidates.map((candidate) => this.delegates[candidate]).find(Boolean);
|
|
2433
|
+
if (resolved) return resolved;
|
|
2434
|
+
throw new MissingDelegateException("Prisma delegate could not be resolved for adapter target.", {
|
|
2435
|
+
operation: "getDelegate",
|
|
2436
|
+
model: target.modelName,
|
|
2437
|
+
delegate: target.table,
|
|
2438
|
+
meta: {
|
|
2439
|
+
target,
|
|
2440
|
+
candidates
|
|
2441
|
+
}
|
|
2442
|
+
});
|
|
2443
|
+
}
|
|
2444
|
+
/**
|
|
2445
|
+
* @todo Implement relationLoads by performing separate queries and merging results
|
|
2446
|
+
* in-memory, since Prisma does not support nested reads with constraints, ordering, or
|
|
2447
|
+
* pagination on related models as of now.
|
|
2448
|
+
*
|
|
2449
|
+
* @param spec
|
|
2450
|
+
* @returns
|
|
2451
|
+
*/
|
|
2452
|
+
async select(spec) {
|
|
2453
|
+
const delegate = this.resolveDelegate(spec.target);
|
|
2454
|
+
const args = this.buildFindArgs(spec);
|
|
2455
|
+
return await this.runWithDebug("select", spec.target, async () => {
|
|
2456
|
+
return await delegate.findMany(args);
|
|
2457
|
+
}, { args });
|
|
2458
|
+
}
|
|
2459
|
+
/**
|
|
2460
|
+
* Selects a single record matching the specified criteria.
|
|
2461
|
+
*
|
|
2462
|
+
* @param spec
|
|
2463
|
+
* @returns
|
|
2464
|
+
*/
|
|
2465
|
+
async selectOne(spec) {
|
|
2466
|
+
const delegate = this.resolveDelegate(spec.target);
|
|
2467
|
+
const args = this.buildFindArgs(spec);
|
|
2468
|
+
return await this.runWithDebug("selectOne", spec.target, async () => {
|
|
2469
|
+
return await delegate.findFirst(args);
|
|
2470
|
+
}, { args });
|
|
2471
|
+
}
|
|
2472
|
+
/**
|
|
2473
|
+
* Inserts a single record into the database and returns the created record.
|
|
2474
|
+
*
|
|
2475
|
+
* @param spec
|
|
2476
|
+
* @returns
|
|
2477
|
+
*/
|
|
2478
|
+
async insert(spec) {
|
|
2479
|
+
const delegate = this.resolveDelegate(spec.target);
|
|
2480
|
+
return await this.runWithDebug("insert", spec.target, async () => {
|
|
2481
|
+
return await delegate.create({ data: spec.values });
|
|
2482
|
+
}, { values: spec.values });
|
|
2483
|
+
}
|
|
2484
|
+
/**
|
|
2485
|
+
* Inserts multiple records into the database.
|
|
2486
|
+
*
|
|
2487
|
+
* @param spec
|
|
2488
|
+
* @returns
|
|
2489
|
+
*/
|
|
2490
|
+
async insertMany(spec) {
|
|
2491
|
+
const delegate = this.resolveDelegate(spec.target);
|
|
2492
|
+
const meta = {
|
|
2493
|
+
values: spec.values,
|
|
2494
|
+
ignoreDuplicates: spec.ignoreDuplicates
|
|
2495
|
+
};
|
|
2496
|
+
if (typeof delegate.createMany === "function") {
|
|
2497
|
+
const result = await this.runWithDebug("insertMany", spec.target, async () => {
|
|
2498
|
+
return await delegate.createMany?.({
|
|
2499
|
+
data: spec.values,
|
|
2500
|
+
skipDuplicates: spec.ignoreDuplicates
|
|
2501
|
+
});
|
|
2502
|
+
}, meta);
|
|
2503
|
+
if (typeof result === "number") return result;
|
|
2504
|
+
return typeof result?.count === "number" ? result.count : spec.values.length;
|
|
2505
|
+
}
|
|
2506
|
+
let inserted = 0;
|
|
2507
|
+
for (const values of spec.values) try {
|
|
2508
|
+
await delegate.create({ data: values });
|
|
2509
|
+
inserted += 1;
|
|
2510
|
+
} catch (error) {
|
|
2511
|
+
if (!spec.ignoreDuplicates) throw error;
|
|
2512
|
+
}
|
|
2513
|
+
return spec.ignoreDuplicates ? inserted : spec.values.length;
|
|
2514
|
+
}
|
|
2515
|
+
/**
|
|
2516
|
+
* Updates a single record matching the specified criteria and returns the updated record.
|
|
2517
|
+
*
|
|
2518
|
+
* @param spec
|
|
2519
|
+
* @returns
|
|
2520
|
+
*/
|
|
2521
|
+
async update(spec) {
|
|
2522
|
+
const delegate = this.resolveDelegate(spec.target);
|
|
2523
|
+
const where = this.toQueryWhere(spec.where);
|
|
2524
|
+
if (!where) return null;
|
|
2525
|
+
return await this.runWithDebug("update", spec.target, async () => {
|
|
2526
|
+
return await delegate.update({
|
|
2527
|
+
where,
|
|
2528
|
+
data: spec.values
|
|
2529
|
+
});
|
|
2530
|
+
}, {
|
|
2531
|
+
where,
|
|
2532
|
+
values: spec.values
|
|
2533
|
+
});
|
|
2534
|
+
}
|
|
2535
|
+
/**
|
|
2536
|
+
* Updates multiple records matching the specified criteria.
|
|
2537
|
+
*
|
|
2538
|
+
* @param spec
|
|
2539
|
+
* @returns
|
|
2540
|
+
*/
|
|
2541
|
+
async updateMany(spec) {
|
|
2542
|
+
const delegate = this.resolveDelegate(spec.target);
|
|
2543
|
+
const where = this.toQueryWhere(spec.where);
|
|
2544
|
+
const meta = {
|
|
2545
|
+
where,
|
|
2546
|
+
values: spec.values
|
|
2547
|
+
};
|
|
2548
|
+
if (typeof delegate.updateMany === "function") {
|
|
2549
|
+
const result = await this.runWithDebug("updateMany", spec.target, async () => {
|
|
2550
|
+
return await delegate.updateMany?.({
|
|
2551
|
+
where,
|
|
2552
|
+
data: spec.values
|
|
2553
|
+
});
|
|
2554
|
+
}, meta);
|
|
2555
|
+
if (typeof result === "number") return result;
|
|
2556
|
+
return typeof result?.count === "number" ? result.count : 0;
|
|
2557
|
+
}
|
|
2558
|
+
const rows = await delegate.findMany({ where });
|
|
2559
|
+
await Promise.all(rows.map(async (row) => {
|
|
2560
|
+
await delegate.update({
|
|
2561
|
+
where: row,
|
|
2562
|
+
data: spec.values
|
|
2563
|
+
});
|
|
2564
|
+
}));
|
|
2565
|
+
return rows.length;
|
|
2566
|
+
}
|
|
2567
|
+
/**
|
|
2568
|
+
* Deletes a single record matching the specified criteria and returns the deleted record.
|
|
2569
|
+
*
|
|
2570
|
+
* @param spec
|
|
2571
|
+
* @returns
|
|
2572
|
+
*/
|
|
2573
|
+
async delete(spec) {
|
|
2574
|
+
const delegate = this.resolveDelegate(spec.target);
|
|
2575
|
+
const where = this.toQueryWhere(spec.where);
|
|
2576
|
+
if (!where) return null;
|
|
2577
|
+
return await this.runWithDebug("delete", spec.target, async () => {
|
|
2578
|
+
return await delegate.delete({ where });
|
|
2579
|
+
}, { where });
|
|
2580
|
+
}
|
|
2581
|
+
/**
|
|
2582
|
+
* Deletes multiple records matching the specified criteria.
|
|
2583
|
+
*
|
|
2584
|
+
* @param spec
|
|
2585
|
+
* @returns
|
|
2586
|
+
*/
|
|
2587
|
+
async deleteMany(spec) {
|
|
2588
|
+
const delegate = this.resolveDelegate(spec.target);
|
|
2589
|
+
const where = this.toQueryWhere(spec.where);
|
|
2590
|
+
const rows = await this.runWithDebug("deleteMany", spec.target, async () => {
|
|
2591
|
+
return await delegate.findMany({ where });
|
|
2592
|
+
}, { where });
|
|
2593
|
+
await Promise.all(rows.map(async (row) => {
|
|
2594
|
+
await delegate.delete({ where: row });
|
|
2595
|
+
}));
|
|
2596
|
+
return rows.length;
|
|
2597
|
+
}
|
|
2598
|
+
/**
|
|
2599
|
+
* Counts the number of records matching the specified criteria.
|
|
2600
|
+
*
|
|
2601
|
+
* @param spec
|
|
2602
|
+
* @returns
|
|
2603
|
+
*/
|
|
2604
|
+
async count(spec) {
|
|
2605
|
+
const delegate = this.resolveDelegate(spec.target);
|
|
2606
|
+
const where = this.toQueryWhere(spec.where);
|
|
2607
|
+
return await this.runWithDebug("count", spec.target, async () => {
|
|
2608
|
+
return await delegate.count({ where });
|
|
2609
|
+
}, { where });
|
|
2610
|
+
}
|
|
2611
|
+
/**
|
|
2612
|
+
* Checks for the existence of records matching the specified criteria.
|
|
2613
|
+
*
|
|
2614
|
+
* @param spec
|
|
2615
|
+
* @returns
|
|
2616
|
+
*/
|
|
2617
|
+
async exists(spec) {
|
|
2618
|
+
return await this.selectOne({
|
|
2619
|
+
...spec,
|
|
2620
|
+
limit: 1
|
|
2621
|
+
}) != null;
|
|
2622
|
+
}
|
|
2623
|
+
/**
|
|
2624
|
+
* Loads related models for a batch of parent records based on the specified relation load plans.
|
|
2625
|
+
*
|
|
2626
|
+
* @param _spec
|
|
2627
|
+
*/
|
|
2628
|
+
async loadRelations(_spec) {
|
|
2629
|
+
throw new UnsupportedAdapterFeatureException("Relation batch loading is not supported by the Prisma compatibility adapter yet.", {
|
|
2630
|
+
operation: "adapter.loadRelations",
|
|
2631
|
+
meta: { feature: "relationLoads" }
|
|
2632
|
+
});
|
|
2633
|
+
}
|
|
2634
|
+
/**
|
|
2635
|
+
* Executes a series of database operations within a transaction.
|
|
2636
|
+
* If the underlying Prisma client does not support transactions, an exception is thrown.
|
|
2637
|
+
*
|
|
2638
|
+
* @param callback
|
|
2639
|
+
* @param context
|
|
2640
|
+
* @returns
|
|
2641
|
+
*/
|
|
2642
|
+
async transaction(callback, context = {}) {
|
|
2643
|
+
if (!this.hasTransactionSupport(this.prisma)) throw new UnsupportedAdapterFeatureException("Transactions are not supported by the Prisma compatibility adapter.", {
|
|
2644
|
+
operation: "adapter.transaction",
|
|
2645
|
+
meta: { feature: "transactions" }
|
|
2646
|
+
});
|
|
2647
|
+
return await this.prisma.$transaction(async (transactionClient) => {
|
|
2648
|
+
return await callback(new PrismaDatabaseAdapter(transactionClient, this.mapping));
|
|
2649
|
+
}, {
|
|
2650
|
+
isolationLevel: context.isolationLevel,
|
|
2651
|
+
maxWait: context.maxWait,
|
|
2652
|
+
timeout: context.timeout
|
|
2653
|
+
});
|
|
2654
|
+
}
|
|
2655
|
+
};
|
|
2656
|
+
|
|
1606
2657
|
//#endregion
|
|
1607
2658
|
//#region src/cli/CliApp.ts
|
|
1608
2659
|
/**
|
|
@@ -1624,6 +2675,9 @@ var CliApp = class {
|
|
|
1624
2675
|
* @returns The entire configuration object or the value of the specified key
|
|
1625
2676
|
*/
|
|
1626
2677
|
getConfig = getUserConfig;
|
|
2678
|
+
isUsingPrismaAdapter() {
|
|
2679
|
+
return this.getConfig("adapter") instanceof PrismaDatabaseAdapter;
|
|
2680
|
+
}
|
|
1627
2681
|
/**
|
|
1628
2682
|
* Utility to ensure directory exists
|
|
1629
2683
|
*
|
|
@@ -1836,14 +2890,16 @@ var CliApp = class {
|
|
|
1836
2890
|
const factoryName = `${baseName}Factory`;
|
|
1837
2891
|
const factoryPath = join$1(this.resolveConfigPath("factories", join$1(process.cwd(), "database", "factories")), `${factoryName}.${outputExt}`);
|
|
1838
2892
|
const factoryImportPath = `./${relative(dirname$1(outputPath), factoryPath).replace(/\\/g, "/").replace(/\.(ts|tsx|mts|cts|js|mjs|cjs)$/i, "")}${outputExt === "js" ? ".js" : ""}`;
|
|
1839
|
-
|
|
2893
|
+
let stubPath;
|
|
2894
|
+
if (options.pivot) stubPath = this.resolveStubPath("pivot-model.stub");
|
|
2895
|
+
else stubPath = this.resolveStubPath(outputExt === "js" ? "model.js.stub" : "model.stub");
|
|
1840
2896
|
const modelPath = this.generateFile(stubPath, outputPath, {
|
|
1841
2897
|
ModelName: modelName,
|
|
1842
2898
|
DelegateName: delegateName,
|
|
1843
2899
|
FactoryImport: shouldBuildFactory ? `import { ${factoryName} } from '${factoryImportPath}'\n` : "",
|
|
1844
2900
|
FactoryLink: shouldBuildFactory ? outputExt === "js" ? `\n static factoryClass = ${factoryName}` : `\n protected static override factoryClass = ${factoryName}` : ""
|
|
1845
2901
|
}, options);
|
|
1846
|
-
const prisma = this.ensurePrismaModelEntry(modelName, delegateName);
|
|
2902
|
+
const prisma = this.isUsingPrismaAdapter() ? this.ensurePrismaModelEntry(modelName, delegateName) : void 0;
|
|
1847
2903
|
const created = {
|
|
1848
2904
|
model: {
|
|
1849
2905
|
name: modelName,
|
|
@@ -1873,10 +2929,7 @@ var CliApp = class {
|
|
|
1873
2929
|
*/
|
|
1874
2930
|
ensurePrismaModelEntry(modelName, delegateName) {
|
|
1875
2931
|
const schemaPath = join$1(process.cwd(), "prisma", "schema.prisma");
|
|
1876
|
-
if (!existsSync$1(schemaPath)) return
|
|
1877
|
-
path: schemaPath,
|
|
1878
|
-
updated: false
|
|
1879
|
-
};
|
|
2932
|
+
if (!existsSync$1(schemaPath)) return void 0;
|
|
1880
2933
|
const source = readFileSync$1(schemaPath, "utf-8");
|
|
1881
2934
|
const existingByTable = findModelBlock(source, delegateName);
|
|
1882
2935
|
const existingByName = new RegExp(`model\\s+${modelName}\\s*\\{`, "m").test(source);
|
|
@@ -2135,6 +3188,35 @@ var CliApp = class {
|
|
|
2135
3188
|
skipped
|
|
2136
3189
|
};
|
|
2137
3190
|
}
|
|
3191
|
+
applyPersistedFieldMetadata(structure) {
|
|
3192
|
+
const persistedMetadata = getPersistedTableMetadata(structure.table, {
|
|
3193
|
+
features: resolvePersistedMetadataFeatures(this.getConfig("features")),
|
|
3194
|
+
strict: true
|
|
3195
|
+
});
|
|
3196
|
+
if (Object.keys(persistedMetadata.columns).length === 0 && Object.keys(persistedMetadata.enums).length === 0) return structure;
|
|
3197
|
+
const attributesByColumn = Object.entries(persistedMetadata.columns).reduce((all, [attribute, column]) => {
|
|
3198
|
+
all[column] = attribute;
|
|
3199
|
+
return all;
|
|
3200
|
+
}, {});
|
|
3201
|
+
return {
|
|
3202
|
+
...structure,
|
|
3203
|
+
fields: structure.fields.map((field) => {
|
|
3204
|
+
const logicalName = attributesByColumn[field.name] ?? field.name;
|
|
3205
|
+
const enumValues = persistedMetadata.enums[logicalName] ?? persistedMetadata.enums[field.name];
|
|
3206
|
+
if (!enumValues || enumValues.length === 0) return {
|
|
3207
|
+
...field,
|
|
3208
|
+
name: logicalName
|
|
3209
|
+
};
|
|
3210
|
+
const enumType = getPersistedEnumTsType(enumValues);
|
|
3211
|
+
const isArray = /^Array<.+>$/.test(field.type);
|
|
3212
|
+
return {
|
|
3213
|
+
...field,
|
|
3214
|
+
name: logicalName,
|
|
3215
|
+
type: isArray ? `Array<${enumType}>` : enumType
|
|
3216
|
+
};
|
|
3217
|
+
})
|
|
3218
|
+
};
|
|
3219
|
+
}
|
|
2138
3220
|
/**
|
|
2139
3221
|
* Parse Prisma enum definitions from a schema and return their member names.
|
|
2140
3222
|
*
|
|
@@ -2293,7 +3375,10 @@ var CliApp = class {
|
|
|
2293
3375
|
return all;
|
|
2294
3376
|
}, /* @__PURE__ */ new Map());
|
|
2295
3377
|
const discovered = await adapter.introspectModels({ tables: [...new Set([...sources.values()].map((source) => source.table))] });
|
|
2296
|
-
const structuresByTable = new Map(discovered.map((model) =>
|
|
3378
|
+
const structuresByTable = new Map(discovered.map((model) => {
|
|
3379
|
+
const enriched = this.applyPersistedFieldMetadata(model);
|
|
3380
|
+
return [enriched.table, enriched];
|
|
3381
|
+
}));
|
|
2297
3382
|
const result = this.syncModelFiles(modelFiles, (filePath) => {
|
|
2298
3383
|
const parsed = sources.get(filePath);
|
|
2299
3384
|
return parsed ? structuresByTable.get(parsed.table) : void 0;
|
|
@@ -2457,6 +3542,7 @@ var MakeModelCommand = class extends Command {
|
|
|
2457
3542
|
{--factory : Create and link a factory}
|
|
2458
3543
|
{--seeder : Create a seeder}
|
|
2459
3544
|
{--migration : Create a migration}
|
|
3545
|
+
{--p|pivot : Indicate the required model is an intermediate pivot model}
|
|
2460
3546
|
{--all : Create and link factory, seeder, and migration}
|
|
2461
3547
|
`;
|
|
2462
3548
|
description = "Create a new model and optional linked resources";
|
|
@@ -2470,14 +3556,13 @@ var MakeModelCommand = class extends Command {
|
|
|
2470
3556
|
const name = this.argument("name");
|
|
2471
3557
|
if (!name) return void this.error("Error: Name argument is required.");
|
|
2472
3558
|
const created = this.app.makeModel(name, this.options());
|
|
3559
|
+
const createdFiles = [["Model", created.model.path]];
|
|
3560
|
+
if (created.prisma) createdFiles.push([`Prisma schema ${created.prisma.updated ? "(updated)" : "(already up to date)"}`, created.prisma.path]);
|
|
3561
|
+
if (created.factory) createdFiles.push(["Factory", created.factory.path]);
|
|
3562
|
+
if (created.seeder) createdFiles.push(["Seeder", created.seeder.path]);
|
|
3563
|
+
if (created.migration) createdFiles.push(["Migration", created.migration.path]);
|
|
2473
3564
|
this.success("Created files:");
|
|
2474
|
-
[
|
|
2475
|
-
["Model", created.model.path],
|
|
2476
|
-
[`Prisma schema ${created.prisma.updated ? "(updated)" : "(already up to date)"}`, created.prisma.path],
|
|
2477
|
-
created.factory ? ["Factory", created.factory.path] : "",
|
|
2478
|
-
created.seeder ? ["Seeder", created.seeder.path] : "",
|
|
2479
|
-
created.migration ? ["Migration", created.migration.path] : ""
|
|
2480
|
-
].filter(Boolean).map(([name, path]) => this.success(this.app.splitLogger(name, path)));
|
|
3565
|
+
createdFiles.map(([fileType, path]) => this.success(this.app.splitLogger(fileType, path)));
|
|
2481
3566
|
}
|
|
2482
3567
|
};
|
|
2483
3568
|
|
|
@@ -2507,112 +3592,6 @@ var MakeSeederCommand = class extends Command {
|
|
|
2507
3592
|
}
|
|
2508
3593
|
};
|
|
2509
3594
|
|
|
2510
|
-
//#endregion
|
|
2511
|
-
//#region src/helpers/migration-history.ts
|
|
2512
|
-
const DEFAULT_STATE = {
|
|
2513
|
-
version: 1,
|
|
2514
|
-
migrations: [],
|
|
2515
|
-
runs: []
|
|
2516
|
-
};
|
|
2517
|
-
const resolveMigrationStateFilePath = (cwd, configuredPath) => {
|
|
2518
|
-
if (configuredPath && configuredPath.trim().length > 0) return resolve(configuredPath);
|
|
2519
|
-
return join(cwd, ".arkormx", "migrations.applied.json");
|
|
2520
|
-
};
|
|
2521
|
-
const buildMigrationIdentity = (filePath, className) => {
|
|
2522
|
-
const fileName = filePath.split("/").pop()?.split("\\").pop() ?? filePath;
|
|
2523
|
-
return `${fileName.slice(0, fileName.length - extname(fileName).length)}:${className}`;
|
|
2524
|
-
};
|
|
2525
|
-
const computeMigrationChecksum = (filePath) => {
|
|
2526
|
-
const source = readFileSync(filePath, "utf-8");
|
|
2527
|
-
return createHash("sha256").update(source).digest("hex");
|
|
2528
|
-
};
|
|
2529
|
-
const readAppliedMigrationsState = (stateFilePath) => {
|
|
2530
|
-
if (!existsSync(stateFilePath)) return { ...DEFAULT_STATE };
|
|
2531
|
-
try {
|
|
2532
|
-
const parsed = JSON.parse(readFileSync(stateFilePath, "utf-8"));
|
|
2533
|
-
if (!Array.isArray(parsed.migrations)) return { ...DEFAULT_STATE };
|
|
2534
|
-
return {
|
|
2535
|
-
version: 1,
|
|
2536
|
-
migrations: parsed.migrations.filter((migration) => {
|
|
2537
|
-
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");
|
|
2538
|
-
}),
|
|
2539
|
-
runs: Array.isArray(parsed.runs) ? parsed.runs.filter((run) => {
|
|
2540
|
-
return typeof run?.id === "string" && typeof run?.appliedAt === "string" && Array.isArray(run?.migrationIds) && run.migrationIds.every((item) => typeof item === "string");
|
|
2541
|
-
}) : []
|
|
2542
|
-
};
|
|
2543
|
-
} catch {
|
|
2544
|
-
return { ...DEFAULT_STATE };
|
|
2545
|
-
}
|
|
2546
|
-
};
|
|
2547
|
-
const writeAppliedMigrationsState = (stateFilePath, state) => {
|
|
2548
|
-
const directory = dirname(stateFilePath);
|
|
2549
|
-
if (!existsSync(directory)) mkdirSync(directory, { recursive: true });
|
|
2550
|
-
writeFileSync(stateFilePath, JSON.stringify(state, null, 2));
|
|
2551
|
-
};
|
|
2552
|
-
const isMigrationApplied = (state, identity, checksum) => {
|
|
2553
|
-
const matched = state.migrations.find((migration) => migration.id === identity);
|
|
2554
|
-
if (!matched) return false;
|
|
2555
|
-
if (checksum && matched.checksum) return matched.checksum === checksum;
|
|
2556
|
-
if (checksum && !matched.checksum) return false;
|
|
2557
|
-
return true;
|
|
2558
|
-
};
|
|
2559
|
-
const findAppliedMigration = (state, identity) => {
|
|
2560
|
-
return state.migrations.find((migration) => migration.id === identity);
|
|
2561
|
-
};
|
|
2562
|
-
const markMigrationApplied = (state, entry) => {
|
|
2563
|
-
const next = state.migrations.filter((migration) => migration.id !== entry.id);
|
|
2564
|
-
next.push(entry);
|
|
2565
|
-
return {
|
|
2566
|
-
version: 1,
|
|
2567
|
-
migrations: next,
|
|
2568
|
-
runs: state.runs ?? []
|
|
2569
|
-
};
|
|
2570
|
-
};
|
|
2571
|
-
const removeAppliedMigration = (state, identity) => {
|
|
2572
|
-
return {
|
|
2573
|
-
version: 1,
|
|
2574
|
-
migrations: state.migrations.filter((migration) => migration.id !== identity),
|
|
2575
|
-
runs: (state.runs ?? []).map((run) => ({
|
|
2576
|
-
...run,
|
|
2577
|
-
migrationIds: run.migrationIds.filter((id) => id !== identity)
|
|
2578
|
-
})).filter((run) => run.migrationIds.length > 0)
|
|
2579
|
-
};
|
|
2580
|
-
};
|
|
2581
|
-
const buildMigrationRunId = () => {
|
|
2582
|
-
return `run_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
2583
|
-
};
|
|
2584
|
-
const markMigrationRun = (state, run) => {
|
|
2585
|
-
const nextRuns = (state.runs ?? []).filter((existing) => existing.id !== run.id);
|
|
2586
|
-
nextRuns.push(run);
|
|
2587
|
-
return {
|
|
2588
|
-
version: 1,
|
|
2589
|
-
migrations: state.migrations,
|
|
2590
|
-
runs: nextRuns
|
|
2591
|
-
};
|
|
2592
|
-
};
|
|
2593
|
-
const getLastMigrationRun = (state) => {
|
|
2594
|
-
const runs = state.runs ?? [];
|
|
2595
|
-
if (runs.length === 0) return void 0;
|
|
2596
|
-
return runs.map((run, index) => ({
|
|
2597
|
-
run,
|
|
2598
|
-
index
|
|
2599
|
-
})).sort((left, right) => {
|
|
2600
|
-
const appliedAtOrder = right.run.appliedAt.localeCompare(left.run.appliedAt);
|
|
2601
|
-
if (appliedAtOrder !== 0) return appliedAtOrder;
|
|
2602
|
-
return right.index - left.index;
|
|
2603
|
-
})[0]?.run;
|
|
2604
|
-
};
|
|
2605
|
-
const getLatestAppliedMigrations = (state, steps) => {
|
|
2606
|
-
return state.migrations.map((migration, index) => ({
|
|
2607
|
-
migration,
|
|
2608
|
-
index
|
|
2609
|
-
})).sort((left, right) => {
|
|
2610
|
-
const appliedAtOrder = right.migration.appliedAt.localeCompare(left.migration.appliedAt);
|
|
2611
|
-
if (appliedAtOrder !== 0) return appliedAtOrder;
|
|
2612
|
-
return right.index - left.index;
|
|
2613
|
-
}).slice(0, Math.max(0, steps)).map((entry) => entry.migration);
|
|
2614
|
-
};
|
|
2615
|
-
|
|
2616
3595
|
//#endregion
|
|
2617
3596
|
//#region src/database/Migration.ts
|
|
2618
3597
|
const MIGRATION_BRAND = Symbol.for("arkormx.migration");
|
|
@@ -2667,7 +3646,10 @@ var MigrateCommand = class extends Command {
|
|
|
2667
3646
|
const classes = this.option("all") || !this.argument("name") ? await this.loadAllMigrations(migrationsDir) : (await this.loadNamedMigration(migrationsDir, this.argument("name"))).filter(([cls]) => cls !== void 0);
|
|
2668
3647
|
if (classes.length === 0) return void this.error("Error: No migration classes found to run.");
|
|
2669
3648
|
const stateFilePath = resolveMigrationStateFilePath(process.cwd(), this.option("state-file") ? String(this.option("state-file")) : void 0);
|
|
2670
|
-
let appliedState =
|
|
3649
|
+
let appliedState = await readAppliedMigrationsStateFromStore(this.app.getConfig("adapter"), stateFilePath);
|
|
3650
|
+
const adapter = this.app.getConfig("adapter");
|
|
3651
|
+
const useDatabaseMigrations = supportsDatabaseMigrationExecution(adapter);
|
|
3652
|
+
const persistedFeatures = resolvePersistedMetadataFeatures(this.app.getConfig("features"));
|
|
2671
3653
|
const skipped = [];
|
|
2672
3654
|
const changed = [];
|
|
2673
3655
|
const pending = classes.filter(([migrationClass, file]) => {
|
|
@@ -2686,13 +3668,31 @@ var MigrateCommand = class extends Command {
|
|
|
2686
3668
|
this.success(this.app.splitLogger("Changed", `${file} (${migrationClass.name})`));
|
|
2687
3669
|
});
|
|
2688
3670
|
if (pending.length === 0) {
|
|
3671
|
+
if (appliedState) try {
|
|
3672
|
+
await syncPersistedColumnMappingsFromState(process.cwd(), appliedState, await this.loadAllMigrations(migrationsDir), persistedFeatures);
|
|
3673
|
+
} catch (error) {
|
|
3674
|
+
this.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
3675
|
+
return;
|
|
3676
|
+
}
|
|
2689
3677
|
this.success("No pending migration classes to apply.");
|
|
2690
3678
|
return;
|
|
2691
3679
|
}
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
3680
|
+
if (useDatabaseMigrations) try {
|
|
3681
|
+
await validatePersistedMetadataFeaturesForMigrations(pending, persistedFeatures);
|
|
3682
|
+
} catch (error) {
|
|
3683
|
+
this.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
3684
|
+
return;
|
|
3685
|
+
}
|
|
3686
|
+
for (const [MigrationClassItem] of pending) {
|
|
3687
|
+
if (useDatabaseMigrations) {
|
|
3688
|
+
await applyMigrationToDatabase(adapter, MigrationClassItem);
|
|
3689
|
+
continue;
|
|
3690
|
+
}
|
|
3691
|
+
await applyMigrationToPrismaSchema(MigrationClassItem, {
|
|
3692
|
+
schemaPath,
|
|
3693
|
+
write: true
|
|
3694
|
+
});
|
|
3695
|
+
}
|
|
2696
3696
|
if (appliedState) {
|
|
2697
3697
|
const runAppliedIds = [];
|
|
2698
3698
|
for (const [migrationClass, file] of pending) {
|
|
@@ -2711,10 +3711,16 @@ var MigrateCommand = class extends Command {
|
|
|
2711
3711
|
appliedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2712
3712
|
migrationIds: runAppliedIds
|
|
2713
3713
|
});
|
|
2714
|
-
|
|
3714
|
+
await writeAppliedMigrationsStateToStore(adapter, stateFilePath, appliedState);
|
|
3715
|
+
try {
|
|
3716
|
+
await syncPersistedColumnMappingsFromState(process.cwd(), appliedState, await this.loadAllMigrations(migrationsDir), persistedFeatures);
|
|
3717
|
+
} catch (error) {
|
|
3718
|
+
this.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
3719
|
+
return;
|
|
3720
|
+
}
|
|
2715
3721
|
}
|
|
2716
|
-
if (!this.option("skip-generate")) runPrismaCommand(["generate"], process.cwd());
|
|
2717
|
-
if (!this.option("skip-migrate")) if (this.option("deploy")) runPrismaCommand(["migrate", "deploy"], process.cwd());
|
|
3722
|
+
if (!useDatabaseMigrations && !this.option("skip-generate")) runPrismaCommand(["generate"], process.cwd());
|
|
3723
|
+
if (!useDatabaseMigrations && !this.option("skip-migrate")) if (this.option("deploy")) runPrismaCommand(["migrate", "deploy"], process.cwd());
|
|
2718
3724
|
else runPrismaCommand([
|
|
2719
3725
|
"migrate",
|
|
2720
3726
|
"dev",
|
|
@@ -2774,6 +3780,103 @@ var MigrateCommand = class extends Command {
|
|
|
2774
3780
|
}
|
|
2775
3781
|
};
|
|
2776
3782
|
|
|
3783
|
+
//#endregion
|
|
3784
|
+
//#region src/cli/commands/MigrateFreshCommand.ts
|
|
3785
|
+
var MigrateFreshCommand = class extends Command {
|
|
3786
|
+
signature = `migrate:fresh
|
|
3787
|
+
{--skip-generate : Skip prisma generate}
|
|
3788
|
+
{--skip-migrate : Skip prisma database sync}
|
|
3789
|
+
{--state-file= : Path to applied migration state file}
|
|
3790
|
+
{--schema= : Explicit prisma schema path}
|
|
3791
|
+
`;
|
|
3792
|
+
description = "Reset the database and rerun all migration classes";
|
|
3793
|
+
async handle() {
|
|
3794
|
+
this.app.command = this;
|
|
3795
|
+
const configuredMigrationsDir = this.app.getConfig("paths")?.migrations ?? join(process.cwd(), "database", "migrations");
|
|
3796
|
+
const migrationsDir = this.app.resolveRuntimeDirectoryPath(configuredMigrationsDir);
|
|
3797
|
+
if (!existsSync(migrationsDir)) return void this.error(`Error: Migrations directory not found: ${this.app.formatPathForLog(configuredMigrationsDir)}`);
|
|
3798
|
+
const adapter = this.app.getConfig("adapter");
|
|
3799
|
+
const useDatabaseMigrations = supportsDatabaseMigrationExecution(adapter);
|
|
3800
|
+
const persistedFeatures = resolvePersistedMetadataFeatures(this.app.getConfig("features"));
|
|
3801
|
+
const schemaPath = this.option("schema") ? resolve(String(this.option("schema"))) : join(process.cwd(), "prisma", "schema.prisma");
|
|
3802
|
+
const stateFilePath = resolveMigrationStateFilePath(process.cwd(), this.option("state-file") ? String(this.option("state-file")) : void 0);
|
|
3803
|
+
const migrations = await this.loadAllMigrations(migrationsDir);
|
|
3804
|
+
if (migrations.length === 0) return void this.error("Error: No migration classes found to run.");
|
|
3805
|
+
if (useDatabaseMigrations) try {
|
|
3806
|
+
await validatePersistedMetadataFeaturesForMigrations(migrations, persistedFeatures);
|
|
3807
|
+
} catch (error) {
|
|
3808
|
+
this.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
3809
|
+
return;
|
|
3810
|
+
}
|
|
3811
|
+
if (useDatabaseMigrations) {
|
|
3812
|
+
if (!supportsDatabaseReset(adapter)) {
|
|
3813
|
+
this.error("Error: Your current database adapter does not support database reset.");
|
|
3814
|
+
return;
|
|
3815
|
+
}
|
|
3816
|
+
await adapter.resetDatabase();
|
|
3817
|
+
} else {
|
|
3818
|
+
if (!existsSync(schemaPath)) return void this.error(`Error: Prisma schema file not found: ${this.app.formatPathForLog(schemaPath)}`);
|
|
3819
|
+
writeFileSync(schemaPath, stripPrismaSchemaModelsAndEnums(readFileSync(schemaPath, "utf-8")));
|
|
3820
|
+
}
|
|
3821
|
+
let appliedState = createEmptyAppliedMigrationsState();
|
|
3822
|
+
await writeAppliedMigrationsStateToStore(adapter, stateFilePath, appliedState);
|
|
3823
|
+
for (const [MigrationClassItem] of migrations) {
|
|
3824
|
+
if (useDatabaseMigrations) {
|
|
3825
|
+
await applyMigrationToDatabase(adapter, MigrationClassItem);
|
|
3826
|
+
continue;
|
|
3827
|
+
}
|
|
3828
|
+
await applyMigrationToPrismaSchema(MigrationClassItem, {
|
|
3829
|
+
schemaPath,
|
|
3830
|
+
write: true
|
|
3831
|
+
});
|
|
3832
|
+
}
|
|
3833
|
+
for (const [migrationClass, file] of migrations) appliedState = markMigrationApplied(appliedState, {
|
|
3834
|
+
id: buildMigrationIdentity(file, migrationClass.name),
|
|
3835
|
+
file,
|
|
3836
|
+
className: migrationClass.name,
|
|
3837
|
+
appliedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3838
|
+
checksum: computeMigrationChecksum(file)
|
|
3839
|
+
});
|
|
3840
|
+
appliedState = markMigrationRun(appliedState, {
|
|
3841
|
+
id: buildMigrationRunId(),
|
|
3842
|
+
appliedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3843
|
+
migrationIds: appliedState.migrations.map((migration) => migration.id)
|
|
3844
|
+
});
|
|
3845
|
+
await writeAppliedMigrationsStateToStore(adapter, stateFilePath, appliedState);
|
|
3846
|
+
try {
|
|
3847
|
+
await syncPersistedColumnMappingsFromState(process.cwd(), appliedState, migrations, persistedFeatures);
|
|
3848
|
+
} catch (error) {
|
|
3849
|
+
this.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
3850
|
+
return;
|
|
3851
|
+
}
|
|
3852
|
+
if (!useDatabaseMigrations) {
|
|
3853
|
+
const schemaArgs = this.option("schema") ? ["--schema", schemaPath] : [];
|
|
3854
|
+
if (!this.option("skip-generate")) runPrismaCommand(["generate", ...schemaArgs], process.cwd());
|
|
3855
|
+
if (!this.option("skip-migrate")) runPrismaCommand([
|
|
3856
|
+
"db",
|
|
3857
|
+
"push",
|
|
3858
|
+
"--force-reset",
|
|
3859
|
+
...schemaArgs
|
|
3860
|
+
], process.cwd());
|
|
3861
|
+
}
|
|
3862
|
+
this.success(`Refreshed database with ${migrations.length} migration(s).`);
|
|
3863
|
+
migrations.forEach(([_, file]) => this.success(this.app.splitLogger("Migrated", file)));
|
|
3864
|
+
}
|
|
3865
|
+
async loadAllMigrations(migrationsDir) {
|
|
3866
|
+
const files = readdirSync(migrationsDir).filter((file) => /\.(ts|js|mjs|cjs)$/i.test(file)).sort((left, right) => left.localeCompare(right)).map((file) => this.app.resolveRuntimeScriptPath(join(migrationsDir, file)));
|
|
3867
|
+
return (await Promise.all(files.map(async (file) => (await this.loadMigrationClassesFromFile(file)).map((cls) => [cls, file])))).flat();
|
|
3868
|
+
}
|
|
3869
|
+
async loadMigrationClassesFromFile(filePath) {
|
|
3870
|
+
const imported = await RuntimeModuleLoader.load(filePath);
|
|
3871
|
+
return Object.values(imported).filter((value) => {
|
|
3872
|
+
if (typeof value !== "function") return false;
|
|
3873
|
+
const candidate = value;
|
|
3874
|
+
const prototype = candidate.prototype;
|
|
3875
|
+
return candidate[MIGRATION_BRAND] === true || typeof prototype?.up === "function" && typeof prototype?.down === "function";
|
|
3876
|
+
});
|
|
3877
|
+
}
|
|
3878
|
+
};
|
|
3879
|
+
|
|
2777
3880
|
//#endregion
|
|
2778
3881
|
//#region src/cli/commands/MigrateRollbackCommand.ts
|
|
2779
3882
|
/**
|
|
@@ -2802,7 +3905,10 @@ var MigrateRollbackCommand = class extends Command {
|
|
|
2802
3905
|
if (!existsSync(migrationsDir)) return void this.error(`Error: Migrations directory not found: ${this.app.formatPathForLog(configuredMigrationsDir)}`);
|
|
2803
3906
|
const schemaPath = this.option("schema") ? resolve(String(this.option("schema"))) : join(process.cwd(), "prisma", "schema.prisma");
|
|
2804
3907
|
const stateFilePath = resolveMigrationStateFilePath(process.cwd(), this.option("state-file") ? String(this.option("state-file")) : void 0);
|
|
2805
|
-
|
|
3908
|
+
const adapter = this.app.getConfig("adapter");
|
|
3909
|
+
const useDatabaseMigrations = supportsDatabaseMigrationExecution(adapter);
|
|
3910
|
+
const persistedFeatures = resolvePersistedMetadataFeatures(this.app.getConfig("features"));
|
|
3911
|
+
let appliedState = await readAppliedMigrationsStateFromStore(adapter, stateFilePath);
|
|
2806
3912
|
const stepOption = this.option("step");
|
|
2807
3913
|
const stepCount = stepOption == null ? void 0 : Number(stepOption);
|
|
2808
3914
|
if (stepCount != null && (!Number.isFinite(stepCount) || stepCount <= 0 || !Number.isInteger(stepCount))) return void this.error("Error: --step must be a positive integer.");
|
|
@@ -2824,17 +3930,29 @@ var MigrateRollbackCommand = class extends Command {
|
|
|
2824
3930
|
rollbackClasses.forEach(([_, file]) => this.success(this.app.splitLogger("WouldRollback", file)));
|
|
2825
3931
|
return;
|
|
2826
3932
|
}
|
|
2827
|
-
for (const [MigrationClassItem] of rollbackClasses)
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
3933
|
+
for (const [MigrationClassItem] of rollbackClasses) {
|
|
3934
|
+
if (useDatabaseMigrations) {
|
|
3935
|
+
await applyMigrationRollbackToDatabase(adapter, MigrationClassItem);
|
|
3936
|
+
continue;
|
|
3937
|
+
}
|
|
3938
|
+
await applyMigrationRollbackToPrismaSchema(MigrationClassItem, {
|
|
3939
|
+
schemaPath,
|
|
3940
|
+
write: true
|
|
3941
|
+
});
|
|
3942
|
+
}
|
|
2831
3943
|
for (const [migrationClass, file] of rollbackClasses) {
|
|
2832
3944
|
const identity = buildMigrationIdentity(file, migrationClass.name);
|
|
2833
3945
|
appliedState = removeAppliedMigration(appliedState, identity);
|
|
2834
3946
|
}
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
3947
|
+
await writeAppliedMigrationsStateToStore(adapter, stateFilePath, appliedState);
|
|
3948
|
+
try {
|
|
3949
|
+
await syncPersistedColumnMappingsFromState(process.cwd(), appliedState, available, persistedFeatures);
|
|
3950
|
+
} catch (error) {
|
|
3951
|
+
this.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
3952
|
+
return;
|
|
3953
|
+
}
|
|
3954
|
+
if (!useDatabaseMigrations && !this.option("skip-generate")) runPrismaCommand(["generate"], process.cwd());
|
|
3955
|
+
if (!useDatabaseMigrations && !this.option("skip-migrate")) if (this.option("deploy")) runPrismaCommand(["migrate", "deploy"], process.cwd());
|
|
2838
3956
|
else runPrismaCommand([
|
|
2839
3957
|
"migrate",
|
|
2840
3958
|
"dev",
|
|
@@ -2878,32 +3996,39 @@ var MigrationHistoryCommand = class extends Command {
|
|
|
2878
3996
|
async handle() {
|
|
2879
3997
|
this.app.command = this;
|
|
2880
3998
|
const stateFilePath = resolveMigrationStateFilePath(process.cwd(), this.option("state-file") ? String(this.option("state-file")) : void 0);
|
|
3999
|
+
const adapter = this.app.getConfig("adapter");
|
|
4000
|
+
const usesDatabaseState = supportsDatabaseMigrationState(adapter);
|
|
2881
4001
|
if (this.option("delete")) {
|
|
4002
|
+
if (usesDatabaseState) {
|
|
4003
|
+
await adapter.writeAppliedMigrationsState(createEmptyAppliedMigrationsState());
|
|
4004
|
+
deletePersistedColumnMappingsState(resolveColumnMappingsFilePath(process.cwd()));
|
|
4005
|
+
this.success("Deleted tracked migration state from database.");
|
|
4006
|
+
return;
|
|
4007
|
+
}
|
|
2882
4008
|
if (!existsSync(stateFilePath)) {
|
|
2883
4009
|
this.success(`No migration state file found at ${this.app.formatPathForLog(stateFilePath)}`);
|
|
2884
4010
|
return;
|
|
2885
4011
|
}
|
|
2886
4012
|
rmSync(stateFilePath);
|
|
4013
|
+
deletePersistedColumnMappingsState(resolveColumnMappingsFilePath(process.cwd()));
|
|
2887
4014
|
this.success(`Deleted migration state file: ${this.app.formatPathForLog(stateFilePath)}`);
|
|
2888
4015
|
return;
|
|
2889
4016
|
}
|
|
2890
4017
|
if (this.option("reset")) {
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
});
|
|
2895
|
-
this.success(`Reset migration state: ${this.app.formatPathForLog(stateFilePath)}`);
|
|
4018
|
+
await writeAppliedMigrationsStateToStore(adapter, stateFilePath, createEmptyAppliedMigrationsState());
|
|
4019
|
+
deletePersistedColumnMappingsState(resolveColumnMappingsFilePath(process.cwd()));
|
|
4020
|
+
this.success(usesDatabaseState ? "Reset migration state in database." : `Reset migration state: ${this.app.formatPathForLog(stateFilePath)}`);
|
|
2896
4021
|
return;
|
|
2897
4022
|
}
|
|
2898
|
-
const state =
|
|
4023
|
+
const state = await readAppliedMigrationsStateFromStore(adapter, stateFilePath);
|
|
2899
4024
|
if (this.option("json")) {
|
|
2900
4025
|
this.success(JSON.stringify({
|
|
2901
|
-
path: stateFilePath,
|
|
4026
|
+
path: usesDatabaseState ? "database" : stateFilePath,
|
|
2902
4027
|
...state
|
|
2903
4028
|
}, null, 2));
|
|
2904
4029
|
return;
|
|
2905
4030
|
}
|
|
2906
|
-
this.success(this.app.splitLogger("State", stateFilePath));
|
|
4031
|
+
this.success(this.app.splitLogger("State", usesDatabaseState ? "database" : stateFilePath));
|
|
2907
4032
|
this.success(this.app.splitLogger("Tracked", String(state.migrations.length)));
|
|
2908
4033
|
if (state.migrations.length === 0) {
|
|
2909
4034
|
this.success("No tracked migrations found.");
|
|
@@ -2925,10 +4050,16 @@ var ModelsSyncCommand = class extends Command {
|
|
|
2925
4050
|
description = "Sync model declare attributes from the active adapter when supported, otherwise fall back to the Prisma schema";
|
|
2926
4051
|
async handle() {
|
|
2927
4052
|
this.app.command = this;
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
4053
|
+
let result;
|
|
4054
|
+
try {
|
|
4055
|
+
result = await this.app.syncModels({
|
|
4056
|
+
schemaPath: this.option("schema") ? resolve(String(this.option("schema"))) : void 0,
|
|
4057
|
+
modelsDir: this.option("models") ? resolve(String(this.option("models"))) : void 0
|
|
4058
|
+
});
|
|
4059
|
+
} catch (error) {
|
|
4060
|
+
this.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
4061
|
+
return;
|
|
4062
|
+
}
|
|
2932
4063
|
const updatedLines = result.updated.length === 0 ? [this.app.splitLogger("Updated", "none")] : result.updated.map((path) => this.app.splitLogger("Updated", path));
|
|
2933
4064
|
this.success("SUCCESS: Model sync completed with the following results:");
|
|
2934
4065
|
[
|
|
@@ -3096,6 +4227,7 @@ await Kernel.init(app, {
|
|
|
3096
4227
|
ModelsSyncCommand,
|
|
3097
4228
|
SeedCommand,
|
|
3098
4229
|
MigrateCommand,
|
|
4230
|
+
MigrateFreshCommand,
|
|
3099
4231
|
MigrateRollbackCommand,
|
|
3100
4232
|
MigrationHistoryCommand
|
|
3101
4233
|
],
|