@bunnykit/orm 0.1.24 → 0.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
- private quoteIdentifier;
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 (!this.schema || this.driverName === "sqlite" || table.includes("."))
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 type TenantResolution = {
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
- return cached;
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
- return this.tenantCache.get(tenantId);
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
- this.connections.delete(context.connectionName);
200
- await connection?.close();
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;
@@ -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
- const result = await new Builder(this.connection, "migrations")
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
- 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();
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
- const batch = await this.getLastBatchNumber();
109
- if (batch === 0) {
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 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
- });
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 new Builder(this.connection, "migrations")
362
+ const results = await this.scopedMigrations()
273
363
  .orderBy("id", "asc")
274
364
  .get();
275
365
  const ran = new Set();
@@ -7,6 +7,10 @@ export declare class Schema {
7
7
  private static getGrammar;
8
8
  static create(table: string, callback: (blueprint: Blueprint) => void): Promise<void>;
9
9
  static createIfNotExists(table: string, callback: (blueprint: Blueprint) => void): Promise<void>;
10
+ static createSchema(schema: string): Promise<void>;
11
+ static dropSchema(schema: string, options?: {
12
+ cascade?: boolean;
13
+ }): Promise<void>;
10
14
  static table(table: string, callback: (blueprint: Blueprint) => void): Promise<void>;
11
15
  static drop(table: string): Promise<void>;
12
16
  static dropIfExists(table: string): Promise<void>;
@@ -1,3 +1,4 @@
1
+ import { Connection } from "../connection/Connection.js";
1
2
  import { Blueprint } from "./Blueprint.js";
2
3
  import { SQLiteGrammar } from "./grammars/SQLiteGrammar.js";
3
4
  import { MySqlGrammar } from "./grammars/MySqlGrammar.js";
@@ -61,6 +62,29 @@ export class Schema {
61
62
  await this.getConnection().run(fkSql);
62
63
  }
63
64
  }
65
+ static async createSchema(schema) {
66
+ Connection.assertSafeIdentifier(schema, "schema name");
67
+ const connection = this.getConnection();
68
+ const driver = connection.getDriverName();
69
+ if (driver === "sqlite") {
70
+ throw new Error("Schema creation is not supported for SQLite connections.");
71
+ }
72
+ const grammar = this.getGrammar();
73
+ const keyword = driver === "mysql" ? "DATABASE" : "SCHEMA";
74
+ await connection.run(`CREATE ${keyword} IF NOT EXISTS ${grammar.wrap(schema)}`);
75
+ }
76
+ static async dropSchema(schema, options = {}) {
77
+ Connection.assertSafeIdentifier(schema, "schema name");
78
+ const connection = this.getConnection();
79
+ const driver = connection.getDriverName();
80
+ if (driver === "sqlite") {
81
+ throw new Error("Schema dropping is not supported for SQLite connections.");
82
+ }
83
+ const grammar = this.getGrammar();
84
+ const keyword = driver === "mysql" ? "DATABASE" : "SCHEMA";
85
+ const cascade = driver === "postgres" && options.cascade ? " CASCADE" : "";
86
+ await connection.run(`DROP ${keyword} IF EXISTS ${grammar.wrap(schema)}${cascade}`);
87
+ }
64
88
  static async table(table, callback) {
65
89
  const blueprint = new Blueprint(table);
66
90
  callback(blueprint);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bunnykit/orm",
3
- "version": "0.1.24",
3
+ "version": "0.1.25",
4
4
  "description": "An Eloquent-inspired ORM for Bun's native SQL client supporting SQLite, MySQL, and PostgreSQL.",
5
5
  "license": "MIT",
6
6
  "packageManager": "bun@1.3.12",