@bunnykit/orm 0.1.24 → 0.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -1
- package/dist/bin/bunny.js +41 -0
- package/dist/src/config/BunnyConfig.d.ts +1 -0
- 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 +5 -2
- package/dist/src/index.js +2 -0
- package/dist/src/migration/Migrator.d.ts +30 -6
- package/dist/src/migration/Migrator.js +230 -45
- package/dist/src/schema/Schema.d.ts +24 -0
- package/dist/src/schema/Schema.js +182 -0
- package/dist/src/seeding/Factory.d.ts +19 -0
- package/dist/src/seeding/Factory.js +56 -0
- package/dist/src/seeding/Seeder.d.ts +16 -0
- package/dist/src/seeding/Seeder.js +80 -0
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { existsSync } from "fs";
|
|
2
|
-
import { mkdir, readdir, writeFile } from "fs/promises";
|
|
3
|
+
import { mkdir, readdir, readFile, writeFile } from "fs/promises";
|
|
3
4
|
import { basename, join, relative, resolve } from "path";
|
|
4
5
|
import { Schema } from "../schema/Schema.js";
|
|
5
6
|
import { Builder } from "../query/Builder.js";
|
|
@@ -10,12 +11,14 @@ export class Migrator {
|
|
|
10
11
|
path;
|
|
11
12
|
typesOutDir;
|
|
12
13
|
typeGeneratorOptions;
|
|
14
|
+
options;
|
|
13
15
|
static listeners = new Map();
|
|
14
|
-
constructor(connection, path, typesOutDir, typeGeneratorOptions = {}) {
|
|
16
|
+
constructor(connection, path, typesOutDir, typeGeneratorOptions = {}, options = {}) {
|
|
15
17
|
this.connection = connection;
|
|
16
18
|
this.path = path;
|
|
17
19
|
this.typesOutDir = typesOutDir;
|
|
18
20
|
this.typeGeneratorOptions = typeGeneratorOptions;
|
|
21
|
+
this.options = options;
|
|
19
22
|
Schema.setConnection(connection);
|
|
20
23
|
}
|
|
21
24
|
getPaths() {
|
|
@@ -27,10 +30,79 @@ export class Migrator {
|
|
|
27
30
|
await Schema.create("migrations", (table) => {
|
|
28
31
|
table.increments("id");
|
|
29
32
|
table.string("migration");
|
|
33
|
+
table.string("tenant").nullable().index();
|
|
34
|
+
table.string("checksum").nullable();
|
|
30
35
|
table.integer("batch");
|
|
31
36
|
});
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (!(await Schema.hasColumn("migrations", "tenant"))) {
|
|
40
|
+
await Schema.table("migrations", (table) => {
|
|
41
|
+
table.string("tenant").nullable().index();
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (!(await Schema.hasColumn("migrations", "checksum"))) {
|
|
45
|
+
await Schema.table("migrations", (table) => {
|
|
46
|
+
table.string("checksum").nullable();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
getTenantId() {
|
|
51
|
+
return this.options.tenantId ?? null;
|
|
52
|
+
}
|
|
53
|
+
scopedMigrations() {
|
|
54
|
+
const builder = new Builder(this.connection, "migrations");
|
|
55
|
+
const tenantId = this.getTenantId();
|
|
56
|
+
return tenantId === null ? builder.whereNull("tenant") : builder.where("tenant", tenantId);
|
|
57
|
+
}
|
|
58
|
+
async ensureMigrationLocksTable() {
|
|
59
|
+
if (await Schema.hasTable("migration_locks"))
|
|
60
|
+
return;
|
|
61
|
+
await Schema.create("migration_locks", (table) => {
|
|
62
|
+
table.string("name").primary();
|
|
63
|
+
table.string("owner");
|
|
64
|
+
table.string("created_at");
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
getLockName() {
|
|
68
|
+
const tenantId = this.getTenantId();
|
|
69
|
+
return tenantId === null ? "migrations:default" : `migrations:tenant:${tenantId}`;
|
|
70
|
+
}
|
|
71
|
+
shouldLock() {
|
|
72
|
+
return this.options.lock !== false;
|
|
73
|
+
}
|
|
74
|
+
async acquireLock() {
|
|
75
|
+
if (!this.shouldLock())
|
|
76
|
+
return false;
|
|
77
|
+
await this.ensureMigrationLocksTable();
|
|
78
|
+
const lockName = this.getLockName();
|
|
79
|
+
const timeoutMs = this.options.lockTimeoutMs ?? 30000;
|
|
80
|
+
const owner = `${process.pid}:${Date.now()}:${Math.random().toString(36).slice(2)}`;
|
|
81
|
+
const started = Date.now();
|
|
82
|
+
while (true) {
|
|
83
|
+
try {
|
|
84
|
+
await new Builder(this.connection, "migration_locks").insert({
|
|
85
|
+
name: lockName,
|
|
86
|
+
owner,
|
|
87
|
+
created_at: new Date().toISOString(),
|
|
88
|
+
});
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
if (Date.now() - started >= timeoutMs) {
|
|
93
|
+
throw new Error(`Could not acquire migration lock "${lockName}" within ${timeoutMs}ms.`);
|
|
94
|
+
}
|
|
95
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
96
|
+
}
|
|
32
97
|
}
|
|
33
98
|
}
|
|
99
|
+
async releaseLock() {
|
|
100
|
+
if (!this.shouldLock())
|
|
101
|
+
return;
|
|
102
|
+
await new Builder(this.connection, "migration_locks")
|
|
103
|
+
.where("name", this.getLockName())
|
|
104
|
+
.delete();
|
|
105
|
+
}
|
|
34
106
|
static on(event, listener) {
|
|
35
107
|
const listeners = this.listeners.get(event) || new Set();
|
|
36
108
|
listeners.add(listener);
|
|
@@ -49,7 +121,8 @@ export class Migrator {
|
|
|
49
121
|
}
|
|
50
122
|
}
|
|
51
123
|
async getLastBatchNumber() {
|
|
52
|
-
|
|
124
|
+
await this.ensureMigrationsTable();
|
|
125
|
+
const result = await this.scopedMigrations()
|
|
53
126
|
.select("MAX(batch) as batch")
|
|
54
127
|
.first();
|
|
55
128
|
return result?.batch || 0;
|
|
@@ -68,22 +141,29 @@ export class Migrator {
|
|
|
68
141
|
id: toPosixPath(relative(process.cwd(), fullPath)),
|
|
69
142
|
fileName,
|
|
70
143
|
fullPath,
|
|
144
|
+
checksum: await this.checksumFile(fullPath),
|
|
71
145
|
});
|
|
72
146
|
}
|
|
73
147
|
}
|
|
74
148
|
return files.sort((a, b) => a.fileName.localeCompare(b.fileName) || a.id.localeCompare(b.id));
|
|
75
149
|
}
|
|
150
|
+
async checksumFile(path) {
|
|
151
|
+
const contents = await readFile(path);
|
|
152
|
+
return createHash("sha256").update(contents).digest("hex");
|
|
153
|
+
}
|
|
76
154
|
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();
|
|
155
|
+
await this.ensureMigrationsTable();
|
|
156
|
+
const locked = await this.acquireLock();
|
|
86
157
|
try {
|
|
158
|
+
const ran = await this.getRan();
|
|
159
|
+
const files = await this.getMigrationFiles();
|
|
160
|
+
const pending = files.filter((f) => !ran.has(f.id) && !ran.has(f.fileName));
|
|
161
|
+
if (pending.length === 0) {
|
|
162
|
+
console.log("Nothing to migrate.");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const batch = (await this.getLastBatchNumber()) + 1;
|
|
166
|
+
await this.connection.beginTransaction();
|
|
87
167
|
for (const file of pending) {
|
|
88
168
|
const migration = await this.resolve(file.id);
|
|
89
169
|
console.log(`Migrating: ${file.id}`);
|
|
@@ -91,6 +171,8 @@ export class Migrator {
|
|
|
91
171
|
await migration.up();
|
|
92
172
|
await new Builder(this.connection, "migrations").insert({
|
|
93
173
|
migration: file.id,
|
|
174
|
+
tenant: this.getTenantId(),
|
|
175
|
+
checksum: file.checksum,
|
|
94
176
|
batch,
|
|
95
177
|
});
|
|
96
178
|
await this.emit("migrated", { migration: file.id, batch });
|
|
@@ -103,32 +185,38 @@ export class Migrator {
|
|
|
103
185
|
await this.connection.rollback();
|
|
104
186
|
throw error;
|
|
105
187
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (batch === 0) {
|
|
110
|
-
console.log("Nothing to rollback.");
|
|
111
|
-
return;
|
|
188
|
+
finally {
|
|
189
|
+
if (locked)
|
|
190
|
+
await this.releaseLock();
|
|
112
191
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (records.length === 0) {
|
|
118
|
-
console.log("Nothing to rollback.");
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
await this.connection.beginTransaction();
|
|
192
|
+
}
|
|
193
|
+
async rollback(steps = 1) {
|
|
194
|
+
await this.ensureMigrationsTable();
|
|
195
|
+
const locked = await this.acquireLock();
|
|
122
196
|
try {
|
|
197
|
+
const batches = await this.getRollbackBatches(steps);
|
|
198
|
+
if (batches.length === 0) {
|
|
199
|
+
console.log("Nothing to rollback.");
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const records = (await this.scopedMigrations()
|
|
203
|
+
.whereIn("batch", batches)
|
|
204
|
+
.orderBy("id", "desc")
|
|
205
|
+
.get());
|
|
206
|
+
if (records.length === 0) {
|
|
207
|
+
console.log("Nothing to rollback.");
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
await this.connection.beginTransaction();
|
|
123
211
|
for (const record of records) {
|
|
124
212
|
const migration = await this.resolve(record.migration);
|
|
125
213
|
console.log(`Rolling back: ${record.migration}`);
|
|
126
|
-
await this.emit("rollingBack", { migration: record.migration, batch });
|
|
214
|
+
await this.emit("rollingBack", { migration: record.migration, batch: record.batch });
|
|
127
215
|
await migration.down();
|
|
128
216
|
await new Builder(this.connection, "migrations")
|
|
129
217
|
.where("id", record.id)
|
|
130
218
|
.delete();
|
|
131
|
-
await this.emit("rolledBack", { migration: record.migration, batch });
|
|
219
|
+
await this.emit("rolledBack", { migration: record.migration, batch: record.batch });
|
|
132
220
|
console.log(`Rolled back: ${record.migration}`);
|
|
133
221
|
}
|
|
134
222
|
await this.connection.commit();
|
|
@@ -138,6 +226,40 @@ export class Migrator {
|
|
|
138
226
|
await this.connection.rollback();
|
|
139
227
|
throw error;
|
|
140
228
|
}
|
|
229
|
+
finally {
|
|
230
|
+
if (locked)
|
|
231
|
+
await this.releaseLock();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
async getRollbackBatches(steps) {
|
|
235
|
+
await this.ensureMigrationsTable();
|
|
236
|
+
const rows = await this.scopedMigrations()
|
|
237
|
+
.select("batch")
|
|
238
|
+
.orderBy("batch", "desc")
|
|
239
|
+
.get();
|
|
240
|
+
const batches = [];
|
|
241
|
+
for (const row of rows) {
|
|
242
|
+
const batch = Number(row.batch);
|
|
243
|
+
if (!Number.isFinite(batch) || batches.includes(batch))
|
|
244
|
+
continue;
|
|
245
|
+
batches.push(batch);
|
|
246
|
+
if (batches.length >= steps)
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
return batches;
|
|
250
|
+
}
|
|
251
|
+
async reset() {
|
|
252
|
+
while ((await this.getLastBatchNumber()) > 0) {
|
|
253
|
+
await this.rollback();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
async refresh() {
|
|
257
|
+
await this.reset();
|
|
258
|
+
await this.run();
|
|
259
|
+
}
|
|
260
|
+
async fresh() {
|
|
261
|
+
await this.dropAllTables();
|
|
262
|
+
await this.run();
|
|
141
263
|
}
|
|
142
264
|
async generateTypesIfNeeded() {
|
|
143
265
|
const modelDirectories = normalizePathList(this.typeGeneratorOptions.modelDirectories || this.typeGeneratorOptions.modelDirectory);
|
|
@@ -154,12 +276,21 @@ export class Migrator {
|
|
|
154
276
|
console.log(`Regenerated types in ${label}`);
|
|
155
277
|
}
|
|
156
278
|
async status() {
|
|
157
|
-
|
|
279
|
+
await this.ensureMigrationsTable();
|
|
280
|
+
const ran = await this.getRanRecords();
|
|
158
281
|
const files = await this.getMigrationFiles();
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
282
|
+
const tenant = this.getTenantId();
|
|
283
|
+
return files.map((file) => {
|
|
284
|
+
const record = ran.get(file.id) || ran.get(file.fileName);
|
|
285
|
+
const storedChecksum = record?.checksum ?? null;
|
|
286
|
+
return {
|
|
287
|
+
migration: file.id,
|
|
288
|
+
status: !record ? "Pending" : storedChecksum && storedChecksum !== file.checksum ? "Changed" : "Ran",
|
|
289
|
+
tenant,
|
|
290
|
+
checksum: file.checksum,
|
|
291
|
+
storedChecksum,
|
|
292
|
+
};
|
|
293
|
+
});
|
|
163
294
|
}
|
|
164
295
|
async dumpSchema(path) {
|
|
165
296
|
const sql = await this.getSchemaDumpSql();
|
|
@@ -173,12 +304,21 @@ export class Migrator {
|
|
|
173
304
|
const files = await this.getMigrationFiles();
|
|
174
305
|
await this.ensureMigrationsTable();
|
|
175
306
|
const batch = (await this.getLastBatchNumber()) + 1;
|
|
176
|
-
await
|
|
177
|
-
|
|
178
|
-
await
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
307
|
+
const locked = await this.acquireLock();
|
|
308
|
+
try {
|
|
309
|
+
await this.scopedMigrations().delete();
|
|
310
|
+
for (const file of files) {
|
|
311
|
+
await new Builder(this.connection, "migrations").insert({
|
|
312
|
+
migration: file.id,
|
|
313
|
+
tenant: this.getTenantId(),
|
|
314
|
+
checksum: file.checksum,
|
|
315
|
+
batch,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
finally {
|
|
320
|
+
if (locked)
|
|
321
|
+
await this.releaseLock();
|
|
182
322
|
}
|
|
183
323
|
await this.emit("schemaSquashed", { path, batch });
|
|
184
324
|
return sql;
|
|
@@ -242,6 +382,42 @@ export class Migrator {
|
|
|
242
382
|
}
|
|
243
383
|
return statements.join("\n\n") + "\n";
|
|
244
384
|
}
|
|
385
|
+
async dropAllTables() {
|
|
386
|
+
const driver = this.connection.getDriverName();
|
|
387
|
+
const grammar = this.connection.getGrammar();
|
|
388
|
+
if (driver === "sqlite") {
|
|
389
|
+
await this.connection.run("PRAGMA foreign_keys = OFF");
|
|
390
|
+
try {
|
|
391
|
+
const rows = await this.connection.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'");
|
|
392
|
+
for (const row of rows) {
|
|
393
|
+
await this.connection.run(`DROP TABLE IF EXISTS ${grammar.wrap(String(row.name))}`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
finally {
|
|
397
|
+
await this.connection.run("PRAGMA foreign_keys = ON");
|
|
398
|
+
}
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (driver === "mysql") {
|
|
402
|
+
const tables = await this.connection.query("SHOW TABLES");
|
|
403
|
+
const key = Object.keys(tables[0] ?? {})[0];
|
|
404
|
+
await this.connection.run("SET FOREIGN_KEY_CHECKS = 0");
|
|
405
|
+
try {
|
|
406
|
+
for (const row of tables) {
|
|
407
|
+
await this.connection.run(`DROP TABLE IF EXISTS ${grammar.wrap(String(row[key]))}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
finally {
|
|
411
|
+
await this.connection.run("SET FOREIGN_KEY_CHECKS = 1");
|
|
412
|
+
}
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
const schema = this.connection.getSchema() || "public";
|
|
416
|
+
const tables = await this.connection.query("SELECT table_name FROM information_schema.tables WHERE table_schema = $1 AND table_type = 'BASE TABLE'", [schema]);
|
|
417
|
+
for (const row of tables) {
|
|
418
|
+
await this.connection.run(`DROP TABLE IF EXISTS ${grammar.wrap(`${schema}.${row.table_name}`)} CASCADE`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
245
421
|
async resolve(file) {
|
|
246
422
|
const normalized = toPosixPath(file);
|
|
247
423
|
const candidates = new Set();
|
|
@@ -268,16 +444,25 @@ export class Migrator {
|
|
|
268
444
|
return new MigrationClass();
|
|
269
445
|
}
|
|
270
446
|
async getRan() {
|
|
447
|
+
const results = await this.getRanRecords();
|
|
448
|
+
const ran = new Set();
|
|
449
|
+
for (const migration of results.keys()) {
|
|
450
|
+
ran.add(migration);
|
|
451
|
+
ran.add(basename(migration));
|
|
452
|
+
}
|
|
453
|
+
return ran;
|
|
454
|
+
}
|
|
455
|
+
async getRanRecords() {
|
|
271
456
|
await this.ensureMigrationsTable();
|
|
272
|
-
const results = await
|
|
457
|
+
const results = await this.scopedMigrations()
|
|
273
458
|
.orderBy("id", "asc")
|
|
274
459
|
.get();
|
|
275
|
-
const
|
|
460
|
+
const records = new Map();
|
|
276
461
|
for (const row of results) {
|
|
277
462
|
const migration = toPosixPath(String(row.migration));
|
|
278
|
-
|
|
279
|
-
|
|
463
|
+
records.set(migration, row);
|
|
464
|
+
records.set(basename(migration), row);
|
|
280
465
|
}
|
|
281
|
-
return
|
|
466
|
+
return records;
|
|
282
467
|
}
|
|
283
468
|
}
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { Connection } from "../connection/Connection.js";
|
|
2
2
|
import { Blueprint } from "./Blueprint.js";
|
|
3
|
+
export interface SchemaIndex {
|
|
4
|
+
name: string;
|
|
5
|
+
columns: string[];
|
|
6
|
+
unique: boolean;
|
|
7
|
+
primary?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface SchemaForeignKey {
|
|
10
|
+
name?: string;
|
|
11
|
+
columns: string[];
|
|
12
|
+
references: string[];
|
|
13
|
+
onTable: string;
|
|
14
|
+
onDelete?: string;
|
|
15
|
+
onUpdate?: string;
|
|
16
|
+
}
|
|
3
17
|
export declare class Schema {
|
|
4
18
|
static connection: Connection;
|
|
5
19
|
static setConnection(connection: Connection): void;
|
|
@@ -7,12 +21,22 @@ export declare class Schema {
|
|
|
7
21
|
private static getGrammar;
|
|
8
22
|
static create(table: string, callback: (blueprint: Blueprint) => void): Promise<void>;
|
|
9
23
|
static createIfNotExists(table: string, callback: (blueprint: Blueprint) => void): Promise<void>;
|
|
24
|
+
static createSchema(schema: string): Promise<void>;
|
|
25
|
+
static dropSchema(schema: string, options?: {
|
|
26
|
+
cascade?: boolean;
|
|
27
|
+
}): Promise<void>;
|
|
10
28
|
static table(table: string, callback: (blueprint: Blueprint) => void): Promise<void>;
|
|
11
29
|
static drop(table: string): Promise<void>;
|
|
12
30
|
static dropIfExists(table: string): Promise<void>;
|
|
13
31
|
static rename(from: string, to: string): Promise<void>;
|
|
14
32
|
static hasTable(table: string): Promise<boolean>;
|
|
15
33
|
static hasColumn(table: string, column: string): Promise<boolean>;
|
|
34
|
+
static getIndexes(table: string): Promise<SchemaIndex[]>;
|
|
35
|
+
static hasIndex(table: string, indexOrColumns: string | string[]): Promise<boolean>;
|
|
36
|
+
static getForeignKeys(table: string): Promise<SchemaForeignKey[]>;
|
|
37
|
+
static hasForeignKey(table: string, keyOrColumns: string | string[]): Promise<boolean>;
|
|
38
|
+
private static groupIndexRows;
|
|
39
|
+
private static groupForeignKeyRows;
|
|
16
40
|
static getColumn(table: string, column: string): Promise<{
|
|
17
41
|
name: string;
|
|
18
42
|
type: string;
|
|
@@ -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);
|
|
@@ -174,6 +198,164 @@ export class Schema {
|
|
|
174
198
|
const result = await connection.query(sql, bindings);
|
|
175
199
|
return result.length > 0;
|
|
176
200
|
}
|
|
201
|
+
static async getIndexes(table) {
|
|
202
|
+
const connection = this.getConnection();
|
|
203
|
+
const driver = connection.getDriverName();
|
|
204
|
+
const grammar = this.getGrammar();
|
|
205
|
+
const schema = connection.getSchema() || "public";
|
|
206
|
+
if (driver === "sqlite") {
|
|
207
|
+
const indexes = await connection.query(`PRAGMA index_list(${grammar.wrap(table)})`);
|
|
208
|
+
const results = [];
|
|
209
|
+
for (const index of indexes) {
|
|
210
|
+
const name = String(index.name);
|
|
211
|
+
const columns = await connection.query(`PRAGMA index_info(${grammar.wrap(name)})`);
|
|
212
|
+
results.push({
|
|
213
|
+
name,
|
|
214
|
+
columns: columns.map((row) => String(row.name)),
|
|
215
|
+
unique: Number(index.unique) === 1,
|
|
216
|
+
primary: String(index.origin || "") === "pk",
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
return results;
|
|
220
|
+
}
|
|
221
|
+
if (driver === "mysql") {
|
|
222
|
+
const rows = await connection.query(`SELECT index_name, column_name, non_unique
|
|
223
|
+
FROM information_schema.statistics
|
|
224
|
+
WHERE table_schema = DATABASE() AND table_name = ?
|
|
225
|
+
ORDER BY index_name, seq_in_index`, [table]);
|
|
226
|
+
return this.groupIndexRows(rows, "index_name", "column_name", (row) => Number(row.non_unique) === 0);
|
|
227
|
+
}
|
|
228
|
+
const rows = await connection.query(`SELECT
|
|
229
|
+
i.relname AS index_name,
|
|
230
|
+
a.attname AS column_name,
|
|
231
|
+
ix.indisunique AS is_unique,
|
|
232
|
+
ix.indisprimary AS is_primary,
|
|
233
|
+
k.ordinality
|
|
234
|
+
FROM pg_class t
|
|
235
|
+
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
236
|
+
JOIN pg_index ix ON t.oid = ix.indrelid
|
|
237
|
+
JOIN pg_class i ON i.oid = ix.indexrelid
|
|
238
|
+
JOIN unnest(ix.indkey) WITH ORDINALITY AS k(attnum, ordinality) ON true
|
|
239
|
+
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum
|
|
240
|
+
WHERE n.nspname = $1 AND t.relname = $2
|
|
241
|
+
ORDER BY i.relname, k.ordinality`, [schema, table]);
|
|
242
|
+
return this.groupIndexRows(rows, "index_name", "column_name", (row) => !!row.is_unique, (row) => !!row.is_primary);
|
|
243
|
+
}
|
|
244
|
+
static async hasIndex(table, indexOrColumns) {
|
|
245
|
+
const expectedColumns = Array.isArray(indexOrColumns) ? indexOrColumns : undefined;
|
|
246
|
+
const indexes = await this.getIndexes(table);
|
|
247
|
+
if (!expectedColumns) {
|
|
248
|
+
return indexes.some((index) => index.name === indexOrColumns);
|
|
249
|
+
}
|
|
250
|
+
return indexes.some((index) => (index.columns.length === expectedColumns.length &&
|
|
251
|
+
index.columns.every((column, indexPosition) => column === expectedColumns[indexPosition])));
|
|
252
|
+
}
|
|
253
|
+
static async getForeignKeys(table) {
|
|
254
|
+
const connection = this.getConnection();
|
|
255
|
+
const driver = connection.getDriverName();
|
|
256
|
+
const grammar = this.getGrammar();
|
|
257
|
+
const schema = connection.getSchema() || "public";
|
|
258
|
+
if (driver === "sqlite") {
|
|
259
|
+
const rows = await connection.query(`PRAGMA foreign_key_list(${grammar.wrap(table)})`);
|
|
260
|
+
const grouped = new Map();
|
|
261
|
+
for (const row of rows) {
|
|
262
|
+
const key = String(row.id);
|
|
263
|
+
const fk = grouped.get(key) || {
|
|
264
|
+
name: undefined,
|
|
265
|
+
columns: [],
|
|
266
|
+
references: [],
|
|
267
|
+
onTable: String(row.table),
|
|
268
|
+
onDelete: row.on_delete ? String(row.on_delete).toLowerCase() : undefined,
|
|
269
|
+
onUpdate: row.on_update ? String(row.on_update).toLowerCase() : undefined,
|
|
270
|
+
};
|
|
271
|
+
fk.columns.push(String(row.from));
|
|
272
|
+
fk.references.push(String(row.to));
|
|
273
|
+
grouped.set(key, fk);
|
|
274
|
+
}
|
|
275
|
+
return [...grouped.values()];
|
|
276
|
+
}
|
|
277
|
+
if (driver === "mysql") {
|
|
278
|
+
const rows = await connection.query(`SELECT
|
|
279
|
+
k.constraint_name,
|
|
280
|
+
k.column_name,
|
|
281
|
+
k.referenced_table_name,
|
|
282
|
+
k.referenced_column_name,
|
|
283
|
+
rc.delete_rule,
|
|
284
|
+
rc.update_rule,
|
|
285
|
+
k.ordinal_position
|
|
286
|
+
FROM information_schema.key_column_usage k
|
|
287
|
+
JOIN information_schema.referential_constraints rc
|
|
288
|
+
ON rc.constraint_schema = k.constraint_schema
|
|
289
|
+
AND rc.constraint_name = k.constraint_name
|
|
290
|
+
WHERE k.table_schema = DATABASE()
|
|
291
|
+
AND k.table_name = ?
|
|
292
|
+
AND k.referenced_table_name IS NOT NULL
|
|
293
|
+
ORDER BY k.constraint_name, k.ordinal_position`, [table]);
|
|
294
|
+
return this.groupForeignKeyRows(rows, "constraint_name", "column_name", "referenced_table_name", "referenced_column_name", "delete_rule", "update_rule");
|
|
295
|
+
}
|
|
296
|
+
const rows = await connection.query(`SELECT
|
|
297
|
+
tc.constraint_name,
|
|
298
|
+
kcu.column_name,
|
|
299
|
+
ccu.table_name AS referenced_table_name,
|
|
300
|
+
ccu.column_name AS referenced_column_name,
|
|
301
|
+
rc.delete_rule,
|
|
302
|
+
rc.update_rule,
|
|
303
|
+
kcu.ordinal_position
|
|
304
|
+
FROM information_schema.table_constraints tc
|
|
305
|
+
JOIN information_schema.key_column_usage kcu
|
|
306
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
307
|
+
AND tc.table_schema = kcu.table_schema
|
|
308
|
+
JOIN information_schema.constraint_column_usage ccu
|
|
309
|
+
ON ccu.constraint_name = tc.constraint_name
|
|
310
|
+
AND ccu.table_schema = tc.table_schema
|
|
311
|
+
JOIN information_schema.referential_constraints rc
|
|
312
|
+
ON rc.constraint_name = tc.constraint_name
|
|
313
|
+
AND rc.constraint_schema = tc.table_schema
|
|
314
|
+
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
315
|
+
AND tc.table_schema = $1
|
|
316
|
+
AND tc.table_name = $2
|
|
317
|
+
ORDER BY tc.constraint_name, kcu.ordinal_position`, [schema, table]);
|
|
318
|
+
return this.groupForeignKeyRows(rows, "constraint_name", "column_name", "referenced_table_name", "referenced_column_name", "delete_rule", "update_rule");
|
|
319
|
+
}
|
|
320
|
+
static async hasForeignKey(table, keyOrColumns) {
|
|
321
|
+
const expectedColumns = Array.isArray(keyOrColumns) ? keyOrColumns : undefined;
|
|
322
|
+
const foreignKeys = await this.getForeignKeys(table);
|
|
323
|
+
if (!expectedColumns) {
|
|
324
|
+
return foreignKeys.some((fk) => fk.name === keyOrColumns);
|
|
325
|
+
}
|
|
326
|
+
return foreignKeys.some((fk) => (fk.columns.length === expectedColumns.length &&
|
|
327
|
+
fk.columns.every((column, indexPosition) => column === expectedColumns[indexPosition])));
|
|
328
|
+
}
|
|
329
|
+
static groupIndexRows(rows, nameKey, columnKey, unique, primary = () => false) {
|
|
330
|
+
const grouped = new Map();
|
|
331
|
+
for (const row of rows) {
|
|
332
|
+
const name = String(row[nameKey]);
|
|
333
|
+
const index = grouped.get(name) || { name, columns: [], unique: unique(row), primary: primary(row) };
|
|
334
|
+
index.columns.push(String(row[columnKey]));
|
|
335
|
+
index.unique = index.unique || unique(row);
|
|
336
|
+
index.primary = index.primary || primary(row);
|
|
337
|
+
grouped.set(name, index);
|
|
338
|
+
}
|
|
339
|
+
return [...grouped.values()];
|
|
340
|
+
}
|
|
341
|
+
static groupForeignKeyRows(rows, nameKey, columnKey, tableKey, referenceKey, deleteKey, updateKey) {
|
|
342
|
+
const grouped = new Map();
|
|
343
|
+
for (const row of rows) {
|
|
344
|
+
const name = String(row[nameKey]);
|
|
345
|
+
const fk = grouped.get(name) || {
|
|
346
|
+
name,
|
|
347
|
+
columns: [],
|
|
348
|
+
references: [],
|
|
349
|
+
onTable: String(row[tableKey]),
|
|
350
|
+
onDelete: row[deleteKey] ? String(row[deleteKey]).toLowerCase() : undefined,
|
|
351
|
+
onUpdate: row[updateKey] ? String(row[updateKey]).toLowerCase() : undefined,
|
|
352
|
+
};
|
|
353
|
+
fk.columns.push(String(row[columnKey]));
|
|
354
|
+
fk.references.push(String(row[referenceKey]));
|
|
355
|
+
grouped.set(name, fk);
|
|
356
|
+
}
|
|
357
|
+
return [...grouped.values()];
|
|
358
|
+
}
|
|
177
359
|
static async getColumn(table, column) {
|
|
178
360
|
const connection = this.getConnection();
|
|
179
361
|
const driver = connection.getDriverName();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ModelAttributeInput, ModelConstructor } from "../model/Model.js";
|
|
2
|
+
import { Model } from "../model/Model.js";
|
|
3
|
+
export type FactoryDefinition<T extends Model> = (sequence: number) => ModelAttributeInput<T>;
|
|
4
|
+
export type FactoryState<T extends Model> = ModelAttributeInput<T> | ((attributes: ModelAttributeInput<T>, sequence: number) => ModelAttributeInput<T>);
|
|
5
|
+
export declare class Factory<T extends Model> {
|
|
6
|
+
private model;
|
|
7
|
+
private definition;
|
|
8
|
+
private amount;
|
|
9
|
+
private states;
|
|
10
|
+
constructor(model: ModelConstructor<T>, definition: FactoryDefinition<T>);
|
|
11
|
+
count(amount: number): Factory<T>;
|
|
12
|
+
state(state: FactoryState<T>): Factory<T>;
|
|
13
|
+
make(overrides?: ModelAttributeInput<T>): T | T[];
|
|
14
|
+
create(overrides?: ModelAttributeInput<T>): Promise<T | T[]>;
|
|
15
|
+
raw(overrides?: ModelAttributeInput<T>): ModelAttributeInput<T> | ModelAttributeInput<T>[];
|
|
16
|
+
private attributesFor;
|
|
17
|
+
private clone;
|
|
18
|
+
}
|
|
19
|
+
export declare function factory<T extends Model>(model: ModelConstructor<T>, definition: FactoryDefinition<T>): Factory<T>;
|