@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
package/README.md
CHANGED
|
@@ -23,7 +23,8 @@ An **Eloquent-inspired ORM** built specifically for [Bun](https://bun.sh)'s nati
|
|
|
23
23
|
- 🧬 **Eloquent-style Models** — Property attributes, defaults, casts, dirty tracking, soft deletes, scopes, find-or-fail, first-or-create
|
|
24
24
|
- 🔗 **Relations** — Standard, many-to-many, polymorphic, through, one-of-many, and relation queries
|
|
25
25
|
- 👁️ **Observers** — Lifecycle hooks (`creating`, `created`, `updating`, `updated`, etc.)
|
|
26
|
-
- 🚀 **Migrations & CLI** — Create, run, and
|
|
26
|
+
- 🚀 **Migrations & CLI** — Create, run, reset, refresh, and inspect migrations from the command line
|
|
27
|
+
- 🌱 **Seeders & Factories** — Run all seeders or target one seeder by name/file, plus lightweight model factories
|
|
27
28
|
- 💬 **REPL** — Inspect models and run queries interactively with `bunny repl`
|
|
28
29
|
- ⚡ **Streaming** — `chunk`, `cursor`, `each`, and `lazy` for memory-efficient large dataset processing
|
|
29
30
|
|
|
@@ -71,6 +72,7 @@ export default {
|
|
|
71
72
|
// }),
|
|
72
73
|
// listTenants: async () => await getAllTenantIds(),
|
|
73
74
|
// },
|
|
75
|
+
seedersPath: "./database/seeders",
|
|
74
76
|
modelsPath: ["./src/models", "./src/admin/models"],
|
|
75
77
|
// Optional legacy type output directory
|
|
76
78
|
// typesOutDir: "./src/generated/model-types",
|
|
@@ -87,6 +89,7 @@ Or use environment variables:
|
|
|
87
89
|
```bash
|
|
88
90
|
export DATABASE_URL="sqlite://app.db"
|
|
89
91
|
export MIGRATIONS_PATH="./database/migrations,./database/tenant-migrations"
|
|
92
|
+
export SEEDERS_PATH="./database/seeders"
|
|
90
93
|
export MODELS_PATH="./src/models,./src/admin/models"
|
|
91
94
|
export TYPES_OUT_DIR="./src/generated/model-types"
|
|
92
95
|
```
|
|
@@ -1108,6 +1111,84 @@ ObserverRegistry.register(User, {
|
|
|
1108
1111
|
|
|
1109
1112
|
---
|
|
1110
1113
|
|
|
1114
|
+
## Seeders and Factories
|
|
1115
|
+
|
|
1116
|
+
Set `seedersPath` in `bunny.config.ts` to define the default directory used by `db:seed`:
|
|
1117
|
+
|
|
1118
|
+
```ts
|
|
1119
|
+
export default {
|
|
1120
|
+
connection: { url: "sqlite://app.db" },
|
|
1121
|
+
seedersPath: "./database/seeders",
|
|
1122
|
+
};
|
|
1123
|
+
```
|
|
1124
|
+
|
|
1125
|
+
`seedersPath` can also be an array:
|
|
1126
|
+
|
|
1127
|
+
```ts
|
|
1128
|
+
export default {
|
|
1129
|
+
connection: { url: "sqlite://app.db" },
|
|
1130
|
+
seedersPath: ["./database/seeders", "./modules/demo/seeders"],
|
|
1131
|
+
};
|
|
1132
|
+
```
|
|
1133
|
+
|
|
1134
|
+
Create a seeder by extending `Seeder`:
|
|
1135
|
+
|
|
1136
|
+
```ts
|
|
1137
|
+
import { Seeder } from "@bunnykit/orm";
|
|
1138
|
+
import { User } from "../models/User";
|
|
1139
|
+
|
|
1140
|
+
export default class UserSeeder extends Seeder {
|
|
1141
|
+
async run(): Promise<void> {
|
|
1142
|
+
await User.create({ name: "Ada Lovelace", email: "ada@example.test" });
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
```
|
|
1146
|
+
|
|
1147
|
+
Run every seeder in `seedersPath`:
|
|
1148
|
+
|
|
1149
|
+
```bash
|
|
1150
|
+
bun run bunny db:seed
|
|
1151
|
+
```
|
|
1152
|
+
|
|
1153
|
+
Run one seeder by class/file name from `seedersPath`:
|
|
1154
|
+
|
|
1155
|
+
```bash
|
|
1156
|
+
bun run bunny db:seed UserSeeder
|
|
1157
|
+
```
|
|
1158
|
+
|
|
1159
|
+
Run one seeder by direct file path:
|
|
1160
|
+
|
|
1161
|
+
```bash
|
|
1162
|
+
bun run bunny db:seed ./database/seeders/UserSeeder.ts
|
|
1163
|
+
```
|
|
1164
|
+
|
|
1165
|
+
Programmatic seeding is available through `SeederRunner`:
|
|
1166
|
+
|
|
1167
|
+
```ts
|
|
1168
|
+
import { SeederRunner } from "@bunnykit/orm";
|
|
1169
|
+
|
|
1170
|
+
await new SeederRunner(connection).runTarget("UserSeeder", "./database/seeders");
|
|
1171
|
+
await new SeederRunner(connection).runFile("./database/seeders/UserSeeder.ts");
|
|
1172
|
+
```
|
|
1173
|
+
|
|
1174
|
+
Factories can create raw attributes, unsaved models, or persisted records:
|
|
1175
|
+
|
|
1176
|
+
```ts
|
|
1177
|
+
import { factory } from "@bunnykit/orm";
|
|
1178
|
+
import { User } from "../models/User";
|
|
1179
|
+
|
|
1180
|
+
const users = factory(User, (sequence) => ({
|
|
1181
|
+
name: `User ${sequence}`,
|
|
1182
|
+
email: `user${sequence}@example.test`,
|
|
1183
|
+
}));
|
|
1184
|
+
|
|
1185
|
+
const attributes = users.raw();
|
|
1186
|
+
const model = users.make();
|
|
1187
|
+
const created = await users.count(3).state({ role: "admin" }).create();
|
|
1188
|
+
```
|
|
1189
|
+
|
|
1190
|
+
---
|
|
1191
|
+
|
|
1111
1192
|
## Migrations
|
|
1112
1193
|
|
|
1113
1194
|
### CLI Commands
|
|
@@ -1125,9 +1206,27 @@ bun run bunny migrate
|
|
|
1125
1206
|
# Rollback the last batch
|
|
1126
1207
|
bun run bunny migrate:rollback
|
|
1127
1208
|
|
|
1209
|
+
# Rollback all migrations
|
|
1210
|
+
bun run bunny migrate:reset
|
|
1211
|
+
|
|
1212
|
+
# Reset and rerun migrations
|
|
1213
|
+
bun run bunny migrate:refresh
|
|
1214
|
+
|
|
1215
|
+
# Drop all tables and rerun migrations
|
|
1216
|
+
bun run bunny migrate:fresh
|
|
1217
|
+
|
|
1128
1218
|
# Show migration status
|
|
1129
1219
|
bun run bunny migrate:status
|
|
1130
1220
|
|
|
1221
|
+
# Run all seeders in seedersPath
|
|
1222
|
+
bun run bunny db:seed
|
|
1223
|
+
|
|
1224
|
+
# Run one seeder by name from seedersPath
|
|
1225
|
+
bun run bunny db:seed UserSeeder
|
|
1226
|
+
|
|
1227
|
+
# Run one seeder by direct file path
|
|
1228
|
+
bun run bunny db:seed ./database/seeders/UserSeeder.ts
|
|
1229
|
+
|
|
1131
1230
|
# Dump the current database schema
|
|
1132
1231
|
bun run bunny schema:dump ./database/schema.sql
|
|
1133
1232
|
|
package/dist/bin/bunny.js
CHANGED
|
@@ -5,6 +5,7 @@ import { TenantContext } from "../src/connection/TenantContext.js";
|
|
|
5
5
|
import { configureBunny } from "../src/config/BunnyConfig.js";
|
|
6
6
|
import { Migrator } from "../src/migration/Migrator.js";
|
|
7
7
|
import { MigrationCreator } from "../src/migration/MigrationCreator.js";
|
|
8
|
+
import { SeederRunner } from "../src/seeding/Seeder.js";
|
|
8
9
|
import { TypeGenerator } from "../src/typegen/TypeGenerator.js";
|
|
9
10
|
import { existsSync } from "fs";
|
|
10
11
|
import { mkdir, rm, writeFile } from "fs/promises";
|
|
@@ -64,6 +65,18 @@ async function runMigratorCommand(command, migrator, statusLabel) {
|
|
|
64
65
|
await migrator.rollback();
|
|
65
66
|
return;
|
|
66
67
|
}
|
|
68
|
+
if (command === "migrate:reset") {
|
|
69
|
+
await migrator.reset();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (command === "migrate:refresh") {
|
|
73
|
+
await migrator.refresh();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (command === "migrate:fresh") {
|
|
77
|
+
await migrator.fresh();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
67
80
|
const status = await migrator.status();
|
|
68
81
|
if (statusLabel) {
|
|
69
82
|
console.log(statusLabel);
|
|
@@ -338,6 +351,7 @@ async function loadConfig(allowFallback = false) {
|
|
|
338
351
|
return {
|
|
339
352
|
connection: { url },
|
|
340
353
|
migrationsPath: parseEnvPathSetting(process.env.MIGRATIONS_PATH) || "./database/migrations",
|
|
354
|
+
seedersPath: parseEnvPathSetting(process.env.SEEDERS_PATH),
|
|
341
355
|
modelsPath: parseEnvPathSetting(process.env.MODELS_PATH),
|
|
342
356
|
};
|
|
343
357
|
}
|
|
@@ -354,6 +368,7 @@ async function loadConfig(allowFallback = false) {
|
|
|
354
368
|
filename: process.env.DB_DATABASE,
|
|
355
369
|
},
|
|
356
370
|
migrationsPath: parseEnvPathSetting(process.env.MIGRATIONS_PATH) || "./database/migrations",
|
|
371
|
+
seedersPath: parseEnvPathSetting(process.env.SEEDERS_PATH),
|
|
357
372
|
modelsPath: parseEnvPathSetting(process.env.MODELS_PATH),
|
|
358
373
|
};
|
|
359
374
|
}
|
|
@@ -361,6 +376,7 @@ async function loadConfig(allowFallback = false) {
|
|
|
361
376
|
return {
|
|
362
377
|
connection: { url: "sqlite://:memory:" },
|
|
363
378
|
migrationsPath: parseEnvPathSetting(process.env.MIGRATIONS_PATH) || "./database/migrations",
|
|
379
|
+
seedersPath: parseEnvPathSetting(process.env.SEEDERS_PATH),
|
|
364
380
|
modelsPath: parseEnvPathSetting(process.env.MODELS_PATH),
|
|
365
381
|
};
|
|
366
382
|
}
|
|
@@ -432,9 +448,29 @@ async function main() {
|
|
|
432
448
|
else if (command === "migrate:rollback") {
|
|
433
449
|
await runConfiguredMigrationCommand(command, config, connection, parseMigrationTarget(args.slice(1)));
|
|
434
450
|
}
|
|
451
|
+
else if (command === "migrate:reset") {
|
|
452
|
+
await runConfiguredMigrationCommand(command, config, connection, parseMigrationTarget(args.slice(1)));
|
|
453
|
+
}
|
|
454
|
+
else if (command === "migrate:refresh") {
|
|
455
|
+
await runConfiguredMigrationCommand(command, config, connection, parseMigrationTarget(args.slice(1)));
|
|
456
|
+
}
|
|
457
|
+
else if (command === "migrate:fresh") {
|
|
458
|
+
await runConfiguredMigrationCommand(command, config, connection, parseMigrationTarget(args.slice(1)));
|
|
459
|
+
}
|
|
435
460
|
else if (command === "migrate:status") {
|
|
436
461
|
await runConfiguredMigrationCommand(command, config, connection, parseMigrationTarget(args.slice(1)));
|
|
437
462
|
}
|
|
463
|
+
else if (command === "db:seed") {
|
|
464
|
+
const target = args[1];
|
|
465
|
+
const seederPath = config.seedersPath || "./database/seeders";
|
|
466
|
+
const runner = new SeederRunner(connection);
|
|
467
|
+
if (target) {
|
|
468
|
+
await runner.runTarget(target, seederPath);
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
await runner.runPaths(seederPath);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
438
474
|
else {
|
|
439
475
|
console.log("Usage:");
|
|
440
476
|
console.log(" bun run bunny migrate Run landlord migrations, then all tenant migrations when configured");
|
|
@@ -443,7 +479,12 @@ async function main() {
|
|
|
443
479
|
console.log(" bun run bunny migrate --tenant <id> Run one tenant's migrations only");
|
|
444
480
|
console.log(" bun run bunny migrate:make <name> [dir] Create a new migration");
|
|
445
481
|
console.log(" bun run bunny migrate:rollback Rollback the last batch");
|
|
482
|
+
console.log(" bun run bunny migrate:reset Rollback all migrations");
|
|
483
|
+
console.log(" bun run bunny migrate:refresh Reset and rerun migrations");
|
|
484
|
+
console.log(" bun run bunny migrate:fresh Drop all tables and rerun migrations");
|
|
446
485
|
console.log(" bun run bunny migrate:status Show migration status");
|
|
486
|
+
console.log(" bun run bunny db:seed Run seeders from seedersPath");
|
|
487
|
+
console.log(" bun run bunny db:seed <seeder> Run one seeder by file path or name");
|
|
447
488
|
console.log(" bun run bunny schema:dump [path] Dump the current database schema");
|
|
448
489
|
console.log(" bun run bunny schema:squash [path] Dump schema and mark configured migrations as ran");
|
|
449
490
|
console.log(" bun run bunny types:generate [dir] Generate model type declarations from DB schema");
|
|
@@ -5,6 +5,7 @@ import type { ConnectionConfig } from "../types/index.js";
|
|
|
5
5
|
export interface BunnyConfig {
|
|
6
6
|
connection: ConnectionConfig;
|
|
7
7
|
migrationsPath?: string | string[];
|
|
8
|
+
seedersPath?: string | string[];
|
|
8
9
|
migrations?: {
|
|
9
10
|
landlord?: string | string[];
|
|
10
11
|
tenant?: string | string[];
|
|
@@ -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,10 @@ 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";
|
|
30
|
+
export { Seeder, SeederRunner } from "./seeding/Seeder.js";
|
|
31
|
+
export { Factory, factory } from "./seeding/Factory.js";
|
|
32
|
+
export type { FactoryDefinition, FactoryState } from "./seeding/Factory.js";
|
package/dist/src/index.js
CHANGED
|
@@ -21,3 +21,5 @@ export { Migrator } from "./migration/Migrator.js";
|
|
|
21
21
|
export { MigrationCreator } from "./migration/MigrationCreator.js";
|
|
22
22
|
export { TypeGenerator } from "./typegen/TypeGenerator.js";
|
|
23
23
|
export { TypeMapper } from "./typegen/TypeMapper.js";
|
|
24
|
+
export { Seeder, SeederRunner } from "./seeding/Seeder.js";
|
|
25
|
+
export { Factory, factory } from "./seeding/Factory.js";
|
|
@@ -1,5 +1,17 @@
|
|
|
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
|
+
checksum?: string;
|
|
8
|
+
storedChecksum?: string | null;
|
|
9
|
+
}
|
|
10
|
+
export interface MigratorOptions {
|
|
11
|
+
tenantId?: string | null;
|
|
12
|
+
lock?: boolean;
|
|
13
|
+
lockTimeoutMs?: number;
|
|
14
|
+
}
|
|
3
15
|
export type MigrationEvent = "migrating" | "migrated" | "rollingBack" | "rolledBack" | "schemaDumped" | "schemaSquashed";
|
|
4
16
|
export interface MigrationEventPayload {
|
|
5
17
|
migration?: string;
|
|
@@ -12,25 +24,37 @@ export declare class Migrator {
|
|
|
12
24
|
private path;
|
|
13
25
|
private typesOutDir?;
|
|
14
26
|
private typeGeneratorOptions;
|
|
27
|
+
private options;
|
|
15
28
|
private static listeners;
|
|
16
|
-
constructor(connection: Connection, path: string | string[], typesOutDir?: string | undefined, typeGeneratorOptions?: Omit<TypeGeneratorOptions, "outDir"
|
|
29
|
+
constructor(connection: Connection, path: string | string[], typesOutDir?: string | undefined, typeGeneratorOptions?: Omit<TypeGeneratorOptions, "outDir">, options?: MigratorOptions);
|
|
17
30
|
private getPaths;
|
|
18
31
|
private ensureMigrationsTable;
|
|
32
|
+
private getTenantId;
|
|
33
|
+
private scopedMigrations;
|
|
34
|
+
private ensureMigrationLocksTable;
|
|
35
|
+
private getLockName;
|
|
36
|
+
private shouldLock;
|
|
37
|
+
private acquireLock;
|
|
38
|
+
private releaseLock;
|
|
19
39
|
static on(event: MigrationEvent, listener: MigrationEventListener): () => void;
|
|
20
40
|
static clearListeners(event?: MigrationEvent): void;
|
|
21
41
|
private emit;
|
|
22
42
|
private getLastBatchNumber;
|
|
23
43
|
private getMigrationFiles;
|
|
44
|
+
private checksumFile;
|
|
24
45
|
run(): Promise<void>;
|
|
25
|
-
rollback(): Promise<void>;
|
|
46
|
+
rollback(steps?: number): Promise<void>;
|
|
47
|
+
private getRollbackBatches;
|
|
48
|
+
reset(): Promise<void>;
|
|
49
|
+
refresh(): Promise<void>;
|
|
50
|
+
fresh(): Promise<void>;
|
|
26
51
|
private generateTypesIfNeeded;
|
|
27
|
-
status(): Promise<
|
|
28
|
-
migration: string;
|
|
29
|
-
status: string;
|
|
30
|
-
}[]>;
|
|
52
|
+
status(): Promise<MigrationStatusRow[]>;
|
|
31
53
|
dumpSchema(path: string): Promise<string>;
|
|
32
54
|
squash(path: string): Promise<string>;
|
|
33
55
|
private getSchemaDumpSql;
|
|
56
|
+
private dropAllTables;
|
|
34
57
|
private resolve;
|
|
35
58
|
private getRan;
|
|
59
|
+
private getRanRecords;
|
|
36
60
|
}
|