@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.
@@ -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
- const result = await new Builder(this.connection, "migrations")
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
- const ran = await this.getRan();
78
- const files = await this.getMigrationFiles();
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
- async rollback() {
108
- const batch = await this.getLastBatchNumber();
109
- if (batch === 0) {
110
- console.log("Nothing to rollback.");
111
- return;
188
+ finally {
189
+ if (locked)
190
+ await this.releaseLock();
112
191
  }
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();
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
- const ran = await this.getRan();
279
+ await this.ensureMigrationsTable();
280
+ const ran = await this.getRanRecords();
158
281
  const files = await this.getMigrationFiles();
159
- return files.map((file) => ({
160
- migration: file.id,
161
- status: ran.has(file.id) || ran.has(file.fileName) ? "Ran" : "Pending",
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 new Builder(this.connection, "migrations").delete();
177
- for (const file of files) {
178
- await new Builder(this.connection, "migrations").insert({
179
- migration: file.id,
180
- batch,
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 new Builder(this.connection, "migrations")
457
+ const results = await this.scopedMigrations()
273
458
  .orderBy("id", "asc")
274
459
  .get();
275
- const ran = new Set();
460
+ const records = new Map();
276
461
  for (const row of results) {
277
462
  const migration = toPosixPath(String(row.migration));
278
- ran.add(migration);
279
- ran.add(basename(migration));
463
+ records.set(migration, row);
464
+ records.set(basename(migration), row);
280
465
  }
281
- return ran;
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>;