@digilogiclabs/platform-core 1.0.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 +545 -0
- package/dist/ConsoleEmail-XeUBAnwc.d.mts +2786 -0
- package/dist/ConsoleEmail-XeUBAnwc.d.ts +2786 -0
- package/dist/index-C_2W7Byw.d.mts +379 -0
- package/dist/index-C_2W7Byw.d.ts +379 -0
- package/dist/index.d.mts +2045 -0
- package/dist/index.d.ts +2045 -0
- package/dist/index.js +6690 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +6550 -0
- package/dist/index.mjs.map +1 -0
- package/dist/migrate.js +1101 -0
- package/dist/migrate.js.map +1 -0
- package/dist/migrations/index.d.mts +1 -0
- package/dist/migrations/index.d.ts +1 -0
- package/dist/migrations/index.js +508 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/migrations/index.mjs +468 -0
- package/dist/migrations/index.mjs.map +1 -0
- package/dist/testing.d.mts +97 -0
- package/dist/testing.d.ts +97 -0
- package/dist/testing.js +1789 -0
- package/dist/testing.js.map +1 -0
- package/dist/testing.mjs +1743 -0
- package/dist/testing.mjs.map +1 -0
- package/package.json +152 -0
package/dist/migrate.js
ADDED
|
@@ -0,0 +1,1101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
#!/usr/bin/env node
|
|
3
|
+
"use strict";
|
|
4
|
+
var __create = Object.create;
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
7
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
8
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
9
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
10
|
+
var __esm = (fn, res) => function __init() {
|
|
11
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
12
|
+
};
|
|
13
|
+
var __export = (target, all) => {
|
|
14
|
+
for (var name in all)
|
|
15
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
16
|
+
};
|
|
17
|
+
var __copyProps = (to, from, except, desc) => {
|
|
18
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
19
|
+
for (let key of __getOwnPropNames(from))
|
|
20
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
21
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
22
|
+
}
|
|
23
|
+
return to;
|
|
24
|
+
};
|
|
25
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
26
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
27
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
28
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
29
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
30
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
31
|
+
mod
|
|
32
|
+
));
|
|
33
|
+
|
|
34
|
+
// src/adapters/postgres/PostgresDatabase.ts
|
|
35
|
+
var PostgresDatabase_exports = {};
|
|
36
|
+
__export(PostgresDatabase_exports, {
|
|
37
|
+
PostgresDatabase: () => PostgresDatabase
|
|
38
|
+
});
|
|
39
|
+
function toPoolConfig(config) {
|
|
40
|
+
const poolConfig = {};
|
|
41
|
+
if (config.connectionString) {
|
|
42
|
+
poolConfig.connectionString = config.connectionString;
|
|
43
|
+
} else {
|
|
44
|
+
poolConfig.host = config.host;
|
|
45
|
+
poolConfig.port = config.port;
|
|
46
|
+
poolConfig.database = config.database;
|
|
47
|
+
poolConfig.user = config.user;
|
|
48
|
+
poolConfig.password = config.password;
|
|
49
|
+
}
|
|
50
|
+
if (config.ssl !== void 0) {
|
|
51
|
+
poolConfig.ssl = config.ssl;
|
|
52
|
+
}
|
|
53
|
+
poolConfig.max = config.max ?? 10;
|
|
54
|
+
poolConfig.idleTimeoutMillis = config.idleTimeoutMillis ?? 3e4;
|
|
55
|
+
poolConfig.connectionTimeoutMillis = config.connectionTimeoutMillis ?? 1e4;
|
|
56
|
+
poolConfig.application_name = config.applicationName ?? "platform-core";
|
|
57
|
+
if (config.statementTimeout) {
|
|
58
|
+
poolConfig.statement_timeout = config.statementTimeout;
|
|
59
|
+
}
|
|
60
|
+
if (config.queryTimeout) {
|
|
61
|
+
poolConfig.query_timeout = config.queryTimeout;
|
|
62
|
+
}
|
|
63
|
+
return poolConfig;
|
|
64
|
+
}
|
|
65
|
+
var PostgresDatabase, TransactionDatabase, PostgresQueryBuilder;
|
|
66
|
+
var init_PostgresDatabase = __esm({
|
|
67
|
+
"src/adapters/postgres/PostgresDatabase.ts"() {
|
|
68
|
+
"use strict";
|
|
69
|
+
PostgresDatabase = class _PostgresDatabase {
|
|
70
|
+
pool;
|
|
71
|
+
config;
|
|
72
|
+
constructor(pool, config = {}) {
|
|
73
|
+
this.pool = pool;
|
|
74
|
+
this.config = config;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Create a PostgresDatabase from configuration
|
|
78
|
+
*/
|
|
79
|
+
static async create(config) {
|
|
80
|
+
const { Pool } = await import("pg");
|
|
81
|
+
const poolConfig = toPoolConfig(config);
|
|
82
|
+
const pool = new Pool(poolConfig);
|
|
83
|
+
const client = await pool.connect();
|
|
84
|
+
client.release();
|
|
85
|
+
return new _PostgresDatabase(pool, config);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Create from environment variables
|
|
89
|
+
*/
|
|
90
|
+
static async fromEnv() {
|
|
91
|
+
const config = {
|
|
92
|
+
connectionString: process.env.DATABASE_URL,
|
|
93
|
+
host: process.env.POSTGRES_HOST ?? process.env.PGHOST,
|
|
94
|
+
port: parseInt(process.env.POSTGRES_PORT ?? process.env.PGPORT ?? "5432", 10),
|
|
95
|
+
database: process.env.POSTGRES_DATABASE ?? process.env.PGDATABASE,
|
|
96
|
+
user: process.env.POSTGRES_USER ?? process.env.PGUSER,
|
|
97
|
+
password: process.env.POSTGRES_PASSWORD ?? process.env.PGPASSWORD,
|
|
98
|
+
max: parseInt(process.env.POSTGRES_POOL_MAX ?? "10", 10),
|
|
99
|
+
ssl: process.env.POSTGRES_SSL === "true" ? { rejectUnauthorized: process.env.POSTGRES_SSL_REJECT_UNAUTHORIZED !== "false" } : void 0,
|
|
100
|
+
applicationName: process.env.POSTGRES_APP_NAME ?? "platform-core"
|
|
101
|
+
};
|
|
102
|
+
return _PostgresDatabase.create(config);
|
|
103
|
+
}
|
|
104
|
+
from(table) {
|
|
105
|
+
return new PostgresQueryBuilder(this.pool, table);
|
|
106
|
+
}
|
|
107
|
+
async raw(sql, params) {
|
|
108
|
+
try {
|
|
109
|
+
const result = await this.pool.query(sql, params);
|
|
110
|
+
return {
|
|
111
|
+
data: result.rows,
|
|
112
|
+
count: result.rowCount ?? void 0
|
|
113
|
+
};
|
|
114
|
+
} catch (error) {
|
|
115
|
+
return {
|
|
116
|
+
data: [],
|
|
117
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Execute a function within a database transaction.
|
|
123
|
+
* Provides true ACID guarantees - either all operations succeed or all are rolled back.
|
|
124
|
+
*/
|
|
125
|
+
async transaction(fn) {
|
|
126
|
+
const client = await this.pool.connect();
|
|
127
|
+
try {
|
|
128
|
+
await client.query("BEGIN");
|
|
129
|
+
const txDatabase = new TransactionDatabase(client);
|
|
130
|
+
const result = await fn(txDatabase);
|
|
131
|
+
await client.query("COMMIT");
|
|
132
|
+
return result;
|
|
133
|
+
} catch (error) {
|
|
134
|
+
await client.query("ROLLBACK");
|
|
135
|
+
throw error;
|
|
136
|
+
} finally {
|
|
137
|
+
client.release();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async healthCheck() {
|
|
141
|
+
try {
|
|
142
|
+
const result = await this.pool.query("SELECT 1 as health");
|
|
143
|
+
return result.rows.length > 0;
|
|
144
|
+
} catch {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async close() {
|
|
149
|
+
await this.pool.end();
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Get pool statistics for monitoring
|
|
153
|
+
*/
|
|
154
|
+
getPoolStats() {
|
|
155
|
+
return {
|
|
156
|
+
totalCount: this.pool.totalCount,
|
|
157
|
+
idleCount: this.pool.idleCount,
|
|
158
|
+
waitingCount: this.pool.waitingCount
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
TransactionDatabase = class {
|
|
163
|
+
constructor(client) {
|
|
164
|
+
this.client = client;
|
|
165
|
+
}
|
|
166
|
+
from(table) {
|
|
167
|
+
return new PostgresQueryBuilder(this.client, table);
|
|
168
|
+
}
|
|
169
|
+
async raw(sql, params) {
|
|
170
|
+
try {
|
|
171
|
+
const result = await this.client.query(sql, params);
|
|
172
|
+
return {
|
|
173
|
+
data: result.rows,
|
|
174
|
+
count: result.rowCount ?? void 0
|
|
175
|
+
};
|
|
176
|
+
} catch (error) {
|
|
177
|
+
return {
|
|
178
|
+
data: [],
|
|
179
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async transaction(fn) {
|
|
184
|
+
const savepointName = `sp_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
185
|
+
try {
|
|
186
|
+
await this.client.query(`SAVEPOINT ${savepointName}`);
|
|
187
|
+
const result = await fn(this);
|
|
188
|
+
await this.client.query(`RELEASE SAVEPOINT ${savepointName}`);
|
|
189
|
+
return result;
|
|
190
|
+
} catch (error) {
|
|
191
|
+
await this.client.query(`ROLLBACK TO SAVEPOINT ${savepointName}`);
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async healthCheck() {
|
|
196
|
+
try {
|
|
197
|
+
await this.client.query("SELECT 1");
|
|
198
|
+
return true;
|
|
199
|
+
} catch {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async close() {
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
PostgresQueryBuilder = class {
|
|
207
|
+
client;
|
|
208
|
+
tableName;
|
|
209
|
+
_select = ["*"];
|
|
210
|
+
_where = [];
|
|
211
|
+
_whereIn = [];
|
|
212
|
+
_orderBy = null;
|
|
213
|
+
_limit = null;
|
|
214
|
+
_offset = 0;
|
|
215
|
+
_insertData = null;
|
|
216
|
+
_updateData = null;
|
|
217
|
+
_deleteFlag = false;
|
|
218
|
+
_returning = ["*"];
|
|
219
|
+
constructor(client, tableName) {
|
|
220
|
+
this.client = client;
|
|
221
|
+
this.tableName = this.escapeIdentifier(tableName);
|
|
222
|
+
}
|
|
223
|
+
select(columns) {
|
|
224
|
+
if (columns) {
|
|
225
|
+
this._select = Array.isArray(columns) ? columns : [columns];
|
|
226
|
+
}
|
|
227
|
+
return this;
|
|
228
|
+
}
|
|
229
|
+
insert(data) {
|
|
230
|
+
this._insertData = Array.isArray(data) ? data : [data];
|
|
231
|
+
return this;
|
|
232
|
+
}
|
|
233
|
+
update(data) {
|
|
234
|
+
this._updateData = data;
|
|
235
|
+
return this;
|
|
236
|
+
}
|
|
237
|
+
delete() {
|
|
238
|
+
this._deleteFlag = true;
|
|
239
|
+
return this;
|
|
240
|
+
}
|
|
241
|
+
where(column, operator, value) {
|
|
242
|
+
this._where.push({ column, operator, value });
|
|
243
|
+
return this;
|
|
244
|
+
}
|
|
245
|
+
whereIn(column, values) {
|
|
246
|
+
this._whereIn.push({ column, values });
|
|
247
|
+
return this;
|
|
248
|
+
}
|
|
249
|
+
orderBy(column, direction = "asc") {
|
|
250
|
+
this._orderBy = { column, direction };
|
|
251
|
+
return this;
|
|
252
|
+
}
|
|
253
|
+
limit(count) {
|
|
254
|
+
this._limit = count;
|
|
255
|
+
return this;
|
|
256
|
+
}
|
|
257
|
+
offset(count) {
|
|
258
|
+
this._offset = count;
|
|
259
|
+
return this;
|
|
260
|
+
}
|
|
261
|
+
async single() {
|
|
262
|
+
this._limit = 1;
|
|
263
|
+
const result = await this.execute();
|
|
264
|
+
if (result.error) {
|
|
265
|
+
return { data: null, error: result.error };
|
|
266
|
+
}
|
|
267
|
+
return { data: result.data[0] ?? null };
|
|
268
|
+
}
|
|
269
|
+
async execute() {
|
|
270
|
+
try {
|
|
271
|
+
if (this._insertData) {
|
|
272
|
+
return await this.executeInsert();
|
|
273
|
+
}
|
|
274
|
+
if (this._updateData) {
|
|
275
|
+
return await this.executeUpdate();
|
|
276
|
+
}
|
|
277
|
+
if (this._deleteFlag) {
|
|
278
|
+
return await this.executeDelete();
|
|
279
|
+
}
|
|
280
|
+
return await this.executeSelect();
|
|
281
|
+
} catch (error) {
|
|
282
|
+
return {
|
|
283
|
+
data: [],
|
|
284
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
async executeSelect() {
|
|
289
|
+
const params = [];
|
|
290
|
+
let paramIndex = 1;
|
|
291
|
+
const selectClause = this._select.map((col) => this.escapeIdentifier(col)).join(", ");
|
|
292
|
+
let sql = `SELECT ${selectClause} FROM ${this.tableName}`;
|
|
293
|
+
const whereClauses = [];
|
|
294
|
+
for (const { column, operator, value } of this._where) {
|
|
295
|
+
whereClauses.push(`${this.escapeIdentifier(column)} ${this.mapOperator(operator)} $${paramIndex++}`);
|
|
296
|
+
params.push(value);
|
|
297
|
+
}
|
|
298
|
+
for (const { column, values } of this._whereIn) {
|
|
299
|
+
const placeholders = values.map(() => `$${paramIndex++}`).join(", ");
|
|
300
|
+
whereClauses.push(`${this.escapeIdentifier(column)} IN (${placeholders})`);
|
|
301
|
+
params.push(...values);
|
|
302
|
+
}
|
|
303
|
+
if (whereClauses.length > 0) {
|
|
304
|
+
sql += ` WHERE ${whereClauses.join(" AND ")}`;
|
|
305
|
+
}
|
|
306
|
+
if (this._orderBy) {
|
|
307
|
+
sql += ` ORDER BY ${this.escapeIdentifier(this._orderBy.column)} ${this._orderBy.direction.toUpperCase()}`;
|
|
308
|
+
}
|
|
309
|
+
if (this._limit !== null) {
|
|
310
|
+
sql += ` LIMIT $${paramIndex++}`;
|
|
311
|
+
params.push(this._limit);
|
|
312
|
+
}
|
|
313
|
+
if (this._offset > 0) {
|
|
314
|
+
sql += ` OFFSET $${paramIndex++}`;
|
|
315
|
+
params.push(this._offset);
|
|
316
|
+
}
|
|
317
|
+
const result = await this.client.query(sql, params);
|
|
318
|
+
return {
|
|
319
|
+
data: result.rows,
|
|
320
|
+
count: result.rowCount ?? void 0
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
async executeInsert() {
|
|
324
|
+
if (!this._insertData || this._insertData.length === 0) {
|
|
325
|
+
return { data: [] };
|
|
326
|
+
}
|
|
327
|
+
const params = [];
|
|
328
|
+
let paramIndex = 1;
|
|
329
|
+
const columns = /* @__PURE__ */ new Set();
|
|
330
|
+
for (const row of this._insertData) {
|
|
331
|
+
Object.keys(row).forEach((key) => columns.add(key));
|
|
332
|
+
}
|
|
333
|
+
const columnList = Array.from(columns);
|
|
334
|
+
const columnClause = columnList.map((col) => this.escapeIdentifier(col)).join(", ");
|
|
335
|
+
const valueRows = [];
|
|
336
|
+
for (const row of this._insertData) {
|
|
337
|
+
const rowValues = [];
|
|
338
|
+
for (const col of columnList) {
|
|
339
|
+
rowValues.push(`$${paramIndex++}`);
|
|
340
|
+
params.push(row[col] ?? null);
|
|
341
|
+
}
|
|
342
|
+
valueRows.push(`(${rowValues.join(", ")})`);
|
|
343
|
+
}
|
|
344
|
+
const sql = `INSERT INTO ${this.tableName} (${columnClause}) VALUES ${valueRows.join(", ")} RETURNING ${this._returning.join(", ")}`;
|
|
345
|
+
const result = await this.client.query(sql, params);
|
|
346
|
+
return {
|
|
347
|
+
data: result.rows,
|
|
348
|
+
count: result.rowCount ?? void 0
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
async executeUpdate() {
|
|
352
|
+
if (!this._updateData) {
|
|
353
|
+
return { data: [] };
|
|
354
|
+
}
|
|
355
|
+
const params = [];
|
|
356
|
+
let paramIndex = 1;
|
|
357
|
+
const setClauses = [];
|
|
358
|
+
for (const [key, value] of Object.entries(this._updateData)) {
|
|
359
|
+
setClauses.push(`${this.escapeIdentifier(key)} = $${paramIndex++}`);
|
|
360
|
+
params.push(value);
|
|
361
|
+
}
|
|
362
|
+
let sql = `UPDATE ${this.tableName} SET ${setClauses.join(", ")}`;
|
|
363
|
+
const whereClauses = [];
|
|
364
|
+
for (const { column, operator, value } of this._where) {
|
|
365
|
+
whereClauses.push(`${this.escapeIdentifier(column)} ${this.mapOperator(operator)} $${paramIndex++}`);
|
|
366
|
+
params.push(value);
|
|
367
|
+
}
|
|
368
|
+
for (const { column, values } of this._whereIn) {
|
|
369
|
+
const placeholders = values.map(() => `$${paramIndex++}`).join(", ");
|
|
370
|
+
whereClauses.push(`${this.escapeIdentifier(column)} IN (${placeholders})`);
|
|
371
|
+
params.push(...values);
|
|
372
|
+
}
|
|
373
|
+
if (whereClauses.length > 0) {
|
|
374
|
+
sql += ` WHERE ${whereClauses.join(" AND ")}`;
|
|
375
|
+
}
|
|
376
|
+
sql += ` RETURNING ${this._returning.join(", ")}`;
|
|
377
|
+
const result = await this.client.query(sql, params);
|
|
378
|
+
return {
|
|
379
|
+
data: result.rows,
|
|
380
|
+
count: result.rowCount ?? void 0
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
async executeDelete() {
|
|
384
|
+
const params = [];
|
|
385
|
+
let paramIndex = 1;
|
|
386
|
+
let sql = `DELETE FROM ${this.tableName}`;
|
|
387
|
+
const whereClauses = [];
|
|
388
|
+
for (const { column, operator, value } of this._where) {
|
|
389
|
+
whereClauses.push(`${this.escapeIdentifier(column)} ${this.mapOperator(operator)} $${paramIndex++}`);
|
|
390
|
+
params.push(value);
|
|
391
|
+
}
|
|
392
|
+
for (const { column, values } of this._whereIn) {
|
|
393
|
+
const placeholders = values.map(() => `$${paramIndex++}`).join(", ");
|
|
394
|
+
whereClauses.push(`${this.escapeIdentifier(column)} IN (${placeholders})`);
|
|
395
|
+
params.push(...values);
|
|
396
|
+
}
|
|
397
|
+
if (whereClauses.length > 0) {
|
|
398
|
+
sql += ` WHERE ${whereClauses.join(" AND ")}`;
|
|
399
|
+
}
|
|
400
|
+
sql += ` RETURNING ${this._returning.join(", ")}`;
|
|
401
|
+
const result = await this.client.query(sql, params);
|
|
402
|
+
return {
|
|
403
|
+
data: result.rows,
|
|
404
|
+
count: result.rowCount ?? void 0
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
escapeIdentifier(identifier) {
|
|
408
|
+
if (identifier === "*") return "*";
|
|
409
|
+
if (identifier.includes(".")) {
|
|
410
|
+
return identifier.split(".").map((part) => `"${part.replace(/"/g, '""')}"`).join(".");
|
|
411
|
+
}
|
|
412
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
413
|
+
}
|
|
414
|
+
mapOperator(operator) {
|
|
415
|
+
switch (operator.toLowerCase()) {
|
|
416
|
+
case "=":
|
|
417
|
+
case "==":
|
|
418
|
+
return "=";
|
|
419
|
+
case "!=":
|
|
420
|
+
case "<>":
|
|
421
|
+
return "<>";
|
|
422
|
+
case "<":
|
|
423
|
+
return "<";
|
|
424
|
+
case "<=":
|
|
425
|
+
return "<=";
|
|
426
|
+
case ">":
|
|
427
|
+
return ">";
|
|
428
|
+
case ">=":
|
|
429
|
+
return ">=";
|
|
430
|
+
case "like":
|
|
431
|
+
return "LIKE";
|
|
432
|
+
case "ilike":
|
|
433
|
+
return "ILIKE";
|
|
434
|
+
case "is":
|
|
435
|
+
return "IS";
|
|
436
|
+
case "is not":
|
|
437
|
+
return "IS NOT";
|
|
438
|
+
default:
|
|
439
|
+
return "=";
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// src/migrations/Migrator.ts
|
|
447
|
+
var DEFAULT_CONFIG = {
|
|
448
|
+
tableName: "_migrations",
|
|
449
|
+
schema: "public",
|
|
450
|
+
lockTimeout: 60,
|
|
451
|
+
validateChecksums: true,
|
|
452
|
+
migrationsDir: void 0
|
|
453
|
+
};
|
|
454
|
+
function generateChecksum(content) {
|
|
455
|
+
let hash = 0;
|
|
456
|
+
for (let i = 0; i < content.length; i++) {
|
|
457
|
+
const char = content.charCodeAt(i);
|
|
458
|
+
hash = (hash << 5) - hash + char;
|
|
459
|
+
hash = hash & hash;
|
|
460
|
+
}
|
|
461
|
+
return Math.abs(hash).toString(16).padStart(8, "0");
|
|
462
|
+
}
|
|
463
|
+
function sortMigrations(migrations) {
|
|
464
|
+
return [...migrations].sort((a, b) => a.version.localeCompare(b.version));
|
|
465
|
+
}
|
|
466
|
+
var Migrator = class {
|
|
467
|
+
db;
|
|
468
|
+
config;
|
|
469
|
+
migrations = [];
|
|
470
|
+
initialized = false;
|
|
471
|
+
locked = false;
|
|
472
|
+
constructor(db, config = {}) {
|
|
473
|
+
this.db = db;
|
|
474
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Get the fully qualified migration table name
|
|
478
|
+
*/
|
|
479
|
+
get tableName() {
|
|
480
|
+
return `"${this.config.schema}"."${this.config.tableName}"`;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Get the lock table name
|
|
484
|
+
*/
|
|
485
|
+
get lockTableName() {
|
|
486
|
+
return `"${this.config.schema}"."${this.config.tableName}_lock"`;
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Initialize migration tracking tables
|
|
490
|
+
*/
|
|
491
|
+
async initialize() {
|
|
492
|
+
if (this.initialized) return;
|
|
493
|
+
await this.db.raw(`CREATE SCHEMA IF NOT EXISTS "${this.config.schema}"`);
|
|
494
|
+
await this.db.raw(`
|
|
495
|
+
CREATE TABLE IF NOT EXISTS ${this.tableName} (
|
|
496
|
+
version VARCHAR(255) PRIMARY KEY,
|
|
497
|
+
name VARCHAR(255) NOT NULL,
|
|
498
|
+
applied_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
499
|
+
execution_time INTEGER NOT NULL,
|
|
500
|
+
checksum VARCHAR(64)
|
|
501
|
+
)
|
|
502
|
+
`);
|
|
503
|
+
await this.db.raw(`
|
|
504
|
+
CREATE TABLE IF NOT EXISTS ${this.lockTableName} (
|
|
505
|
+
id INTEGER PRIMARY KEY DEFAULT 1,
|
|
506
|
+
locked_at TIMESTAMP WITH TIME ZONE,
|
|
507
|
+
locked_by VARCHAR(255),
|
|
508
|
+
CONSTRAINT single_row CHECK (id = 1)
|
|
509
|
+
)
|
|
510
|
+
`);
|
|
511
|
+
await this.db.raw(`
|
|
512
|
+
INSERT INTO ${this.lockTableName} (id, locked_at, locked_by)
|
|
513
|
+
VALUES (1, NULL, NULL)
|
|
514
|
+
ON CONFLICT (id) DO NOTHING
|
|
515
|
+
`);
|
|
516
|
+
this.initialized = true;
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Acquire migration lock
|
|
520
|
+
*/
|
|
521
|
+
async lock() {
|
|
522
|
+
await this.initialize();
|
|
523
|
+
const lockId = `${process.pid}-${Date.now()}`;
|
|
524
|
+
const lockTimeout = new Date(Date.now() - this.config.lockTimeout * 1e3);
|
|
525
|
+
const result = await this.db.raw(
|
|
526
|
+
`
|
|
527
|
+
UPDATE ${this.lockTableName}
|
|
528
|
+
SET locked_at = CURRENT_TIMESTAMP, locked_by = $1
|
|
529
|
+
WHERE id = 1 AND (locked_at IS NULL OR locked_at < $2)
|
|
530
|
+
RETURNING locked_at
|
|
531
|
+
`,
|
|
532
|
+
[lockId, lockTimeout]
|
|
533
|
+
);
|
|
534
|
+
if (result.data.length > 0) {
|
|
535
|
+
this.locked = true;
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Release migration lock
|
|
542
|
+
*/
|
|
543
|
+
async unlock() {
|
|
544
|
+
if (!this.locked) return;
|
|
545
|
+
await this.db.raw(
|
|
546
|
+
`
|
|
547
|
+
UPDATE ${this.lockTableName}
|
|
548
|
+
SET locked_at = NULL, locked_by = NULL
|
|
549
|
+
WHERE id = 1
|
|
550
|
+
`
|
|
551
|
+
);
|
|
552
|
+
this.locked = false;
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Add migrations to the migrator
|
|
556
|
+
*/
|
|
557
|
+
addMigrations(migrations) {
|
|
558
|
+
for (const migration of migrations) {
|
|
559
|
+
if (!migration.checksum) {
|
|
560
|
+
const content = typeof migration.up === "string" ? migration.up : migration.up.toString();
|
|
561
|
+
migration.checksum = generateChecksum(content);
|
|
562
|
+
}
|
|
563
|
+
if (!this.migrations.find((m) => m.version === migration.version)) {
|
|
564
|
+
this.migrations.push(migration);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Load migrations from directory
|
|
570
|
+
* Expects files named: {version}_{name}.ts or {version}_{name}.sql
|
|
571
|
+
*/
|
|
572
|
+
async loadMigrations(dir) {
|
|
573
|
+
const { readdir, readFile } = await import("fs/promises");
|
|
574
|
+
const { join } = await import("path");
|
|
575
|
+
const files = await readdir(dir);
|
|
576
|
+
const migrationFiles = files.filter(
|
|
577
|
+
(f) => f.match(/^\d+_.*\.(ts|js|sql)$/)
|
|
578
|
+
);
|
|
579
|
+
for (const file of migrationFiles) {
|
|
580
|
+
const filePath = join(dir, file);
|
|
581
|
+
const [version, ...nameParts] = file.replace(/\.(ts|js|sql)$/, "").split("_");
|
|
582
|
+
const name = nameParts.join("_");
|
|
583
|
+
if (file.endsWith(".sql")) {
|
|
584
|
+
const content = await readFile(filePath, "utf-8");
|
|
585
|
+
const [up, down] = this.parseSqlMigration(content);
|
|
586
|
+
this.addMigrations([
|
|
587
|
+
{
|
|
588
|
+
version,
|
|
589
|
+
name,
|
|
590
|
+
up,
|
|
591
|
+
down
|
|
592
|
+
}
|
|
593
|
+
]);
|
|
594
|
+
} else {
|
|
595
|
+
const module2 = await import(filePath);
|
|
596
|
+
this.addMigrations([
|
|
597
|
+
{
|
|
598
|
+
version,
|
|
599
|
+
name,
|
|
600
|
+
up: module2.up,
|
|
601
|
+
down: module2.down,
|
|
602
|
+
transactional: module2.transactional
|
|
603
|
+
}
|
|
604
|
+
]);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Parse SQL migration file with -- UP and -- DOWN sections
|
|
610
|
+
*/
|
|
611
|
+
parseSqlMigration(content) {
|
|
612
|
+
const upMatch = content.match(/--\s*UP\s*\n([\s\S]*?)(?=--\s*DOWN|$)/i);
|
|
613
|
+
const downMatch = content.match(/--\s*DOWN\s*\n([\s\S]*?)$/i);
|
|
614
|
+
const up = upMatch?.[1]?.trim() ?? content.trim();
|
|
615
|
+
const down = downMatch?.[1]?.trim() ?? "";
|
|
616
|
+
return [up, down];
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Get current migration status
|
|
620
|
+
*/
|
|
621
|
+
async status() {
|
|
622
|
+
await this.initialize();
|
|
623
|
+
const result = await this.db.raw(
|
|
624
|
+
`SELECT version, name, applied_at, execution_time, checksum
|
|
625
|
+
FROM ${this.tableName}
|
|
626
|
+
ORDER BY version ASC`
|
|
627
|
+
);
|
|
628
|
+
const applied = result.data.map((row) => ({
|
|
629
|
+
version: row.version,
|
|
630
|
+
name: row.name,
|
|
631
|
+
appliedAt: new Date(row.applied_at),
|
|
632
|
+
executionTime: row.execution_time,
|
|
633
|
+
checksum: row.checksum ?? void 0
|
|
634
|
+
}));
|
|
635
|
+
const appliedVersions = new Set(applied.map((m) => m.version));
|
|
636
|
+
const sortedMigrations = sortMigrations(this.migrations);
|
|
637
|
+
const pending = sortedMigrations.filter(
|
|
638
|
+
(m) => !appliedVersions.has(m.version)
|
|
639
|
+
);
|
|
640
|
+
return {
|
|
641
|
+
applied,
|
|
642
|
+
pending,
|
|
643
|
+
currentVersion: applied.length > 0 ? applied[applied.length - 1].version : null,
|
|
644
|
+
latestVersion: sortedMigrations.length > 0 ? sortedMigrations[sortedMigrations.length - 1].version : null
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Run all pending migrations (or up to a specific version)
|
|
649
|
+
*/
|
|
650
|
+
async up(options = {}) {
|
|
651
|
+
const { dryRun = false, to } = options;
|
|
652
|
+
const results = [];
|
|
653
|
+
if (!dryRun) {
|
|
654
|
+
const acquired = await this.lock();
|
|
655
|
+
if (!acquired) {
|
|
656
|
+
throw new Error("Could not acquire migration lock. Another migration may be in progress.");
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
try {
|
|
660
|
+
const status = await this.status();
|
|
661
|
+
let pending = status.pending;
|
|
662
|
+
if (to) {
|
|
663
|
+
const targetIndex = pending.findIndex((m) => m.version === to);
|
|
664
|
+
if (targetIndex === -1) {
|
|
665
|
+
throw new Error(`Target version ${to} not found in pending migrations`);
|
|
666
|
+
}
|
|
667
|
+
pending = pending.slice(0, targetIndex + 1);
|
|
668
|
+
}
|
|
669
|
+
for (const migration of pending) {
|
|
670
|
+
const result = await this.runMigration(migration, "up", dryRun);
|
|
671
|
+
results.push(result);
|
|
672
|
+
if (!result.success) {
|
|
673
|
+
break;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
} finally {
|
|
677
|
+
if (!dryRun) {
|
|
678
|
+
await this.unlock();
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return results;
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Rollback migrations
|
|
685
|
+
*/
|
|
686
|
+
async down(options = {}) {
|
|
687
|
+
const { dryRun = false, steps = 1 } = options;
|
|
688
|
+
const results = [];
|
|
689
|
+
if (!dryRun) {
|
|
690
|
+
const acquired = await this.lock();
|
|
691
|
+
if (!acquired) {
|
|
692
|
+
throw new Error("Could not acquire migration lock. Another migration may be in progress.");
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
try {
|
|
696
|
+
const status = await this.status();
|
|
697
|
+
const toRollback = status.applied.slice(-steps).reverse();
|
|
698
|
+
for (const record of toRollback) {
|
|
699
|
+
const migration = this.migrations.find((m) => m.version === record.version);
|
|
700
|
+
if (!migration) {
|
|
701
|
+
throw new Error(
|
|
702
|
+
`Migration ${record.version} was applied but is not registered. Cannot rollback.`
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
const result = await this.runMigration(migration, "down", dryRun);
|
|
706
|
+
results.push(result);
|
|
707
|
+
if (!result.success) {
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
} finally {
|
|
712
|
+
if (!dryRun) {
|
|
713
|
+
await this.unlock();
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return results;
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Rollback all migrations and re-run them
|
|
720
|
+
*/
|
|
721
|
+
async reset(options = {}) {
|
|
722
|
+
const status = await this.status();
|
|
723
|
+
const downResults = await this.down({
|
|
724
|
+
dryRun: options.dryRun,
|
|
725
|
+
steps: status.applied.length
|
|
726
|
+
});
|
|
727
|
+
if (downResults.every((r) => r.success)) {
|
|
728
|
+
const upResults = await this.up({ dryRun: options.dryRun });
|
|
729
|
+
return [...downResults, ...upResults];
|
|
730
|
+
}
|
|
731
|
+
return downResults;
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Migrate to a specific version
|
|
735
|
+
*/
|
|
736
|
+
async goto(version, options = {}) {
|
|
737
|
+
const status = await this.status();
|
|
738
|
+
const currentVersion = status.currentVersion;
|
|
739
|
+
const results = [];
|
|
740
|
+
if (currentVersion === version) {
|
|
741
|
+
return results;
|
|
742
|
+
}
|
|
743
|
+
const isUp = !currentVersion || version > currentVersion;
|
|
744
|
+
if (isUp) {
|
|
745
|
+
return this.up({ ...options, to: version });
|
|
746
|
+
} else {
|
|
747
|
+
const currentIndex = status.applied.findIndex((m) => m.version === currentVersion);
|
|
748
|
+
const targetIndex = status.applied.findIndex((m) => m.version === version);
|
|
749
|
+
if (targetIndex === -1) {
|
|
750
|
+
throw new Error(`Target version ${version} not found in applied migrations`);
|
|
751
|
+
}
|
|
752
|
+
const steps = currentIndex - targetIndex;
|
|
753
|
+
return this.down({ ...options, steps });
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Run a single migration
|
|
758
|
+
*/
|
|
759
|
+
async runMigration(migration, direction, dryRun) {
|
|
760
|
+
const startTime = Date.now();
|
|
761
|
+
const action = direction === "up" ? migration.up : migration.down;
|
|
762
|
+
try {
|
|
763
|
+
if (dryRun) {
|
|
764
|
+
return {
|
|
765
|
+
migration,
|
|
766
|
+
success: true,
|
|
767
|
+
executionTime: 0,
|
|
768
|
+
dryRun: true
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
const transactional = migration.transactional !== false;
|
|
772
|
+
const runAction = async (db) => {
|
|
773
|
+
if (typeof action === "string") {
|
|
774
|
+
const result = await db.raw(action);
|
|
775
|
+
if (result.error) {
|
|
776
|
+
throw result.error;
|
|
777
|
+
}
|
|
778
|
+
} else {
|
|
779
|
+
await action(db);
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
if (transactional) {
|
|
783
|
+
await this.db.transaction(async (tx) => {
|
|
784
|
+
await runAction(tx);
|
|
785
|
+
});
|
|
786
|
+
} else {
|
|
787
|
+
await runAction(this.db);
|
|
788
|
+
}
|
|
789
|
+
const executionTime = Date.now() - startTime;
|
|
790
|
+
if (direction === "up") {
|
|
791
|
+
await this.db.raw(
|
|
792
|
+
`INSERT INTO ${this.tableName} (version, name, execution_time, checksum)
|
|
793
|
+
VALUES ($1, $2, $3, $4)`,
|
|
794
|
+
[migration.version, migration.name, executionTime, migration.checksum]
|
|
795
|
+
);
|
|
796
|
+
} else {
|
|
797
|
+
await this.db.raw(
|
|
798
|
+
`DELETE FROM ${this.tableName} WHERE version = $1`,
|
|
799
|
+
[migration.version]
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
return {
|
|
803
|
+
migration,
|
|
804
|
+
success: true,
|
|
805
|
+
executionTime,
|
|
806
|
+
dryRun: false
|
|
807
|
+
};
|
|
808
|
+
} catch (error) {
|
|
809
|
+
return {
|
|
810
|
+
migration,
|
|
811
|
+
success: false,
|
|
812
|
+
executionTime: Date.now() - startTime,
|
|
813
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
814
|
+
dryRun: false
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Create a new migration file
|
|
820
|
+
*/
|
|
821
|
+
async create(name) {
|
|
822
|
+
const { writeFile, mkdir } = await import("fs/promises");
|
|
823
|
+
const { join } = await import("path");
|
|
824
|
+
if (!this.config.migrationsDir) {
|
|
825
|
+
throw new Error("migrationsDir must be configured to create migrations");
|
|
826
|
+
}
|
|
827
|
+
await mkdir(this.config.migrationsDir, { recursive: true });
|
|
828
|
+
const now = /* @__PURE__ */ new Date();
|
|
829
|
+
const version = [
|
|
830
|
+
now.getFullYear(),
|
|
831
|
+
String(now.getMonth() + 1).padStart(2, "0"),
|
|
832
|
+
String(now.getDate()).padStart(2, "0"),
|
|
833
|
+
String(now.getHours()).padStart(2, "0"),
|
|
834
|
+
String(now.getMinutes()).padStart(2, "0"),
|
|
835
|
+
String(now.getSeconds()).padStart(2, "0")
|
|
836
|
+
].join("");
|
|
837
|
+
const safeName = name.replace(/[^a-zA-Z0-9_]/g, "_").toLowerCase();
|
|
838
|
+
const fileName = `${version}_${safeName}.sql`;
|
|
839
|
+
const filePath = join(this.config.migrationsDir, fileName);
|
|
840
|
+
const template = `-- Migration: ${name}
|
|
841
|
+
-- Version: ${version}
|
|
842
|
+
-- Created: ${now.toISOString()}
|
|
843
|
+
|
|
844
|
+
-- UP
|
|
845
|
+
-- Add your forward migration SQL here
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
-- DOWN
|
|
849
|
+
-- Add your rollback migration SQL here
|
|
850
|
+
|
|
851
|
+
`;
|
|
852
|
+
await writeFile(filePath, template, "utf-8");
|
|
853
|
+
return filePath;
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Validate migration integrity
|
|
857
|
+
*/
|
|
858
|
+
async validate() {
|
|
859
|
+
const errors = [];
|
|
860
|
+
const status = await this.status();
|
|
861
|
+
if (!this.config.validateChecksums) {
|
|
862
|
+
return { valid: true, errors };
|
|
863
|
+
}
|
|
864
|
+
for (const applied of status.applied) {
|
|
865
|
+
const migration = this.migrations.find((m) => m.version === applied.version);
|
|
866
|
+
if (!migration) {
|
|
867
|
+
errors.push(
|
|
868
|
+
`Migration ${applied.version} (${applied.name}) was applied but is not registered`
|
|
869
|
+
);
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
if (applied.checksum && migration.checksum !== applied.checksum) {
|
|
873
|
+
errors.push(
|
|
874
|
+
`Migration ${applied.version} (${applied.name}) has been modified since it was applied`
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
return {
|
|
879
|
+
valid: errors.length === 0,
|
|
880
|
+
errors
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
|
|
885
|
+
// src/migrations/cli.ts
|
|
886
|
+
var HELP_TEXT = `
|
|
887
|
+
Database Migration CLI
|
|
888
|
+
|
|
889
|
+
USAGE:
|
|
890
|
+
platform-migrate <command> [options]
|
|
891
|
+
|
|
892
|
+
COMMANDS:
|
|
893
|
+
status Show current migration status
|
|
894
|
+
up Run all pending migrations
|
|
895
|
+
down Rollback the last migration
|
|
896
|
+
reset Rollback all and re-run migrations
|
|
897
|
+
goto <ver> Migrate to a specific version
|
|
898
|
+
create <name> Create a new migration file
|
|
899
|
+
validate Validate migration integrity
|
|
900
|
+
|
|
901
|
+
OPTIONS:
|
|
902
|
+
--dry-run Preview changes without applying
|
|
903
|
+
--steps <n> Number of migrations to rollback (for down command)
|
|
904
|
+
--to <ver> Target version (for up command)
|
|
905
|
+
--dir <path> Migrations directory (default: ./migrations)
|
|
906
|
+
--help, -h Show this help message
|
|
907
|
+
|
|
908
|
+
ENVIRONMENT:
|
|
909
|
+
DATABASE_URL PostgreSQL connection string (required)
|
|
910
|
+
MIGRATIONS_DIR Default migrations directory
|
|
911
|
+
MIGRATIONS_TABLE Migration tracking table name
|
|
912
|
+
|
|
913
|
+
EXAMPLES:
|
|
914
|
+
platform-migrate status
|
|
915
|
+
platform-migrate up
|
|
916
|
+
platform-migrate up --dry-run
|
|
917
|
+
platform-migrate down --steps 3
|
|
918
|
+
platform-migrate create add_users_table
|
|
919
|
+
platform-migrate goto 20240101000000
|
|
920
|
+
`;
|
|
921
|
+
function parseArgs(args) {
|
|
922
|
+
const options = {
|
|
923
|
+
command: "",
|
|
924
|
+
dryRun: false,
|
|
925
|
+
steps: 1,
|
|
926
|
+
migrationsDir: process.env.MIGRATIONS_DIR ?? "./migrations",
|
|
927
|
+
help: false
|
|
928
|
+
};
|
|
929
|
+
for (let i = 0; i < args.length; i++) {
|
|
930
|
+
const arg = args[i];
|
|
931
|
+
if (arg === "--dry-run") {
|
|
932
|
+
options.dryRun = true;
|
|
933
|
+
} else if (arg === "--steps" && args[i + 1]) {
|
|
934
|
+
options.steps = parseInt(args[++i], 10);
|
|
935
|
+
} else if (arg === "--to" && args[i + 1]) {
|
|
936
|
+
options.to = args[++i];
|
|
937
|
+
} else if ((arg === "--dir" || arg === "-d") && args[i + 1]) {
|
|
938
|
+
options.migrationsDir = args[++i];
|
|
939
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
940
|
+
options.help = true;
|
|
941
|
+
} else if (!arg.startsWith("-") && !options.command) {
|
|
942
|
+
options.command = arg;
|
|
943
|
+
} else if (!arg.startsWith("-") && options.command && !options.name) {
|
|
944
|
+
options.name = arg;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
return options;
|
|
948
|
+
}
|
|
949
|
+
function formatResults(results) {
|
|
950
|
+
if (results.length === 0) {
|
|
951
|
+
console.log("No migrations to run.");
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
for (const result of results) {
|
|
955
|
+
const icon = result.success ? "\u2713" : "\u2717";
|
|
956
|
+
const status = result.dryRun ? "[DRY RUN]" : "";
|
|
957
|
+
const time = result.success ? `(${result.executionTime}ms)` : "";
|
|
958
|
+
if (result.success) {
|
|
959
|
+
console.log(` ${icon} ${result.migration.version} - ${result.migration.name} ${time} ${status}`);
|
|
960
|
+
} else {
|
|
961
|
+
console.error(` ${icon} ${result.migration.version} - ${result.migration.name}`);
|
|
962
|
+
console.error(` Error: ${result.error?.message}`);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
const successful = results.filter((r) => r.success).length;
|
|
966
|
+
const failed = results.filter((r) => !r.success).length;
|
|
967
|
+
console.log(`
|
|
968
|
+
${successful} successful, ${failed} failed`);
|
|
969
|
+
}
|
|
970
|
+
async function main() {
|
|
971
|
+
const args = process.argv.slice(2);
|
|
972
|
+
const options = parseArgs(args);
|
|
973
|
+
if (options.help || !options.command) {
|
|
974
|
+
console.log(HELP_TEXT);
|
|
975
|
+
process.exit(options.help ? 0 : 1);
|
|
976
|
+
}
|
|
977
|
+
if (!process.env.DATABASE_URL) {
|
|
978
|
+
console.error("Error: DATABASE_URL environment variable is required");
|
|
979
|
+
process.exit(1);
|
|
980
|
+
}
|
|
981
|
+
try {
|
|
982
|
+
const { PostgresDatabase: PostgresDatabase2 } = await Promise.resolve().then(() => (init_PostgresDatabase(), PostgresDatabase_exports));
|
|
983
|
+
const db = await PostgresDatabase2.fromEnv();
|
|
984
|
+
const migrator = new Migrator(db, {
|
|
985
|
+
tableName: process.env.MIGRATIONS_TABLE ?? "_migrations",
|
|
986
|
+
migrationsDir: options.migrationsDir
|
|
987
|
+
});
|
|
988
|
+
try {
|
|
989
|
+
await migrator.loadMigrations(options.migrationsDir);
|
|
990
|
+
} catch (e) {
|
|
991
|
+
if (options.command !== "create") {
|
|
992
|
+
console.warn(`Warning: Could not load migrations from ${options.migrationsDir}`);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
switch (options.command) {
|
|
996
|
+
case "status": {
|
|
997
|
+
const status = await migrator.status();
|
|
998
|
+
console.log("\n=== Migration Status ===\n");
|
|
999
|
+
console.log(`Current Version: ${status.currentVersion ?? "None"}`);
|
|
1000
|
+
console.log(`Latest Version: ${status.latestVersion ?? "None"}`);
|
|
1001
|
+
console.log(`
|
|
1002
|
+
Applied (${status.applied.length}):`);
|
|
1003
|
+
for (const m of status.applied) {
|
|
1004
|
+
console.log(` \u2713 ${m.version} - ${m.name} (${m.appliedAt.toISOString()})`);
|
|
1005
|
+
}
|
|
1006
|
+
console.log(`
|
|
1007
|
+
Pending (${status.pending.length}):`);
|
|
1008
|
+
for (const m of status.pending) {
|
|
1009
|
+
console.log(` \u25CB ${m.version} - ${m.name}`);
|
|
1010
|
+
}
|
|
1011
|
+
break;
|
|
1012
|
+
}
|
|
1013
|
+
case "up": {
|
|
1014
|
+
console.log(options.dryRun ? "\n=== Dry Run: Migrate Up ===\n" : "\n=== Migrating Up ===\n");
|
|
1015
|
+
const results = await migrator.up({
|
|
1016
|
+
dryRun: options.dryRun,
|
|
1017
|
+
to: options.to
|
|
1018
|
+
});
|
|
1019
|
+
formatResults(results);
|
|
1020
|
+
if (results.some((r) => !r.success)) {
|
|
1021
|
+
process.exit(1);
|
|
1022
|
+
}
|
|
1023
|
+
break;
|
|
1024
|
+
}
|
|
1025
|
+
case "down": {
|
|
1026
|
+
console.log(options.dryRun ? "\n=== Dry Run: Rollback ===\n" : "\n=== Rolling Back ===\n");
|
|
1027
|
+
const results = await migrator.down({
|
|
1028
|
+
dryRun: options.dryRun,
|
|
1029
|
+
steps: options.steps
|
|
1030
|
+
});
|
|
1031
|
+
formatResults(results);
|
|
1032
|
+
if (results.some((r) => !r.success)) {
|
|
1033
|
+
process.exit(1);
|
|
1034
|
+
}
|
|
1035
|
+
break;
|
|
1036
|
+
}
|
|
1037
|
+
case "reset": {
|
|
1038
|
+
console.log(options.dryRun ? "\n=== Dry Run: Reset ===\n" : "\n=== Resetting Database ===\n");
|
|
1039
|
+
const results = await migrator.reset({ dryRun: options.dryRun });
|
|
1040
|
+
formatResults(results);
|
|
1041
|
+
if (results.some((r) => !r.success)) {
|
|
1042
|
+
process.exit(1);
|
|
1043
|
+
}
|
|
1044
|
+
break;
|
|
1045
|
+
}
|
|
1046
|
+
case "goto": {
|
|
1047
|
+
if (!options.name) {
|
|
1048
|
+
console.error("Error: Version required. Usage: platform-migrate goto <version>");
|
|
1049
|
+
process.exit(1);
|
|
1050
|
+
}
|
|
1051
|
+
console.log(`
|
|
1052
|
+
=== Migrating to Version ${options.name} ===
|
|
1053
|
+
`);
|
|
1054
|
+
const results = await migrator.goto(options.name, { dryRun: options.dryRun });
|
|
1055
|
+
formatResults(results);
|
|
1056
|
+
if (results.some((r) => !r.success)) {
|
|
1057
|
+
process.exit(1);
|
|
1058
|
+
}
|
|
1059
|
+
break;
|
|
1060
|
+
}
|
|
1061
|
+
case "create": {
|
|
1062
|
+
if (!options.name) {
|
|
1063
|
+
console.error("Error: Migration name required. Usage: platform-migrate create <name>");
|
|
1064
|
+
process.exit(1);
|
|
1065
|
+
}
|
|
1066
|
+
const filePath = await migrator.create(options.name);
|
|
1067
|
+
console.log(`
|
|
1068
|
+
Created migration: ${filePath}
|
|
1069
|
+
`);
|
|
1070
|
+
break;
|
|
1071
|
+
}
|
|
1072
|
+
case "validate": {
|
|
1073
|
+
console.log("\n=== Validating Migrations ===\n");
|
|
1074
|
+
const { valid, errors } = await migrator.validate();
|
|
1075
|
+
if (valid) {
|
|
1076
|
+
console.log("\u2713 All migrations are valid");
|
|
1077
|
+
} else {
|
|
1078
|
+
console.error("\u2717 Validation failed:");
|
|
1079
|
+
for (const error of errors) {
|
|
1080
|
+
console.error(` - ${error}`);
|
|
1081
|
+
}
|
|
1082
|
+
process.exit(1);
|
|
1083
|
+
}
|
|
1084
|
+
break;
|
|
1085
|
+
}
|
|
1086
|
+
default:
|
|
1087
|
+
console.error(`Unknown command: ${options.command}`);
|
|
1088
|
+
console.log(HELP_TEXT);
|
|
1089
|
+
process.exit(1);
|
|
1090
|
+
}
|
|
1091
|
+
await db.close();
|
|
1092
|
+
} catch (error) {
|
|
1093
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
1094
|
+
process.exit(1);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
main().catch((error) => {
|
|
1098
|
+
console.error("Fatal error:", error);
|
|
1099
|
+
process.exit(1);
|
|
1100
|
+
});
|
|
1101
|
+
//# sourceMappingURL=migrate.js.map
|