@seedcord/plugins 0.3.3 → 0.4.1
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/index.cjs +670 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +420 -71
- package/dist/index.d.ts +420 -71
- package/dist/index.mjs +658 -27
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -4
package/dist/index.cjs
CHANGED
|
@@ -1,37 +1,45 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var mongoose2 = require('mongoose');
|
|
4
3
|
require('reflect-metadata');
|
|
5
|
-
var
|
|
4
|
+
var chalk3 = require('chalk');
|
|
6
5
|
var envapt = require('envapt');
|
|
6
|
+
var mongoose = require('mongoose');
|
|
7
7
|
var seedcord = require('seedcord');
|
|
8
|
+
var pg = require('pg');
|
|
9
|
+
var fs = require('fs');
|
|
10
|
+
var path = require('path');
|
|
11
|
+
var url = require('url');
|
|
12
|
+
var util = require('util');
|
|
13
|
+
var utils = require('@seedcord/utils');
|
|
14
|
+
var kysely = require('kysely');
|
|
8
15
|
|
|
9
16
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
10
17
|
|
|
11
|
-
var
|
|
12
|
-
var
|
|
18
|
+
var chalk3__default = /*#__PURE__*/_interopDefault(chalk3);
|
|
19
|
+
var mongoose__default = /*#__PURE__*/_interopDefault(mongoose);
|
|
20
|
+
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
13
21
|
|
|
14
22
|
var __defProp = Object.defineProperty;
|
|
15
23
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
24
|
+
|
|
25
|
+
// src/mongo/decorators/RegisterMongoService.ts
|
|
26
|
+
var ServiceMetadataKey = Symbol("db:serviceKey");
|
|
27
|
+
function RegisterMongoService(key) {
|
|
28
|
+
return (ctor) => {
|
|
29
|
+
Reflect.defineMetadata(ServiceMetadataKey, key, ctor);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
__name(RegisterMongoService, "RegisterMongoService");
|
|
16
33
|
var ModelMetadataKey = Symbol("db:model");
|
|
17
|
-
function
|
|
34
|
+
function RegisterMongoModel(collection) {
|
|
18
35
|
return (target, propertyKey) => {
|
|
19
36
|
const schema = target[propertyKey];
|
|
20
37
|
const name = String(collection);
|
|
21
|
-
const model =
|
|
38
|
+
const model = mongoose__default.default.model(name, schema);
|
|
22
39
|
Reflect.defineMetadata(ModelMetadataKey, model, target);
|
|
23
40
|
};
|
|
24
41
|
}
|
|
25
|
-
__name(
|
|
26
|
-
|
|
27
|
-
// src/mongo/decorators/DatabaseService.ts
|
|
28
|
-
var ServiceMetadataKey = Symbol("db:serviceKey");
|
|
29
|
-
function DatabaseService(key) {
|
|
30
|
-
return (ctor) => {
|
|
31
|
-
Reflect.defineMetadata(ServiceMetadataKey, key, ctor);
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
__name(DatabaseService, "DatabaseService");
|
|
42
|
+
__name(RegisterMongoModel, "RegisterMongoModel");
|
|
35
43
|
|
|
36
44
|
// src/mongo/MongoService.ts
|
|
37
45
|
var MongoService = class {
|
|
@@ -46,13 +54,15 @@ var MongoService = class {
|
|
|
46
54
|
this.core = core;
|
|
47
55
|
const ctor = this.constructor;
|
|
48
56
|
const key = Reflect.getMetadata(ServiceMetadataKey, ctor);
|
|
49
|
-
if (!key) throw new Error(`Missing @
|
|
57
|
+
if (!key) throw new Error(`Missing @RegisterMongoService on ${ctor.name}`);
|
|
50
58
|
const model = Reflect.getMetadata(ModelMetadataKey, ctor);
|
|
51
|
-
if (!model) throw new Error(`Missing @
|
|
59
|
+
if (!model) throw new Error(`Missing @RegisterMongoModel on ${ctor.name}`);
|
|
52
60
|
this.model = model;
|
|
53
61
|
db._register(key, this);
|
|
54
62
|
}
|
|
55
63
|
};
|
|
64
|
+
|
|
65
|
+
// src/mongo/Mongo.ts
|
|
56
66
|
var Mongo = class extends seedcord.Plugin {
|
|
57
67
|
static {
|
|
58
68
|
__name(this, "Mongo");
|
|
@@ -64,7 +74,7 @@ var Mongo = class extends seedcord.Plugin {
|
|
|
64
74
|
uri;
|
|
65
75
|
/**
|
|
66
76
|
* Map of all loaded services.
|
|
67
|
-
* Keys come from `@
|
|
77
|
+
* Keys come from `@RegisterMongoService('key')`
|
|
68
78
|
*/
|
|
69
79
|
services = {};
|
|
70
80
|
constructor(core, options) {
|
|
@@ -82,40 +92,662 @@ var Mongo = class extends seedcord.Plugin {
|
|
|
82
92
|
await this.disconnect();
|
|
83
93
|
}
|
|
84
94
|
async connect() {
|
|
85
|
-
await
|
|
95
|
+
this.connection = await mongoose__default.default.connect(this.uri, {
|
|
86
96
|
dbName: this.options.name,
|
|
87
97
|
...envapt.Envapter.isProduction && {
|
|
88
98
|
tls: true,
|
|
89
99
|
ssl: true
|
|
90
100
|
}
|
|
91
|
-
}).then((
|
|
101
|
+
}).then((conn) => {
|
|
102
|
+
this.logger.info(chalk3__default.default.green.bold(`Connected to MongoDB: ${chalk3__default.default.magenta.bold(conn.connection.name)}`));
|
|
103
|
+
return conn;
|
|
104
|
+
}).catch((err) => {
|
|
92
105
|
throw new Error(`Could not connect to MongoDB`, err);
|
|
93
106
|
});
|
|
94
107
|
}
|
|
95
108
|
async disconnect() {
|
|
96
|
-
await
|
|
109
|
+
await this.connection.disconnect().then(() => this.logger.info(chalk3__default.default.red.bold("Disconnected from MongoDB"))).catch((err) => this.logger.error(`Could not disconnect from MongoDB: ${err.message}`));
|
|
97
110
|
}
|
|
98
111
|
async loadServices() {
|
|
99
112
|
const servicesDir = this.options.dir;
|
|
100
|
-
this.logger.info(
|
|
113
|
+
this.logger.info(chalk3__default.default.bold(servicesDir));
|
|
101
114
|
await seedcord.traverseDirectory(servicesDir, (_full, rel, mod) => {
|
|
102
115
|
for (const Service of Object.values(mod)) {
|
|
103
116
|
if (this.isServiceClass(Service)) {
|
|
104
117
|
const instance = new Service(this, this.core);
|
|
105
|
-
this.logger.info(`${
|
|
118
|
+
this.logger.info(`${chalk3__default.default.italic("Registered")} ${chalk3__default.default.bold.yellow(instance.constructor.name)} from ${chalk3__default.default.gray(rel)}`);
|
|
106
119
|
}
|
|
107
120
|
}
|
|
108
121
|
}, this.logger);
|
|
109
|
-
this.logger.info(`${
|
|
122
|
+
this.logger.info(`${chalk3__default.default.bold.green("Loaded")}: ${chalk3__default.default.magenta(Object.keys(this.services).length)} services`);
|
|
110
123
|
}
|
|
111
124
|
isServiceClass(obj) {
|
|
112
125
|
return typeof obj === "function" && obj.prototype instanceof MongoService && Reflect.hasMetadata(ServiceMetadataKey, obj);
|
|
113
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Register hook used by decorated services.
|
|
129
|
+
*
|
|
130
|
+
* @internal
|
|
131
|
+
*/
|
|
114
132
|
_register(key, instance) {
|
|
115
133
|
this.services[key] = instance;
|
|
116
134
|
}
|
|
117
135
|
};
|
|
118
|
-
|
|
136
|
+
var KpgDatabaseBootstrapper = class _KpgDatabaseBootstrapper {
|
|
137
|
+
static {
|
|
138
|
+
__name(this, "KpgDatabaseBootstrapper");
|
|
139
|
+
}
|
|
140
|
+
logger;
|
|
141
|
+
static ADMIN_DB = "postgres";
|
|
142
|
+
static DATABASE_EXISTS_SQL = 'SELECT EXISTS (SELECT 1 FROM pg_database WHERE datname = $1) AS "exists"';
|
|
143
|
+
constructor(logger) {
|
|
144
|
+
this.logger = logger;
|
|
145
|
+
}
|
|
146
|
+
resolveDatabaseName(config) {
|
|
147
|
+
return _KpgDatabaseBootstrapper.parseDatabaseName(config);
|
|
148
|
+
}
|
|
149
|
+
resolveDatabaseFromPool(pool) {
|
|
150
|
+
const config = {};
|
|
151
|
+
const { options } = pool;
|
|
152
|
+
if (typeof options.database === "string") {
|
|
153
|
+
config.database = options.database;
|
|
154
|
+
}
|
|
155
|
+
if (typeof options.connectionString === "string") {
|
|
156
|
+
config.connectionString = options.connectionString;
|
|
157
|
+
}
|
|
158
|
+
return this.resolveDatabaseName(config);
|
|
159
|
+
}
|
|
160
|
+
async ensure(baseConfig) {
|
|
161
|
+
const targetDb = this.resolveDatabaseName(baseConfig);
|
|
162
|
+
if (!targetDb) {
|
|
163
|
+
this.logger.info(chalk3__default.default.gray("Skipping database existence check (no database specified)."));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (targetDb === _KpgDatabaseBootstrapper.ADMIN_DB) {
|
|
167
|
+
this.logger.info(chalk3__default.default.gray("Target database is postgres; skipping creation."));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const adminConfig = this.buildAdminConfig(baseConfig);
|
|
171
|
+
if (!adminConfig) {
|
|
172
|
+
this.logger.warn(`Unable to derive admin connection when ensuring database ${targetDb}`);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
this.logger.info(chalk3__default.default.gray(`Ensuring database ${chalk3__default.default.yellow(targetDb)} exists...`));
|
|
176
|
+
const adminPool = new pg.Pool(adminConfig);
|
|
177
|
+
try {
|
|
178
|
+
const exists = await this.databaseExists(adminPool, targetDb);
|
|
179
|
+
if (exists) {
|
|
180
|
+
this.logger.info(chalk3__default.default.gray(`Database ${chalk3__default.default.yellow(targetDb)} already exists.`));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
await this.createDatabase(adminPool, targetDb);
|
|
184
|
+
this.logger.info(chalk3__default.default.green(`Created database ${chalk3__default.default.bold(targetDb)}.`));
|
|
185
|
+
} catch (error) {
|
|
186
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
187
|
+
this.logger.error(`Failed to ensure database ${targetDb}: ${err.message}`);
|
|
188
|
+
throw err;
|
|
189
|
+
} finally {
|
|
190
|
+
await adminPool.end();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
buildAdminConfig(baseConfig) {
|
|
194
|
+
const adminConfig = {
|
|
195
|
+
...baseConfig
|
|
196
|
+
};
|
|
197
|
+
const { connectionString } = adminConfig;
|
|
198
|
+
if (connectionString) {
|
|
199
|
+
const connection = _KpgDatabaseBootstrapper.applyDatabaseToConnectionString(connectionString, _KpgDatabaseBootstrapper.ADMIN_DB);
|
|
200
|
+
if (!connection) return null;
|
|
201
|
+
adminConfig.connectionString = connection;
|
|
202
|
+
}
|
|
203
|
+
adminConfig.database = _KpgDatabaseBootstrapper.ADMIN_DB;
|
|
204
|
+
return adminConfig;
|
|
205
|
+
}
|
|
206
|
+
async databaseExists(pool, database) {
|
|
207
|
+
const client = await pool.connect();
|
|
208
|
+
try {
|
|
209
|
+
const { rows } = await client.query(_KpgDatabaseBootstrapper.DATABASE_EXISTS_SQL, [
|
|
210
|
+
database
|
|
211
|
+
]);
|
|
212
|
+
return Boolean(rows[0]?.exists);
|
|
213
|
+
} finally {
|
|
214
|
+
client.release();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async createDatabase(pool, database) {
|
|
218
|
+
const client = await pool.connect();
|
|
219
|
+
try {
|
|
220
|
+
const createSql = `CREATE DATABASE ${_KpgDatabaseBootstrapper.escapeIdentifier(database)}`;
|
|
221
|
+
await client.query(createSql);
|
|
222
|
+
} finally {
|
|
223
|
+
client.release();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
static parseDatabaseName(config) {
|
|
227
|
+
if (typeof config.database === "string" && config.database.trim().length > 0) {
|
|
228
|
+
return config.database.trim();
|
|
229
|
+
}
|
|
230
|
+
const connectionString = config.connectionString;
|
|
231
|
+
if (!connectionString) return null;
|
|
232
|
+
try {
|
|
233
|
+
const url = new URL(connectionString);
|
|
234
|
+
const pathname = url.pathname.replace(/^\//, "");
|
|
235
|
+
if (!pathname) return null;
|
|
236
|
+
const [candidate] = pathname.split("/");
|
|
237
|
+
return candidate ? decodeURIComponent(candidate) : null;
|
|
238
|
+
} catch {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
static applyDatabaseToConnectionString(connectionString, database) {
|
|
243
|
+
try {
|
|
244
|
+
const url = new URL(connectionString);
|
|
245
|
+
url.pathname = `/${encodeURIComponent(database)}`;
|
|
246
|
+
return url.toString();
|
|
247
|
+
} catch {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
static escapeIdentifier(identifier) {
|
|
252
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
var KpgMigrationManager = class {
|
|
256
|
+
static {
|
|
257
|
+
__name(this, "KpgMigrationManager");
|
|
258
|
+
}
|
|
259
|
+
ctx;
|
|
260
|
+
constructor(ctx) {
|
|
261
|
+
this.ctx = ctx;
|
|
262
|
+
}
|
|
263
|
+
async migrate(options) {
|
|
264
|
+
const { target, direction = "latest", steps } = options ?? {};
|
|
265
|
+
if (typeof target !== "undefined") {
|
|
266
|
+
const label = target === kysely.NO_MIGRATIONS ? "NO_MIGRATIONS" : target;
|
|
267
|
+
await this.runMigration((migrator) => migrator.migrateTo(target), `Migrating to ${chalk3__default.default.yellow(label)}...`);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
switch (direction) {
|
|
271
|
+
case "latest":
|
|
272
|
+
await this.runMigration((migrator) => migrator.migrateToLatest());
|
|
273
|
+
return;
|
|
274
|
+
case "up":
|
|
275
|
+
case "down": {
|
|
276
|
+
const stepCount = steps ?? 1;
|
|
277
|
+
if (!Number.isInteger(stepCount) || stepCount < 0) {
|
|
278
|
+
throw new Error("Migration step count must be a non-negative integer");
|
|
279
|
+
}
|
|
280
|
+
if (stepCount === 0) {
|
|
281
|
+
this.logMigrationResults([]);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const runner = direction === "up" ? (migrator) => migrator.migrateUp() : (migrator) => migrator.migrateDown();
|
|
285
|
+
await this.runStepwise(stepCount, direction, runner);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
default:
|
|
289
|
+
throw new Error(`Unknown migration direction: ${String(direction)}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
async migrateUp(options) {
|
|
293
|
+
if (typeof options?.steps === "undefined") {
|
|
294
|
+
await this.migrate({
|
|
295
|
+
direction: "up"
|
|
296
|
+
});
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
await this.migrate({
|
|
300
|
+
direction: "up",
|
|
301
|
+
steps: options.steps
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
async migrateDown(options) {
|
|
305
|
+
if (typeof options?.steps === "undefined") {
|
|
306
|
+
await this.migrate({
|
|
307
|
+
direction: "down"
|
|
308
|
+
});
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
await this.migrate({
|
|
312
|
+
direction: "down",
|
|
313
|
+
steps: options.steps
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
async listMigrations() {
|
|
317
|
+
const migrator = await this.createMigrator();
|
|
318
|
+
return migrator.getMigrations();
|
|
319
|
+
}
|
|
320
|
+
async runMigration(runner, runningMessage = "Running migrations...") {
|
|
321
|
+
this.ctx.logger.info(chalk3__default.default.gray("Preparing migrations..."));
|
|
322
|
+
const migrator = await this.createMigrator();
|
|
323
|
+
this.ctx.logger.info(chalk3__default.default.gray(runningMessage));
|
|
324
|
+
const { error, results } = await runner(migrator);
|
|
325
|
+
this.logMigrationResults(results ?? []);
|
|
326
|
+
if (error) {
|
|
327
|
+
this.handleMigrationError(error);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
async runStepwise(steps, direction, runner) {
|
|
331
|
+
this.ctx.logger.info(chalk3__default.default.gray("Preparing migrations..."));
|
|
332
|
+
const migrator = await this.createMigrator();
|
|
333
|
+
const directionLabel = direction === "up" ? "Running" : "Reverting";
|
|
334
|
+
const countLabel = steps === 1 ? "one migration" : `${chalk3__default.default.yellow(String(steps))} migrations`;
|
|
335
|
+
this.ctx.logger.info(chalk3__default.default.gray(`${directionLabel} ${countLabel}...`));
|
|
336
|
+
const aggregated = [];
|
|
337
|
+
let encounteredError;
|
|
338
|
+
for (let index = 0; index < steps; index += 1) {
|
|
339
|
+
const { error, results } = await runner(migrator);
|
|
340
|
+
if (results?.length) {
|
|
341
|
+
aggregated.push(...results);
|
|
342
|
+
}
|
|
343
|
+
if (error) {
|
|
344
|
+
encounteredError = error;
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
if (!results?.length) {
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
this.logMigrationResults(aggregated);
|
|
352
|
+
if (encounteredError) {
|
|
353
|
+
this.handleMigrationError(encounteredError);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
async createMigrator() {
|
|
357
|
+
const provider = await this.getMigrationProvider();
|
|
358
|
+
const { config } = this.ctx;
|
|
359
|
+
return new kysely.Migrator({
|
|
360
|
+
db: this.ctx.db,
|
|
361
|
+
provider,
|
|
362
|
+
allowUnorderedMigrations: config.allowUnorderedMigrations ?? false,
|
|
363
|
+
...utils.keepDefined(config, "migrationTableName", "migrationLockTableName", "migrationTableSchema")
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
async getMigrationProvider() {
|
|
367
|
+
const { path: target } = this.ctx.config;
|
|
368
|
+
const resolvedTarget = Array.isArray(target) ? target.map((entry) => this.resolvePath(entry)) : this.resolvePath(target);
|
|
369
|
+
if (Array.isArray(resolvedTarget)) {
|
|
370
|
+
this.logMigrationFiles(resolvedTarget);
|
|
371
|
+
return this.createModuleProvider(resolvedTarget);
|
|
372
|
+
}
|
|
373
|
+
let migrationStat;
|
|
374
|
+
try {
|
|
375
|
+
migrationStat = await fs.promises.stat(resolvedTarget);
|
|
376
|
+
} catch {
|
|
377
|
+
migrationStat = void 0;
|
|
378
|
+
}
|
|
379
|
+
if (migrationStat?.isDirectory()) {
|
|
380
|
+
const directory = this.relativePath(resolvedTarget);
|
|
381
|
+
this.ctx.logger.info(chalk3__default.default.gray(`Loading migrations directory ${chalk3__default.default.yellow(directory)}`));
|
|
382
|
+
return new kysely.FileMigrationProvider({
|
|
383
|
+
fs: fs.promises,
|
|
384
|
+
path: path__default.default,
|
|
385
|
+
migrationFolder: resolvedTarget
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
if (migrationStat?.isFile() ?? true) {
|
|
389
|
+
this.logMigrationFiles([
|
|
390
|
+
resolvedTarget
|
|
391
|
+
]);
|
|
392
|
+
return this.createModuleProvider([
|
|
393
|
+
resolvedTarget
|
|
394
|
+
]);
|
|
395
|
+
}
|
|
396
|
+
const label = Array.isArray(target) ? target.join(", ") : target;
|
|
397
|
+
throw new Error(`Unable to resolve migrations at path: ${label}`);
|
|
398
|
+
}
|
|
399
|
+
async createModuleProvider(files) {
|
|
400
|
+
if (files.length === 0) {
|
|
401
|
+
throw new Error("No migration files provided");
|
|
402
|
+
}
|
|
403
|
+
const comparator = this.ctx.config.nameComparator ?? ((nameA, nameB) => nameA.localeCompare(nameB));
|
|
404
|
+
const entries = await Promise.all(files.map(async (filePath) => {
|
|
405
|
+
const moduleUrl = url.pathToFileURL(filePath).href;
|
|
406
|
+
const mod = await import(moduleUrl);
|
|
407
|
+
if (!this.isMigrationModule(mod)) {
|
|
408
|
+
throw new Error(`Migration file ${filePath} must export async functions up and down`);
|
|
409
|
+
}
|
|
410
|
+
const { up, down } = mod;
|
|
411
|
+
const name = path__default.default.basename(filePath);
|
|
412
|
+
const migration = {
|
|
413
|
+
async up(db) {
|
|
414
|
+
await up(db);
|
|
415
|
+
},
|
|
416
|
+
async down(db) {
|
|
417
|
+
await down(db);
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
return [
|
|
421
|
+
name,
|
|
422
|
+
migration
|
|
423
|
+
];
|
|
424
|
+
}));
|
|
425
|
+
const sorted = entries.sort(([a], [b]) => comparator(a, b));
|
|
426
|
+
this.logPreparedMigrations(sorted);
|
|
427
|
+
return {
|
|
428
|
+
getMigrations: /* @__PURE__ */ __name(() => Promise.resolve(Object.fromEntries(sorted)), "getMigrations")
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
logMigrationFiles(files) {
|
|
432
|
+
if (!files.length) return;
|
|
433
|
+
this.ctx.logger.info("Loading migration file(s):");
|
|
434
|
+
for (const file of files) {
|
|
435
|
+
this.ctx.logger.info(`\u2192 ${chalk3__default.default.yellow(this.relativePath(file))}`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
logPreparedMigrations(entries) {
|
|
439
|
+
if (!entries.length) return;
|
|
440
|
+
this.ctx.logger.info("Prepared migrations:");
|
|
441
|
+
for (const [name] of entries) {
|
|
442
|
+
this.ctx.logger.info(`\u2192 ${chalk3__default.default.green(name)}`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
logMigrationResults(results) {
|
|
446
|
+
if (!results.length) {
|
|
447
|
+
this.ctx.logger.info(chalk3__default.default.gray("No migrations executed."));
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
this.ctx.logger.info("Migration results:");
|
|
451
|
+
for (const result of results) {
|
|
452
|
+
if (result.status === "Success") {
|
|
453
|
+
this.ctx.logger.info(` ${chalk3__default.default.green("\u2713")} ${chalk3__default.default.bold(result.migrationName)}`);
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
if (result.status === "Error") {
|
|
457
|
+
this.ctx.logger.error(` ${chalk3__default.default.red("\u2717")} ${chalk3__default.default.bold(result.migrationName)}`);
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
this.ctx.logger.info(` ${chalk3__default.default.yellow("\u2022")} ${chalk3__default.default.bold(result.migrationName)} ${chalk3__default.default.gray("(skipped)")}`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
relativePath(filePath) {
|
|
464
|
+
const relative = path__default.default.relative(this.ctx.baseDir, filePath);
|
|
465
|
+
return relative.startsWith("..") ? filePath : relative;
|
|
466
|
+
}
|
|
467
|
+
resolvePath(target) {
|
|
468
|
+
if (path__default.default.isAbsolute(target)) return target;
|
|
469
|
+
return path__default.default.resolve(this.ctx.baseDir, target);
|
|
470
|
+
}
|
|
471
|
+
handleMigrationError(error) {
|
|
472
|
+
const message = error instanceof Error ? error.message : util.inspect(error);
|
|
473
|
+
this.ctx.logger.error(`Migration failure: ${message}`);
|
|
474
|
+
if (error instanceof Error) {
|
|
475
|
+
throw error;
|
|
476
|
+
}
|
|
477
|
+
throw new Error(message);
|
|
478
|
+
}
|
|
479
|
+
isMigrationModule(value) {
|
|
480
|
+
if (!value || typeof value !== "object") return false;
|
|
481
|
+
if (!("up" in value) || !("down" in value)) return false;
|
|
482
|
+
const { up, down } = value;
|
|
483
|
+
return typeof up === "function" && typeof down === "function";
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
// src/kysely-pg/decorators/RegisterKpgService.ts
|
|
488
|
+
var PgServiceMetadataKey = Symbol("db:pgServiceKey");
|
|
489
|
+
var PgTableMetadataKey = Symbol("db:pgTable");
|
|
490
|
+
function RegisterKpgService(key, options) {
|
|
491
|
+
return (ctor) => {
|
|
492
|
+
Reflect.defineMetadata(PgServiceMetadataKey, key, ctor);
|
|
493
|
+
const tableName = options?.table ?? String(key);
|
|
494
|
+
Reflect.defineMetadata(PgTableMetadataKey, tableName, ctor);
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
__name(RegisterKpgService, "RegisterKpgService");
|
|
498
|
+
|
|
499
|
+
// src/kysely-pg/KpgService.ts
|
|
500
|
+
var KpgService = class {
|
|
501
|
+
static {
|
|
502
|
+
__name(this, "KpgService");
|
|
503
|
+
}
|
|
504
|
+
kysely;
|
|
505
|
+
core;
|
|
506
|
+
table;
|
|
507
|
+
constructor(kysely, core) {
|
|
508
|
+
this.kysely = kysely;
|
|
509
|
+
this.core = core;
|
|
510
|
+
const ctor = this.constructor;
|
|
511
|
+
const key = Reflect.getMetadata(PgServiceMetadataKey, ctor);
|
|
512
|
+
if (!key) throw new Error(`Missing @RegisterKpgService on ${ctor.name}`);
|
|
513
|
+
const table = Reflect.getMetadata(PgTableMetadataKey, ctor);
|
|
514
|
+
if (!table) {
|
|
515
|
+
throw new Error(`Missing table metadata for ${ctor.name}. Provide a table via @RegisterKpgService().`);
|
|
516
|
+
}
|
|
517
|
+
this.table = table;
|
|
518
|
+
this.kysely._register(key, this);
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Shared Kysely instance used to interact with the Postgres database.
|
|
522
|
+
*/
|
|
523
|
+
get db() {
|
|
524
|
+
return this.kysely.connection;
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
var KpgServiceRegistry = class {
|
|
528
|
+
static {
|
|
529
|
+
__name(this, "KpgServiceRegistry");
|
|
530
|
+
}
|
|
531
|
+
plugin;
|
|
532
|
+
core;
|
|
533
|
+
logger;
|
|
534
|
+
services = /* @__PURE__ */ Object.create(null);
|
|
535
|
+
constructor(plugin, core, logger) {
|
|
536
|
+
this.plugin = plugin;
|
|
537
|
+
this.core = core;
|
|
538
|
+
this.logger = logger;
|
|
539
|
+
}
|
|
540
|
+
get map() {
|
|
541
|
+
return this.services;
|
|
542
|
+
}
|
|
543
|
+
register(key, instance) {
|
|
544
|
+
this.services[key] = instance;
|
|
545
|
+
}
|
|
546
|
+
async loadFromDirectory(dir) {
|
|
547
|
+
this.logger.info(chalk3__default.default.bold(dir));
|
|
548
|
+
await seedcord.traverseDirectory(dir, (_full, rel, mod) => {
|
|
549
|
+
for (const Service of Object.values(mod)) {
|
|
550
|
+
if (this.isServiceClass(Service)) {
|
|
551
|
+
const instance = new Service(this.plugin, this.core);
|
|
552
|
+
this.logger.info(`${chalk3__default.default.italic("Registered")} ${chalk3__default.default.bold.yellow(instance.constructor.name)} from ${chalk3__default.default.gray(rel)}`);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}, this.logger);
|
|
556
|
+
this.logger.info(`${chalk3__default.default.bold.green("Loaded")}: ${chalk3__default.default.magenta(Object.keys(this.services).length)} services`);
|
|
557
|
+
}
|
|
558
|
+
isServiceClass(obj) {
|
|
559
|
+
return typeof obj === "function" && obj.prototype instanceof KpgService && Reflect.hasMetadata(PgServiceMetadataKey, obj);
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
var KyselyPg = class extends seedcord.Plugin {
|
|
563
|
+
static {
|
|
564
|
+
__name(this, "KyselyPg");
|
|
565
|
+
}
|
|
566
|
+
core;
|
|
567
|
+
options;
|
|
568
|
+
logger = new seedcord.Logger("KyselyPg");
|
|
569
|
+
isInitialised = false;
|
|
570
|
+
pool = null;
|
|
571
|
+
migrationManager = null;
|
|
572
|
+
serviceRegistry;
|
|
573
|
+
databaseBootstrapper;
|
|
574
|
+
databaseName = null;
|
|
575
|
+
/**
|
|
576
|
+
* Map of all services registered with the plugin, keyed by their decorator name.
|
|
577
|
+
*/
|
|
578
|
+
get services() {
|
|
579
|
+
return this.serviceRegistry.map;
|
|
580
|
+
}
|
|
581
|
+
constructor(core, options) {
|
|
582
|
+
super(core), this.core = core, this.options = options;
|
|
583
|
+
this.serviceRegistry = new KpgServiceRegistry(this, core, this.logger);
|
|
584
|
+
this.databaseBootstrapper = new KpgDatabaseBootstrapper(this.logger);
|
|
585
|
+
this.core.shutdown.addTask(seedcord.ShutdownPhase.ExternalResources, "stop-kyselypg", async () => await this.stop());
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Connects to Postgres, runs any startup migrations, and loads decorated services.
|
|
589
|
+
*
|
|
590
|
+
* Safe to call multiple times; subsequent calls exit early.
|
|
591
|
+
*/
|
|
592
|
+
async init() {
|
|
593
|
+
if (this.isInitialised) return;
|
|
594
|
+
this.isInitialised = true;
|
|
595
|
+
await this.connect();
|
|
596
|
+
const startupConfig = this.options.migrations.onStartup;
|
|
597
|
+
if (startupConfig !== false) {
|
|
598
|
+
if (startupConfig && typeof startupConfig !== "boolean") {
|
|
599
|
+
await this.migrate(startupConfig);
|
|
600
|
+
} else {
|
|
601
|
+
await this.migrate();
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
await this.serviceRegistry.loadFromDirectory(this.options.dir);
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Tears down the connection pool and clears the migration manager reference.
|
|
608
|
+
*/
|
|
609
|
+
async stop() {
|
|
610
|
+
await this.disconnect();
|
|
611
|
+
}
|
|
612
|
+
async connect() {
|
|
613
|
+
const pool = await this.resolvePool();
|
|
614
|
+
this.pool = pool;
|
|
615
|
+
this.registerOnConnectStatements(pool, this.options.onConnectSQL);
|
|
616
|
+
try {
|
|
617
|
+
await this.testPoolConnection(pool);
|
|
618
|
+
this.connection = new kysely.Kysely({
|
|
619
|
+
dialect: new kysely.PostgresDialect({
|
|
620
|
+
pool
|
|
621
|
+
}),
|
|
622
|
+
...seedcord.keepDefined(this.options.kysely ?? {})
|
|
623
|
+
});
|
|
624
|
+
this.migrationManager = new KpgMigrationManager({
|
|
625
|
+
db: this.connection,
|
|
626
|
+
logger: this.logger,
|
|
627
|
+
config: this.options.migrations,
|
|
628
|
+
baseDir: process.cwd()
|
|
629
|
+
});
|
|
630
|
+
const dbLabel = this.databaseName ?? "unknown";
|
|
631
|
+
this.logger.info(`Connected to Postgres database ${chalk3__default.default.bold.magenta(dbLabel)}`);
|
|
632
|
+
} catch (err) {
|
|
633
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
634
|
+
this.logger.error(`Could not connect to Postgres: ${error.message}`);
|
|
635
|
+
throw error;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
async disconnect() {
|
|
639
|
+
const pool = this.pool;
|
|
640
|
+
if (!pool) return;
|
|
641
|
+
this.pool = null;
|
|
642
|
+
this.migrationManager = null;
|
|
643
|
+
this.logger.info(chalk3__default.default.gray("Closing Postgres pool."));
|
|
644
|
+
await pool.end().catch((err) => {
|
|
645
|
+
this.logger.error(`Could not close pg pool: ${err.message}`);
|
|
646
|
+
});
|
|
647
|
+
this.logger.info(chalk3__default.default.red.bold("Disconnected from Postgres"));
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Runs migrations using the supplied options or defaults to `latest`.
|
|
651
|
+
*
|
|
652
|
+
* @param options - Target migration or direction overrides
|
|
653
|
+
*/
|
|
654
|
+
async migrate(options) {
|
|
655
|
+
await this.getMigrationManager().migrate(options);
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Runs a single upwards migration step unless a custom count is provided.
|
|
659
|
+
*
|
|
660
|
+
* @param options - Optional configuration for step-based execution
|
|
661
|
+
*/
|
|
662
|
+
async migrateUp(options) {
|
|
663
|
+
await this.getMigrationManager().migrateUp(options);
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Runs a single downwards migration step unless a custom count is provided.
|
|
667
|
+
*
|
|
668
|
+
* @param options - Optional configuration for step-based execution
|
|
669
|
+
*/
|
|
670
|
+
async migrateDown(options) {
|
|
671
|
+
await this.getMigrationManager().migrateDown(options);
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Lists every migration the manager knows about along with its execution state.
|
|
675
|
+
*/
|
|
676
|
+
listMigrations() {
|
|
677
|
+
return this.getMigrationManager().listMigrations();
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Lists unapplied migrations.
|
|
681
|
+
*/
|
|
682
|
+
async listPendingMigrations() {
|
|
683
|
+
const all = await this.listMigrations();
|
|
684
|
+
return all.filter((m) => !m.executedAt);
|
|
685
|
+
}
|
|
686
|
+
getMigrationManager() {
|
|
687
|
+
if (this.migrationManager) return this.migrationManager;
|
|
688
|
+
const manager = new KpgMigrationManager({
|
|
689
|
+
db: this.connection,
|
|
690
|
+
logger: this.logger,
|
|
691
|
+
config: this.options.migrations,
|
|
692
|
+
baseDir: process.cwd()
|
|
693
|
+
});
|
|
694
|
+
this.migrationManager = manager;
|
|
695
|
+
return manager;
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Register hook used by decorated services.
|
|
699
|
+
*
|
|
700
|
+
* @internal
|
|
701
|
+
*/
|
|
702
|
+
_register(key, instance) {
|
|
703
|
+
this.serviceRegistry.register(key, instance);
|
|
704
|
+
}
|
|
705
|
+
async resolvePool() {
|
|
706
|
+
const { pool: providedPool, connectionString } = this.options;
|
|
707
|
+
if (providedPool instanceof pg.Pool) {
|
|
708
|
+
this.logger.info(chalk3__default.default.gray("Reusing provided Postgres pool instance."));
|
|
709
|
+
this.databaseName = this.databaseBootstrapper.resolveDatabaseFromPool(providedPool);
|
|
710
|
+
return providedPool;
|
|
711
|
+
}
|
|
712
|
+
const baseConfig = this.createPoolConfig(providedPool, connectionString);
|
|
713
|
+
await this.databaseBootstrapper.ensure(baseConfig);
|
|
714
|
+
this.databaseName = this.databaseBootstrapper.resolveDatabaseName(baseConfig);
|
|
715
|
+
this.logger.info(chalk3__default.default.gray("Creating new Postgres pool."));
|
|
716
|
+
return new pg.Pool(baseConfig);
|
|
717
|
+
}
|
|
718
|
+
createPoolConfig(poolConfig, connectionString) {
|
|
719
|
+
const config = poolConfig ? {
|
|
720
|
+
...poolConfig
|
|
721
|
+
} : {};
|
|
722
|
+
if (connectionString) {
|
|
723
|
+
config.connectionString = connectionString;
|
|
724
|
+
}
|
|
725
|
+
if (this.options.forceInsecureSSL) {
|
|
726
|
+
config.ssl = {
|
|
727
|
+
rejectUnauthorized: false
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
return config;
|
|
731
|
+
}
|
|
732
|
+
registerOnConnectStatements(pool, statements) {
|
|
733
|
+
if (!statements?.length) return;
|
|
734
|
+
const queuedStatements = [
|
|
735
|
+
...statements
|
|
736
|
+
];
|
|
737
|
+
pool.on("connect", (client) => {
|
|
738
|
+
void (async () => {
|
|
739
|
+
for (const sql of queuedStatements) {
|
|
740
|
+
await client.query(sql);
|
|
741
|
+
}
|
|
742
|
+
})();
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
async testPoolConnection(pool) {
|
|
746
|
+
const client = await pool.connect();
|
|
747
|
+
client.release();
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
function WrapDatabaseError(errorMessage) {
|
|
119
751
|
return function(_target, _propertyKey, descriptor) {
|
|
120
752
|
const originalMethod = descriptor.value;
|
|
121
753
|
descriptor.value = async function(...args) {
|
|
@@ -134,14 +766,22 @@ function DBCatchable(errorMessage) {
|
|
|
134
766
|
};
|
|
135
767
|
};
|
|
136
768
|
}
|
|
137
|
-
__name(
|
|
769
|
+
__name(WrapDatabaseError, "WrapDatabaseError");
|
|
138
770
|
|
|
139
|
-
exports.
|
|
140
|
-
exports.
|
|
141
|
-
exports.
|
|
771
|
+
exports.KpgDatabaseBootstrapper = KpgDatabaseBootstrapper;
|
|
772
|
+
exports.KpgMigrationManager = KpgMigrationManager;
|
|
773
|
+
exports.KpgService = KpgService;
|
|
774
|
+
exports.KpgServiceRegistry = KpgServiceRegistry;
|
|
775
|
+
exports.KyselyPg = KyselyPg;
|
|
142
776
|
exports.ModelMetadataKey = ModelMetadataKey;
|
|
143
777
|
exports.Mongo = Mongo;
|
|
144
778
|
exports.MongoService = MongoService;
|
|
779
|
+
exports.PgServiceMetadataKey = PgServiceMetadataKey;
|
|
780
|
+
exports.PgTableMetadataKey = PgTableMetadataKey;
|
|
781
|
+
exports.RegisterKpgService = RegisterKpgService;
|
|
782
|
+
exports.RegisterMongoModel = RegisterMongoModel;
|
|
783
|
+
exports.RegisterMongoService = RegisterMongoService;
|
|
145
784
|
exports.ServiceMetadataKey = ServiceMetadataKey;
|
|
785
|
+
exports.WrapDatabaseError = WrapDatabaseError;
|
|
146
786
|
//# sourceMappingURL=index.cjs.map
|
|
147
787
|
//# sourceMappingURL=index.cjs.map
|