@bunnykit/orm 0.1.24 → 0.1.25
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/src/connection/Connection.d.ts +4 -1
- package/dist/src/connection/Connection.js +23 -1
- package/dist/src/connection/ConnectionManager.d.ts +15 -4
- package/dist/src/connection/ConnectionManager.js +50 -5
- package/dist/src/connection/TenantContext.d.ts +4 -0
- package/dist/src/index.d.ts +2 -2
- package/dist/src/migration/Migrator.d.ts +20 -5
- package/dist/src/migration/Migrator.js +122 -32
- package/dist/src/schema/Schema.d.ts +4 -0
- package/dist/src/schema/Schema.js +24 -0
- package/package.json +1 -1
|
@@ -18,10 +18,13 @@ export declare class Connection {
|
|
|
18
18
|
getDriverName(): "sqlite" | "mysql" | "postgres";
|
|
19
19
|
getGrammar(): Grammar;
|
|
20
20
|
getSchema(): string | undefined;
|
|
21
|
+
static isSafeIdentifier(value: string): boolean;
|
|
22
|
+
static assertSafeIdentifier(value: string, label?: string): void;
|
|
23
|
+
static assertSafeQualifiedIdentifier(value: string, label?: string): void;
|
|
21
24
|
withSchema(schema: string): Connection;
|
|
22
25
|
withoutSchema(): Connection;
|
|
23
26
|
qualifyTable(table: string): string;
|
|
24
|
-
|
|
27
|
+
quoteIdentifier(value: string): string;
|
|
25
28
|
query(sqlString: string, bindings?: any[]): Promise<any[]>;
|
|
26
29
|
run(sqlString: string, bindings?: any[]): Promise<any>;
|
|
27
30
|
beginTransaction(): Promise<void>;
|
|
@@ -59,7 +59,22 @@ export class Connection {
|
|
|
59
59
|
getSchema() {
|
|
60
60
|
return this.schema;
|
|
61
61
|
}
|
|
62
|
+
static isSafeIdentifier(value) {
|
|
63
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(value);
|
|
64
|
+
}
|
|
65
|
+
static assertSafeIdentifier(value, label = "identifier") {
|
|
66
|
+
if (!this.isSafeIdentifier(value)) {
|
|
67
|
+
throw new Error(`Invalid ${label}: ${value}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
static assertSafeQualifiedIdentifier(value, label = "identifier") {
|
|
71
|
+
const parts = value.split(".");
|
|
72
|
+
if (parts.length === 0 || parts.some((part) => !this.isSafeIdentifier(part))) {
|
|
73
|
+
throw new Error(`Invalid ${label}: ${value}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
62
76
|
withSchema(schema) {
|
|
77
|
+
Connection.assertSafeIdentifier(schema, "schema name");
|
|
63
78
|
if (this.schema === schema)
|
|
64
79
|
return this;
|
|
65
80
|
return new Connection(this.config, { driver: this.driver, schema, ownsDriver: false });
|
|
@@ -70,8 +85,14 @@ export class Connection {
|
|
|
70
85
|
return new Connection(this.config, { driver: this.driver, ownsDriver: false });
|
|
71
86
|
}
|
|
72
87
|
qualifyTable(table) {
|
|
73
|
-
if (
|
|
88
|
+
if (table.includes(".")) {
|
|
89
|
+
Connection.assertSafeQualifiedIdentifier(table, "qualified table name");
|
|
90
|
+
return table;
|
|
91
|
+
}
|
|
92
|
+
if (!this.schema || this.driverName === "sqlite")
|
|
74
93
|
return table;
|
|
94
|
+
Connection.assertSafeIdentifier(this.schema, "schema name");
|
|
95
|
+
Connection.assertSafeIdentifier(table, "table name");
|
|
75
96
|
return `${this.schema}.${table}`;
|
|
76
97
|
}
|
|
77
98
|
quoteIdentifier(value) {
|
|
@@ -144,6 +165,7 @@ export class Connection {
|
|
|
144
165
|
if (this.driverName !== "postgres") {
|
|
145
166
|
throw new Error("search_path schema switching is only supported for PostgreSQL connections.");
|
|
146
167
|
}
|
|
168
|
+
Connection.assertSafeIdentifier(schema, "schema name");
|
|
147
169
|
return await this.transaction(async (connection) => {
|
|
148
170
|
await connection.run(`SET LOCAL search_path TO ${connection.quoteIdentifier(schema)}`);
|
|
149
171
|
return await callback(connection.withoutSchema());
|
|
@@ -1,25 +1,32 @@
|
|
|
1
1
|
import { Connection } from "./Connection.js";
|
|
2
2
|
import type { ConnectionConfig } from "../types/index.js";
|
|
3
3
|
import type { ActiveTenantContext } from "./TenantContext.js";
|
|
4
|
-
export
|
|
4
|
+
export interface TenantCachePolicy {
|
|
5
|
+
ttl?: number;
|
|
6
|
+
closeOnPurge?: boolean;
|
|
7
|
+
}
|
|
8
|
+
type TenantResolutionOptions = TenantCachePolicy & {
|
|
9
|
+
cache?: TenantCachePolicy;
|
|
10
|
+
};
|
|
11
|
+
export type TenantResolution = ({
|
|
5
12
|
strategy: "database";
|
|
6
13
|
name: string;
|
|
7
14
|
config: ConnectionConfig;
|
|
8
|
-
} | {
|
|
15
|
+
} & TenantResolutionOptions) | ({
|
|
9
16
|
strategy: "schema";
|
|
10
17
|
name: string;
|
|
11
18
|
config?: ConnectionConfig;
|
|
12
19
|
connection?: string | Connection;
|
|
13
20
|
schema: string;
|
|
14
21
|
mode?: "qualify" | "search_path";
|
|
15
|
-
} | {
|
|
22
|
+
} & TenantResolutionOptions) | ({
|
|
16
23
|
strategy: "rls";
|
|
17
24
|
name: string;
|
|
18
25
|
config?: ConnectionConfig;
|
|
19
26
|
connection?: string | Connection;
|
|
20
27
|
tenantId?: string;
|
|
21
28
|
setting?: string;
|
|
22
|
-
};
|
|
29
|
+
} & TenantResolutionOptions);
|
|
23
30
|
export type TenantResolver = (tenantId: string) => TenantResolution | Promise<TenantResolution>;
|
|
24
31
|
export interface PoolConfig {
|
|
25
32
|
maxConnections?: number;
|
|
@@ -50,6 +57,10 @@ export declare class ConnectionManager {
|
|
|
50
57
|
static resolveTenant(tenantId: string): Promise<ActiveTenantContext>;
|
|
51
58
|
static getResolvedTenant(tenantId: string): ActiveTenantContext | undefined;
|
|
52
59
|
static purgeTenant(tenantId: string): void;
|
|
60
|
+
static purgeExpiredTenants(options?: {
|
|
61
|
+
close?: boolean;
|
|
62
|
+
}): Promise<string[]>;
|
|
53
63
|
static closeTenant(tenantId: string): Promise<void>;
|
|
54
64
|
static closeAll(): Promise<void>;
|
|
55
65
|
}
|
|
66
|
+
export {};
|
|
@@ -139,14 +139,20 @@ export class ConnectionManager {
|
|
|
139
139
|
}
|
|
140
140
|
static async resolveTenant(tenantId) {
|
|
141
141
|
const cached = this.tenantCache.get(tenantId);
|
|
142
|
-
if (cached)
|
|
143
|
-
|
|
142
|
+
if (cached) {
|
|
143
|
+
if (!cached.expiresAt || cached.expiresAt > Date.now())
|
|
144
|
+
return cached;
|
|
145
|
+
await this.closeTenant(tenantId);
|
|
146
|
+
}
|
|
144
147
|
if (!this.tenantResolver) {
|
|
145
148
|
throw new Error("No tenant resolver configured.");
|
|
146
149
|
}
|
|
147
150
|
const resolution = await this.tenantResolver(tenantId);
|
|
151
|
+
const policy = { ...resolution.cache, ttl: resolution.ttl ?? resolution.cache?.ttl, closeOnPurge: resolution.closeOnPurge ?? resolution.cache?.closeOnPurge };
|
|
152
|
+
const resolvedAt = Date.now();
|
|
148
153
|
const schema = resolution.strategy === "schema" ? resolution.schema : undefined;
|
|
149
154
|
const schemaMode = resolution.strategy === "schema" ? resolution.mode || "qualify" : undefined;
|
|
155
|
+
let ownsConnection = false;
|
|
150
156
|
let connection = (resolution.strategy === "schema" || resolution.strategy === "rls") && resolution.connection instanceof Connection
|
|
151
157
|
? resolution.connection
|
|
152
158
|
: (resolution.strategy === "schema" || resolution.strategy === "rls") && typeof resolution.connection === "string"
|
|
@@ -167,6 +173,7 @@ export class ConnectionManager {
|
|
|
167
173
|
}
|
|
168
174
|
connection = new Connection(config, { schema });
|
|
169
175
|
this.connections.set(resolution.name, connection);
|
|
176
|
+
ownsConnection = true;
|
|
170
177
|
}
|
|
171
178
|
else if (schema && schemaMode === "qualify") {
|
|
172
179
|
connection = connection.withSchema(schema);
|
|
@@ -176,6 +183,10 @@ export class ConnectionManager {
|
|
|
176
183
|
connection,
|
|
177
184
|
connectionName: resolution.name,
|
|
178
185
|
strategy: resolution.strategy,
|
|
186
|
+
resolvedAt,
|
|
187
|
+
expiresAt: policy.ttl ? resolvedAt + policy.ttl : undefined,
|
|
188
|
+
closeOnPurge: policy.closeOnPurge ?? ownsConnection,
|
|
189
|
+
ownsConnection,
|
|
179
190
|
schema,
|
|
180
191
|
schemaMode,
|
|
181
192
|
rlsTenantId: resolution.strategy === "rls" ? resolution.tenantId || tenantId : undefined,
|
|
@@ -185,19 +196,53 @@ export class ConnectionManager {
|
|
|
185
196
|
return context;
|
|
186
197
|
}
|
|
187
198
|
static getResolvedTenant(tenantId) {
|
|
188
|
-
|
|
199
|
+
const context = this.tenantCache.get(tenantId);
|
|
200
|
+
if (!context || !context.expiresAt || context.expiresAt > Date.now())
|
|
201
|
+
return context;
|
|
202
|
+
this.tenantCache.delete(tenantId);
|
|
203
|
+
return undefined;
|
|
189
204
|
}
|
|
190
205
|
static purgeTenant(tenantId) {
|
|
191
206
|
this.tenantCache.delete(tenantId);
|
|
192
207
|
}
|
|
208
|
+
static async purgeExpiredTenants(options = {}) {
|
|
209
|
+
const now = Date.now();
|
|
210
|
+
const purged = [];
|
|
211
|
+
for (const [tenantId, context] of [...this.tenantCache.entries()]) {
|
|
212
|
+
if (!context.expiresAt || context.expiresAt > now)
|
|
213
|
+
continue;
|
|
214
|
+
purged.push(tenantId);
|
|
215
|
+
if (options.close ?? context.closeOnPurge) {
|
|
216
|
+
await this.closeTenant(tenantId);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
this.tenantCache.delete(tenantId);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return purged;
|
|
223
|
+
}
|
|
193
224
|
static async closeTenant(tenantId) {
|
|
194
225
|
const context = this.tenantCache.get(tenantId);
|
|
195
226
|
if (!context)
|
|
196
227
|
return;
|
|
197
228
|
this.tenantCache.delete(tenantId);
|
|
229
|
+
if (!context.closeOnPurge)
|
|
230
|
+
return;
|
|
198
231
|
const connection = this.connections.get(context.connectionName);
|
|
199
|
-
|
|
200
|
-
|
|
232
|
+
if (connection === context.connection) {
|
|
233
|
+
this.connections.delete(context.connectionName);
|
|
234
|
+
await connection.close();
|
|
235
|
+
}
|
|
236
|
+
else if (context.ownsConnection) {
|
|
237
|
+
await context.connection.close();
|
|
238
|
+
}
|
|
239
|
+
const pool = this.pools.get(context.connectionName);
|
|
240
|
+
if (pool) {
|
|
241
|
+
this.pools.delete(context.connectionName);
|
|
242
|
+
for (const { connection } of pool) {
|
|
243
|
+
await connection.close().catch(() => null);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
201
246
|
}
|
|
202
247
|
static async closeAll() {
|
|
203
248
|
const connections = new Set(this.connections.values());
|
|
@@ -4,6 +4,10 @@ export interface ActiveTenantContext {
|
|
|
4
4
|
connection: Connection;
|
|
5
5
|
connectionName: string;
|
|
6
6
|
strategy: "database" | "schema" | "rls";
|
|
7
|
+
resolvedAt: number;
|
|
8
|
+
expiresAt?: number;
|
|
9
|
+
closeOnPurge: boolean;
|
|
10
|
+
ownsConnection: boolean;
|
|
7
11
|
schema?: string;
|
|
8
12
|
schemaMode?: "qualify" | "search_path";
|
|
9
13
|
rlsTenantId?: string;
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { Connection } from "./connection/Connection.js";
|
|
2
2
|
export { ConnectionManager } from "./connection/ConnectionManager.js";
|
|
3
|
-
export type { TenantResolution, TenantResolver } from "./connection/ConnectionManager.js";
|
|
3
|
+
export type { TenantCachePolicy, TenantResolution, TenantResolver } from "./connection/ConnectionManager.js";
|
|
4
4
|
export { TenantContext } from "./connection/TenantContext.js";
|
|
5
5
|
export type { ActiveTenantContext } from "./connection/TenantContext.js";
|
|
6
6
|
export { configureBunny } from "./config/BunnyConfig.js";
|
|
@@ -23,7 +23,7 @@ export { BelongsToMany } from "./model/BelongsToMany.js";
|
|
|
23
23
|
export { IdentityMap } from "./model/IdentityMap.js";
|
|
24
24
|
export { Migration } from "./migration/Migration.js";
|
|
25
25
|
export { Migrator } from "./migration/Migrator.js";
|
|
26
|
-
export type { MigrationEvent, MigrationEventListener, MigrationEventPayload } from "./migration/Migrator.js";
|
|
26
|
+
export type { MigrationEvent, MigrationEventListener, MigrationEventPayload, MigrationStatusRow, MigratorOptions } from "./migration/Migrator.js";
|
|
27
27
|
export { MigrationCreator } from "./migration/MigrationCreator.js";
|
|
28
28
|
export { TypeGenerator } from "./typegen/TypeGenerator.js";
|
|
29
29
|
export { TypeMapper } from "./typegen/TypeMapper.js";
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { Connection } from "../connection/Connection.js";
|
|
2
2
|
import type { TypeGeneratorOptions } from "../typegen/TypeGenerator.js";
|
|
3
|
+
export interface MigrationStatusRow {
|
|
4
|
+
migration: string;
|
|
5
|
+
status: string;
|
|
6
|
+
tenant: string | null;
|
|
7
|
+
}
|
|
8
|
+
export interface MigratorOptions {
|
|
9
|
+
tenantId?: string | null;
|
|
10
|
+
lock?: boolean;
|
|
11
|
+
lockTimeoutMs?: number;
|
|
12
|
+
}
|
|
3
13
|
export type MigrationEvent = "migrating" | "migrated" | "rollingBack" | "rolledBack" | "schemaDumped" | "schemaSquashed";
|
|
4
14
|
export interface MigrationEventPayload {
|
|
5
15
|
migration?: string;
|
|
@@ -12,10 +22,18 @@ export declare class Migrator {
|
|
|
12
22
|
private path;
|
|
13
23
|
private typesOutDir?;
|
|
14
24
|
private typeGeneratorOptions;
|
|
25
|
+
private options;
|
|
15
26
|
private static listeners;
|
|
16
|
-
constructor(connection: Connection, path: string | string[], typesOutDir?: string | undefined, typeGeneratorOptions?: Omit<TypeGeneratorOptions, "outDir"
|
|
27
|
+
constructor(connection: Connection, path: string | string[], typesOutDir?: string | undefined, typeGeneratorOptions?: Omit<TypeGeneratorOptions, "outDir">, options?: MigratorOptions);
|
|
17
28
|
private getPaths;
|
|
18
29
|
private ensureMigrationsTable;
|
|
30
|
+
private getTenantId;
|
|
31
|
+
private scopedMigrations;
|
|
32
|
+
private ensureMigrationLocksTable;
|
|
33
|
+
private getLockName;
|
|
34
|
+
private shouldLock;
|
|
35
|
+
private acquireLock;
|
|
36
|
+
private releaseLock;
|
|
19
37
|
static on(event: MigrationEvent, listener: MigrationEventListener): () => void;
|
|
20
38
|
static clearListeners(event?: MigrationEvent): void;
|
|
21
39
|
private emit;
|
|
@@ -24,10 +42,7 @@ export declare class Migrator {
|
|
|
24
42
|
run(): Promise<void>;
|
|
25
43
|
rollback(): Promise<void>;
|
|
26
44
|
private generateTypesIfNeeded;
|
|
27
|
-
status(): Promise<
|
|
28
|
-
migration: string;
|
|
29
|
-
status: string;
|
|
30
|
-
}[]>;
|
|
45
|
+
status(): Promise<MigrationStatusRow[]>;
|
|
31
46
|
dumpSchema(path: string): Promise<string>;
|
|
32
47
|
squash(path: string): Promise<string>;
|
|
33
48
|
private getSchemaDumpSql;
|
|
@@ -10,12 +10,14 @@ export class Migrator {
|
|
|
10
10
|
path;
|
|
11
11
|
typesOutDir;
|
|
12
12
|
typeGeneratorOptions;
|
|
13
|
+
options;
|
|
13
14
|
static listeners = new Map();
|
|
14
|
-
constructor(connection, path, typesOutDir, typeGeneratorOptions = {}) {
|
|
15
|
+
constructor(connection, path, typesOutDir, typeGeneratorOptions = {}, options = {}) {
|
|
15
16
|
this.connection = connection;
|
|
16
17
|
this.path = path;
|
|
17
18
|
this.typesOutDir = typesOutDir;
|
|
18
19
|
this.typeGeneratorOptions = typeGeneratorOptions;
|
|
20
|
+
this.options = options;
|
|
19
21
|
Schema.setConnection(connection);
|
|
20
22
|
}
|
|
21
23
|
getPaths() {
|
|
@@ -27,10 +29,73 @@ export class Migrator {
|
|
|
27
29
|
await Schema.create("migrations", (table) => {
|
|
28
30
|
table.increments("id");
|
|
29
31
|
table.string("migration");
|
|
32
|
+
table.string("tenant").nullable().index();
|
|
30
33
|
table.integer("batch");
|
|
31
34
|
});
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (!(await Schema.hasColumn("migrations", "tenant"))) {
|
|
38
|
+
await Schema.table("migrations", (table) => {
|
|
39
|
+
table.string("tenant").nullable().index();
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
getTenantId() {
|
|
44
|
+
return this.options.tenantId ?? null;
|
|
45
|
+
}
|
|
46
|
+
scopedMigrations() {
|
|
47
|
+
const builder = new Builder(this.connection, "migrations");
|
|
48
|
+
const tenantId = this.getTenantId();
|
|
49
|
+
return tenantId === null ? builder.whereNull("tenant") : builder.where("tenant", tenantId);
|
|
50
|
+
}
|
|
51
|
+
async ensureMigrationLocksTable() {
|
|
52
|
+
if (await Schema.hasTable("migration_locks"))
|
|
53
|
+
return;
|
|
54
|
+
await Schema.create("migration_locks", (table) => {
|
|
55
|
+
table.string("name").primary();
|
|
56
|
+
table.string("owner");
|
|
57
|
+
table.string("created_at");
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
getLockName() {
|
|
61
|
+
const tenantId = this.getTenantId();
|
|
62
|
+
return tenantId === null ? "migrations:default" : `migrations:tenant:${tenantId}`;
|
|
63
|
+
}
|
|
64
|
+
shouldLock() {
|
|
65
|
+
return this.options.lock !== false;
|
|
66
|
+
}
|
|
67
|
+
async acquireLock() {
|
|
68
|
+
if (!this.shouldLock())
|
|
69
|
+
return false;
|
|
70
|
+
await this.ensureMigrationLocksTable();
|
|
71
|
+
const lockName = this.getLockName();
|
|
72
|
+
const timeoutMs = this.options.lockTimeoutMs ?? 30000;
|
|
73
|
+
const owner = `${process.pid}:${Date.now()}:${Math.random().toString(36).slice(2)}`;
|
|
74
|
+
const started = Date.now();
|
|
75
|
+
while (true) {
|
|
76
|
+
try {
|
|
77
|
+
await new Builder(this.connection, "migration_locks").insert({
|
|
78
|
+
name: lockName,
|
|
79
|
+
owner,
|
|
80
|
+
created_at: new Date().toISOString(),
|
|
81
|
+
});
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
if (Date.now() - started >= timeoutMs) {
|
|
86
|
+
throw new Error(`Could not acquire migration lock "${lockName}" within ${timeoutMs}ms.`);
|
|
87
|
+
}
|
|
88
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
89
|
+
}
|
|
32
90
|
}
|
|
33
91
|
}
|
|
92
|
+
async releaseLock() {
|
|
93
|
+
if (!this.shouldLock())
|
|
94
|
+
return;
|
|
95
|
+
await new Builder(this.connection, "migration_locks")
|
|
96
|
+
.where("name", this.getLockName())
|
|
97
|
+
.delete();
|
|
98
|
+
}
|
|
34
99
|
static on(event, listener) {
|
|
35
100
|
const listeners = this.listeners.get(event) || new Set();
|
|
36
101
|
listeners.add(listener);
|
|
@@ -49,7 +114,8 @@ export class Migrator {
|
|
|
49
114
|
}
|
|
50
115
|
}
|
|
51
116
|
async getLastBatchNumber() {
|
|
52
|
-
|
|
117
|
+
await this.ensureMigrationsTable();
|
|
118
|
+
const result = await this.scopedMigrations()
|
|
53
119
|
.select("MAX(batch) as batch")
|
|
54
120
|
.first();
|
|
55
121
|
return result?.batch || 0;
|
|
@@ -74,16 +140,18 @@ export class Migrator {
|
|
|
74
140
|
return files.sort((a, b) => a.fileName.localeCompare(b.fileName) || a.id.localeCompare(b.id));
|
|
75
141
|
}
|
|
76
142
|
async run() {
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
const pending = files.filter((f) => !ran.has(f.id) && !ran.has(f.fileName));
|
|
80
|
-
if (pending.length === 0) {
|
|
81
|
-
console.log("Nothing to migrate.");
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
const batch = (await this.getLastBatchNumber()) + 1;
|
|
85
|
-
await this.connection.beginTransaction();
|
|
143
|
+
await this.ensureMigrationsTable();
|
|
144
|
+
const locked = await this.acquireLock();
|
|
86
145
|
try {
|
|
146
|
+
const ran = await this.getRan();
|
|
147
|
+
const files = await this.getMigrationFiles();
|
|
148
|
+
const pending = files.filter((f) => !ran.has(f.id) && !ran.has(f.fileName));
|
|
149
|
+
if (pending.length === 0) {
|
|
150
|
+
console.log("Nothing to migrate.");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const batch = (await this.getLastBatchNumber()) + 1;
|
|
154
|
+
await this.connection.beginTransaction();
|
|
87
155
|
for (const file of pending) {
|
|
88
156
|
const migration = await this.resolve(file.id);
|
|
89
157
|
console.log(`Migrating: ${file.id}`);
|
|
@@ -91,6 +159,7 @@ export class Migrator {
|
|
|
91
159
|
await migration.up();
|
|
92
160
|
await new Builder(this.connection, "migrations").insert({
|
|
93
161
|
migration: file.id,
|
|
162
|
+
tenant: this.getTenantId(),
|
|
94
163
|
batch,
|
|
95
164
|
});
|
|
96
165
|
await this.emit("migrated", { migration: file.id, batch });
|
|
@@ -103,23 +172,29 @@ export class Migrator {
|
|
|
103
172
|
await this.connection.rollback();
|
|
104
173
|
throw error;
|
|
105
174
|
}
|
|
175
|
+
finally {
|
|
176
|
+
if (locked)
|
|
177
|
+
await this.releaseLock();
|
|
178
|
+
}
|
|
106
179
|
}
|
|
107
180
|
async rollback() {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
console.log("Nothing to rollback.");
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
const records = (await new Builder(this.connection, "migrations")
|
|
114
|
-
.where("batch", batch)
|
|
115
|
-
.orderBy("id", "desc")
|
|
116
|
-
.get());
|
|
117
|
-
if (records.length === 0) {
|
|
118
|
-
console.log("Nothing to rollback.");
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
await this.connection.beginTransaction();
|
|
181
|
+
await this.ensureMigrationsTable();
|
|
182
|
+
const locked = await this.acquireLock();
|
|
122
183
|
try {
|
|
184
|
+
const batch = await this.getLastBatchNumber();
|
|
185
|
+
if (batch === 0) {
|
|
186
|
+
console.log("Nothing to rollback.");
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const records = (await this.scopedMigrations()
|
|
190
|
+
.where("batch", batch)
|
|
191
|
+
.orderBy("id", "desc")
|
|
192
|
+
.get());
|
|
193
|
+
if (records.length === 0) {
|
|
194
|
+
console.log("Nothing to rollback.");
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
await this.connection.beginTransaction();
|
|
123
198
|
for (const record of records) {
|
|
124
199
|
const migration = await this.resolve(record.migration);
|
|
125
200
|
console.log(`Rolling back: ${record.migration}`);
|
|
@@ -138,6 +213,10 @@ export class Migrator {
|
|
|
138
213
|
await this.connection.rollback();
|
|
139
214
|
throw error;
|
|
140
215
|
}
|
|
216
|
+
finally {
|
|
217
|
+
if (locked)
|
|
218
|
+
await this.releaseLock();
|
|
219
|
+
}
|
|
141
220
|
}
|
|
142
221
|
async generateTypesIfNeeded() {
|
|
143
222
|
const modelDirectories = normalizePathList(this.typeGeneratorOptions.modelDirectories || this.typeGeneratorOptions.modelDirectory);
|
|
@@ -154,11 +233,14 @@ export class Migrator {
|
|
|
154
233
|
console.log(`Regenerated types in ${label}`);
|
|
155
234
|
}
|
|
156
235
|
async status() {
|
|
236
|
+
await this.ensureMigrationsTable();
|
|
157
237
|
const ran = await this.getRan();
|
|
158
238
|
const files = await this.getMigrationFiles();
|
|
239
|
+
const tenant = this.getTenantId();
|
|
159
240
|
return files.map((file) => ({
|
|
160
241
|
migration: file.id,
|
|
161
242
|
status: ran.has(file.id) || ran.has(file.fileName) ? "Ran" : "Pending",
|
|
243
|
+
tenant,
|
|
162
244
|
}));
|
|
163
245
|
}
|
|
164
246
|
async dumpSchema(path) {
|
|
@@ -173,12 +255,20 @@ export class Migrator {
|
|
|
173
255
|
const files = await this.getMigrationFiles();
|
|
174
256
|
await this.ensureMigrationsTable();
|
|
175
257
|
const batch = (await this.getLastBatchNumber()) + 1;
|
|
176
|
-
await
|
|
177
|
-
|
|
178
|
-
await
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
258
|
+
const locked = await this.acquireLock();
|
|
259
|
+
try {
|
|
260
|
+
await this.scopedMigrations().delete();
|
|
261
|
+
for (const file of files) {
|
|
262
|
+
await new Builder(this.connection, "migrations").insert({
|
|
263
|
+
migration: file.id,
|
|
264
|
+
tenant: this.getTenantId(),
|
|
265
|
+
batch,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
finally {
|
|
270
|
+
if (locked)
|
|
271
|
+
await this.releaseLock();
|
|
182
272
|
}
|
|
183
273
|
await this.emit("schemaSquashed", { path, batch });
|
|
184
274
|
return sql;
|
|
@@ -269,7 +359,7 @@ export class Migrator {
|
|
|
269
359
|
}
|
|
270
360
|
async getRan() {
|
|
271
361
|
await this.ensureMigrationsTable();
|
|
272
|
-
const results = await
|
|
362
|
+
const results = await this.scopedMigrations()
|
|
273
363
|
.orderBy("id", "asc")
|
|
274
364
|
.get();
|
|
275
365
|
const ran = new Set();
|
|
@@ -7,6 +7,10 @@ export declare class Schema {
|
|
|
7
7
|
private static getGrammar;
|
|
8
8
|
static create(table: string, callback: (blueprint: Blueprint) => void): Promise<void>;
|
|
9
9
|
static createIfNotExists(table: string, callback: (blueprint: Blueprint) => void): Promise<void>;
|
|
10
|
+
static createSchema(schema: string): Promise<void>;
|
|
11
|
+
static dropSchema(schema: string, options?: {
|
|
12
|
+
cascade?: boolean;
|
|
13
|
+
}): Promise<void>;
|
|
10
14
|
static table(table: string, callback: (blueprint: Blueprint) => void): Promise<void>;
|
|
11
15
|
static drop(table: string): Promise<void>;
|
|
12
16
|
static dropIfExists(table: string): Promise<void>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Connection } from "../connection/Connection.js";
|
|
1
2
|
import { Blueprint } from "./Blueprint.js";
|
|
2
3
|
import { SQLiteGrammar } from "./grammars/SQLiteGrammar.js";
|
|
3
4
|
import { MySqlGrammar } from "./grammars/MySqlGrammar.js";
|
|
@@ -61,6 +62,29 @@ export class Schema {
|
|
|
61
62
|
await this.getConnection().run(fkSql);
|
|
62
63
|
}
|
|
63
64
|
}
|
|
65
|
+
static async createSchema(schema) {
|
|
66
|
+
Connection.assertSafeIdentifier(schema, "schema name");
|
|
67
|
+
const connection = this.getConnection();
|
|
68
|
+
const driver = connection.getDriverName();
|
|
69
|
+
if (driver === "sqlite") {
|
|
70
|
+
throw new Error("Schema creation is not supported for SQLite connections.");
|
|
71
|
+
}
|
|
72
|
+
const grammar = this.getGrammar();
|
|
73
|
+
const keyword = driver === "mysql" ? "DATABASE" : "SCHEMA";
|
|
74
|
+
await connection.run(`CREATE ${keyword} IF NOT EXISTS ${grammar.wrap(schema)}`);
|
|
75
|
+
}
|
|
76
|
+
static async dropSchema(schema, options = {}) {
|
|
77
|
+
Connection.assertSafeIdentifier(schema, "schema name");
|
|
78
|
+
const connection = this.getConnection();
|
|
79
|
+
const driver = connection.getDriverName();
|
|
80
|
+
if (driver === "sqlite") {
|
|
81
|
+
throw new Error("Schema dropping is not supported for SQLite connections.");
|
|
82
|
+
}
|
|
83
|
+
const grammar = this.getGrammar();
|
|
84
|
+
const keyword = driver === "mysql" ? "DATABASE" : "SCHEMA";
|
|
85
|
+
const cascade = driver === "postgres" && options.cascade ? " CASCADE" : "";
|
|
86
|
+
await connection.run(`DROP ${keyword} IF EXISTS ${grammar.wrap(schema)}${cascade}`);
|
|
87
|
+
}
|
|
64
88
|
static async table(table, callback) {
|
|
65
89
|
const blueprint = new Blueprint(table);
|
|
66
90
|
callback(blueprint);
|
package/package.json
CHANGED