@c9up/eon 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -0
- package/dist/EonProvider.d.ts +51 -0
- package/dist/EonProvider.d.ts.map +1 -0
- package/dist/EonProvider.js +154 -0
- package/dist/EonProvider.js.map +1 -0
- package/dist/connection/EonConnection.d.ts +116 -0
- package/dist/connection/EonConnection.d.ts.map +1 -0
- package/dist/connection/EonConnection.js +15 -0
- package/dist/connection/EonConnection.js.map +1 -0
- package/dist/connection/config.d.ts +56 -0
- package/dist/connection/config.d.ts.map +1 -0
- package/dist/connection/config.js +14 -0
- package/dist/connection/config.js.map +1 -0
- package/dist/connection/websocket.d.ts +16 -0
- package/dist/connection/websocket.d.ts.map +1 -0
- package/dist/connection/websocket.js +383 -0
- package/dist/connection/websocket.js.map +1 -0
- package/dist/decorators/superTable.d.ts +73 -0
- package/dist/decorators/superTable.d.ts.map +1 -0
- package/dist/decorators/superTable.js +98 -0
- package/dist/decorators/superTable.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/ingest/schemaless.d.ts +22 -0
- package/dist/ingest/schemaless.d.ts.map +1 -0
- package/dist/ingest/schemaless.js +161 -0
- package/dist/ingest/schemaless.js.map +1 -0
- package/dist/ingest/sql.d.ts +23 -0
- package/dist/ingest/sql.d.ts.map +1 -0
- package/dist/ingest/sql.js +59 -0
- package/dist/ingest/sql.js.map +1 -0
- package/dist/ingest/stmt.d.ts +82 -0
- package/dist/ingest/stmt.d.ts.map +1 -0
- package/dist/ingest/stmt.js +246 -0
- package/dist/ingest/stmt.js.map +1 -0
- package/dist/metadata-keys.d.ts +17 -0
- package/dist/metadata-keys.d.ts.map +1 -0
- package/dist/metadata-keys.js +17 -0
- package/dist/metadata-keys.js.map +1 -0
- package/dist/query/TimeSeriesQuery.d.ts +121 -0
- package/dist/query/TimeSeriesQuery.d.ts.map +1 -0
- package/dist/query/TimeSeriesQuery.js +291 -0
- package/dist/query/TimeSeriesQuery.js.map +1 -0
- package/dist/query/native.d.ts +22 -0
- package/dist/query/native.d.ts.map +1 -0
- package/dist/query/native.js +135 -0
- package/dist/query/native.js.map +1 -0
- package/dist/repository/SuperTableRepository.d.ts +80 -0
- package/dist/repository/SuperTableRepository.d.ts.map +1 -0
- package/dist/repository/SuperTableRepository.js +232 -0
- package/dist/repository/SuperTableRepository.js.map +1 -0
- package/dist/schema/CreateStableSpec.d.ts +140 -0
- package/dist/schema/CreateStableSpec.d.ts.map +1 -0
- package/dist/schema/CreateStableSpec.js +41 -0
- package/dist/schema/CreateStableSpec.js.map +1 -0
- package/dist/schema/EonMigrationRunner.d.ts +71 -0
- package/dist/schema/EonMigrationRunner.d.ts.map +1 -0
- package/dist/schema/EonMigrationRunner.js +295 -0
- package/dist/schema/EonMigrationRunner.js.map +1 -0
- package/dist/schema/EonSchema.d.ts +128 -0
- package/dist/schema/EonSchema.d.ts.map +1 -0
- package/dist/schema/EonSchema.js +309 -0
- package/dist/schema/EonSchema.js.map +1 -0
- package/dist/schema/Migration.d.ts +47 -0
- package/dist/schema/Migration.d.ts.map +1 -0
- package/dist/schema/Migration.js +55 -0
- package/dist/schema/Migration.js.map +1 -0
- package/dist/schema/compile.d.ts +33 -0
- package/dist/schema/compile.d.ts.map +1 -0
- package/dist/schema/compile.js +70 -0
- package/dist/schema/compile.js.map +1 -0
- package/dist/schema/sync.d.ts +45 -0
- package/dist/schema/sync.d.ts.map +1 -0
- package/dist/schema/sync.js +102 -0
- package/dist/schema/sync.js.map +1 -0
- package/dist/services/connection.d.ts +33 -0
- package/dist/services/connection.d.ts.map +1 -0
- package/dist/services/connection.js +71 -0
- package/dist/services/connection.js.map +1 -0
- package/dist/testing/Factory.d.ts +43 -0
- package/dist/testing/Factory.d.ts.map +1 -0
- package/dist/testing/Factory.js +134 -0
- package/dist/testing/Factory.js.map +1 -0
- package/dist/testing/FakeEonConnection.d.ts +43 -0
- package/dist/testing/FakeEonConnection.d.ts.map +1 -0
- package/dist/testing/FakeEonConnection.js +315 -0
- package/dist/testing/FakeEonConnection.js.map +1 -0
- package/dist/testing/index.d.ts +34 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +67 -0
- package/dist/testing/index.js.map +1 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +65 -0
- package/scripts/copy-napi.mjs +72 -0
- package/src/EonProvider.ts +222 -0
- package/src/connection/EonConnection.ts +146 -0
- package/src/connection/config.ts +60 -0
- package/src/connection/websocket.ts +436 -0
- package/src/decorators/superTable.ts +169 -0
- package/src/index.ts +118 -0
- package/src/ingest/schemaless.ts +211 -0
- package/src/ingest/sql.ts +73 -0
- package/src/ingest/stmt.ts +335 -0
- package/src/metadata-keys.ts +20 -0
- package/src/query/TimeSeriesQuery.ts +480 -0
- package/src/query/native.ts +180 -0
- package/src/repository/SuperTableRepository.ts +347 -0
- package/src/schema/CreateStableSpec.ts +184 -0
- package/src/schema/EonMigrationRunner.ts +378 -0
- package/src/schema/EonSchema.ts +396 -0
- package/src/schema/Migration.ts +66 -0
- package/src/schema/compile.ts +109 -0
- package/src/schema/sync.ts +142 -0
- package/src/services/connection.ts +89 -0
- package/src/testing/Factory.ts +186 -0
- package/src/testing/FakeEonConnection.ts +351 -0
- package/src/testing/index.ts +81 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `EonMigrationRunner` — discovers, executes, and tracks eon (TDengine)
|
|
3
|
+
* migrations. Mirrors atlas `MigrationRunner` (`schema/MigrationRunner.ts`):
|
|
4
|
+
* sorted-file discovery, a `ream_`-prefixed tracking table, batch-based
|
|
5
|
+
* rollback, `init/status/migrate/rollback/reset/refresh/fresh/dryRun`.
|
|
6
|
+
*
|
|
7
|
+
* ── TWO named TDengine deviations from atlas (AC6) ──────────────────────────
|
|
8
|
+
*
|
|
9
|
+
* 1. **No transactions / no engine rollback.** TDengine DDL is non-transactional
|
|
10
|
+
* — a batch CANNOT be applied atomically. The runner executes statements
|
|
11
|
+
* **sequentially**; a mid-migration failure leaves earlier statements
|
|
12
|
+
* applied. The mitigation is idempotent DDL (`IF (NOT) EXISTS`, the
|
|
13
|
+
* `EonSchema` default) so a re-run converges. There is NO `runInTransaction`.
|
|
14
|
+
*
|
|
15
|
+
* 2. **No `UNIQUE`, no auto-increment.** The tracking table is a basic table
|
|
16
|
+
* `ream_eon_migrations(executed_at TIMESTAMP, name VARCHAR(255), batch INT)`;
|
|
17
|
+
* the applied-set is de-duped **by name in JS**, and each record is written
|
|
18
|
+
* with a strictly-increasing `executed_at` so rollback can delete it by that
|
|
19
|
+
* unique timestamp key (TDengine only allows a `DELETE` predicate on the
|
|
20
|
+
* primary timestamp column).
|
|
21
|
+
*
|
|
22
|
+
* Agnostic leaf: takes the connection structurally and a plain directory path —
|
|
23
|
+
* no `@c9up/ream` import, no app helper (`project_package_extraction`).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import * as fsp from "node:fs/promises";
|
|
27
|
+
import * as path from "node:path";
|
|
28
|
+
import { pathToFileURL } from "node:url";
|
|
29
|
+
import type { EonConnection } from "../connection/EonConnection.js";
|
|
30
|
+
import { compileStatementNative } from "../query/native.js";
|
|
31
|
+
import type { Migration } from "./Migration.js";
|
|
32
|
+
|
|
33
|
+
const DEFAULT_DIR = "database/eon-migrations";
|
|
34
|
+
const DEFAULT_TABLE = "ream_eon_migrations";
|
|
35
|
+
const TABLE_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
36
|
+
/** Migration filename (no extension): no path separators, no `..`. */
|
|
37
|
+
const MIGRATION_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
|
|
38
|
+
|
|
39
|
+
export interface EonMigrationOptions {
|
|
40
|
+
/** Directory holding migration files. Default `database/eon-migrations`. */
|
|
41
|
+
migrationsDir?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Tracking-table name. Default `ream_eon_migrations`. Must match
|
|
44
|
+
* `/^[A-Za-z_][A-Za-z0-9_]*$/`. The `ream_` prefix marks it a framework/system
|
|
45
|
+
* table (`feedback_underscore_policy`) — keep it so cleanup helpers skip it.
|
|
46
|
+
*/
|
|
47
|
+
tableName?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type MigrationState = "applied" | "pending";
|
|
51
|
+
|
|
52
|
+
export interface MigrationStatus {
|
|
53
|
+
name: string;
|
|
54
|
+
state: MigrationState;
|
|
55
|
+
batch?: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** One tracking record row as read back from the connection. */
|
|
59
|
+
interface MigrationRecord {
|
|
60
|
+
name: string;
|
|
61
|
+
batch: number;
|
|
62
|
+
executed_at: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class EonMigrationRunner {
|
|
66
|
+
readonly #conn: EonConnection;
|
|
67
|
+
readonly #dir: string;
|
|
68
|
+
readonly #table: string;
|
|
69
|
+
|
|
70
|
+
constructor(conn: EonConnection, options: EonMigrationOptions = {}) {
|
|
71
|
+
this.#conn = conn;
|
|
72
|
+
this.#dir = options.migrationsDir ?? DEFAULT_DIR;
|
|
73
|
+
const table = options.tableName ?? DEFAULT_TABLE;
|
|
74
|
+
if (!TABLE_NAME_PATTERN.test(table)) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`[E_EON_MIGRATION_INVALID] invalid tracking-table name '${table}'; must match ${TABLE_NAME_PATTERN}`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
this.#table = table;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Create the tracking table (`IF NOT EXISTS`) via the basic-table DDL path. */
|
|
83
|
+
async init(): Promise<void> {
|
|
84
|
+
const { statements } = compileStatementNative(
|
|
85
|
+
{
|
|
86
|
+
kind: "createTable",
|
|
87
|
+
name: this.#table,
|
|
88
|
+
ifNotExists: true,
|
|
89
|
+
columns: [
|
|
90
|
+
{
|
|
91
|
+
name: "executed_at",
|
|
92
|
+
kind: "timestamp",
|
|
93
|
+
length: null,
|
|
94
|
+
precision: null,
|
|
95
|
+
scale: null,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: "name",
|
|
99
|
+
kind: "varchar",
|
|
100
|
+
length: 255,
|
|
101
|
+
precision: null,
|
|
102
|
+
scale: null,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: "batch",
|
|
106
|
+
kind: "int",
|
|
107
|
+
length: null,
|
|
108
|
+
precision: null,
|
|
109
|
+
scale: null,
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
"tdengine",
|
|
114
|
+
);
|
|
115
|
+
for (const sql of statements) await this.#conn.exec(sql);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The status of every discovered migration (applied vs pending). */
|
|
119
|
+
async status(): Promise<MigrationStatus[]> {
|
|
120
|
+
await this.init();
|
|
121
|
+
const applied = await this.#appliedRecords();
|
|
122
|
+
const byName = new Map(applied.map((r) => [r.name, r.batch]));
|
|
123
|
+
const files = await this.#discoverFiles();
|
|
124
|
+
return files.map((name) => {
|
|
125
|
+
const batch = byName.get(name);
|
|
126
|
+
return batch === undefined
|
|
127
|
+
? { name, state: "pending" }
|
|
128
|
+
: { name, state: "applied", batch };
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Run every pending migration (filename order), recording each. */
|
|
133
|
+
async migrate(): Promise<string[]> {
|
|
134
|
+
await this.init();
|
|
135
|
+
const applied = await this.#appliedRecords();
|
|
136
|
+
const appliedNames = new Set(applied.map((r) => r.name));
|
|
137
|
+
const files = await this.#discoverFiles();
|
|
138
|
+
const pending = files.filter((f) => !appliedNames.has(f));
|
|
139
|
+
if (pending.length === 0) return [];
|
|
140
|
+
|
|
141
|
+
const batch = this.#maxBatch(applied) + 1;
|
|
142
|
+
// Strictly-increasing executed_at (unique key for rollback deletes) — never
|
|
143
|
+
// below any existing record, never colliding within this run.
|
|
144
|
+
let clock = Math.max(Date.now(), this.#maxExecutedAt(applied) + 1);
|
|
145
|
+
const executed: string[] = [];
|
|
146
|
+
|
|
147
|
+
for (const name of pending) {
|
|
148
|
+
const migration = await this.#loadMigration(name);
|
|
149
|
+
for (const sql of await migration.getUpSQL()) {
|
|
150
|
+
await this.#conn.exec(sql);
|
|
151
|
+
}
|
|
152
|
+
await this.#writeRecord(name, batch, clock);
|
|
153
|
+
clock += 1;
|
|
154
|
+
executed.push(name);
|
|
155
|
+
}
|
|
156
|
+
return executed;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Roll back the most-recent batch (files in reverse order), running `down()`. */
|
|
160
|
+
async rollback(): Promise<string[]> {
|
|
161
|
+
await this.init();
|
|
162
|
+
const applied = await this.#appliedRecords();
|
|
163
|
+
if (applied.length === 0) return [];
|
|
164
|
+
const batch = this.#maxBatch(applied);
|
|
165
|
+
|
|
166
|
+
const inBatch = new Map(
|
|
167
|
+
applied.filter((r) => r.batch === batch).map((r) => [r.name, r]),
|
|
168
|
+
);
|
|
169
|
+
const files = await this.#discoverFiles();
|
|
170
|
+
// Reverse of application order = reverse of filename order within the batch.
|
|
171
|
+
const ordered = files.filter((f) => inBatch.has(f)).reverse();
|
|
172
|
+
|
|
173
|
+
const rolled: string[] = [];
|
|
174
|
+
for (const name of ordered) {
|
|
175
|
+
const record = inBatch.get(name);
|
|
176
|
+
if (record === undefined) continue;
|
|
177
|
+
const migration = await this.#loadMigration(name);
|
|
178
|
+
for (const sql of await migration.getDownSQL()) {
|
|
179
|
+
await this.#conn.exec(sql);
|
|
180
|
+
}
|
|
181
|
+
await this.#deleteRecord(record.executed_at);
|
|
182
|
+
rolled.push(name);
|
|
183
|
+
}
|
|
184
|
+
return rolled;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Roll back every applied batch (Lucid `migrate:reset`). */
|
|
188
|
+
async reset(): Promise<string[]> {
|
|
189
|
+
await this.init();
|
|
190
|
+
const all: string[] = [];
|
|
191
|
+
// Guard against a stuck loop: each rollback must shrink the applied set.
|
|
192
|
+
for (;;) {
|
|
193
|
+
const rolled = await this.rollback();
|
|
194
|
+
if (rolled.length === 0) break;
|
|
195
|
+
all.push(...rolled);
|
|
196
|
+
}
|
|
197
|
+
return all;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Reset then re-run every migration (Lucid `migrate:refresh`). */
|
|
201
|
+
async refresh(): Promise<{ rolled: string[]; executed: string[] }> {
|
|
202
|
+
const rolled = await this.reset();
|
|
203
|
+
const executed = await this.migrate();
|
|
204
|
+
return { rolled, executed };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Alias of {@link refresh} (Lucid `migrate:fresh`). */
|
|
208
|
+
fresh(): Promise<{ rolled: string[]; executed: string[] }> {
|
|
209
|
+
return this.refresh();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Compute the SQL each pending migration would emit, WITHOUT executing it. */
|
|
213
|
+
async dryRun(): Promise<Array<{ name: string; sql: string[] }>> {
|
|
214
|
+
await this.init();
|
|
215
|
+
const applied = await this.#appliedRecords();
|
|
216
|
+
const appliedNames = new Set(applied.map((r) => r.name));
|
|
217
|
+
const pending = (await this.#discoverFiles()).filter(
|
|
218
|
+
(f) => !appliedNames.has(f),
|
|
219
|
+
);
|
|
220
|
+
const result: Array<{ name: string; sql: string[] }> = [];
|
|
221
|
+
for (const name of pending) {
|
|
222
|
+
const migration = await this.#loadMigration(name);
|
|
223
|
+
result.push({ name, sql: await migration.getUpSQL() });
|
|
224
|
+
}
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Read every tracking record (name, batch, executed_at). */
|
|
229
|
+
async #appliedRecords(): Promise<MigrationRecord[]> {
|
|
230
|
+
const { statements } = compileStatementNative(
|
|
231
|
+
{
|
|
232
|
+
kind: "select",
|
|
233
|
+
table: this.#table,
|
|
234
|
+
select: ["name", "batch", "executed_at"],
|
|
235
|
+
},
|
|
236
|
+
"tdengine",
|
|
237
|
+
);
|
|
238
|
+
const sql = statements[0];
|
|
239
|
+
if (sql === undefined) return [];
|
|
240
|
+
const rows = await this.#conn.query<{
|
|
241
|
+
name: unknown;
|
|
242
|
+
batch: unknown;
|
|
243
|
+
executed_at: unknown;
|
|
244
|
+
}>(sql);
|
|
245
|
+
return rows.map((r) => ({
|
|
246
|
+
name: String(r.name),
|
|
247
|
+
batch: Number(r.batch),
|
|
248
|
+
executed_at: Number(r.executed_at),
|
|
249
|
+
}));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
#maxBatch(records: readonly MigrationRecord[]): number {
|
|
253
|
+
return records.reduce((max, r) => Math.max(max, r.batch), 0);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
#maxExecutedAt(records: readonly MigrationRecord[]): number {
|
|
257
|
+
return records.reduce((max, r) => Math.max(max, r.executed_at), 0);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Insert a tracking record via the literal INSERT compiler (never TS interpolation). */
|
|
261
|
+
async #writeRecord(
|
|
262
|
+
name: string,
|
|
263
|
+
batch: number,
|
|
264
|
+
executedAt: number,
|
|
265
|
+
): Promise<void> {
|
|
266
|
+
const { statements } = compileStatementNative(
|
|
267
|
+
{
|
|
268
|
+
kind: "insert",
|
|
269
|
+
table: this.#table,
|
|
270
|
+
columns: ["executed_at", "name", "batch"],
|
|
271
|
+
rows: [[executedAt, name, batch]],
|
|
272
|
+
literal: true,
|
|
273
|
+
},
|
|
274
|
+
"tdengine",
|
|
275
|
+
);
|
|
276
|
+
for (const sql of statements) await this.#conn.exec(sql);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Delete a tracking record by its unique `executed_at` key (compiled in Rust). */
|
|
280
|
+
async #deleteRecord(executedAt: number): Promise<void> {
|
|
281
|
+
const { statements } = compileStatementNative(
|
|
282
|
+
{
|
|
283
|
+
kind: "delete",
|
|
284
|
+
table: this.#table,
|
|
285
|
+
column: "executed_at",
|
|
286
|
+
value: executedAt,
|
|
287
|
+
},
|
|
288
|
+
"tdengine",
|
|
289
|
+
);
|
|
290
|
+
for (const sql of statements) await this.#conn.exec(sql);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Discover migration files (`.ts`/`.js`), sorted, extension stripped. */
|
|
294
|
+
async #discoverFiles(): Promise<string[]> {
|
|
295
|
+
try {
|
|
296
|
+
const entries = await fsp.readdir(this.#dir);
|
|
297
|
+
return entries
|
|
298
|
+
.filter((f) => f.endsWith(".ts") || f.endsWith(".js"))
|
|
299
|
+
.sort()
|
|
300
|
+
.map((f) => f.replace(/\.(ts|js)$/, ""));
|
|
301
|
+
} catch (err) {
|
|
302
|
+
// A missing migrations directory → no migrations. Anything else propagates.
|
|
303
|
+
if (
|
|
304
|
+
typeof err === "object" &&
|
|
305
|
+
err !== null &&
|
|
306
|
+
Reflect.get(err, "code") === "ENOENT"
|
|
307
|
+
) {
|
|
308
|
+
return [];
|
|
309
|
+
}
|
|
310
|
+
throw err;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Load + instantiate a migration's default-export class. */
|
|
315
|
+
async #loadMigration(name: string): Promise<Migration> {
|
|
316
|
+
if (!MIGRATION_NAME_PATTERN.test(name) || name.includes("..")) {
|
|
317
|
+
throw new Error(
|
|
318
|
+
`[E_EON_MIGRATION_INVALID] unsafe migration name '${name}'`,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
const tsPath = path.join(this.#dir, `${name}.ts`);
|
|
322
|
+
const jsPath = path.join(this.#dir, `${name}.js`);
|
|
323
|
+
const tsExists = await pathExists(tsPath);
|
|
324
|
+
// Fail loud with the runner's own error (atlas parity) instead of letting
|
|
325
|
+
// `import` throw a raw ERR_MODULE_NOT_FOUND on a missing/renamed file.
|
|
326
|
+
if (!tsExists && !(await pathExists(jsPath))) {
|
|
327
|
+
throw new Error(
|
|
328
|
+
`[E_EON_MIGRATION_INVALID] migration '${name}' not found (no .ts or .js file in ${this.#dir})`,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
const filePath = tsExists ? tsPath : jsPath;
|
|
332
|
+
// ESM dynamic import needs a file:// URL (Windows rejects a bare path).
|
|
333
|
+
const mod: { default?: unknown } = await import(
|
|
334
|
+
pathToFileURL(path.resolve(filePath)).href
|
|
335
|
+
);
|
|
336
|
+
const MigrationClass = mod.default;
|
|
337
|
+
if (!isConstructor(MigrationClass)) {
|
|
338
|
+
throw new Error(
|
|
339
|
+
`[E_EON_MIGRATION_INVALID] migration '${name}' must export a default class`,
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
const instance: unknown = new MigrationClass();
|
|
343
|
+
if (!isMigration(instance)) {
|
|
344
|
+
throw new Error(
|
|
345
|
+
`[E_EON_MIGRATION_INVALID] migration '${name}' default export is not a Migration (missing up/down)`,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
return instance;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** A no-arg constructor — narrows an `unknown` default export before `new`. */
|
|
353
|
+
type NoArgConstructor = new () => unknown;
|
|
354
|
+
|
|
355
|
+
function isConstructor(value: unknown): value is NoArgConstructor {
|
|
356
|
+
return typeof value === "function";
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Structural Migration guard — avoids importing the class value for an
|
|
361
|
+
* `instanceof`. Reads through the prototype chain (methods live on the
|
|
362
|
+
* prototype, not as own enumerable props) via `Reflect.get`.
|
|
363
|
+
*/
|
|
364
|
+
function isMigration(value: unknown): value is Migration {
|
|
365
|
+
if (typeof value !== "object" || value === null) return false;
|
|
366
|
+
const up = Reflect.get(value, "getUpSQL");
|
|
367
|
+
const down = Reflect.get(value, "getDownSQL");
|
|
368
|
+
return typeof up === "function" && typeof down === "function";
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function pathExists(p: string): Promise<boolean> {
|
|
372
|
+
try {
|
|
373
|
+
await fsp.access(p);
|
|
374
|
+
return true;
|
|
375
|
+
} catch {
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
}
|