@lenne.tech/nest-server 11.2.0 → 11.4.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/bin/migrate.js +40 -0
- package/dist/core/common/decorators/unified-field.decorator.d.ts +2 -0
- package/dist/core/common/decorators/unified-field.decorator.js +26 -9
- package/dist/core/common/decorators/unified-field.decorator.js.map +1 -1
- package/dist/core/common/models/core-persistence.model.js +2 -2
- package/dist/core/common/models/core-persistence.model.js.map +1 -1
- package/dist/core/modules/file/core-file-info.model.js +41 -23
- package/dist/core/modules/file/core-file-info.model.js.map +1 -1
- package/dist/core/modules/migrate/cli/migrate-cli.d.ts +3 -0
- package/dist/core/modules/migrate/cli/migrate-cli.js +221 -0
- package/dist/core/modules/migrate/cli/migrate-cli.js.map +1 -0
- package/dist/core/modules/migrate/helpers/migration.helper.d.ts +12 -0
- package/dist/core/modules/migrate/helpers/migration.helper.js +57 -0
- package/dist/core/modules/migrate/helpers/migration.helper.js.map +1 -0
- package/dist/core/modules/migrate/helpers/ts-compiler.d.ts +2 -0
- package/dist/core/modules/migrate/helpers/ts-compiler.js +3 -0
- package/dist/core/modules/migrate/helpers/ts-compiler.js.map +1 -0
- package/dist/core/modules/migrate/index.d.ts +4 -0
- package/dist/core/modules/migrate/index.js +21 -0
- package/dist/core/modules/migrate/index.js.map +1 -0
- package/dist/core/modules/migrate/migration-runner.d.ts +26 -0
- package/dist/core/modules/migrate/migration-runner.js +124 -0
- package/dist/core/modules/migrate/migration-runner.js.map +1 -0
- package/dist/core/modules/migrate/mongo-state-store.d.ts +30 -0
- package/dist/core/modules/migrate/mongo-state-store.js +105 -0
- package/dist/core/modules/migrate/mongo-state-store.js.map +1 -0
- package/dist/core/modules/migrate/templates/migration-with-helper.template.d.ts +2 -0
- package/dist/core/modules/migrate/templates/migration-with-helper.template.js +10 -0
- package/dist/core/modules/migrate/templates/migration-with-helper.template.js.map +1 -0
- package/dist/core/modules/migrate/templates/migration.template.d.ts +2 -0
- package/dist/core/modules/migrate/templates/migration.template.js +15 -0
- package/dist/core/modules/migrate/templates/migration.template.js.map +1 -0
- package/dist/core/modules/user/core-user.model.js +95 -54
- package/dist/core/modules/user/core-user.model.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/main.js +22 -0
- package/dist/main.js.map +1 -1
- package/dist/server/common/models/persistence.model.js +13 -11
- package/dist/server/common/models/persistence.model.js.map +1 -1
- package/dist/server/modules/auth/auth.model.js +6 -2
- package/dist/server/modules/auth/auth.model.js.map +1 -1
- package/dist/server/modules/user/user.controller.d.ts +19 -0
- package/dist/server/modules/user/user.controller.js +256 -0
- package/dist/server/modules/user/user.controller.js.map +1 -0
- package/dist/server/modules/user/user.model.js +37 -24
- package/dist/server/modules/user/user.model.js.map +1 -1
- package/dist/server/modules/user/user.module.js +2 -1
- package/dist/server/modules/user/user.module.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +33 -29
- package/src/core/common/decorators/unified-field.decorator.ts +49 -10
- package/src/core/common/models/core-persistence.model.ts +3 -3
- package/src/core/modules/file/core-file-info.model.ts +40 -22
- package/src/core/modules/migrate/MIGRATION_FROM_NODEPIT.md +219 -0
- package/src/core/modules/migrate/README.md +452 -0
- package/src/core/modules/migrate/cli/migrate-cli.ts +319 -0
- package/src/core/modules/migrate/helpers/migration.helper.ts +117 -0
- package/src/core/modules/migrate/helpers/ts-compiler.js +14 -0
- package/src/core/modules/migrate/index.ts +41 -0
- package/src/core/modules/migrate/migration-runner.ts +230 -0
- package/src/core/modules/migrate/mongo-state-store.ts +283 -0
- package/src/core/modules/migrate/templates/migration-with-helper.template.ts +72 -0
- package/src/core/modules/migrate/templates/migration.template.ts +59 -0
- package/src/core/modules/user/core-user.model.ts +120 -78
- package/src/index.ts +9 -3
- package/src/main.ts +25 -0
- package/src/server/common/models/persistence.model.ts +15 -13
- package/src/server/modules/auth/auth.model.ts +7 -3
- package/src/server/modules/user/user.controller.ts +242 -0
- package/src/server/modules/user/user.model.ts +39 -26
- package/src/server/modules/user/user.module.ts +2 -1
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
// Console output is required for migration runner feedback
|
|
3
|
+
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
|
|
7
|
+
import { MongoStateStore } from './mongo-state-store';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Migration file interface
|
|
11
|
+
*/
|
|
12
|
+
export interface MigrationFile {
|
|
13
|
+
/** Down function */
|
|
14
|
+
down?: () => Promise<void>;
|
|
15
|
+
/** File path */
|
|
16
|
+
filePath: string;
|
|
17
|
+
/** Timestamp when migration was created */
|
|
18
|
+
timestamp: number;
|
|
19
|
+
/** Migration name/title */
|
|
20
|
+
title: string;
|
|
21
|
+
/** Up function */
|
|
22
|
+
up: () => Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Migration runner configuration
|
|
27
|
+
*/
|
|
28
|
+
export interface MigrationRunnerOptions {
|
|
29
|
+
/** Directory containing migration files */
|
|
30
|
+
migrationsDirectory: string;
|
|
31
|
+
/** Pattern to match migration files (default: *.ts, *.js) */
|
|
32
|
+
pattern?: RegExp;
|
|
33
|
+
/** State store for tracking migrations */
|
|
34
|
+
stateStore: MongoStateStore;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Simple migration runner for NestJS applications
|
|
39
|
+
*
|
|
40
|
+
* This provides a programmatic way to run migrations without requiring the `migrate` CLI.
|
|
41
|
+
* It's a lightweight alternative for projects that want to run migrations from code.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* import { MigrationRunner, MongoStateStore } from '@lenne.tech/nest-server';
|
|
46
|
+
*
|
|
47
|
+
* const runner = new MigrationRunner({
|
|
48
|
+
* stateStore: new MongoStateStore('mongodb://localhost/mydb'),
|
|
49
|
+
* migrationsDirectory: './migrations'
|
|
50
|
+
* });
|
|
51
|
+
*
|
|
52
|
+
* // Run all pending migrations
|
|
53
|
+
* await runner.up();
|
|
54
|
+
*
|
|
55
|
+
* // Rollback last migration
|
|
56
|
+
* await runner.down();
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export class MigrationRunner {
|
|
60
|
+
private options: MigrationRunnerOptions;
|
|
61
|
+
private pattern: RegExp;
|
|
62
|
+
|
|
63
|
+
constructor(options: MigrationRunnerOptions) {
|
|
64
|
+
this.options = options;
|
|
65
|
+
this.pattern = options.pattern || /\.(ts|js)$/;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Load all migration files from the migrations directory
|
|
70
|
+
*/
|
|
71
|
+
private async loadMigrationFiles(): Promise<MigrationFile[]> {
|
|
72
|
+
const files = fs
|
|
73
|
+
.readdirSync(this.options.migrationsDirectory)
|
|
74
|
+
.filter((file) => this.pattern.test(file))
|
|
75
|
+
.sort(); // Sort alphabetically (timestamp-based filenames will be in order)
|
|
76
|
+
|
|
77
|
+
const migrations: MigrationFile[] = [];
|
|
78
|
+
|
|
79
|
+
for (const file of files) {
|
|
80
|
+
const filePath = path.join(this.options.migrationsDirectory, file);
|
|
81
|
+
|
|
82
|
+
const module = require(filePath);
|
|
83
|
+
|
|
84
|
+
if (!module.up) {
|
|
85
|
+
console.warn(`Migration ${file} has no 'up' function, skipping...`);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Extract timestamp from filename (format: TIMESTAMP-name.js)
|
|
90
|
+
const timestampMatch = file.match(/^(\d+)-/);
|
|
91
|
+
const timestamp = timestampMatch ? parseInt(timestampMatch[1], 10) : Date.now();
|
|
92
|
+
|
|
93
|
+
migrations.push({
|
|
94
|
+
down: module.down,
|
|
95
|
+
filePath,
|
|
96
|
+
timestamp,
|
|
97
|
+
title: file,
|
|
98
|
+
up: module.up,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return migrations;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Run all pending migrations (up)
|
|
107
|
+
*/
|
|
108
|
+
async up(): Promise<void> {
|
|
109
|
+
const allMigrations = await this.loadMigrationFiles();
|
|
110
|
+
const state = await this.options.stateStore.loadAsync();
|
|
111
|
+
const completedMigrations = (state.migrations || []).map((m) => m.title);
|
|
112
|
+
|
|
113
|
+
const pendingMigrations = allMigrations.filter((m) => !completedMigrations.includes(m.title));
|
|
114
|
+
|
|
115
|
+
if (pendingMigrations.length === 0) {
|
|
116
|
+
console.log('No pending migrations');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log(`Running ${pendingMigrations.length} pending migration(s)...`);
|
|
121
|
+
|
|
122
|
+
for (const migration of pendingMigrations) {
|
|
123
|
+
console.log(`Running migration: ${migration.title}`);
|
|
124
|
+
await migration.up();
|
|
125
|
+
|
|
126
|
+
// Update state
|
|
127
|
+
const newState = await this.options.stateStore.loadAsync();
|
|
128
|
+
const migrations = newState.migrations || [];
|
|
129
|
+
migrations.push({
|
|
130
|
+
timestamp: migration.timestamp,
|
|
131
|
+
title: migration.title,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
await this.options.stateStore.saveAsync({
|
|
135
|
+
lastRun: migration.title,
|
|
136
|
+
migrations,
|
|
137
|
+
up: () => {},
|
|
138
|
+
} as any);
|
|
139
|
+
|
|
140
|
+
console.log(`✓ Migration completed: ${migration.title}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
console.log('All migrations completed successfully');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Rollback the last migration (down)
|
|
148
|
+
*/
|
|
149
|
+
async down(): Promise<void> {
|
|
150
|
+
const state = await this.options.stateStore.loadAsync();
|
|
151
|
+
const completedMigrations = state.migrations || [];
|
|
152
|
+
|
|
153
|
+
if (completedMigrations.length === 0) {
|
|
154
|
+
console.log('No migrations to rollback');
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const lastMigration = completedMigrations[completedMigrations.length - 1];
|
|
159
|
+
const allMigrations = await this.loadMigrationFiles();
|
|
160
|
+
const migrationToRollback = allMigrations.find((m) => m.title === lastMigration.title);
|
|
161
|
+
|
|
162
|
+
if (!migrationToRollback) {
|
|
163
|
+
throw new Error(`Migration file not found: ${lastMigration.title}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!migrationToRollback.down) {
|
|
167
|
+
throw new Error(`Migration ${lastMigration.title} has no 'down' function`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
console.log(`Rolling back migration: ${migrationToRollback.title}`);
|
|
171
|
+
await migrationToRollback.down();
|
|
172
|
+
|
|
173
|
+
// Update state
|
|
174
|
+
const newMigrations = completedMigrations.slice(0, -1);
|
|
175
|
+
await this.options.stateStore.saveAsync({
|
|
176
|
+
lastRun: newMigrations.length > 0 ? newMigrations[newMigrations.length - 1].title : undefined,
|
|
177
|
+
migrations: newMigrations,
|
|
178
|
+
up: () => {},
|
|
179
|
+
} as any);
|
|
180
|
+
|
|
181
|
+
console.log(`✓ Migration rolled back: ${migrationToRollback.title}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get migration status
|
|
186
|
+
*/
|
|
187
|
+
async status(): Promise<{
|
|
188
|
+
completed: string[];
|
|
189
|
+
pending: string[];
|
|
190
|
+
}> {
|
|
191
|
+
const allMigrations = await this.loadMigrationFiles();
|
|
192
|
+
const state = await this.options.stateStore.loadAsync();
|
|
193
|
+
const completedMigrations = (state.migrations || []).map((m) => m.title);
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
completed: completedMigrations,
|
|
197
|
+
pending: allMigrations.filter((m) => !completedMigrations.includes(m.title)).map((m) => m.title),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Create a new migration file
|
|
203
|
+
*/
|
|
204
|
+
static async create(migrationsDirectory: string, name: string): Promise<string> {
|
|
205
|
+
const timestamp = Date.now();
|
|
206
|
+
const fileName = `${timestamp}-${name}.ts`;
|
|
207
|
+
const filePath = path.join(migrationsDirectory, fileName);
|
|
208
|
+
|
|
209
|
+
const template = `/**
|
|
210
|
+
* Migration: ${name}
|
|
211
|
+
* Created: ${new Date().toISOString()}
|
|
212
|
+
*/
|
|
213
|
+
|
|
214
|
+
export const up = async () => {
|
|
215
|
+
// TODO: Implement migration
|
|
216
|
+
console.log('Running migration: ${name}');
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
export const down = async () => {
|
|
220
|
+
// TODO: Implement rollback
|
|
221
|
+
console.log('Rolling back migration: ${name}');
|
|
222
|
+
};
|
|
223
|
+
`;
|
|
224
|
+
|
|
225
|
+
fs.writeFileSync(filePath, template, 'utf-8');
|
|
226
|
+
console.log(`✓ Created migration: ${fileName}`);
|
|
227
|
+
|
|
228
|
+
return filePath;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { Db, MongoClient } from 'mongodb';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Migration options interface (compatible with migrate package)
|
|
6
|
+
*/
|
|
7
|
+
export interface MigrationOptions {
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
stateStore: MongoStateStore;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Migration set interface (compatible with migrate package)
|
|
14
|
+
*/
|
|
15
|
+
export interface MigrationSet {
|
|
16
|
+
down?: (done?: (err?: Error) => void) => void;
|
|
17
|
+
lastRun?: string;
|
|
18
|
+
migrations: Array<{ timestamp?: number; title: string }>;
|
|
19
|
+
up: (done?: (err?: Error) => void) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Options for MongoStateStore configuration
|
|
24
|
+
*/
|
|
25
|
+
export interface MongoStateStoreOptions {
|
|
26
|
+
/** Name of the collection to store migration state (default: 'migrations') */
|
|
27
|
+
collectionName?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Optionally specify a collection to use for locking. This is intended for
|
|
30
|
+
* clusters with multiple nodes to ensure that not more than one migration
|
|
31
|
+
* can run at any given time. You must use the `synchronizedMigration` or
|
|
32
|
+
* `synchronizedUp` function, instead of triggering the migration via
|
|
33
|
+
* `migrate` directly.
|
|
34
|
+
*/
|
|
35
|
+
lockCollectionName?: string;
|
|
36
|
+
/** MongoDB connection URI */
|
|
37
|
+
uri: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* MongoDB State Store for migration state management
|
|
42
|
+
*
|
|
43
|
+
* This class provides a MongoDB-based state store for migration frameworks,
|
|
44
|
+
* allowing migration states to be persisted directly in MongoDB instead of
|
|
45
|
+
* in separate files. It supports MongoDB 6+ and provides a locking mechanism
|
|
46
|
+
* for clustered environments.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```typescript
|
|
50
|
+
* const stateStore = new MongoStateStore('mongodb://localhost/mydb');
|
|
51
|
+
* // or with options
|
|
52
|
+
* const stateStore = new MongoStateStore({
|
|
53
|
+
* uri: 'mongodb://localhost/mydb',
|
|
54
|
+
* collectionName: 'custom_migrations',
|
|
55
|
+
* lockCollectionName: 'migration_lock'
|
|
56
|
+
* });
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export class MongoStateStore {
|
|
60
|
+
/** Collection name for storing migration state */
|
|
61
|
+
private readonly collectionName: string;
|
|
62
|
+
|
|
63
|
+
/** MongoDB connection URI */
|
|
64
|
+
readonly mongodbHost: string;
|
|
65
|
+
|
|
66
|
+
/** Optional collection name for locking mechanism */
|
|
67
|
+
readonly lockCollectionName?: string;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Creates a new MongoStateStore instance
|
|
71
|
+
*
|
|
72
|
+
* @param objectOrHost - MongoDB URI string or configuration object
|
|
73
|
+
*/
|
|
74
|
+
constructor(objectOrHost: MongoStateStoreOptions | string) {
|
|
75
|
+
this.mongodbHost = typeof objectOrHost === 'string' ? objectOrHost : objectOrHost.uri;
|
|
76
|
+
this.collectionName =
|
|
77
|
+
typeof objectOrHost === 'string' ? 'migrations' : (objectOrHost.collectionName ?? 'migrations');
|
|
78
|
+
this.lockCollectionName = typeof objectOrHost !== 'string' ? objectOrHost.lockCollectionName : undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Loads the migration state from MongoDB
|
|
83
|
+
*
|
|
84
|
+
* @param fn - Callback function receiving error or migration set
|
|
85
|
+
*/
|
|
86
|
+
load(fn: (err?: Error, set?: MigrationSet) => void): void {
|
|
87
|
+
this.loadAsync()
|
|
88
|
+
.then((result) => fn(undefined, result))
|
|
89
|
+
.catch((err) => fn(err));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Loads the migration state from MongoDB (async version)
|
|
94
|
+
*
|
|
95
|
+
* @returns Promise with migration set
|
|
96
|
+
*/
|
|
97
|
+
async loadAsync(): Promise<MigrationSet> {
|
|
98
|
+
return dbRequest(this.mongodbHost, async (db) => {
|
|
99
|
+
const result = await db.collection(this.collectionName).find({}).toArray();
|
|
100
|
+
|
|
101
|
+
if (result.length > 1) {
|
|
102
|
+
throw new Error(`Expected exactly one result, but got ${result.length}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (result.length === 0) {
|
|
106
|
+
console.debug('No migrations found, probably running the very first time');
|
|
107
|
+
// Return empty object for compatibility with @nodepit/migrate-state-store-mongodb
|
|
108
|
+
return {} as MigrationSet;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return result[0] as unknown as MigrationSet;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Saves the migration state to MongoDB
|
|
117
|
+
*
|
|
118
|
+
* @param set - Migration set to save
|
|
119
|
+
* @param fn - Callback function receiving optional error
|
|
120
|
+
*/
|
|
121
|
+
save(set: MigrationSet, fn: (err?: Error) => void): void {
|
|
122
|
+
this.saveAsync(set)
|
|
123
|
+
.then(() => fn())
|
|
124
|
+
.catch((err) => fn(err));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Saves the migration state to MongoDB (async version)
|
|
129
|
+
*
|
|
130
|
+
* @param set - Migration set to save
|
|
131
|
+
*/
|
|
132
|
+
async saveAsync(set: MigrationSet): Promise<void> {
|
|
133
|
+
const { lastRun, migrations } = set;
|
|
134
|
+
await dbRequest(this.mongodbHost, async (db) => {
|
|
135
|
+
await db.collection(this.collectionName).replaceOne({}, { lastRun, migrations }, { upsert: true });
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Wraps migrations with a lock to prevent simultaneous execution in clustered environments
|
|
142
|
+
*
|
|
143
|
+
* This function ensures that only one instance can run migrations at a time by using
|
|
144
|
+
* a MongoDB-based locking mechanism. To use this functionality, you must set the
|
|
145
|
+
* `lockCollectionName` in the `MongoStateStore` options.
|
|
146
|
+
*
|
|
147
|
+
* @param opts - Migration options including state store
|
|
148
|
+
* @param callback - Callback function that receives the migration set
|
|
149
|
+
* @throws Error if state store is not configured correctly
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```typescript
|
|
153
|
+
* await synchronizedMigration({
|
|
154
|
+
* stateStore: new MongoStateStore({
|
|
155
|
+
* uri: 'mongodb://localhost/db',
|
|
156
|
+
* lockCollectionName: 'migrationlock'
|
|
157
|
+
* })
|
|
158
|
+
* }, async (migrationSet) => {
|
|
159
|
+
* // Only one instance at a time will execute this
|
|
160
|
+
* await promisify(migrationSet.up).call(migrationSet);
|
|
161
|
+
* });
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
export async function synchronizedMigration(
|
|
165
|
+
opts: MigrationOptions,
|
|
166
|
+
callback: (set: MigrationSet) => Promise<void>,
|
|
167
|
+
): Promise<void> {
|
|
168
|
+
if (!opts.stateStore) {
|
|
169
|
+
throw new Error('No `stateStore` in migration options');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const stateStore = opts.stateStore;
|
|
173
|
+
|
|
174
|
+
if (!(stateStore instanceof MongoStateStore)) {
|
|
175
|
+
throw new Error('Given `stateStore` is not `MongoStateStore`');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const lockCollectionName = stateStore.lockCollectionName;
|
|
179
|
+
|
|
180
|
+
if (typeof lockCollectionName !== 'string') {
|
|
181
|
+
throw new Error('`lockCollectionName` in MongoStateStore is not set');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
await acquireLock(stateStore.mongodbHost, lockCollectionName);
|
|
186
|
+
|
|
187
|
+
// Load migration set using async method
|
|
188
|
+
const set = await stateStore.loadAsync();
|
|
189
|
+
await callback(set);
|
|
190
|
+
} finally {
|
|
191
|
+
await releaseLock(stateStore.mongodbHost, lockCollectionName);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Executes all pending migrations in a synchronized manner (for clustered environments)
|
|
197
|
+
*
|
|
198
|
+
* This is a convenience function that wraps `synchronizedMigration` and automatically
|
|
199
|
+
* calls the `up` method on the migration set.
|
|
200
|
+
*
|
|
201
|
+
* @param opts - Migration options including state store
|
|
202
|
+
* @throws Error if state store is not configured correctly
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* ```typescript
|
|
206
|
+
* await synchronizedUp({
|
|
207
|
+
* stateStore: new MongoStateStore({
|
|
208
|
+
* uri: 'mongodb://localhost/db',
|
|
209
|
+
* lockCollectionName: 'migrationlock'
|
|
210
|
+
* })
|
|
211
|
+
* });
|
|
212
|
+
* ```
|
|
213
|
+
*/
|
|
214
|
+
export async function synchronizedUp(opts: MigrationOptions): Promise<void> {
|
|
215
|
+
await synchronizedMigration(opts, async (loadedSet) => {
|
|
216
|
+
await promisify(loadedSet.up).call(loadedSet);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Acquires a lock in MongoDB to ensure only one migration runs at a time
|
|
222
|
+
*
|
|
223
|
+
* @param url - MongoDB connection URI
|
|
224
|
+
* @param lockCollectionName - Name of the collection to use for locking
|
|
225
|
+
*/
|
|
226
|
+
async function acquireLock(url: string, lockCollectionName: string): Promise<void> {
|
|
227
|
+
await dbRequest(url, async (db) => {
|
|
228
|
+
const collection = db.collection(lockCollectionName);
|
|
229
|
+
|
|
230
|
+
// Create unique index for atomicity
|
|
231
|
+
// https://docs.mongodb.com/manual/reference/method/db.collection.update/#use-unique-indexes
|
|
232
|
+
// https://groups.google.com/forum/#!topic/mongodb-user/-fucdS-7kIU
|
|
233
|
+
// https://stackoverflow.com/questions/33346175/mongodb-upsert-operation-seems-not-atomic-which-throws-duplicatekeyexception/34784533
|
|
234
|
+
await collection.createIndex({ lock: 1 }, { unique: true });
|
|
235
|
+
|
|
236
|
+
let showMessage = true;
|
|
237
|
+
|
|
238
|
+
for (;;) {
|
|
239
|
+
// Use updateOne with upsert for atomic lock acquisition (same as original package)
|
|
240
|
+
const result = await collection.updateOne({ lock: 'lock' }, { $set: { lock: 'lock' } }, { upsert: true });
|
|
241
|
+
const lockAcquired = result.upsertedCount > 0;
|
|
242
|
+
|
|
243
|
+
if (lockAcquired) {
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (showMessage) {
|
|
248
|
+
console.debug('Waiting for migration lock release …');
|
|
249
|
+
showMessage = false;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
await promisify(setTimeout)(100);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Executes database operations with automatic connection management
|
|
259
|
+
*
|
|
260
|
+
* @param url - MongoDB connection URI
|
|
261
|
+
* @param callback - Callback function to execute with database instance
|
|
262
|
+
* @returns Promise with callback result
|
|
263
|
+
*/
|
|
264
|
+
async function dbRequest<T>(url: string, callback: (db: Db) => Promise<T> | T): Promise<T> {
|
|
265
|
+
let client: MongoClient | undefined;
|
|
266
|
+
try {
|
|
267
|
+
client = await MongoClient.connect(url);
|
|
268
|
+
const db = client.db();
|
|
269
|
+
return await callback(db);
|
|
270
|
+
} finally {
|
|
271
|
+
await client?.close();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Releases a migration lock in MongoDB
|
|
277
|
+
*
|
|
278
|
+
* @param url - MongoDB connection URI
|
|
279
|
+
* @param lockCollectionName - Name of the collection used for locking
|
|
280
|
+
*/
|
|
281
|
+
async function releaseLock(url: string, lockCollectionName: string): Promise<void> {
|
|
282
|
+
await dbRequest(url, (db) => db.collection(lockCollectionName).deleteOne({ lock: 'lock' }));
|
|
283
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// import { Db } from 'mongodb';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Migration template with helper function
|
|
5
|
+
*
|
|
6
|
+
* This template uses the migration helper from @lenne.tech/nest-server.
|
|
7
|
+
* To use this template, you need to create a helper function in your project
|
|
8
|
+
* that returns the database connection.
|
|
9
|
+
*
|
|
10
|
+
* Example setup in your project's migrations-utils/db.ts:
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import config from '../src/config.env';
|
|
13
|
+
* import { MongoClient } from 'mongodb';
|
|
14
|
+
*
|
|
15
|
+
* export const getDb = async () => {
|
|
16
|
+
* const client = await MongoClient.connect(config.mongoose.uri);
|
|
17
|
+
* return client.db();
|
|
18
|
+
* };
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// Import your project's database helper
|
|
23
|
+
// import { getDb } from '../migrations-utils/db';
|
|
24
|
+
|
|
25
|
+
// Or use the nest-server helper with your config:
|
|
26
|
+
// import config from '../src/config.env';
|
|
27
|
+
// import { getDb } from '@lenne.tech/nest-server';
|
|
28
|
+
// const db = await getDb(config.mongoose.uri);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Run migration
|
|
32
|
+
*/
|
|
33
|
+
export const up = async () => {
|
|
34
|
+
// const db: Db = await getDb();
|
|
35
|
+
/*
|
|
36
|
+
Code your update script here!
|
|
37
|
+
|
|
38
|
+
Example: Add a new field to all documents
|
|
39
|
+
await db.collection('users').updateMany(
|
|
40
|
+
{ email: { $exists: false } },
|
|
41
|
+
{ $set: { email: '' } }
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
Example: Create a new collection
|
|
45
|
+
await db.createCollection('new_collection');
|
|
46
|
+
|
|
47
|
+
Example: Create an index
|
|
48
|
+
await db.collection('users').createIndex({ email: 1 }, { unique: true });
|
|
49
|
+
*/
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Rollback migration
|
|
54
|
+
*/
|
|
55
|
+
export const down = async () => {
|
|
56
|
+
// const db: Db = await getDb();
|
|
57
|
+
/*
|
|
58
|
+
Code your downgrade script here!
|
|
59
|
+
|
|
60
|
+
Example: Remove the field added in up()
|
|
61
|
+
await db.collection('users').updateMany(
|
|
62
|
+
{},
|
|
63
|
+
{ $unset: { email: '' } }
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
Example: Drop the collection
|
|
67
|
+
await db.dropCollection('new_collection');
|
|
68
|
+
|
|
69
|
+
Example: Drop the index
|
|
70
|
+
await db.collection('users').dropIndex('email_1');
|
|
71
|
+
*/
|
|
72
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Db } from 'mongodb';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Migration template for nest-server
|
|
5
|
+
*
|
|
6
|
+
* This template can be used with the migrate CLI:
|
|
7
|
+
* migrate create --template-file ./node_modules/@lenne.tech/nest-server/dist/core/modules/migrate/templates/migration.template.js
|
|
8
|
+
*
|
|
9
|
+
* Or copy this file to your project's migrations-utils folder and customize it.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get database connection
|
|
14
|
+
*
|
|
15
|
+
* IMPORTANT: Replace this function with your actual database connection logic.
|
|
16
|
+
* This is a placeholder that should import your project's config.
|
|
17
|
+
*/
|
|
18
|
+
const getDb = async (): Promise<Db> => {
|
|
19
|
+
// TODO: Import your config and return the database connection
|
|
20
|
+
// Example:
|
|
21
|
+
// import config from '../src/config.env';
|
|
22
|
+
// const { MongoClient } = require('mongodb');
|
|
23
|
+
// const client = await MongoClient.connect(config.mongoose.uri);
|
|
24
|
+
// return client.db();
|
|
25
|
+
|
|
26
|
+
throw new Error(
|
|
27
|
+
'Please configure the getDb() function in this migration file or use the migration helper from @lenne.tech/nest-server',
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Run migration
|
|
33
|
+
*
|
|
34
|
+
* Code your update script here!
|
|
35
|
+
*/
|
|
36
|
+
export const up = async () => {
|
|
37
|
+
const db: Db = await getDb();
|
|
38
|
+
|
|
39
|
+
// Example: Add a new field to all documents in a collection
|
|
40
|
+
// await db.collection('users').updateMany(
|
|
41
|
+
// { email: { $exists: false } },
|
|
42
|
+
// { $set: { email: '' } }
|
|
43
|
+
// );
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Rollback migration
|
|
48
|
+
*
|
|
49
|
+
* Code your downgrade script here!
|
|
50
|
+
*/
|
|
51
|
+
export const down = async () => {
|
|
52
|
+
const db: Db = await getDb();
|
|
53
|
+
|
|
54
|
+
// Example: Remove the field added in the up() function
|
|
55
|
+
// await db.collection('users').updateMany(
|
|
56
|
+
// {},
|
|
57
|
+
// { $unset: { email: '' } }
|
|
58
|
+
// );
|
|
59
|
+
};
|