@bunnykit/orm 0.1.23 → 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/model/Model.d.ts +3 -3
- package/dist/src/model/Model.js +74 -75
- package/dist/src/query/Builder.d.ts +2 -0
- package/dist/src/query/Builder.js +58 -7
- 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();
|
|
@@ -5,7 +5,7 @@ import { BelongsToMany } from "./BelongsToMany.js";
|
|
|
5
5
|
export type ModelConstructor<T extends Model = Model> = (new (...args: any[]) => T) & Omit<typeof Model, "prototype">;
|
|
6
6
|
export type GlobalScope = (builder: Builder<any>, model: ModelConstructor) => void;
|
|
7
7
|
export type LiteralUnion<T extends string> = T | (string & {});
|
|
8
|
-
type BaseModelInstanceKey = "$attributes" | "$original" | "$exists" | "$relations" | "$casts" | "$connection" | "fill" | "setConnection" | "getConnection" | "isFillable" | "getAttribute" | "setAttribute" | "castAttribute" | "serializeCastAttribute" | "mergeCasts" | "getDirty" | "isDirty" | "save" | "updateTimestamps" | "touch" | "increment" | "decrement" | "load" | "delete" | "restore" | "forceDelete" | "refresh" | "toJSON" | "toString" | "freshTimestamp" | "setRelation" | "getRelation" | "hasMany" | "belongsTo" | "hasOne" | "hasManyThrough" | "hasOneThrough" | "belongsToMany" | "morphTo" | "morphOne" | "morphMany" | "morphToMany" | "morphedByMany";
|
|
8
|
+
type BaseModelInstanceKey = "$attributes" | "$original" | "$exists" | "$relations" | "$casts" | "$castCache" | "$connection" | "fill" | "setConnection" | "getConnection" | "isFillable" | "getAttribute" | "setAttribute" | "castAttribute" | "serializeCastAttribute" | "mergeCasts" | "getDirty" | "isDirty" | "save" | "updateTimestamps" | "touch" | "increment" | "decrement" | "load" | "delete" | "restore" | "forceDelete" | "refresh" | "toJSON" | "toString" | "freshTimestamp" | "setRelation" | "getRelation" | "hasMany" | "belongsTo" | "hasOne" | "hasManyThrough" | "hasOneThrough" | "belongsToMany" | "morphTo" | "morphOne" | "morphMany" | "morphToMany" | "morphedByMany";
|
|
9
9
|
export type ModelInstanceAttributeKeys<T> = Extract<Exclude<keyof T, BaseModelInstanceKey>, string>;
|
|
10
10
|
export type ModelAttributes<T> = T extends {
|
|
11
11
|
$attributes: Record<string, any>;
|
|
@@ -111,10 +111,9 @@ export declare class Model<T extends Record<string, any> = Record<string, any>>
|
|
|
111
111
|
$exists: boolean;
|
|
112
112
|
$relations: Record<string, any>;
|
|
113
113
|
$casts: Record<string, CastDefinition>;
|
|
114
|
+
$castCache: Record<string, any>;
|
|
114
115
|
$connection?: Connection;
|
|
115
116
|
constructor(attributes?: Partial<T>);
|
|
116
|
-
private defineAttributeProperty;
|
|
117
|
-
private syncAttributeProperties;
|
|
118
117
|
static getTable(): string;
|
|
119
118
|
static getConnection(): Connection;
|
|
120
119
|
static setConnection(connection: Connection): void;
|
|
@@ -127,6 +126,7 @@ export declare class Model<T extends Record<string, any> = Record<string, any>>
|
|
|
127
126
|
static applyGlobalScopes(builder: Builder<any>): void;
|
|
128
127
|
static getQualifiedDeletedAtColumn(): string;
|
|
129
128
|
static shouldAutoGeneratePrimaryKey(): Promise<boolean>;
|
|
129
|
+
static hydrate<M extends ModelConstructor>(this: M, row: Record<string, any>, connection?: Connection): InstanceType<M>;
|
|
130
130
|
static create<M extends ModelConstructor>(this: M, attributes: ModelAttributeInput<InstanceType<M>>): Promise<InstanceType<M>>;
|
|
131
131
|
static find<M extends ModelConstructor>(this: M, id: any): Promise<InstanceType<M> | null>;
|
|
132
132
|
static findOrFail<M extends ModelConstructor>(this: M, id: any): Promise<InstanceType<M>>;
|
package/dist/src/model/Model.js
CHANGED
|
@@ -8,6 +8,44 @@ import { ModelNotFoundError } from "./ModelNotFoundError.js";
|
|
|
8
8
|
import { ConnectionManager } from "../connection/ConnectionManager.js";
|
|
9
9
|
import { TenantContext } from "../connection/TenantContext.js";
|
|
10
10
|
import { IdentityMap } from "./IdentityMap.js";
|
|
11
|
+
const modelProxyHandler = {
|
|
12
|
+
get(target, prop, receiver) {
|
|
13
|
+
if (typeof prop === "string" && !(prop in target) && prop in target.$attributes) {
|
|
14
|
+
return target.getAttribute(prop);
|
|
15
|
+
}
|
|
16
|
+
return Reflect.get(target, prop, receiver);
|
|
17
|
+
},
|
|
18
|
+
set(target, prop, value, receiver) {
|
|
19
|
+
if (typeof prop === "string" && !prop.startsWith("$") && !(prop in target)) {
|
|
20
|
+
target.setAttribute(prop, value);
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
return Reflect.set(target, prop, value, receiver);
|
|
24
|
+
},
|
|
25
|
+
has(target, prop) {
|
|
26
|
+
if (typeof prop === "string" && prop in target.$attributes)
|
|
27
|
+
return true;
|
|
28
|
+
return Reflect.has(target, prop);
|
|
29
|
+
},
|
|
30
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
31
|
+
if (typeof prop === "string" && prop in target.$attributes) {
|
|
32
|
+
return {
|
|
33
|
+
enumerable: true,
|
|
34
|
+
configurable: true,
|
|
35
|
+
value: target.getAttribute(prop),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return Reflect.getOwnPropertyDescriptor(target, prop);
|
|
39
|
+
},
|
|
40
|
+
ownKeys(target) {
|
|
41
|
+
const keys = new Set(Reflect.ownKeys(target));
|
|
42
|
+
for (const key of Object.keys(target.$attributes)) {
|
|
43
|
+
if (!key.startsWith("$"))
|
|
44
|
+
keys.add(key);
|
|
45
|
+
}
|
|
46
|
+
return Array.from(keys);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
11
49
|
const globalScopes = new WeakMap();
|
|
12
50
|
function getGlobalScopes(model) {
|
|
13
51
|
const scopes = new Map();
|
|
@@ -306,6 +344,7 @@ export class Model {
|
|
|
306
344
|
$exists = false;
|
|
307
345
|
$relations = {};
|
|
308
346
|
$casts = {};
|
|
347
|
+
$castCache = {};
|
|
309
348
|
$connection;
|
|
310
349
|
constructor(attributes) {
|
|
311
350
|
const defaults = this.constructor.attributes;
|
|
@@ -315,72 +354,7 @@ export class Model {
|
|
|
315
354
|
if (attributes) {
|
|
316
355
|
this.fill(attributes);
|
|
317
356
|
}
|
|
318
|
-
this
|
|
319
|
-
// Minimal Proxy fallback for dynamic property access on undefined keys.
|
|
320
|
-
// Pre-defined attribute getters/setters bypass the Proxy entirely.
|
|
321
|
-
return new Proxy(this, {
|
|
322
|
-
get(target, prop, receiver) {
|
|
323
|
-
if (typeof prop === "string" && !(prop in target) && prop in target.$attributes) {
|
|
324
|
-
return target.getAttribute(prop);
|
|
325
|
-
}
|
|
326
|
-
return Reflect.get(target, prop, receiver);
|
|
327
|
-
},
|
|
328
|
-
set(target, prop, value, receiver) {
|
|
329
|
-
if (typeof prop === "string" && !prop.startsWith("$") && !(prop in target)) {
|
|
330
|
-
target.setAttribute(prop, value);
|
|
331
|
-
return true;
|
|
332
|
-
}
|
|
333
|
-
return Reflect.set(target, prop, value, receiver);
|
|
334
|
-
},
|
|
335
|
-
has(target, prop) {
|
|
336
|
-
if (typeof prop === "string" && prop in target.$attributes)
|
|
337
|
-
return true;
|
|
338
|
-
return Reflect.has(target, prop);
|
|
339
|
-
},
|
|
340
|
-
getOwnPropertyDescriptor(target, prop) {
|
|
341
|
-
if (typeof prop === "string" && prop in target.$attributes) {
|
|
342
|
-
return {
|
|
343
|
-
enumerable: true,
|
|
344
|
-
configurable: true,
|
|
345
|
-
value: target.getAttribute(prop),
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
return Reflect.getOwnPropertyDescriptor(target, prop);
|
|
349
|
-
},
|
|
350
|
-
ownKeys(target) {
|
|
351
|
-
const keys = new Set(Reflect.ownKeys(target));
|
|
352
|
-
for (const key of Object.keys(target.$attributes)) {
|
|
353
|
-
if (!key.startsWith("$"))
|
|
354
|
-
keys.add(key);
|
|
355
|
-
}
|
|
356
|
-
return Array.from(keys);
|
|
357
|
-
},
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
defineAttributeProperty(key) {
|
|
361
|
-
if (key in this)
|
|
362
|
-
return;
|
|
363
|
-
Object.defineProperty(this, key, {
|
|
364
|
-
get: () => this.getAttribute(key),
|
|
365
|
-
set: (value) => this.setAttribute(key, value),
|
|
366
|
-
enumerable: true,
|
|
367
|
-
configurable: true,
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
|
-
syncAttributeProperties() {
|
|
371
|
-
const currentKeys = new Set(Object.keys(this.$attributes));
|
|
372
|
-
// Remove stale attribute properties
|
|
373
|
-
for (const key of Reflect.ownKeys(this)) {
|
|
374
|
-
if (key.startsWith("$") || typeof key !== "string")
|
|
375
|
-
continue;
|
|
376
|
-
const desc = Object.getOwnPropertyDescriptor(this, key);
|
|
377
|
-
if (desc && desc.get && desc.configurable && !currentKeys.has(key)) {
|
|
378
|
-
delete this[key];
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
for (const key of currentKeys) {
|
|
382
|
-
this.defineAttributeProperty(key);
|
|
383
|
-
}
|
|
357
|
+
return new Proxy(this, modelProxyHandler);
|
|
384
358
|
}
|
|
385
359
|
static getTable() {
|
|
386
360
|
return this.table || snakeCase(this.name) + "s";
|
|
@@ -459,6 +433,17 @@ export class Model {
|
|
|
459
433
|
const numericTypes = new Set(["integer", "int", "bigint", "smallint", "tinyint", "real", "float", "double", "decimal", "numeric"]);
|
|
460
434
|
return !numericTypes.has(type);
|
|
461
435
|
}
|
|
436
|
+
static hydrate(row, connection) {
|
|
437
|
+
const instance = new this();
|
|
438
|
+
instance.$attributes = { ...instance.$attributes, ...row };
|
|
439
|
+
instance.$original = { ...row };
|
|
440
|
+
instance.$castCache = {};
|
|
441
|
+
instance.$exists = true;
|
|
442
|
+
if (connection) {
|
|
443
|
+
instance.setConnection(connection);
|
|
444
|
+
}
|
|
445
|
+
return instance;
|
|
446
|
+
}
|
|
462
447
|
static async create(attributes) {
|
|
463
448
|
const instance = new this();
|
|
464
449
|
instance.fill(attributes);
|
|
@@ -701,12 +686,19 @@ export class Model {
|
|
|
701
686
|
return true;
|
|
702
687
|
}
|
|
703
688
|
getAttribute(key) {
|
|
689
|
+
if (Object.prototype.hasOwnProperty.call(this.$castCache, key)) {
|
|
690
|
+
return this.$castCache[key];
|
|
691
|
+
}
|
|
704
692
|
const value = this.$attributes[key];
|
|
705
|
-
|
|
693
|
+
const casted = this.castAttribute(key, value);
|
|
694
|
+
if (this.getCastDefinition(key) && value !== null && value !== undefined) {
|
|
695
|
+
this.$castCache[key] = casted;
|
|
696
|
+
}
|
|
697
|
+
return casted;
|
|
706
698
|
}
|
|
707
699
|
setAttribute(key, value) {
|
|
708
700
|
this.$attributes[key] = this.serializeCastAttribute(key, value);
|
|
709
|
-
this
|
|
701
|
+
delete this.$castCache[key];
|
|
710
702
|
}
|
|
711
703
|
castAttribute(key, value) {
|
|
712
704
|
const cast = this.getCastDefinition(key);
|
|
@@ -785,6 +777,7 @@ export class Model {
|
|
|
785
777
|
}
|
|
786
778
|
mergeCasts(casts) {
|
|
787
779
|
this.$casts = { ...this.$casts, ...casts };
|
|
780
|
+
this.$castCache = {};
|
|
788
781
|
return this;
|
|
789
782
|
}
|
|
790
783
|
getCastDefinition(key) {
|
|
@@ -819,6 +812,7 @@ export class Model {
|
|
|
819
812
|
let dirty = this.getDirty();
|
|
820
813
|
if (Object.keys(dirty).length > 0 && constructor.timestamps) {
|
|
821
814
|
this.$attributes["updated_at"] = this.freshTimestamp();
|
|
815
|
+
delete this.$castCache.updated_at;
|
|
822
816
|
dirty = this.getDirty();
|
|
823
817
|
}
|
|
824
818
|
if (Object.keys(dirty).length > 0) {
|
|
@@ -840,6 +834,8 @@ export class Model {
|
|
|
840
834
|
const now = this.freshTimestamp();
|
|
841
835
|
this.$attributes["created_at"] = now;
|
|
842
836
|
this.$attributes["updated_at"] = now;
|
|
837
|
+
delete this.$castCache.created_at;
|
|
838
|
+
delete this.$castCache.updated_at;
|
|
843
839
|
}
|
|
844
840
|
const primaryKey = constructor.primaryKey;
|
|
845
841
|
const primaryKeyValue = this.getAttribute(primaryKey);
|
|
@@ -847,6 +843,7 @@ export class Model {
|
|
|
847
843
|
if ((primaryKeyValue === null || primaryKeyValue === undefined || primaryKeyValue === "") && shouldGeneratePrimaryKey) {
|
|
848
844
|
const generated = crypto.randomUUID();
|
|
849
845
|
this.$attributes[primaryKey] = generated;
|
|
846
|
+
delete this.$castCache[primaryKey];
|
|
850
847
|
}
|
|
851
848
|
const connection = this.getConnection();
|
|
852
849
|
if (shouldGeneratePrimaryKey || primaryKeyValue !== null && primaryKeyValue !== undefined && primaryKeyValue !== "") {
|
|
@@ -856,6 +853,7 @@ export class Model {
|
|
|
856
853
|
const result = await new Builder(connection, connection.qualifyTable(constructor.getTable())).insertGetId(this.$attributes);
|
|
857
854
|
if (result) {
|
|
858
855
|
this.$attributes[constructor.primaryKey] = result;
|
|
856
|
+
delete this.$castCache[constructor.primaryKey];
|
|
859
857
|
}
|
|
860
858
|
}
|
|
861
859
|
this.$exists = true;
|
|
@@ -863,7 +861,6 @@ export class Model {
|
|
|
863
861
|
await ObserverRegistry.dispatch("created", this);
|
|
864
862
|
await ObserverRegistry.dispatch("saved", this);
|
|
865
863
|
}
|
|
866
|
-
this.syncAttributeProperties();
|
|
867
864
|
const identityMap = IdentityMap.current();
|
|
868
865
|
if (identityMap) {
|
|
869
866
|
const pk = this.getAttribute(constructor.primaryKey);
|
|
@@ -879,10 +876,11 @@ export class Model {
|
|
|
879
876
|
return;
|
|
880
877
|
const now = this.freshTimestamp();
|
|
881
878
|
this.$attributes["updated_at"] = now;
|
|
879
|
+
delete this.$castCache.updated_at;
|
|
882
880
|
if (!this.$exists) {
|
|
883
881
|
this.$attributes["created_at"] = now;
|
|
882
|
+
delete this.$castCache.created_at;
|
|
884
883
|
}
|
|
885
|
-
this.syncAttributeProperties();
|
|
886
884
|
}
|
|
887
885
|
async touch() {
|
|
888
886
|
if (!this.$exists)
|
|
@@ -897,8 +895,8 @@ export class Model {
|
|
|
897
895
|
.where(constructor.primaryKey, pk)
|
|
898
896
|
.update({ updated_at: now });
|
|
899
897
|
this.$attributes["updated_at"] = now;
|
|
898
|
+
delete this.$castCache.updated_at;
|
|
900
899
|
this.$original = { ...this.$attributes };
|
|
901
|
-
this.syncAttributeProperties();
|
|
902
900
|
return true;
|
|
903
901
|
}
|
|
904
902
|
async increment(column, amount = 1, extra = {}) {
|
|
@@ -914,11 +912,12 @@ export class Model {
|
|
|
914
912
|
}
|
|
915
913
|
await builder.increment(column, amount, extra);
|
|
916
914
|
this.$attributes[column] = (this.$attributes[column] || 0) + amount;
|
|
915
|
+
delete this.$castCache[column];
|
|
917
916
|
for (const [key, value] of Object.entries(extra)) {
|
|
918
917
|
this.$attributes[key] = value;
|
|
918
|
+
delete this.$castCache[key];
|
|
919
919
|
}
|
|
920
920
|
this.$original = { ...this.$attributes };
|
|
921
|
-
this.syncAttributeProperties();
|
|
922
921
|
return this;
|
|
923
922
|
}
|
|
924
923
|
async decrement(column, amount = 1, extra = {}) {
|
|
@@ -942,8 +941,8 @@ export class Model {
|
|
|
942
941
|
.where(constructor.primaryKey, pk)
|
|
943
942
|
.update({ [constructor.deletedAtColumn]: deletedAt });
|
|
944
943
|
this.$attributes[constructor.deletedAtColumn] = deletedAt;
|
|
944
|
+
delete this.$castCache[constructor.deletedAtColumn];
|
|
945
945
|
this.$original = { ...this.$attributes };
|
|
946
|
-
this.syncAttributeProperties();
|
|
947
946
|
}
|
|
948
947
|
else {
|
|
949
948
|
const connection = this.getConnection();
|
|
@@ -971,9 +970,9 @@ export class Model {
|
|
|
971
970
|
.where(constructor.primaryKey, pk)
|
|
972
971
|
.update({ [constructor.deletedAtColumn]: null });
|
|
973
972
|
this.$attributes[constructor.deletedAtColumn] = null;
|
|
973
|
+
delete this.$castCache[constructor.deletedAtColumn];
|
|
974
974
|
this.$original = { ...this.$attributes };
|
|
975
975
|
this.$exists = true;
|
|
976
|
-
this.syncAttributeProperties();
|
|
977
976
|
return true;
|
|
978
977
|
}
|
|
979
978
|
async forceDelete() {
|
|
@@ -1006,7 +1005,7 @@ export class Model {
|
|
|
1006
1005
|
if (result) {
|
|
1007
1006
|
this.$attributes = result.$attributes;
|
|
1008
1007
|
this.$original = { ...result.$attributes };
|
|
1009
|
-
this
|
|
1008
|
+
this.$castCache = {};
|
|
1010
1009
|
// Ensure this instance is the canonical one in the identity map
|
|
1011
1010
|
if (identityMap) {
|
|
1012
1011
|
IdentityMap.set(constructor.getTable(), pk, this);
|
|
@@ -32,8 +32,10 @@ export declare class Builder<T = Record<string, any>> {
|
|
|
32
32
|
updateJoins: string[];
|
|
33
33
|
bindings: any[];
|
|
34
34
|
private parameterize;
|
|
35
|
+
private sqlCache?;
|
|
35
36
|
constructor(connection: Connection, table: string);
|
|
36
37
|
private get grammar();
|
|
38
|
+
private invalidateSqlCache;
|
|
37
39
|
setModel(model: ModelConstructor): this;
|
|
38
40
|
table(table: string): this;
|
|
39
41
|
select(...columns: ModelColumn<T>[]): this;
|
|
@@ -21,6 +21,7 @@ export class Builder {
|
|
|
21
21
|
updateJoins = [];
|
|
22
22
|
bindings = [];
|
|
23
23
|
parameterize = false;
|
|
24
|
+
sqlCache;
|
|
24
25
|
constructor(connection, table) {
|
|
25
26
|
this.connection = connection;
|
|
26
27
|
this.tableName = table;
|
|
@@ -28,19 +29,25 @@ export class Builder {
|
|
|
28
29
|
get grammar() {
|
|
29
30
|
return this.connection.getGrammar();
|
|
30
31
|
}
|
|
32
|
+
invalidateSqlCache() {
|
|
33
|
+
this.sqlCache = undefined;
|
|
34
|
+
}
|
|
31
35
|
setModel(model) {
|
|
32
36
|
this.model = model;
|
|
33
37
|
return this;
|
|
34
38
|
}
|
|
35
39
|
table(table) {
|
|
40
|
+
this.invalidateSqlCache();
|
|
36
41
|
this.tableName = table;
|
|
37
42
|
return this;
|
|
38
43
|
}
|
|
39
44
|
select(...columns) {
|
|
45
|
+
this.invalidateSqlCache();
|
|
40
46
|
this.columns = columns;
|
|
41
47
|
return this;
|
|
42
48
|
}
|
|
43
49
|
distinct() {
|
|
50
|
+
this.invalidateSqlCache();
|
|
44
51
|
this.distinctFlag = true;
|
|
45
52
|
return this;
|
|
46
53
|
}
|
|
@@ -58,6 +65,7 @@ export class Builder {
|
|
|
58
65
|
value = operator;
|
|
59
66
|
operator = "=";
|
|
60
67
|
}
|
|
68
|
+
this.invalidateSqlCache();
|
|
61
69
|
this.wheres.push({ type: "basic", column, operator, value, boolean, scope });
|
|
62
70
|
return this;
|
|
63
71
|
}
|
|
@@ -65,6 +73,7 @@ export class Builder {
|
|
|
65
73
|
const nested = new Builder(this.connection, this.tableName);
|
|
66
74
|
callback(nested);
|
|
67
75
|
if (nested.wheres.length > 0) {
|
|
76
|
+
this.invalidateSqlCache();
|
|
68
77
|
this.wheres.push({ type: "nested", column: "", query: nested.wheres, boolean, scope: undefined });
|
|
69
78
|
}
|
|
70
79
|
return this;
|
|
@@ -85,26 +94,32 @@ export class Builder {
|
|
|
85
94
|
return this.whereNot(column, value, "or");
|
|
86
95
|
}
|
|
87
96
|
whereIn(column, values, boolean = "and", scope) {
|
|
97
|
+
this.invalidateSqlCache();
|
|
88
98
|
this.wheres.push({ type: "in", column, value: values, boolean, scope });
|
|
89
99
|
return this;
|
|
90
100
|
}
|
|
91
101
|
whereNotIn(column, values, boolean = "and", scope) {
|
|
102
|
+
this.invalidateSqlCache();
|
|
92
103
|
this.wheres.push({ type: "in", column, value: values, boolean, operator: "NOT IN", scope });
|
|
93
104
|
return this;
|
|
94
105
|
}
|
|
95
106
|
whereNull(column, boolean = "and", scope) {
|
|
107
|
+
this.invalidateSqlCache();
|
|
96
108
|
this.wheres.push({ type: "null", column, boolean, scope });
|
|
97
109
|
return this;
|
|
98
110
|
}
|
|
99
111
|
whereNotNull(column, boolean = "and", scope) {
|
|
112
|
+
this.invalidateSqlCache();
|
|
100
113
|
this.wheres.push({ type: "null", column, boolean, operator: "NOT NULL", scope });
|
|
101
114
|
return this;
|
|
102
115
|
}
|
|
103
116
|
whereBetween(column, values, boolean = "and", scope) {
|
|
117
|
+
this.invalidateSqlCache();
|
|
104
118
|
this.wheres.push({ type: "between", column, value: values, boolean, scope });
|
|
105
119
|
return this;
|
|
106
120
|
}
|
|
107
121
|
whereNotBetween(column, values, boolean = "and", scope) {
|
|
122
|
+
this.invalidateSqlCache();
|
|
108
123
|
this.wheres.push({ type: "between", column, value: values, boolean, operator: "NOT BETWEEN", scope });
|
|
109
124
|
return this;
|
|
110
125
|
}
|
|
@@ -139,14 +154,17 @@ export class Builder {
|
|
|
139
154
|
return this.whereTime(column, operator, value, "or");
|
|
140
155
|
}
|
|
141
156
|
whereRaw(sql, boolean = "and", scope) {
|
|
157
|
+
this.invalidateSqlCache();
|
|
142
158
|
this.wheres.push({ type: "raw", column: sql, boolean, scope });
|
|
143
159
|
return this;
|
|
144
160
|
}
|
|
145
161
|
whereColumn(first, operator, second, boolean = "and") {
|
|
162
|
+
this.invalidateSqlCache();
|
|
146
163
|
this.wheres.push({ type: "column", column: first, operator, value: second, boolean });
|
|
147
164
|
return this;
|
|
148
165
|
}
|
|
149
166
|
whereExists(sql, boolean = "and", not = false) {
|
|
167
|
+
this.invalidateSqlCache();
|
|
150
168
|
this.wheres.push({ type: "exists", column: sql, boolean, operator: not ? "NOT EXISTS" : "EXISTS" });
|
|
151
169
|
return this;
|
|
152
170
|
}
|
|
@@ -181,6 +199,7 @@ export class Builder {
|
|
|
181
199
|
return this.whereRaw(sql, "or", scope);
|
|
182
200
|
}
|
|
183
201
|
whereJsonContains(column, value, boolean = "and", not = false) {
|
|
202
|
+
this.invalidateSqlCache();
|
|
184
203
|
this.wheres.push({ type: "json_contains", column, value, boolean, scope: undefined, not });
|
|
185
204
|
return this;
|
|
186
205
|
}
|
|
@@ -189,10 +208,12 @@ export class Builder {
|
|
|
189
208
|
value = operator;
|
|
190
209
|
operator = "=";
|
|
191
210
|
}
|
|
211
|
+
this.invalidateSqlCache();
|
|
192
212
|
this.wheres.push({ type: "json_length", column, operator: String(operator), value, boolean, scope: undefined, not });
|
|
193
213
|
return this;
|
|
194
214
|
}
|
|
195
215
|
whereLike(column, value, boolean = "and", not = false) {
|
|
216
|
+
this.invalidateSqlCache();
|
|
196
217
|
this.wheres.push({ type: "like", column, value, boolean, scope: undefined, not });
|
|
197
218
|
return this;
|
|
198
219
|
}
|
|
@@ -200,23 +221,28 @@ export class Builder {
|
|
|
200
221
|
return this.whereLike(column, value, "and", true);
|
|
201
222
|
}
|
|
202
223
|
whereRegexp(column, value, boolean = "and", not = false) {
|
|
224
|
+
this.invalidateSqlCache();
|
|
203
225
|
this.wheres.push({ type: "regexp", column, value, boolean, scope: undefined, not });
|
|
204
226
|
return this;
|
|
205
227
|
}
|
|
206
228
|
whereFullText(columns, value, boolean = "and", not = false) {
|
|
207
229
|
const cols = Array.isArray(columns) ? columns : [columns];
|
|
230
|
+
this.invalidateSqlCache();
|
|
208
231
|
this.wheres.push({ type: "fulltext", column: "", columns: cols, value, boolean, scope: undefined, not });
|
|
209
232
|
return this;
|
|
210
233
|
}
|
|
211
234
|
whereAll(columns, operator, value, boolean = "and") {
|
|
235
|
+
this.invalidateSqlCache();
|
|
212
236
|
this.wheres.push({ type: "all", column: "", columns: columns, operator, value, boolean, scope: undefined });
|
|
213
237
|
return this;
|
|
214
238
|
}
|
|
215
239
|
whereAny(columns, operator, value, boolean = "and") {
|
|
240
|
+
this.invalidateSqlCache();
|
|
216
241
|
this.wheres.push({ type: "any", column: "", columns: columns, operator, value, boolean, scope: undefined });
|
|
217
242
|
return this;
|
|
218
243
|
}
|
|
219
244
|
orderBy(column, direction = "asc") {
|
|
245
|
+
this.invalidateSqlCache();
|
|
220
246
|
this.orders.push({ column, direction });
|
|
221
247
|
return this;
|
|
222
248
|
}
|
|
@@ -227,6 +253,7 @@ export class Builder {
|
|
|
227
253
|
return this.orderBy(column, "asc");
|
|
228
254
|
}
|
|
229
255
|
inRandomOrder() {
|
|
256
|
+
this.invalidateSqlCache();
|
|
230
257
|
this.randomOrderFlag = true;
|
|
231
258
|
return this;
|
|
232
259
|
}
|
|
@@ -234,6 +261,7 @@ export class Builder {
|
|
|
234
261
|
return this.orderBy(column, "desc");
|
|
235
262
|
}
|
|
236
263
|
reorder(column, direction = "asc") {
|
|
264
|
+
this.invalidateSqlCache();
|
|
237
265
|
this.orders = [];
|
|
238
266
|
this.randomOrderFlag = false;
|
|
239
267
|
if (column) {
|
|
@@ -242,18 +270,22 @@ export class Builder {
|
|
|
242
270
|
return this;
|
|
243
271
|
}
|
|
244
272
|
groupBy(...columns) {
|
|
273
|
+
this.invalidateSqlCache();
|
|
245
274
|
this.groups.push(...columns);
|
|
246
275
|
return this;
|
|
247
276
|
}
|
|
248
277
|
having(column, operator, value) {
|
|
278
|
+
this.invalidateSqlCache();
|
|
249
279
|
this.havings.push({ column, operator, value, boolean: "and" });
|
|
250
280
|
return this;
|
|
251
281
|
}
|
|
252
282
|
orHaving(column, operator, value) {
|
|
283
|
+
this.invalidateSqlCache();
|
|
253
284
|
this.havings.push({ column, operator, value, boolean: "or" });
|
|
254
285
|
return this;
|
|
255
286
|
}
|
|
256
287
|
havingRaw(sql, boolean = "and") {
|
|
288
|
+
this.invalidateSqlCache();
|
|
257
289
|
this.havings.push({ sql, boolean });
|
|
258
290
|
return this;
|
|
259
291
|
}
|
|
@@ -261,10 +293,12 @@ export class Builder {
|
|
|
261
293
|
return this.havingRaw(sql, "or");
|
|
262
294
|
}
|
|
263
295
|
limit(count) {
|
|
296
|
+
this.invalidateSqlCache();
|
|
264
297
|
this.limitValue = count;
|
|
265
298
|
return this;
|
|
266
299
|
}
|
|
267
300
|
offset(count) {
|
|
301
|
+
this.invalidateSqlCache();
|
|
268
302
|
this.offsetValue = count;
|
|
269
303
|
return this;
|
|
270
304
|
}
|
|
@@ -273,6 +307,7 @@ export class Builder {
|
|
|
273
307
|
}
|
|
274
308
|
join(table, first, operator, second, type = "INNER") {
|
|
275
309
|
const joinSql = `${type} JOIN ${this.grammar.wrap(table)} ON ${this.grammar.wrap(first)} ${operator} ${this.grammar.wrap(second)}`;
|
|
310
|
+
this.invalidateSqlCache();
|
|
276
311
|
this.joins.push(joinSql);
|
|
277
312
|
return this;
|
|
278
313
|
}
|
|
@@ -283,11 +318,13 @@ export class Builder {
|
|
|
283
318
|
return this.join(table, first, operator, second, "RIGHT");
|
|
284
319
|
}
|
|
285
320
|
crossJoin(table) {
|
|
321
|
+
this.invalidateSqlCache();
|
|
286
322
|
this.joins.push(`CROSS JOIN ${this.grammar.wrap(table)}`);
|
|
287
323
|
return this;
|
|
288
324
|
}
|
|
289
325
|
union(query, all = false) {
|
|
290
326
|
const sql = typeof query === "string" ? query : query.toSql();
|
|
327
|
+
this.invalidateSqlCache();
|
|
291
328
|
this.unions.push({ query: sql, all });
|
|
292
329
|
return this;
|
|
293
330
|
}
|
|
@@ -299,10 +336,12 @@ export class Builder {
|
|
|
299
336
|
return this;
|
|
300
337
|
}
|
|
301
338
|
withoutGlobalScope(scope) {
|
|
339
|
+
this.invalidateSqlCache();
|
|
302
340
|
this.wheres = this.wheres.filter((where) => where.scope !== scope);
|
|
303
341
|
return this;
|
|
304
342
|
}
|
|
305
343
|
withoutGlobalScopes() {
|
|
344
|
+
this.invalidateSqlCache();
|
|
306
345
|
this.wheres = this.wheres.filter((where) => !where.scope);
|
|
307
346
|
return this;
|
|
308
347
|
}
|
|
@@ -407,6 +446,7 @@ export class Builder {
|
|
|
407
446
|
return this.withAggregate(relationName, column, "MAX", alias);
|
|
408
447
|
}
|
|
409
448
|
addSelect(...columns) {
|
|
449
|
+
this.invalidateSqlCache();
|
|
410
450
|
if (this.columns.length === 1 && this.columns[0] === "*") {
|
|
411
451
|
this.columns = [`${this.tableName}.*`];
|
|
412
452
|
}
|
|
@@ -414,15 +454,18 @@ export class Builder {
|
|
|
414
454
|
return this;
|
|
415
455
|
}
|
|
416
456
|
selectRaw(sql) {
|
|
457
|
+
this.invalidateSqlCache();
|
|
417
458
|
this.columns.push(sql);
|
|
418
459
|
return this;
|
|
419
460
|
}
|
|
420
461
|
fromSub(query, as) {
|
|
421
462
|
const sql = typeof query === "string" ? query : query.toSql();
|
|
463
|
+
this.invalidateSqlCache();
|
|
422
464
|
this.fromRaw = `(${sql}) AS ${this.grammar.wrap(as)}`;
|
|
423
465
|
return this;
|
|
424
466
|
}
|
|
425
467
|
updateFrom(table, first, operator, second) {
|
|
468
|
+
this.invalidateSqlCache();
|
|
426
469
|
this.updateJoins.push(`INNER JOIN ${this.grammar.wrap(table)} ON ${this.grammar.wrap(first)} ${operator} ${this.grammar.wrap(second)}`);
|
|
427
470
|
return this;
|
|
428
471
|
}
|
|
@@ -598,6 +641,8 @@ export class Builder {
|
|
|
598
641
|
return column.includes("(") || /\s+as\s+/i.test(column) || /^[0-9]+$/.test(column);
|
|
599
642
|
}
|
|
600
643
|
toSql() {
|
|
644
|
+
if (!this.parameterize && this.sqlCache)
|
|
645
|
+
return this.sqlCache;
|
|
601
646
|
const distinct = this.distinctFlag ? "DISTINCT " : "";
|
|
602
647
|
const from = this.fromRaw || this.grammar.wrap(this.tableName);
|
|
603
648
|
let sql = `SELECT ${distinct}${this.compileColumns()} FROM ${from}`;
|
|
@@ -613,7 +658,10 @@ export class Builder {
|
|
|
613
658
|
for (const union of this.unions) {
|
|
614
659
|
sql += ` UNION${union.all ? " ALL" : ""} ${union.query}`;
|
|
615
660
|
}
|
|
616
|
-
|
|
661
|
+
const compiled = sql.replace(/\s+/g, " ").trim();
|
|
662
|
+
if (!this.parameterize)
|
|
663
|
+
this.sqlCache = compiled;
|
|
664
|
+
return compiled;
|
|
617
665
|
}
|
|
618
666
|
async get() {
|
|
619
667
|
this.bindings = [];
|
|
@@ -636,12 +684,7 @@ export class Builder {
|
|
|
636
684
|
}
|
|
637
685
|
}
|
|
638
686
|
}
|
|
639
|
-
const instance =
|
|
640
|
-
instance.$exists = true;
|
|
641
|
-
instance.$original = { ...row };
|
|
642
|
-
if (typeof instance.setConnection === "function") {
|
|
643
|
-
instance.setConnection(this.connection);
|
|
644
|
-
}
|
|
687
|
+
const instance = this.model.hydrate(row, this.connection);
|
|
645
688
|
if (identityMap) {
|
|
646
689
|
const pk = row[primaryKey];
|
|
647
690
|
if (pk !== null && pk !== undefined) {
|
|
@@ -725,6 +768,7 @@ export class Builder {
|
|
|
725
768
|
query.offsetValue = undefined;
|
|
726
769
|
query.eagerLoads = [];
|
|
727
770
|
query.lockMode = undefined;
|
|
771
|
+
query.invalidateSqlCache();
|
|
728
772
|
const result = await query.first();
|
|
729
773
|
return result ? result[alias] : null;
|
|
730
774
|
}
|
|
@@ -748,6 +792,7 @@ export class Builder {
|
|
|
748
792
|
countQuery.limitValue = undefined;
|
|
749
793
|
countQuery.offsetValue = undefined;
|
|
750
794
|
countQuery.orders = [];
|
|
795
|
+
countQuery.invalidateSqlCache();
|
|
751
796
|
const total = await countQuery.count();
|
|
752
797
|
const data = await this.clone().forPage(page, perPage).get();
|
|
753
798
|
return {
|
|
@@ -1023,6 +1068,7 @@ export class Builder {
|
|
|
1023
1068
|
lockForUpdate() {
|
|
1024
1069
|
const driver = this.connection.getDriverName();
|
|
1025
1070
|
if (driver !== "sqlite") {
|
|
1071
|
+
this.invalidateSqlCache();
|
|
1026
1072
|
this.lockMode = "FOR UPDATE";
|
|
1027
1073
|
}
|
|
1028
1074
|
return this;
|
|
@@ -1030,21 +1076,25 @@ export class Builder {
|
|
|
1030
1076
|
sharedLock() {
|
|
1031
1077
|
const driver = this.connection.getDriverName();
|
|
1032
1078
|
if (driver === "mysql") {
|
|
1079
|
+
this.invalidateSqlCache();
|
|
1033
1080
|
this.lockMode = "LOCK IN SHARE MODE";
|
|
1034
1081
|
}
|
|
1035
1082
|
else if (driver === "postgres") {
|
|
1083
|
+
this.invalidateSqlCache();
|
|
1036
1084
|
this.lockMode = "FOR SHARE";
|
|
1037
1085
|
}
|
|
1038
1086
|
return this;
|
|
1039
1087
|
}
|
|
1040
1088
|
skipLocked() {
|
|
1041
1089
|
if (this.lockMode) {
|
|
1090
|
+
this.invalidateSqlCache();
|
|
1042
1091
|
this.lockMode += " SKIP LOCKED";
|
|
1043
1092
|
}
|
|
1044
1093
|
return this;
|
|
1045
1094
|
}
|
|
1046
1095
|
noWait() {
|
|
1047
1096
|
if (this.lockMode) {
|
|
1097
|
+
this.invalidateSqlCache();
|
|
1048
1098
|
this.lockMode += " NOWAIT";
|
|
1049
1099
|
}
|
|
1050
1100
|
return this;
|
|
@@ -1054,6 +1104,7 @@ export class Builder {
|
|
|
1054
1104
|
value = operator;
|
|
1055
1105
|
operator = "=";
|
|
1056
1106
|
}
|
|
1107
|
+
this.invalidateSqlCache();
|
|
1057
1108
|
this.wheres.push({ type: "date", column: column, operator, value, boolean, scope: undefined, dateType: type });
|
|
1058
1109
|
return this;
|
|
1059
1110
|
}
|
|
@@ -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