@seedcord/plugins 0.5.0 → 0.6.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 +2 -2
- package/dist/index.cjs +971 -781
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -538
- package/dist/index.d.mts +1273 -0
- package/dist/index.mjs +941 -768
- package/dist/index.mjs.map +1 -1
- package/package.json +50 -28
- package/dist/index.d.ts +0 -538
package/dist/index.mjs
CHANGED
|
@@ -1,792 +1,965 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
import { SeedcordError, SeedcordRangeError } from "@seedcord/services/internal";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { Envapter } from "envapt";
|
|
5
|
+
import mongoose from "mongoose";
|
|
6
|
+
import { CustomError, HmrModuleHandler, Logger, Plugin, SeedcordErrorCode, ShutdownPhase, keepDefined, traverseDirectory } from "seedcord";
|
|
7
|
+
import { Kysely, PostgresDialect } from "kysely";
|
|
8
|
+
import { Pool } from "pg";
|
|
9
|
+
import { promises } from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { pathToFileURL } from "node:url";
|
|
12
|
+
import { inspect } from "node:util";
|
|
13
|
+
import { keepDefined as keepDefined$1 } from "@seedcord/utils";
|
|
14
|
+
import { FileMigrationProvider, Migrator, NO_MIGRATIONS } from "kysely/migration";
|
|
15
|
+
import { DatabaseError, SeedcordError as SeedcordError$1 } from "seedcord/internal";
|
|
16
|
+
import { randomUUID } from "node:crypto";
|
|
17
|
+
import { Logger as Logger$1 } from "@seedcord/services";
|
|
13
18
|
|
|
14
|
-
|
|
15
|
-
|
|
19
|
+
//#region src/mongo/decorators/RegisterMongoModel.ts
|
|
20
|
+
const ModelMetadataKey = Symbol("db:model");
|
|
21
|
+
/**
|
|
22
|
+
* Associates a Mongoose model with a database service
|
|
23
|
+
*
|
|
24
|
+
* Creates a Mongoose model from the decorated static schema property and stores it
|
|
25
|
+
* for service registration. The model becomes available as `this.model` in the service.
|
|
26
|
+
* Must be applied to a `public static schema` property in the service class.
|
|
27
|
+
*
|
|
28
|
+
* @typeParam TService - The service key type
|
|
29
|
+
* @param collection - Collection name for the Mongoose model
|
|
30
|
+
* @decorator
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* \@RegisterMongoService('users')
|
|
34
|
+
* export class Users extends MongoService<IUser> {
|
|
35
|
+
* \@RegisterMongoModel('users')
|
|
36
|
+
* public static schema = new mongoose.Schema<IUser>({
|
|
37
|
+
* username: { type: String, required: true, unique: true }
|
|
38
|
+
* });
|
|
39
|
+
* }
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
function RegisterMongoModel(collection) {
|
|
43
|
+
return (target, propertyKey) => {
|
|
44
|
+
const schema = target[propertyKey];
|
|
45
|
+
const name = String(collection);
|
|
46
|
+
const model = mongoose.model(name, schema);
|
|
47
|
+
Reflect.defineMetadata(ModelMetadataKey, model, target);
|
|
48
|
+
};
|
|
49
|
+
}
|
|
16
50
|
|
|
17
|
-
|
|
18
|
-
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region src/mongo/decorators/RegisterMongoService.ts
|
|
53
|
+
const ServiceMetadataKey = Symbol("db:serviceKey");
|
|
54
|
+
/**
|
|
55
|
+
* Registers a database service with a typed key
|
|
56
|
+
*
|
|
57
|
+
* Associates a service class with a key for dependency injection.
|
|
58
|
+
* The service becomes available via `core.db.services[key]`.
|
|
59
|
+
*
|
|
60
|
+
* @typeParam TService - The service key type
|
|
61
|
+
* @param key - Service key for registration and type-safe access
|
|
62
|
+
* @decorator
|
|
63
|
+
* @example
|
|
64
|
+
* ```typescript
|
|
65
|
+
* \@RegisterMongoService('users')
|
|
66
|
+
* export class Users<Doc extends IUser = IUser> extends MongoService<Doc> {
|
|
67
|
+
* // Some code
|
|
68
|
+
* }
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
19
71
|
function RegisterMongoService(key) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
72
|
+
return (ctor) => {
|
|
73
|
+
Reflect.defineMetadata(ServiceMetadataKey, key, ctor);
|
|
74
|
+
};
|
|
23
75
|
}
|
|
24
|
-
__name(RegisterMongoService, "RegisterMongoService");
|
|
25
|
-
var ModelMetadataKey = Symbol("db:model");
|
|
26
|
-
function RegisterMongoModel(collection) {
|
|
27
|
-
return (target, propertyKey) => {
|
|
28
|
-
const schema = target[propertyKey];
|
|
29
|
-
const name = String(collection);
|
|
30
|
-
const model = mongoose.model(name, schema);
|
|
31
|
-
Reflect.defineMetadata(ModelMetadataKey, model, target);
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
__name(RegisterMongoModel, "RegisterMongoModel");
|
|
35
76
|
|
|
36
|
-
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region src/mongo/MongoService.ts
|
|
79
|
+
/**
|
|
80
|
+
* Base class for MongoDB service layers
|
|
81
|
+
*
|
|
82
|
+
* Provides typed access to MongoDB collections through Mongoose models.
|
|
83
|
+
* Services are automatically registered with the Mongo plugin when instantiated.
|
|
84
|
+
*
|
|
85
|
+
* @typeParam Doc - The document type this service manages
|
|
86
|
+
* @example
|
|
87
|
+
* ```typescript
|
|
88
|
+
* \@RegisterMongoService('users')
|
|
89
|
+
* export class Users extends MongoService<IUser> {
|
|
90
|
+
* \@RegisterMongoModel('users')
|
|
91
|
+
* public static schema = new mongoose.Schema<IUser>({
|
|
92
|
+
* username: { type: String, required: true, unique: true }
|
|
93
|
+
* });
|
|
94
|
+
*
|
|
95
|
+
* // Custom methods here
|
|
96
|
+
* public async findByUsername(username: string) {
|
|
97
|
+
* return this.model.findOne({ username });
|
|
98
|
+
* }
|
|
99
|
+
* }
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
37
102
|
var MongoService = class {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
]);
|
|
53
|
-
}
|
|
54
|
-
const model = Reflect.getMetadata(ModelMetadataKey, ctor);
|
|
55
|
-
if (!model) {
|
|
56
|
-
throw new SeedcordError(SeedcordErrorCode.PluginMongoModelDecoratorMissing, [
|
|
57
|
-
ctor.name
|
|
58
|
-
]);
|
|
59
|
-
}
|
|
60
|
-
this.model = model;
|
|
61
|
-
db._register(key, this);
|
|
62
|
-
}
|
|
103
|
+
db;
|
|
104
|
+
core;
|
|
105
|
+
model;
|
|
106
|
+
constructor(db, core) {
|
|
107
|
+
this.db = db;
|
|
108
|
+
this.core = core;
|
|
109
|
+
const ctor = this.constructor;
|
|
110
|
+
const key = Reflect.getMetadata(ServiceMetadataKey, ctor);
|
|
111
|
+
if (!key) throw new SeedcordError(SeedcordErrorCode.PluginMongoServiceDecoratorMissing, [ctor.name]);
|
|
112
|
+
const model = Reflect.getMetadata(ModelMetadataKey, ctor);
|
|
113
|
+
if (!model) throw new SeedcordError(SeedcordErrorCode.PluginMongoModelDecoratorMissing, [ctor.name]);
|
|
114
|
+
this.model = model;
|
|
115
|
+
db._register(key, this);
|
|
116
|
+
}
|
|
63
117
|
};
|
|
64
118
|
|
|
65
|
-
|
|
119
|
+
//#endregion
|
|
120
|
+
//#region src/mongo/Mongo.ts
|
|
121
|
+
/**
|
|
122
|
+
* MongoDB integration plugin for Seedcord.
|
|
123
|
+
*
|
|
124
|
+
* Manages MongoDB connections, service loading, and provides type-safe
|
|
125
|
+
* access to database services through service registration decorators.
|
|
126
|
+
*/
|
|
66
127
|
var Mongo = class extends Plugin {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
await this.createDatabase(adminPool, targetDb);
|
|
189
|
-
this.logger.info(chalk3.green(`Created database ${chalk3.bold(targetDb)}.`));
|
|
190
|
-
} catch (error) {
|
|
191
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
192
|
-
this.logger.error(`Failed to ensure database ${targetDb}: ${err.message}`);
|
|
193
|
-
throw err;
|
|
194
|
-
} finally {
|
|
195
|
-
await adminPool.end();
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
buildAdminConfig(baseConfig) {
|
|
199
|
-
const adminConfig = {
|
|
200
|
-
...baseConfig
|
|
201
|
-
};
|
|
202
|
-
const { connectionString } = adminConfig;
|
|
203
|
-
if (connectionString) {
|
|
204
|
-
const connection = _KpgDatabaseBootstrapper.applyDatabaseToConnectionString(connectionString, _KpgDatabaseBootstrapper.ADMIN_DB);
|
|
205
|
-
if (!connection) return null;
|
|
206
|
-
adminConfig.connectionString = connection;
|
|
207
|
-
}
|
|
208
|
-
adminConfig.database = _KpgDatabaseBootstrapper.ADMIN_DB;
|
|
209
|
-
return adminConfig;
|
|
210
|
-
}
|
|
211
|
-
async databaseExists(pool, database) {
|
|
212
|
-
const client = await pool.connect();
|
|
213
|
-
try {
|
|
214
|
-
const { rows } = await client.query(_KpgDatabaseBootstrapper.DATABASE_EXISTS_SQL, [
|
|
215
|
-
database
|
|
216
|
-
]);
|
|
217
|
-
return Boolean(rows[0]?.exists);
|
|
218
|
-
} finally {
|
|
219
|
-
client.release();
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
async createDatabase(pool, database) {
|
|
223
|
-
const client = await pool.connect();
|
|
224
|
-
try {
|
|
225
|
-
const createSql = `CREATE DATABASE ${_KpgDatabaseBootstrapper.escapeIdentifier(database)}`;
|
|
226
|
-
await client.query(createSql);
|
|
227
|
-
} finally {
|
|
228
|
-
client.release();
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
static parseDatabaseName(config) {
|
|
232
|
-
if (typeof config.database === "string" && config.database.trim().length > 0) {
|
|
233
|
-
return config.database.trim();
|
|
234
|
-
}
|
|
235
|
-
const connectionString = config.connectionString;
|
|
236
|
-
if (!connectionString) return null;
|
|
237
|
-
try {
|
|
238
|
-
const url = new URL(connectionString);
|
|
239
|
-
const pathname = url.pathname.replace(/^\//, "");
|
|
240
|
-
if (!pathname) return null;
|
|
241
|
-
const [candidate] = pathname.split("/");
|
|
242
|
-
return candidate ? decodeURIComponent(candidate) : null;
|
|
243
|
-
} catch {
|
|
244
|
-
return null;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
static applyDatabaseToConnectionString(connectionString, database) {
|
|
248
|
-
try {
|
|
249
|
-
const url = new URL(connectionString);
|
|
250
|
-
url.pathname = `/${encodeURIComponent(database)}`;
|
|
251
|
-
return url.toString();
|
|
252
|
-
} catch {
|
|
253
|
-
return null;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
static escapeIdentifier(identifier) {
|
|
257
|
-
return `"${identifier.replace(/"/g, '""')}"`;
|
|
258
|
-
}
|
|
259
|
-
};
|
|
260
|
-
var KpgMigrationManager = class {
|
|
261
|
-
static {
|
|
262
|
-
__name(this, "KpgMigrationManager");
|
|
263
|
-
}
|
|
264
|
-
ctx;
|
|
265
|
-
constructor(ctx) {
|
|
266
|
-
this.ctx = ctx;
|
|
267
|
-
}
|
|
268
|
-
async migrate(options) {
|
|
269
|
-
const { target, direction = "latest", steps } = options ?? {};
|
|
270
|
-
if (typeof target !== "undefined") {
|
|
271
|
-
const label = target === NO_MIGRATIONS ? "NO_MIGRATIONS" : target;
|
|
272
|
-
await this.runMigration((migrator) => migrator.migrateTo(target), `Migrating to ${chalk3.yellow(label)}...`);
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
switch (direction) {
|
|
276
|
-
case "latest":
|
|
277
|
-
await this.runMigration((migrator) => migrator.migrateToLatest());
|
|
278
|
-
return;
|
|
279
|
-
case "up":
|
|
280
|
-
case "down": {
|
|
281
|
-
const stepCount = steps ?? 1;
|
|
282
|
-
if (!Number.isInteger(stepCount) || stepCount < 0) {
|
|
283
|
-
throw new SeedcordRangeError(SeedcordErrorCode.PluginKpgInvalidStepCount);
|
|
284
|
-
}
|
|
285
|
-
if (stepCount === 0) {
|
|
286
|
-
this.logMigrationResults([]);
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
const runner = direction === "up" ? (migrator) => migrator.migrateUp() : (migrator) => migrator.migrateDown();
|
|
290
|
-
await this.runStepwise(stepCount, direction, runner);
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
default:
|
|
294
|
-
throw new SeedcordError(SeedcordErrorCode.PluginKpgUnknownDirection, [
|
|
295
|
-
direction
|
|
296
|
-
]);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
async migrateUp(options) {
|
|
300
|
-
if (typeof options?.steps === "undefined") {
|
|
301
|
-
await this.migrate({
|
|
302
|
-
direction: "up"
|
|
303
|
-
});
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
await this.migrate({
|
|
307
|
-
direction: "up",
|
|
308
|
-
steps: options.steps
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
async migrateDown(options) {
|
|
312
|
-
if (typeof options?.steps === "undefined") {
|
|
313
|
-
await this.migrate({
|
|
314
|
-
direction: "down"
|
|
315
|
-
});
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
await this.migrate({
|
|
319
|
-
direction: "down",
|
|
320
|
-
steps: options.steps
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
async listMigrations() {
|
|
324
|
-
const migrator = await this.createMigrator();
|
|
325
|
-
return migrator.getMigrations();
|
|
326
|
-
}
|
|
327
|
-
async runMigration(runner, runningMessage = "Running migrations...") {
|
|
328
|
-
this.ctx.logger.info(chalk3.gray("Preparing migrations..."));
|
|
329
|
-
const migrator = await this.createMigrator();
|
|
330
|
-
this.ctx.logger.info(chalk3.gray(runningMessage));
|
|
331
|
-
const { error, results } = await runner(migrator);
|
|
332
|
-
this.logMigrationResults(results ?? []);
|
|
333
|
-
if (error) {
|
|
334
|
-
this.handleMigrationError(error);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
async runStepwise(steps, direction, runner) {
|
|
338
|
-
this.ctx.logger.info(chalk3.gray("Preparing migrations..."));
|
|
339
|
-
const migrator = await this.createMigrator();
|
|
340
|
-
const directionLabel = direction === "up" ? "Running" : "Reverting";
|
|
341
|
-
const countLabel = steps === 1 ? "one migration" : `${chalk3.yellow(String(steps))} migrations`;
|
|
342
|
-
this.ctx.logger.info(chalk3.gray(`${directionLabel} ${countLabel}...`));
|
|
343
|
-
const aggregated = [];
|
|
344
|
-
let encounteredError;
|
|
345
|
-
for (let index = 0; index < steps; index += 1) {
|
|
346
|
-
const { error, results } = await runner(migrator);
|
|
347
|
-
if (results?.length) {
|
|
348
|
-
aggregated.push(...results);
|
|
349
|
-
}
|
|
350
|
-
if (error) {
|
|
351
|
-
encounteredError = error;
|
|
352
|
-
break;
|
|
353
|
-
}
|
|
354
|
-
if (!results?.length) {
|
|
355
|
-
break;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
this.logMigrationResults(aggregated);
|
|
359
|
-
if (encounteredError) {
|
|
360
|
-
this.handleMigrationError(encounteredError);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
async createMigrator() {
|
|
364
|
-
const provider = await this.getMigrationProvider();
|
|
365
|
-
const { config } = this.ctx;
|
|
366
|
-
return new Migrator({
|
|
367
|
-
db: this.ctx.db,
|
|
368
|
-
provider,
|
|
369
|
-
allowUnorderedMigrations: config.allowUnorderedMigrations ?? false,
|
|
370
|
-
...keepDefined$1(config, "migrationTableName", "migrationLockTableName", "migrationTableSchema")
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
async getMigrationProvider() {
|
|
374
|
-
const { path: target } = this.ctx.config;
|
|
375
|
-
const resolvedTarget = Array.isArray(target) ? target.map((entry) => this.resolvePath(entry)) : this.resolvePath(target);
|
|
376
|
-
if (Array.isArray(resolvedTarget)) {
|
|
377
|
-
this.logMigrationFiles(resolvedTarget);
|
|
378
|
-
return this.createModuleProvider(resolvedTarget);
|
|
379
|
-
}
|
|
380
|
-
let migrationStat;
|
|
381
|
-
try {
|
|
382
|
-
migrationStat = await promises.stat(resolvedTarget);
|
|
383
|
-
} catch {
|
|
384
|
-
migrationStat = void 0;
|
|
385
|
-
}
|
|
386
|
-
if (migrationStat?.isDirectory()) {
|
|
387
|
-
const directory = this.relativePath(resolvedTarget);
|
|
388
|
-
this.ctx.logger.info(chalk3.gray(`Loading migrations directory ${chalk3.yellow(directory)}`));
|
|
389
|
-
return new FileMigrationProvider({
|
|
390
|
-
fs: promises,
|
|
391
|
-
path,
|
|
392
|
-
migrationFolder: resolvedTarget
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
if (migrationStat?.isFile() ?? true) {
|
|
396
|
-
this.logMigrationFiles([
|
|
397
|
-
resolvedTarget
|
|
398
|
-
]);
|
|
399
|
-
return this.createModuleProvider([
|
|
400
|
-
resolvedTarget
|
|
401
|
-
]);
|
|
402
|
-
}
|
|
403
|
-
const label = Array.isArray(target) ? target.join(", ") : target;
|
|
404
|
-
throw new SeedcordError(SeedcordErrorCode.PluginKpgUnresolvedMigrationsPath, [
|
|
405
|
-
label
|
|
406
|
-
]);
|
|
407
|
-
}
|
|
408
|
-
async createModuleProvider(files) {
|
|
409
|
-
if (files.length === 0) {
|
|
410
|
-
throw new SeedcordError(SeedcordErrorCode.PluginKpgNoMigrationFiles);
|
|
411
|
-
}
|
|
412
|
-
const comparator = this.ctx.config.nameComparator ?? ((nameA, nameB) => nameA.localeCompare(nameB));
|
|
413
|
-
const entries = await Promise.all(files.map(async (filePath) => {
|
|
414
|
-
const moduleUrl = pathToFileURL(filePath).href;
|
|
415
|
-
const mod = await import(moduleUrl);
|
|
416
|
-
if (!this.isMigrationModule(mod)) {
|
|
417
|
-
throw new SeedcordError(SeedcordErrorCode.PluginKpgInvalidMigrationModule, [
|
|
418
|
-
filePath
|
|
419
|
-
]);
|
|
420
|
-
}
|
|
421
|
-
const { up, down } = mod;
|
|
422
|
-
const name = path.basename(filePath);
|
|
423
|
-
const migration = {
|
|
424
|
-
async up(db) {
|
|
425
|
-
await up(db);
|
|
426
|
-
},
|
|
427
|
-
async down(db) {
|
|
428
|
-
await down(db);
|
|
429
|
-
}
|
|
430
|
-
};
|
|
431
|
-
return [
|
|
432
|
-
name,
|
|
433
|
-
migration
|
|
434
|
-
];
|
|
435
|
-
}));
|
|
436
|
-
const sorted = entries.sort(([a], [b]) => comparator(a, b));
|
|
437
|
-
this.logPreparedMigrations(sorted);
|
|
438
|
-
return {
|
|
439
|
-
getMigrations: /* @__PURE__ */ __name(() => Promise.resolve(Object.fromEntries(sorted)), "getMigrations")
|
|
440
|
-
};
|
|
441
|
-
}
|
|
442
|
-
logMigrationFiles(files) {
|
|
443
|
-
if (!files.length) return;
|
|
444
|
-
this.ctx.logger.info("Loading migration file(s):");
|
|
445
|
-
for (const file of files) {
|
|
446
|
-
this.ctx.logger.info(`\u2192 ${chalk3.yellow(this.relativePath(file))}`);
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
logPreparedMigrations(entries) {
|
|
450
|
-
if (!entries.length) return;
|
|
451
|
-
this.ctx.logger.info("Prepared migrations:");
|
|
452
|
-
for (const [name] of entries) {
|
|
453
|
-
this.ctx.logger.info(`\u2192 ${chalk3.green(name)}`);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
logMigrationResults(results) {
|
|
457
|
-
if (!results.length) {
|
|
458
|
-
this.ctx.logger.info(chalk3.gray("No migrations executed."));
|
|
459
|
-
return;
|
|
460
|
-
}
|
|
461
|
-
this.ctx.logger.info("Migration results:");
|
|
462
|
-
for (const result of results) {
|
|
463
|
-
if (result.status === "Success") {
|
|
464
|
-
this.ctx.logger.info(`${chalk3.green("\u2713")} ${chalk3.bold(result.migrationName)}`);
|
|
465
|
-
continue;
|
|
466
|
-
}
|
|
467
|
-
if (result.status === "Error") {
|
|
468
|
-
this.ctx.logger.error(`${chalk3.red("\u2717")} ${chalk3.bold(result.migrationName)}`);
|
|
469
|
-
continue;
|
|
470
|
-
}
|
|
471
|
-
this.ctx.logger.info(`${chalk3.yellow("\u2022")} ${chalk3.bold(result.migrationName)} ${chalk3.gray("(skipped)")}`);
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
relativePath(filePath) {
|
|
475
|
-
const relative = path.relative(this.ctx.baseDir, filePath);
|
|
476
|
-
return relative.startsWith("..") ? filePath : relative;
|
|
477
|
-
}
|
|
478
|
-
resolvePath(target) {
|
|
479
|
-
if (path.isAbsolute(target)) return target;
|
|
480
|
-
return path.resolve(this.ctx.baseDir, target);
|
|
481
|
-
}
|
|
482
|
-
handleMigrationError(error) {
|
|
483
|
-
const message = error instanceof Error ? error.message : inspect(error);
|
|
484
|
-
this.ctx.logger.error(`Migration failure: ${message}`);
|
|
485
|
-
if (error instanceof Error) {
|
|
486
|
-
throw error;
|
|
487
|
-
}
|
|
488
|
-
throw new SeedcordError(SeedcordErrorCode.PluginKpgNonErrorFailure, [
|
|
489
|
-
message
|
|
490
|
-
]);
|
|
491
|
-
}
|
|
492
|
-
isMigrationModule(value) {
|
|
493
|
-
if (!value || typeof value !== "object") return false;
|
|
494
|
-
if (!("up" in value) || !("down" in value)) return false;
|
|
495
|
-
const { up, down } = value;
|
|
496
|
-
return typeof up === "function" && typeof down === "function";
|
|
497
|
-
}
|
|
128
|
+
core;
|
|
129
|
+
options;
|
|
130
|
+
logger = new Logger("Mongo");
|
|
131
|
+
isInitialised = false;
|
|
132
|
+
servicesReady = false;
|
|
133
|
+
uri;
|
|
134
|
+
_services = {};
|
|
135
|
+
/**
|
|
136
|
+
* Map of all loaded services. Keys come from `@RegisterMongoService('key')`.
|
|
137
|
+
*
|
|
138
|
+
* @throws A {@link SeedcordError} if accessed before the plugin finishes initializing (e.g. from
|
|
139
|
+
* a plugin that starts in an earlier phase).
|
|
140
|
+
*/
|
|
141
|
+
get services() {
|
|
142
|
+
if (!this.servicesReady) throw new SeedcordError(SeedcordErrorCode.PluginMongoServicesNotReady);
|
|
143
|
+
return this._services;
|
|
144
|
+
}
|
|
145
|
+
hmrHandler;
|
|
146
|
+
constructor(core, options) {
|
|
147
|
+
super(core);
|
|
148
|
+
this.core = core;
|
|
149
|
+
this.options = options;
|
|
150
|
+
this.uri = options.uri;
|
|
151
|
+
this.core.shutdown.addTask(ShutdownPhase.ExternalResources, "stop-database", async () => await this.stop(), this.options.timeout);
|
|
152
|
+
if (!Envapter.isDevelopment) return;
|
|
153
|
+
this.hmrHandler = new HmrModuleHandler({
|
|
154
|
+
handlersDir: this.options.dir,
|
|
155
|
+
isHandler: this.isServiceClass.bind(this),
|
|
156
|
+
registerHandler: this.initializeService.bind(this),
|
|
157
|
+
unregisterHandler: this.unregister.bind(this),
|
|
158
|
+
getArtifacts: this.getArtifacts.bind(this),
|
|
159
|
+
logger: this.logger,
|
|
160
|
+
name: "Mongo"
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
getArtifacts(ctor) {
|
|
164
|
+
const key = Reflect.getMetadata(ServiceMetadataKey, ctor);
|
|
165
|
+
const model = Reflect.getMetadata(ModelMetadataKey, ctor);
|
|
166
|
+
return {
|
|
167
|
+
...key ? { key } : {},
|
|
168
|
+
...model?.modelName ? { modelName: model.modelName } : {}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/** @internal For use in dev mode */
|
|
172
|
+
async onHmr(event) {
|
|
173
|
+
await this.hmrHandler?.handle(event);
|
|
174
|
+
}
|
|
175
|
+
async init() {
|
|
176
|
+
if (this.isInitialised) return;
|
|
177
|
+
this.isInitialised = true;
|
|
178
|
+
await this.connect();
|
|
179
|
+
await this.loadServices();
|
|
180
|
+
this.servicesReady = true;
|
|
181
|
+
}
|
|
182
|
+
async stop() {
|
|
183
|
+
await this.disconnect();
|
|
184
|
+
}
|
|
185
|
+
async connect() {
|
|
186
|
+
this.clearModels();
|
|
187
|
+
this.connection = await mongoose.connect(this.uri, {
|
|
188
|
+
dbName: this.options.name,
|
|
189
|
+
...Envapter.isProduction && {
|
|
190
|
+
tls: true,
|
|
191
|
+
ssl: true
|
|
192
|
+
},
|
|
193
|
+
...keepDefined(this.options.connectionOptions ?? {})
|
|
194
|
+
}).then((conn) => {
|
|
195
|
+
this.logger.info(chalk.green.bold(`Connected to MongoDB: ${chalk.magenta.bold(conn.connection.name)}`));
|
|
196
|
+
return conn;
|
|
197
|
+
}).catch((err) => {
|
|
198
|
+
throw new SeedcordError(SeedcordErrorCode.PluginMongoConnectionFailed, [this.options.name], { cause: err });
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
clearModels() {
|
|
202
|
+
const modelNames = Object.keys(mongoose.models);
|
|
203
|
+
if (modelNames.length > 0) {
|
|
204
|
+
this.logger.debug(`Clearing ${modelNames.length} mongoose models`);
|
|
205
|
+
for (const name of modelNames) mongoose.deleteModel(name);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async disconnect() {
|
|
209
|
+
this.clearModels();
|
|
210
|
+
if (!this.connection) return;
|
|
211
|
+
await this.connection.disconnect().then(() => this.logger.info(chalk.red.bold("Disconnected from MongoDB"))).catch((err) => {
|
|
212
|
+
this.logger.error(`Could not disconnect from MongoDB: ${err.message}`);
|
|
213
|
+
throw new SeedcordError(SeedcordErrorCode.PluginMongoDisconnectFailed, { cause: err });
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
async loadServices() {
|
|
217
|
+
const servicesDir = this.options.dir;
|
|
218
|
+
this.logger.info(chalk.bold(servicesDir));
|
|
219
|
+
await traverseDirectory(servicesDir, (fullPath, rel, mod) => {
|
|
220
|
+
for (const Service of Object.values(mod)) if (this.isServiceClass(Service)) {
|
|
221
|
+
this.initializeService(Service, rel);
|
|
222
|
+
this.hmrHandler?.trackHandler(fullPath, Service);
|
|
223
|
+
}
|
|
224
|
+
}, this.logger);
|
|
225
|
+
this.logger.utils.list([`${chalk.magenta(Object.keys(this._services).length)} services`], chalk.bold.green("Loaded"));
|
|
226
|
+
}
|
|
227
|
+
initializeService(Service, relativePath) {
|
|
228
|
+
const instance = new Service(this, this.core);
|
|
229
|
+
this.logger.utils.registration(instance.constructor.name, relativePath);
|
|
230
|
+
}
|
|
231
|
+
isServiceClass(obj) {
|
|
232
|
+
return typeof obj === "function" && obj.prototype instanceof MongoService && Reflect.hasMetadata(ServiceMetadataKey, obj);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Register hook used by decorated services.
|
|
236
|
+
*
|
|
237
|
+
* @internal
|
|
238
|
+
*/
|
|
239
|
+
_register(key, instance) {
|
|
240
|
+
this._services[key] = instance;
|
|
241
|
+
}
|
|
242
|
+
unregister(Service, artifacts) {
|
|
243
|
+
const key = artifacts?.key ?? Reflect.getMetadata(ServiceMetadataKey, Service);
|
|
244
|
+
const modelName = artifacts?.modelName ?? Reflect.getMetadata(ModelMetadataKey, Service)?.modelName;
|
|
245
|
+
if (key && this._services[key]) delete this._services[key];
|
|
246
|
+
if (modelName) mongoose.deleteModel(modelName);
|
|
247
|
+
}
|
|
498
248
|
};
|
|
499
249
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
250
|
+
//#endregion
|
|
251
|
+
//#region src/kysely-pg/decorators/RegisterKpgService.ts
|
|
252
|
+
const PgServiceMetadataKey = Symbol("db:pgServiceKey");
|
|
253
|
+
const PgTableMetadataKey = Symbol("db:pgTable");
|
|
254
|
+
/**
|
|
255
|
+
*
|
|
256
|
+
* Registers a Kysely PG service with the specified key and options.
|
|
257
|
+
*
|
|
258
|
+
* Associates a service class with a key for dependency injection.
|
|
259
|
+
* The service becomes available via `core.db.services[key]`.
|
|
260
|
+
*
|
|
261
|
+
* @typeParam TKey - The service key type
|
|
262
|
+
* @param key - Service key for registration and type-safe access
|
|
263
|
+
* @param options - Additional registration options
|
|
264
|
+
* @decorator
|
|
265
|
+
* @example
|
|
266
|
+
* ```typescript
|
|
267
|
+
* \@RegisterKpgService('users', { table: 'app_users' })
|
|
268
|
+
* export class UsersService extends KpgService<{ users: IUser }, 'users'> {
|
|
269
|
+
* // Some code
|
|
270
|
+
* }
|
|
271
|
+
* ```
|
|
272
|
+
*
|
|
273
|
+
* @see {@link KpgService}
|
|
274
|
+
*/
|
|
503
275
|
function RegisterKpgService(key, options) {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
276
|
+
return (ctor) => {
|
|
277
|
+
Reflect.defineMetadata(PgServiceMetadataKey, key, ctor);
|
|
278
|
+
const tableName = options?.table ?? String(key);
|
|
279
|
+
Reflect.defineMetadata(PgTableMetadataKey, tableName, ctor);
|
|
280
|
+
};
|
|
509
281
|
}
|
|
510
|
-
__name(RegisterKpgService, "RegisterKpgService");
|
|
511
282
|
|
|
512
|
-
|
|
283
|
+
//#endregion
|
|
284
|
+
//#region src/kysely-pg/KpgService.ts
|
|
285
|
+
/**
|
|
286
|
+
* Base class for KyselyPg services.
|
|
287
|
+
*
|
|
288
|
+
* Provides a small, typed shim around the shared Kysely instance and ensures
|
|
289
|
+
* that subclasses have been decorated with `@RegisterKpgService`.
|
|
290
|
+
*
|
|
291
|
+
* @typeParam Database - The database shape used by Kysely (tables as keys).
|
|
292
|
+
* @typeParam TTable - The specific table key from `Database` this service works with.
|
|
293
|
+
*
|
|
294
|
+
* @example
|
|
295
|
+
* ```typescript
|
|
296
|
+
* \@RegisterKpgService('users')
|
|
297
|
+
* export class UsersService extends KpgService<ImportedDatabaseInterface, 'users'> {
|
|
298
|
+
* public async findById(id: string) {
|
|
299
|
+
* return this.entity
|
|
300
|
+
* .selectFrom(this.table)
|
|
301
|
+
* .selectAll().where('id', '=', id)
|
|
302
|
+
* .executeTakeFirst();
|
|
303
|
+
* }
|
|
304
|
+
* }
|
|
305
|
+
*
|
|
306
|
+
* // Usage inside handlers:
|
|
307
|
+
* const user = await this.core.db.services.users.findById('abc');
|
|
308
|
+
* ```
|
|
309
|
+
*/
|
|
513
310
|
var KpgService = class {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
311
|
+
kysely;
|
|
312
|
+
core;
|
|
313
|
+
table;
|
|
314
|
+
constructor(kysely, core) {
|
|
315
|
+
this.kysely = kysely;
|
|
316
|
+
this.core = core;
|
|
317
|
+
const ctor = this.constructor;
|
|
318
|
+
const key = Reflect.getMetadata(PgServiceMetadataKey, ctor);
|
|
319
|
+
if (!key) throw new SeedcordError(SeedcordErrorCode.PluginKpgServiceDecoratorMissing, [ctor.name]);
|
|
320
|
+
const table = Reflect.getMetadata(PgTableMetadataKey, ctor);
|
|
321
|
+
if (!table) throw new SeedcordError(SeedcordErrorCode.PluginKpgServiceTableMissing, [ctor.name]);
|
|
322
|
+
this.table = table;
|
|
323
|
+
this.kysely._register(key, this);
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Shared Kysely instance used to interact with the Postgres database.
|
|
327
|
+
*/
|
|
328
|
+
get db() {
|
|
329
|
+
return this.kysely.connection;
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
//#endregion
|
|
334
|
+
//#region src/kysely-pg/KpgDatabaseBootstrapper.ts
|
|
335
|
+
/**
|
|
336
|
+
* Ensures the target Postgres database exists, creating it if missing.
|
|
337
|
+
*/
|
|
338
|
+
var KpgDatabaseBootstrapper = class KpgDatabaseBootstrapper {
|
|
339
|
+
logger;
|
|
340
|
+
static ADMIN_DB = "postgres";
|
|
341
|
+
static DATABASE_EXISTS_SQL = "SELECT EXISTS (SELECT 1 FROM pg_database WHERE datname = $1) AS \"exists\"";
|
|
342
|
+
constructor(logger) {
|
|
343
|
+
this.logger = logger;
|
|
344
|
+
}
|
|
345
|
+
resolveDatabaseName(config) {
|
|
346
|
+
return KpgDatabaseBootstrapper.parseDatabaseName(config);
|
|
347
|
+
}
|
|
348
|
+
resolveDatabaseFromPool(pool) {
|
|
349
|
+
const config = {};
|
|
350
|
+
const { options } = pool;
|
|
351
|
+
if (typeof options.database === "string") config.database = options.database;
|
|
352
|
+
if (typeof options.connectionString === "string") config.connectionString = options.connectionString;
|
|
353
|
+
return this.resolveDatabaseName(config);
|
|
354
|
+
}
|
|
355
|
+
async ensure(baseConfig) {
|
|
356
|
+
const targetDb = this.resolveDatabaseName(baseConfig);
|
|
357
|
+
if (!targetDb) {
|
|
358
|
+
this.logger.info(chalk.gray("Skipping database existence check (no database specified)."));
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (targetDb === KpgDatabaseBootstrapper.ADMIN_DB) {
|
|
362
|
+
this.logger.info(chalk.gray("Target database is postgres; skipping creation."));
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const adminConfig = this.buildAdminConfig(baseConfig);
|
|
366
|
+
if (!adminConfig) {
|
|
367
|
+
this.logger.warn(`Unable to derive admin connection when ensuring database ${targetDb}`);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
this.logger.info(chalk.gray(`Ensuring database ${chalk.yellow(targetDb)} exists...`));
|
|
371
|
+
const adminPool = new Pool(adminConfig);
|
|
372
|
+
try {
|
|
373
|
+
if (await this.databaseExists(adminPool, targetDb)) {
|
|
374
|
+
this.logger.info(chalk.gray(`Database ${chalk.yellow(targetDb)} already exists.`));
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
await this.createDatabase(adminPool, targetDb);
|
|
378
|
+
this.logger.info(chalk.green(`Created database ${chalk.bold(targetDb)}.`));
|
|
379
|
+
} catch (error) {
|
|
380
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
381
|
+
this.logger.error(`Failed to ensure database ${targetDb}: ${err.message}`);
|
|
382
|
+
throw err;
|
|
383
|
+
} finally {
|
|
384
|
+
await adminPool.end();
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
buildAdminConfig(baseConfig) {
|
|
388
|
+
const adminConfig = { ...baseConfig };
|
|
389
|
+
const { connectionString } = adminConfig;
|
|
390
|
+
if (connectionString) {
|
|
391
|
+
const connection = KpgDatabaseBootstrapper.applyDatabaseToConnectionString(connectionString, KpgDatabaseBootstrapper.ADMIN_DB);
|
|
392
|
+
if (!connection) return null;
|
|
393
|
+
adminConfig.connectionString = connection;
|
|
394
|
+
}
|
|
395
|
+
adminConfig.database = KpgDatabaseBootstrapper.ADMIN_DB;
|
|
396
|
+
return adminConfig;
|
|
397
|
+
}
|
|
398
|
+
async databaseExists(pool, database) {
|
|
399
|
+
const client = await pool.connect();
|
|
400
|
+
try {
|
|
401
|
+
const { rows } = await client.query(KpgDatabaseBootstrapper.DATABASE_EXISTS_SQL, [database]);
|
|
402
|
+
return Boolean(rows[0]?.exists);
|
|
403
|
+
} finally {
|
|
404
|
+
client.release();
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
async createDatabase(pool, database) {
|
|
408
|
+
const client = await pool.connect();
|
|
409
|
+
try {
|
|
410
|
+
const createSql = `CREATE DATABASE ${KpgDatabaseBootstrapper.escapeIdentifier(database)}`;
|
|
411
|
+
await client.query(createSql);
|
|
412
|
+
} finally {
|
|
413
|
+
client.release();
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
static parseDatabaseName(config) {
|
|
417
|
+
if (typeof config.database === "string" && config.database.trim().length > 0) return config.database.trim();
|
|
418
|
+
const connectionString = config.connectionString;
|
|
419
|
+
if (!connectionString) return null;
|
|
420
|
+
try {
|
|
421
|
+
const pathname = new URL(connectionString).pathname.replace(/^\//, "");
|
|
422
|
+
if (!pathname) return null;
|
|
423
|
+
const [candidate] = pathname.split("/");
|
|
424
|
+
return candidate ? decodeURIComponent(candidate) : null;
|
|
425
|
+
} catch {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
static applyDatabaseToConnectionString(connectionString, database) {
|
|
430
|
+
try {
|
|
431
|
+
const url = new URL(connectionString);
|
|
432
|
+
url.pathname = `/${encodeURIComponent(database)}`;
|
|
433
|
+
return url.toString();
|
|
434
|
+
} catch {
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
static escapeIdentifier(identifier) {
|
|
439
|
+
return `"${identifier.replace(/"/g, "\"\"")}"`;
|
|
440
|
+
}
|
|
545
441
|
};
|
|
442
|
+
|
|
443
|
+
//#endregion
|
|
444
|
+
//#region src/kysely-pg/KpgMigrationManager.ts
|
|
445
|
+
/**
|
|
446
|
+
* Migration tooling for KyselyPg.
|
|
447
|
+
*
|
|
448
|
+
* @sealed
|
|
449
|
+
*/
|
|
450
|
+
var KpgMigrationManager = class {
|
|
451
|
+
ctx;
|
|
452
|
+
constructor(ctx) {
|
|
453
|
+
this.ctx = ctx;
|
|
454
|
+
}
|
|
455
|
+
async migrate(options) {
|
|
456
|
+
const { target, direction = "latest", steps } = options ?? {};
|
|
457
|
+
if (typeof target !== "undefined") {
|
|
458
|
+
const label = target === NO_MIGRATIONS ? "NO_MIGRATIONS" : target;
|
|
459
|
+
await this.runMigration((migrator) => migrator.migrateTo(target), `Migrating to ${chalk.yellow(label)}...`);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
switch (direction) {
|
|
463
|
+
case "latest":
|
|
464
|
+
await this.runMigration((migrator) => migrator.migrateToLatest());
|
|
465
|
+
return;
|
|
466
|
+
case "up":
|
|
467
|
+
case "down": {
|
|
468
|
+
const stepCount = steps ?? 1;
|
|
469
|
+
if (!Number.isInteger(stepCount) || stepCount < 0) throw new SeedcordRangeError(SeedcordErrorCode.PluginKpgInvalidStepCount);
|
|
470
|
+
if (stepCount === 0) {
|
|
471
|
+
this.logMigrationResults([]);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const runner = direction === "up" ? (migrator) => migrator.migrateUp() : (migrator) => migrator.migrateDown();
|
|
475
|
+
await this.runStepwise(stepCount, direction, runner);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
default: throw new SeedcordError(SeedcordErrorCode.PluginKpgUnknownDirection, [direction]);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
async migrateUp(options) {
|
|
482
|
+
if (typeof options?.steps === "undefined") {
|
|
483
|
+
await this.migrate({ direction: "up" });
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
await this.migrate({
|
|
487
|
+
direction: "up",
|
|
488
|
+
steps: options.steps
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
async migrateDown(options) {
|
|
492
|
+
if (typeof options?.steps === "undefined") {
|
|
493
|
+
await this.migrate({ direction: "down" });
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
await this.migrate({
|
|
497
|
+
direction: "down",
|
|
498
|
+
steps: options.steps
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
async listMigrations() {
|
|
502
|
+
return (await this.createMigrator()).getMigrations();
|
|
503
|
+
}
|
|
504
|
+
async runMigration(runner, runningMessage = "Running migrations...") {
|
|
505
|
+
this.ctx.logger.info(chalk.gray("Preparing migrations..."));
|
|
506
|
+
const migrator = await this.createMigrator();
|
|
507
|
+
this.ctx.logger.info(chalk.gray(runningMessage));
|
|
508
|
+
const { error, results } = await runner(migrator);
|
|
509
|
+
this.logMigrationResults(results ?? []);
|
|
510
|
+
if (error) this.handleMigrationError(error);
|
|
511
|
+
}
|
|
512
|
+
async runStepwise(steps, direction, runner) {
|
|
513
|
+
this.ctx.logger.info(chalk.gray("Preparing migrations..."));
|
|
514
|
+
const migrator = await this.createMigrator();
|
|
515
|
+
const directionLabel = direction === "up" ? "Running" : "Reverting";
|
|
516
|
+
const countLabel = steps === 1 ? "one migration" : `${chalk.yellow(String(steps))} migrations`;
|
|
517
|
+
this.ctx.logger.info(chalk.gray(`${directionLabel} ${countLabel}...`));
|
|
518
|
+
const aggregated = [];
|
|
519
|
+
let encounteredError;
|
|
520
|
+
for (let index = 0; index < steps; index += 1) {
|
|
521
|
+
const { error, results } = await runner(migrator);
|
|
522
|
+
if (results?.length) aggregated.push(...results);
|
|
523
|
+
if (error) {
|
|
524
|
+
encounteredError = error;
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
if (!results?.length) break;
|
|
528
|
+
}
|
|
529
|
+
this.logMigrationResults(aggregated);
|
|
530
|
+
if (encounteredError) this.handleMigrationError(encounteredError);
|
|
531
|
+
}
|
|
532
|
+
async createMigrator() {
|
|
533
|
+
const provider = await this.getMigrationProvider();
|
|
534
|
+
const { config } = this.ctx;
|
|
535
|
+
return new Migrator({
|
|
536
|
+
db: this.ctx.db,
|
|
537
|
+
provider,
|
|
538
|
+
allowUnorderedMigrations: config.allowUnorderedMigrations ?? false,
|
|
539
|
+
...keepDefined$1(config, "migrationTableName", "migrationLockTableName", "migrationTableSchema")
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
async getMigrationProvider() {
|
|
543
|
+
const { path: target } = this.ctx.config;
|
|
544
|
+
const resolvedTarget = Array.isArray(target) ? target.map((entry) => this.resolvePath(entry)) : this.resolvePath(target);
|
|
545
|
+
if (Array.isArray(resolvedTarget)) {
|
|
546
|
+
this.logMigrationFiles(resolvedTarget);
|
|
547
|
+
return this.createModuleProvider(resolvedTarget);
|
|
548
|
+
}
|
|
549
|
+
let migrationStat;
|
|
550
|
+
try {
|
|
551
|
+
migrationStat = await promises.stat(resolvedTarget);
|
|
552
|
+
} catch {
|
|
553
|
+
migrationStat = void 0;
|
|
554
|
+
}
|
|
555
|
+
if (migrationStat?.isDirectory()) {
|
|
556
|
+
const directory = this.relativePath(resolvedTarget);
|
|
557
|
+
this.ctx.logger.info(chalk.gray(`Loading migrations directory ${chalk.yellow(directory)}`));
|
|
558
|
+
return new FileMigrationProvider({
|
|
559
|
+
fs: promises,
|
|
560
|
+
path,
|
|
561
|
+
migrationFolder: resolvedTarget
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
if (migrationStat?.isFile() ?? true) {
|
|
565
|
+
this.logMigrationFiles([resolvedTarget]);
|
|
566
|
+
return this.createModuleProvider([resolvedTarget]);
|
|
567
|
+
}
|
|
568
|
+
const label = Array.isArray(target) ? target.join(", ") : target;
|
|
569
|
+
throw new SeedcordError(SeedcordErrorCode.PluginKpgUnresolvedMigrationsPath, [label]);
|
|
570
|
+
}
|
|
571
|
+
async createModuleProvider(files) {
|
|
572
|
+
if (files.length === 0) throw new SeedcordError(SeedcordErrorCode.PluginKpgNoMigrationFiles);
|
|
573
|
+
const comparator = this.ctx.config.nameComparator ?? ((nameA, nameB) => nameA.localeCompare(nameB));
|
|
574
|
+
const sorted = (await Promise.all(files.map(async (filePath) => {
|
|
575
|
+
const mod = await import(pathToFileURL(filePath).href);
|
|
576
|
+
if (!this.isMigrationModule(mod)) throw new SeedcordError(SeedcordErrorCode.PluginKpgInvalidMigrationModule, [filePath]);
|
|
577
|
+
const { up, down } = mod;
|
|
578
|
+
return [path.basename(filePath), {
|
|
579
|
+
async up(db) {
|
|
580
|
+
await up(db);
|
|
581
|
+
},
|
|
582
|
+
async down(db) {
|
|
583
|
+
await down(db);
|
|
584
|
+
}
|
|
585
|
+
}];
|
|
586
|
+
}))).sort(([a], [b]) => comparator(a, b));
|
|
587
|
+
this.logPreparedMigrations(sorted);
|
|
588
|
+
return { getMigrations: () => Promise.resolve(Object.fromEntries(sorted)) };
|
|
589
|
+
}
|
|
590
|
+
logMigrationFiles(files) {
|
|
591
|
+
if (!files.length) return;
|
|
592
|
+
this.ctx.logger.info("Loading migration file(s):");
|
|
593
|
+
for (const file of files) this.ctx.logger.utils.item(`${chalk.yellow(this.relativePath(file))}`);
|
|
594
|
+
}
|
|
595
|
+
logPreparedMigrations(entries) {
|
|
596
|
+
if (!entries.length) return;
|
|
597
|
+
this.ctx.logger.info("Prepared migrations:");
|
|
598
|
+
for (const [name] of entries) this.ctx.logger.utils.item(`${chalk.green(name)}`);
|
|
599
|
+
}
|
|
600
|
+
logMigrationResults(results) {
|
|
601
|
+
if (!results.length) {
|
|
602
|
+
this.ctx.logger.info(chalk.gray("No migrations executed."));
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
this.ctx.logger.info("Migration results:");
|
|
606
|
+
for (const result of results) {
|
|
607
|
+
if (result.status === "Success") {
|
|
608
|
+
this.ctx.logger.info(`${chalk.green("✓")} ${chalk.bold(result.migrationName)}`);
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
if (result.status === "Error") {
|
|
612
|
+
this.ctx.logger.error(`${chalk.red("✗")} ${chalk.bold(result.migrationName)}`);
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
this.ctx.logger.info(`${chalk.yellow("•")} ${chalk.bold(result.migrationName)} ${chalk.gray("(skipped)")}`);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
relativePath(filePath) {
|
|
619
|
+
const relative = path.relative(this.ctx.baseDir, filePath);
|
|
620
|
+
return relative.startsWith("..") ? filePath : relative;
|
|
621
|
+
}
|
|
622
|
+
resolvePath(target) {
|
|
623
|
+
if (path.isAbsolute(target)) return target;
|
|
624
|
+
return path.resolve(this.ctx.baseDir, target);
|
|
625
|
+
}
|
|
626
|
+
handleMigrationError(error) {
|
|
627
|
+
const message = error instanceof Error ? error.message : inspect(error);
|
|
628
|
+
this.ctx.logger.error(`Migration failure: ${message}`);
|
|
629
|
+
if (error instanceof Error) throw error;
|
|
630
|
+
throw new SeedcordError(SeedcordErrorCode.PluginKpgNonErrorFailure, [message]);
|
|
631
|
+
}
|
|
632
|
+
isMigrationModule(value) {
|
|
633
|
+
if (!value || typeof value !== "object") return false;
|
|
634
|
+
if (!("up" in value) || !("down" in value)) return false;
|
|
635
|
+
const { up, down } = value;
|
|
636
|
+
return typeof up === "function" && typeof down === "function";
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
//#endregion
|
|
641
|
+
//#region src/kysely-pg/KpgServiceRegistry.ts
|
|
642
|
+
/**
|
|
643
|
+
* Discovers and registers Postgres services for the plugin.
|
|
644
|
+
*/
|
|
546
645
|
var KpgServiceRegistry = class {
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
646
|
+
plugin;
|
|
647
|
+
core;
|
|
648
|
+
logger;
|
|
649
|
+
services = Object.create(null);
|
|
650
|
+
constructor(plugin, core, logger) {
|
|
651
|
+
this.plugin = plugin;
|
|
652
|
+
this.core = core;
|
|
653
|
+
this.logger = logger;
|
|
654
|
+
}
|
|
655
|
+
get map() {
|
|
656
|
+
return this.services;
|
|
657
|
+
}
|
|
658
|
+
register(key, instance) {
|
|
659
|
+
this.services[key] = instance;
|
|
660
|
+
}
|
|
661
|
+
async loadFromDirectory(dir) {
|
|
662
|
+
this.logger.info(chalk.bold(dir));
|
|
663
|
+
await traverseDirectory(dir, (fullPath, rel, mod) => {
|
|
664
|
+
for (const Service of Object.values(mod)) if (this.isServiceClass(Service)) {
|
|
665
|
+
this.initializeService(Service, rel);
|
|
666
|
+
this.plugin.trackServiceFile(fullPath, Service);
|
|
667
|
+
}
|
|
668
|
+
}, this.logger);
|
|
669
|
+
this.logger.utils.list([`${chalk.magenta(Object.keys(this.services).length)} services`], chalk.bold.green("Loaded"));
|
|
670
|
+
}
|
|
671
|
+
unregister(Service, artifacts) {
|
|
672
|
+
const key = artifacts?.key ?? Reflect.getMetadata(PgServiceMetadataKey, Service);
|
|
673
|
+
if (key && this.services[key]) delete this.services[key];
|
|
674
|
+
}
|
|
675
|
+
initializeService(Service, relativePath) {
|
|
676
|
+
const instance = new Service(this.plugin, this.core);
|
|
677
|
+
this.logger.utils.registration(instance.constructor.name, relativePath);
|
|
678
|
+
}
|
|
679
|
+
isServiceClass(obj) {
|
|
680
|
+
return typeof obj === "function" && obj.prototype instanceof KpgService && Reflect.hasMetadata(PgServiceMetadataKey, obj);
|
|
681
|
+
}
|
|
580
682
|
};
|
|
683
|
+
|
|
684
|
+
//#endregion
|
|
685
|
+
//#region src/kysely-pg/KyselyPg.ts
|
|
686
|
+
/**
|
|
687
|
+
* Postgres plugin using Kysely.
|
|
688
|
+
*
|
|
689
|
+
* Handles setting up the connection pool, applying migrations, and
|
|
690
|
+
* registering decorated services so they can be resolved from the core.
|
|
691
|
+
*/
|
|
581
692
|
var KyselyPg = class extends Plugin {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
693
|
+
core;
|
|
694
|
+
options;
|
|
695
|
+
logger = new Logger("KyselyPg");
|
|
696
|
+
isInitialised = false;
|
|
697
|
+
servicesReady = false;
|
|
698
|
+
pool = null;
|
|
699
|
+
onConnectHandler = null;
|
|
700
|
+
migrationManager = null;
|
|
701
|
+
serviceRegistry;
|
|
702
|
+
databaseBootstrapper;
|
|
703
|
+
databaseName = null;
|
|
704
|
+
hmrHandler;
|
|
705
|
+
/**
|
|
706
|
+
* Map of all services registered with the plugin, keyed by their decorator name.
|
|
707
|
+
*/
|
|
708
|
+
get services() {
|
|
709
|
+
if (!this.servicesReady) throw new SeedcordError(SeedcordErrorCode.PluginKpgServicesNotReady);
|
|
710
|
+
return this.serviceRegistry.map;
|
|
711
|
+
}
|
|
712
|
+
constructor(core, options) {
|
|
713
|
+
super(core);
|
|
714
|
+
this.core = core;
|
|
715
|
+
this.options = options;
|
|
716
|
+
this.serviceRegistry = new KpgServiceRegistry(this, core, this.logger);
|
|
717
|
+
this.databaseBootstrapper = new KpgDatabaseBootstrapper(this.logger);
|
|
718
|
+
this.core.shutdown.addTask(ShutdownPhase.ExternalResources, "stop-kyselypg", async () => await this.stop(), this.options.timeout);
|
|
719
|
+
if (!Envapter.isDevelopment) return;
|
|
720
|
+
const relPaths = this.options.migrations.path;
|
|
721
|
+
super.registerCriticalFiles(Array.isArray(relPaths) ? relPaths : [relPaths]);
|
|
722
|
+
this.hmrHandler = new HmrModuleHandler({
|
|
723
|
+
handlersDir: this.options.dir,
|
|
724
|
+
isHandler: this.serviceRegistry.isServiceClass.bind(this.serviceRegistry),
|
|
725
|
+
registerHandler: this.serviceRegistry.initializeService.bind(this.serviceRegistry),
|
|
726
|
+
unregisterHandler: this.serviceRegistry.unregister.bind(this.serviceRegistry),
|
|
727
|
+
getArtifacts: this.getArtifacts.bind(this),
|
|
728
|
+
logger: this.logger,
|
|
729
|
+
name: "KyselyPg"
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
getArtifacts(ctor) {
|
|
733
|
+
const key = Reflect.getMetadata(PgServiceMetadataKey, ctor);
|
|
734
|
+
return key ? { key } : {};
|
|
735
|
+
}
|
|
736
|
+
/** @internal For use in dev mode */
|
|
737
|
+
async onHmr(event) {
|
|
738
|
+
await this.hmrHandler?.handle(event);
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Connects to Postgres, runs any startup migrations, and loads decorated services.
|
|
742
|
+
*
|
|
743
|
+
* Safe to call multiple times; subsequent calls exit early.
|
|
744
|
+
*/
|
|
745
|
+
async init() {
|
|
746
|
+
if (this.isInitialised) return;
|
|
747
|
+
this.isInitialised = true;
|
|
748
|
+
await this.connect();
|
|
749
|
+
const startupConfig = this.options.migrations.onStartup;
|
|
750
|
+
if (startupConfig !== false) if (startupConfig && typeof startupConfig !== "boolean") await this.migrate(startupConfig);
|
|
751
|
+
else await this.migrate();
|
|
752
|
+
await this.serviceRegistry.loadFromDirectory(this.options.dir);
|
|
753
|
+
this.servicesReady = true;
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Tears down the connection pool and clears the migration manager reference.
|
|
757
|
+
*/
|
|
758
|
+
async stop() {
|
|
759
|
+
await this.disconnect();
|
|
760
|
+
}
|
|
761
|
+
async connect() {
|
|
762
|
+
const pool = await this.resolvePool();
|
|
763
|
+
this.pool = pool;
|
|
764
|
+
this.registerOnConnectStatements(pool, this.options.onConnectSQL);
|
|
765
|
+
try {
|
|
766
|
+
await this.testPoolConnection(pool);
|
|
767
|
+
this.connection = new Kysely({
|
|
768
|
+
dialect: new PostgresDialect({ pool }),
|
|
769
|
+
...keepDefined(this.options.kysely ?? {})
|
|
770
|
+
});
|
|
771
|
+
this.migrationManager = new KpgMigrationManager({
|
|
772
|
+
db: this.connection,
|
|
773
|
+
logger: this.logger,
|
|
774
|
+
config: this.options.migrations,
|
|
775
|
+
baseDir: process.cwd()
|
|
776
|
+
});
|
|
777
|
+
const dbLabel = this.databaseName ?? "unknown";
|
|
778
|
+
this.logger.info(`Connected to Postgres database ${chalk.bold.magenta(dbLabel)}`);
|
|
779
|
+
} catch (err) {
|
|
780
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
781
|
+
this.logger.error(`Could not connect to Postgres: ${error.message}`);
|
|
782
|
+
throw error;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
async disconnect() {
|
|
786
|
+
const pool = this.pool;
|
|
787
|
+
if (!pool) return;
|
|
788
|
+
if (this.onConnectHandler) {
|
|
789
|
+
pool.removeListener("connect", this.onConnectHandler);
|
|
790
|
+
this.onConnectHandler = null;
|
|
791
|
+
}
|
|
792
|
+
this.pool = null;
|
|
793
|
+
this.migrationManager = null;
|
|
794
|
+
this.logger.info(chalk.gray("Closing Postgres pool."));
|
|
795
|
+
await pool.end().catch((err) => {
|
|
796
|
+
this.logger.error(`Could not close pg pool: ${err.message}`);
|
|
797
|
+
throw new SeedcordError(SeedcordErrorCode.PluginKpgDisconnectFailed, { cause: err });
|
|
798
|
+
});
|
|
799
|
+
this.logger.info(chalk.red.bold("Disconnected from Postgres"));
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Runs migrations using the supplied options or defaults to `latest`.
|
|
803
|
+
*
|
|
804
|
+
* @param options - Target migration or direction overrides
|
|
805
|
+
*/
|
|
806
|
+
async migrate(options) {
|
|
807
|
+
await this.getMigrationManager().migrate(options);
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Runs a single upwards migration step unless a custom count is provided.
|
|
811
|
+
*
|
|
812
|
+
* @param options - Optional configuration for step-based execution
|
|
813
|
+
*/
|
|
814
|
+
async migrateUp(options) {
|
|
815
|
+
await this.getMigrationManager().migrateUp(options);
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Runs a single downwards migration step unless a custom count is provided.
|
|
819
|
+
*
|
|
820
|
+
* @param options - Optional configuration for step-based execution
|
|
821
|
+
*/
|
|
822
|
+
async migrateDown(options) {
|
|
823
|
+
await this.getMigrationManager().migrateDown(options);
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Lists every migration registered with the manager along with its execution state.
|
|
827
|
+
*/
|
|
828
|
+
listMigrations() {
|
|
829
|
+
return this.getMigrationManager().listMigrations();
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Lists unapplied migrations.
|
|
833
|
+
*/
|
|
834
|
+
async listPendingMigrations() {
|
|
835
|
+
return (await this.listMigrations()).filter((m) => !m.executedAt);
|
|
836
|
+
}
|
|
837
|
+
getMigrationManager() {
|
|
838
|
+
if (this.migrationManager) return this.migrationManager;
|
|
839
|
+
const manager = new KpgMigrationManager({
|
|
840
|
+
db: this.connection,
|
|
841
|
+
logger: this.logger,
|
|
842
|
+
config: this.options.migrations,
|
|
843
|
+
baseDir: process.cwd()
|
|
844
|
+
});
|
|
845
|
+
this.migrationManager = manager;
|
|
846
|
+
return manager;
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Register hook used by decorated services.
|
|
850
|
+
*
|
|
851
|
+
* @internal
|
|
852
|
+
*/
|
|
853
|
+
_register(key, instance) {
|
|
854
|
+
this.serviceRegistry.register(key, instance);
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Tracks a service file with the HMR handler so dev reloads can swap it. No-op outside dev.
|
|
858
|
+
*
|
|
859
|
+
* @internal Lets {@link KpgServiceRegistry} reach the dev-only HMR handler without poking a
|
|
860
|
+
* private field.
|
|
861
|
+
*/
|
|
862
|
+
trackServiceFile(filePath, ctor) {
|
|
863
|
+
this.hmrHandler?.trackHandler(filePath, ctor);
|
|
864
|
+
}
|
|
865
|
+
async resolvePool() {
|
|
866
|
+
const { pool: providedPool, connectionString } = this.options;
|
|
867
|
+
if (providedPool instanceof Pool) {
|
|
868
|
+
this.logger.info(chalk.gray("Reusing provided Postgres pool instance."));
|
|
869
|
+
this.databaseName = this.databaseBootstrapper.resolveDatabaseFromPool(providedPool);
|
|
870
|
+
return providedPool;
|
|
871
|
+
}
|
|
872
|
+
const baseConfig = this.createPoolConfig(providedPool, connectionString);
|
|
873
|
+
await this.databaseBootstrapper.ensure(baseConfig);
|
|
874
|
+
this.databaseName = this.databaseBootstrapper.resolveDatabaseName(baseConfig);
|
|
875
|
+
this.logger.info(chalk.gray("Creating new Postgres pool."));
|
|
876
|
+
return new Pool(baseConfig);
|
|
877
|
+
}
|
|
878
|
+
createPoolConfig(poolConfig, connectionString) {
|
|
879
|
+
const config = poolConfig ? { ...poolConfig } : {};
|
|
880
|
+
if (connectionString) config.connectionString = connectionString;
|
|
881
|
+
if (this.options.forceInsecureSSL) config.ssl = { rejectUnauthorized: false };
|
|
882
|
+
return config;
|
|
883
|
+
}
|
|
884
|
+
registerOnConnectStatements(pool, statements) {
|
|
885
|
+
if (!statements?.length) return;
|
|
886
|
+
const queuedStatements = [...statements];
|
|
887
|
+
const handler = (client) => {
|
|
888
|
+
(async () => {
|
|
889
|
+
for (const sql of queuedStatements) await client.query(sql);
|
|
890
|
+
})().catch((err) => this.logger.error("Failed to run onConnect SQL", err));
|
|
891
|
+
};
|
|
892
|
+
this.onConnectHandler = handler;
|
|
893
|
+
pool.on("connect", handler);
|
|
894
|
+
}
|
|
895
|
+
async testPoolConnection(pool) {
|
|
896
|
+
(await pool.connect()).release();
|
|
897
|
+
}
|
|
768
898
|
};
|
|
899
|
+
|
|
900
|
+
//#endregion
|
|
901
|
+
//#region src/shared/throwDatabaseError.ts
|
|
902
|
+
const logger = new Logger$1("DatabaseError");
|
|
903
|
+
/**
|
|
904
|
+
* Wraps an unknown error in a {@link DatabaseError} with a generated UUID for correlation, then
|
|
905
|
+
* throws it. Used by `@WrapDatabaseError` to normalize raw database failures.
|
|
906
|
+
*
|
|
907
|
+
* @param error - The original error or value
|
|
908
|
+
* @param message - Fallback message used when `error` is not an `Error`
|
|
909
|
+
* @throws A {@link DatabaseError} carrying the message and a fresh UUID
|
|
910
|
+
*
|
|
911
|
+
* @internal
|
|
912
|
+
*/
|
|
913
|
+
function throwDatabaseError(error, message) {
|
|
914
|
+
logger.error("Throwing DatabaseError", error instanceof Error ? error.name : String(error));
|
|
915
|
+
throw new DatabaseError(error instanceof Error ? error.message : message, randomUUID());
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
//#endregion
|
|
919
|
+
//#region src/shared/WrapDatabaseError.ts
|
|
920
|
+
/**
|
|
921
|
+
* Catches and wraps database operation errors.
|
|
922
|
+
*
|
|
923
|
+
* Wraps non-CustomError exceptions in DatabaseError instances
|
|
924
|
+
* with UUID tracking. Should be applied to database service methods.
|
|
925
|
+
*
|
|
926
|
+
* @typeParam TypeReturn - The return type of the decorated method
|
|
927
|
+
* @param errorMessage - Message to include when wrapping errors
|
|
928
|
+
* @decorator
|
|
929
|
+
* @example
|
|
930
|
+
* ```typescript
|
|
931
|
+
* class UserService extends MongoService<IUser> {
|
|
932
|
+
* \@WrapDatabaseError('Failed to find user')
|
|
933
|
+
* async findById(id: string) {
|
|
934
|
+
* return this.model.findById(id);
|
|
935
|
+
* }
|
|
936
|
+
* }
|
|
937
|
+
* ```
|
|
938
|
+
*
|
|
939
|
+
* @see {@link DatabaseError}
|
|
940
|
+
* @see {@link CustomError}
|
|
941
|
+
* @see {@link MongoService}
|
|
942
|
+
*/
|
|
769
943
|
function WrapDatabaseError(errorMessage) {
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
throw error;
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
};
|
|
786
|
-
};
|
|
944
|
+
return function(_target, _propertyKey, descriptor) {
|
|
945
|
+
const originalMethod = descriptor.value;
|
|
946
|
+
descriptor.value = async function(...args) {
|
|
947
|
+
if (!originalMethod) throw new SeedcordError$1(SeedcordErrorCode.DecoratorMethodNotFound);
|
|
948
|
+
try {
|
|
949
|
+
return await originalMethod.apply(this, args);
|
|
950
|
+
} catch (error) {
|
|
951
|
+
if (!(error instanceof CustomError)) throwDatabaseError(error, errorMessage);
|
|
952
|
+
else throw error;
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
};
|
|
787
956
|
}
|
|
788
|
-
__name(WrapDatabaseError, "WrapDatabaseError");
|
|
789
957
|
|
|
790
|
-
|
|
791
|
-
//#
|
|
958
|
+
//#endregion
|
|
959
|
+
//#region src/index.ts
|
|
960
|
+
/** Package version */
|
|
961
|
+
const version = "0.6.0";
|
|
962
|
+
|
|
963
|
+
//#endregion
|
|
964
|
+
export { KpgService, KyselyPg, Mongo, MongoService, RegisterKpgService, RegisterMongoModel, RegisterMongoService, WrapDatabaseError, version };
|
|
792
965
|
//# sourceMappingURL=index.mjs.map
|