@bunnykit/orm 0.1.13 → 0.1.15
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 +84 -0
- package/dist/bin/bunny.js +15 -1
- package/dist/src/connection/Connection.js +0 -1
- package/dist/src/connection/ConnectionManager.d.ts +13 -0
- package/dist/src/connection/ConnectionManager.js +81 -0
- package/dist/src/index.d.ts +2 -1
- package/dist/src/migration/Migrator.d.ts +14 -0
- package/dist/src/migration/Migrator.js +104 -1
- package/dist/src/model/Model.d.ts +5 -1
- package/dist/src/model/MorphRelations.d.ts +7 -1
- package/dist/src/model/MorphRelations.js +40 -27
- package/dist/src/query/Builder.d.ts +4 -3
- package/dist/src/query/Builder.js +30 -8
- package/dist/src/schema/Blueprint.d.ts +16 -0
- package/dist/src/schema/Blueprint.js +85 -3
- package/dist/src/schema/Schema.js +18 -0
- package/dist/src/schema/grammars/Grammar.d.ts +1 -0
- package/dist/src/schema/grammars/Grammar.js +2 -1
- package/dist/src/schema/grammars/MySqlGrammar.d.ts +1 -0
- package/dist/src/schema/grammars/MySqlGrammar.js +3 -0
- package/dist/src/schema/grammars/PostgresGrammar.d.ts +1 -0
- package/dist/src/schema/grammars/PostgresGrammar.js +10 -0
- package/dist/src/schema/grammars/SQLiteGrammar.d.ts +1 -0
- package/dist/src/schema/grammars/SQLiteGrammar.js +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -354,6 +354,8 @@ await Schema.create("products", (table) => {
|
|
|
354
354
|
| `jsonb(name)` | JSONB (Postgres) |
|
|
355
355
|
| `binary(name)` | BLOB / BYTEA |
|
|
356
356
|
| `uuid(name)` | UUID |
|
|
357
|
+
| `foreignId(name)` | Unsigned big integer foreign key |
|
|
358
|
+
| `foreignUuid(name)` | UUID foreign key |
|
|
357
359
|
| `enum(name, values)` | ENUM |
|
|
358
360
|
|
|
359
361
|
### Column Modifiers
|
|
@@ -376,6 +378,11 @@ await Schema.table("users", (table) => {
|
|
|
376
378
|
table.timestamp("last_login").nullable();
|
|
377
379
|
});
|
|
378
380
|
|
|
381
|
+
// Change columns on MySQL/PostgreSQL
|
|
382
|
+
await Schema.table("users", (table) => {
|
|
383
|
+
table.string("name", 150).nullable().change();
|
|
384
|
+
});
|
|
385
|
+
|
|
379
386
|
// Rename
|
|
380
387
|
await Schema.rename("users", "customers");
|
|
381
388
|
|
|
@@ -413,6 +420,32 @@ await Schema.create("posts", (table) => {
|
|
|
413
420
|
});
|
|
414
421
|
```
|
|
415
422
|
|
|
423
|
+
Shortcut form:
|
|
424
|
+
|
|
425
|
+
```ts
|
|
426
|
+
await Schema.create("posts", (table) => {
|
|
427
|
+
table.increments("id");
|
|
428
|
+
table.foreignId("user_id").constrained().cascadeOnDelete();
|
|
429
|
+
table.string("title");
|
|
430
|
+
table.timestamps();
|
|
431
|
+
});
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
Polymorphic column shortcuts:
|
|
435
|
+
|
|
436
|
+
```ts
|
|
437
|
+
await Schema.create("comments", (table) => {
|
|
438
|
+
table.increments("id");
|
|
439
|
+
table.uuidMorphs("commentable"); // commentable_type + UUID commentable_id + index
|
|
440
|
+
table.text("body");
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
await Schema.create("activity", (table) => {
|
|
444
|
+
table.increments("id");
|
|
445
|
+
table.nullableMorphs("subject"); // nullable subject_type + subject_id + index
|
|
446
|
+
});
|
|
447
|
+
```
|
|
448
|
+
|
|
416
449
|
---
|
|
417
450
|
|
|
418
451
|
## Query Builder
|
|
@@ -996,6 +1029,20 @@ class Video extends Model {
|
|
|
996
1029
|
}
|
|
997
1030
|
```
|
|
998
1031
|
|
|
1032
|
+
`morphTo` relations can be eager loaded:
|
|
1033
|
+
|
|
1034
|
+
```ts
|
|
1035
|
+
const comments = await Comment.with("commentable").get();
|
|
1036
|
+
|
|
1037
|
+
comments[0].getRelation("commentable"); // Post | Video | null
|
|
1038
|
+
```
|
|
1039
|
+
|
|
1040
|
+
Relation names are inferred for `with(...)` when your model methods return relation objects, while raw strings still work for dynamic relation names:
|
|
1041
|
+
|
|
1042
|
+
```ts
|
|
1043
|
+
await Post.with("comments").get();
|
|
1044
|
+
```
|
|
1045
|
+
|
|
999
1046
|
### Many-to-Many Polymorphic
|
|
1000
1047
|
|
|
1001
1048
|
```ts
|
|
@@ -1080,6 +1127,12 @@ bun run bunny migrate:rollback
|
|
|
1080
1127
|
|
|
1081
1128
|
# Show migration status
|
|
1082
1129
|
bun run bunny migrate:status
|
|
1130
|
+
|
|
1131
|
+
# Dump the current database schema
|
|
1132
|
+
bun run bunny schema:dump ./database/schema.sql
|
|
1133
|
+
|
|
1134
|
+
# Dump schema and mark configured migrations as ran
|
|
1135
|
+
bun run bunny schema:squash ./database/schema.sql
|
|
1083
1136
|
```
|
|
1084
1137
|
|
|
1085
1138
|
### Migration File Structure
|
|
@@ -1105,6 +1158,37 @@ export default class CreateUsersTable extends Migration {
|
|
|
1105
1158
|
|
|
1106
1159
|
Migrations are tracked in a `migrations` table (auto-created on first run).
|
|
1107
1160
|
|
|
1161
|
+
### Migration Events
|
|
1162
|
+
|
|
1163
|
+
Listen to migration lifecycle events when running migrations programmatically:
|
|
1164
|
+
|
|
1165
|
+
```ts
|
|
1166
|
+
import { Migrator } from "@bunnykit/orm";
|
|
1167
|
+
|
|
1168
|
+
Migrator.on("migrating", ({ migration }) => {
|
|
1169
|
+
console.log(`Starting ${migration}`);
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
Migrator.on("migrated", ({ migration }) => {
|
|
1173
|
+
console.log(`Finished ${migration}`);
|
|
1174
|
+
});
|
|
1175
|
+
```
|
|
1176
|
+
|
|
1177
|
+
Available events: `migrating`, `migrated`, `rollingBack`, `rolledBack`, `schemaDumped`, and `schemaSquashed`.
|
|
1178
|
+
|
|
1179
|
+
### Schema Dumps
|
|
1180
|
+
|
|
1181
|
+
Programmatic schema dumps are available through the migrator:
|
|
1182
|
+
|
|
1183
|
+
```ts
|
|
1184
|
+
const migrator = new Migrator(connection, "./database/migrations");
|
|
1185
|
+
|
|
1186
|
+
await migrator.dumpSchema("./database/schema.sql");
|
|
1187
|
+
await migrator.squash("./database/schema.sql");
|
|
1188
|
+
```
|
|
1189
|
+
|
|
1190
|
+
`squash()` writes the schema dump and marks the configured migration files as already ran in the migrations table.
|
|
1191
|
+
|
|
1108
1192
|
### Auto Type Generation
|
|
1109
1193
|
|
|
1110
1194
|
If you set `typesOutDir` in your config, types are **automatically regenerated** after every `migrate` and `migrate:rollback`.
|
package/dist/bin/bunny.js
CHANGED
|
@@ -414,7 +414,19 @@ async function main() {
|
|
|
414
414
|
}
|
|
415
415
|
const config = await loadConfig();
|
|
416
416
|
const { connection } = configureBunny(config);
|
|
417
|
-
if (command === "
|
|
417
|
+
if (command === "schema:dump" || command === "schema:squash") {
|
|
418
|
+
const outputPath = args[1] || "./database/schema.sql";
|
|
419
|
+
const migrator = new Migrator(connection, getDefaultMigrationsPath(config), config.typesOutDir, createTypeGeneratorOptions(config));
|
|
420
|
+
if (command === "schema:dump") {
|
|
421
|
+
await migrator.dumpSchema(outputPath);
|
|
422
|
+
console.log(`Schema dumped to ${outputPath}`);
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
await migrator.squash(outputPath);
|
|
426
|
+
console.log(`Schema squashed to ${outputPath}`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
else if (command === "migrate") {
|
|
418
430
|
await runConfiguredMigrationCommand(command, config, connection, parseMigrationTarget(args.slice(1)));
|
|
419
431
|
}
|
|
420
432
|
else if (command === "migrate:rollback") {
|
|
@@ -432,6 +444,8 @@ async function main() {
|
|
|
432
444
|
console.log(" bun run bunny migrate:make <name> [dir] Create a new migration");
|
|
433
445
|
console.log(" bun run bunny migrate:rollback Rollback the last batch");
|
|
434
446
|
console.log(" bun run bunny migrate:status Show migration status");
|
|
447
|
+
console.log(" bun run bunny schema:dump [path] Dump the current database schema");
|
|
448
|
+
console.log(" bun run bunny schema:squash [path] Dump schema and mark configured migrations as ran");
|
|
435
449
|
console.log(" bun run bunny types:generate [dir] Generate model type declarations from DB schema");
|
|
436
450
|
console.log(" bun run bunny repl Start a Bunny REPL with Model, Schema, and db loaded");
|
|
437
451
|
console.log(" Falls back to in-memory SQLite when no config is present");
|
|
@@ -21,15 +21,28 @@ export type TenantResolution = {
|
|
|
21
21
|
setting?: string;
|
|
22
22
|
};
|
|
23
23
|
export type TenantResolver = (tenantId: string) => TenantResolution | Promise<TenantResolution>;
|
|
24
|
+
export interface PoolConfig {
|
|
25
|
+
maxConnections?: number;
|
|
26
|
+
minConnections?: number;
|
|
27
|
+
idleTimeout?: number;
|
|
28
|
+
}
|
|
24
29
|
export declare class ConnectionManager {
|
|
25
30
|
private static defaultConnection?;
|
|
26
31
|
private static connections;
|
|
32
|
+
private static pools;
|
|
33
|
+
private static poolConfigs;
|
|
27
34
|
private static tenantResolver?;
|
|
28
35
|
private static tenantCache;
|
|
29
36
|
static setDefault(connection: Connection): void;
|
|
30
37
|
static getDefault(): Connection | undefined;
|
|
38
|
+
static setPoolConfig(name: string, config: PoolConfig): void;
|
|
39
|
+
static getPoolConfig(name: string): PoolConfig | undefined;
|
|
40
|
+
private static getPooledConnection;
|
|
41
|
+
private static releasePooledConnection;
|
|
31
42
|
static add(name: string, connection: Connection | ConnectionConfig): Connection;
|
|
32
43
|
static get(name: string): Connection | undefined;
|
|
44
|
+
static getPooled(name: string, config?: ConnectionConfig): Promise<Connection>;
|
|
45
|
+
static release(name: string, connection: Connection): void;
|
|
33
46
|
static require(name: string): Connection;
|
|
34
47
|
static setTenantResolver(resolver: TenantResolver): void;
|
|
35
48
|
static resolveTenant(tenantId: string): Promise<ActiveTenantContext>;
|
|
@@ -2,6 +2,8 @@ import { Connection } from "./Connection.js";
|
|
|
2
2
|
export class ConnectionManager {
|
|
3
3
|
static defaultConnection;
|
|
4
4
|
static connections = new Map();
|
|
5
|
+
static pools = new Map();
|
|
6
|
+
static poolConfigs = new Map();
|
|
5
7
|
static tenantResolver;
|
|
6
8
|
static tenantCache = new Map();
|
|
7
9
|
static setDefault(connection) {
|
|
@@ -10,6 +12,67 @@ export class ConnectionManager {
|
|
|
10
12
|
static getDefault() {
|
|
11
13
|
return this.defaultConnection;
|
|
12
14
|
}
|
|
15
|
+
static setPoolConfig(name, config) {
|
|
16
|
+
this.poolConfigs.set(name, { maxConnections: 10, minConnections: 1, idleTimeout: 30000, ...config });
|
|
17
|
+
}
|
|
18
|
+
static getPoolConfig(name) {
|
|
19
|
+
return this.poolConfigs.get(name);
|
|
20
|
+
}
|
|
21
|
+
static async getPooledConnection(name, config) {
|
|
22
|
+
const poolConfig = this.poolConfigs.get(name) || { maxConnections: 10, minConnections: 1, idleTimeout: 30000 };
|
|
23
|
+
let pool = this.pools.get(name);
|
|
24
|
+
if (!pool) {
|
|
25
|
+
pool = [];
|
|
26
|
+
this.pools.set(name, pool);
|
|
27
|
+
}
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
const idleTimeout = poolConfig.idleTimeout || 30000;
|
|
30
|
+
while (pool.length > 0) {
|
|
31
|
+
const idx = pool.findIndex((c) => !c.inUse && (now - c.lastUsed) < idleTimeout);
|
|
32
|
+
if (idx === -1)
|
|
33
|
+
break;
|
|
34
|
+
const pooled = pool[idx];
|
|
35
|
+
pool.splice(idx, 1);
|
|
36
|
+
try {
|
|
37
|
+
pooled.connection.query("SELECT 1").catch(() => null);
|
|
38
|
+
pooled.inUse = true;
|
|
39
|
+
return pooled.connection;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
await pooled.connection.close().catch(() => null);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (pool.length < (poolConfig.maxConnections || 10)) {
|
|
46
|
+
const connection = new Connection(config);
|
|
47
|
+
pool.push({ connection, lastUsed: Date.now(), inUse: true });
|
|
48
|
+
return connection;
|
|
49
|
+
}
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const checkInterval = setInterval(() => {
|
|
52
|
+
const available = pool.find((c) => !c.inUse);
|
|
53
|
+
if (available) {
|
|
54
|
+
clearInterval(checkInterval);
|
|
55
|
+
available.inUse = true;
|
|
56
|
+
available.lastUsed = Date.now();
|
|
57
|
+
resolve(available.connection);
|
|
58
|
+
}
|
|
59
|
+
}, 50);
|
|
60
|
+
setTimeout(() => {
|
|
61
|
+
clearInterval(checkInterval);
|
|
62
|
+
reject(new Error(`Connection pool exhausted for "${name}"`));
|
|
63
|
+
}, 30000);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
static releasePooledConnection(name, connection) {
|
|
67
|
+
const pool = this.pools.get(name);
|
|
68
|
+
if (!pool)
|
|
69
|
+
return;
|
|
70
|
+
const pooled = pool.find((p) => p.connection === connection);
|
|
71
|
+
if (pooled) {
|
|
72
|
+
pooled.inUse = false;
|
|
73
|
+
pooled.lastUsed = Date.now();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
13
76
|
static add(name, connection) {
|
|
14
77
|
const resolved = connection instanceof Connection ? connection : new Connection(connection);
|
|
15
78
|
this.connections.set(name, resolved);
|
|
@@ -18,6 +81,18 @@ export class ConnectionManager {
|
|
|
18
81
|
static get(name) {
|
|
19
82
|
return this.connections.get(name);
|
|
20
83
|
}
|
|
84
|
+
static async getPooled(name, config) {
|
|
85
|
+
if (config) {
|
|
86
|
+
return this.getPooledConnection(name, config);
|
|
87
|
+
}
|
|
88
|
+
const existing = this.connections.get(name);
|
|
89
|
+
if (existing)
|
|
90
|
+
return existing;
|
|
91
|
+
throw new Error(`No connection registered for "${name}". Use add() first or provide config.`);
|
|
92
|
+
}
|
|
93
|
+
static release(name, connection) {
|
|
94
|
+
this.releasePooledConnection(name, connection);
|
|
95
|
+
}
|
|
21
96
|
static require(name) {
|
|
22
97
|
const connection = this.get(name);
|
|
23
98
|
if (!connection) {
|
|
@@ -93,9 +168,15 @@ export class ConnectionManager {
|
|
|
93
168
|
}
|
|
94
169
|
static async closeAll() {
|
|
95
170
|
const connections = new Set(this.connections.values());
|
|
171
|
+
for (const pool of this.pools.values()) {
|
|
172
|
+
for (const { connection } of pool) {
|
|
173
|
+
connections.add(connection);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
96
176
|
if (this.defaultConnection)
|
|
97
177
|
connections.add(this.defaultConnection);
|
|
98
178
|
this.connections.clear();
|
|
179
|
+
this.pools.clear();
|
|
99
180
|
this.tenantCache.clear();
|
|
100
181
|
for (const connection of connections) {
|
|
101
182
|
await connection.close();
|
package/dist/src/index.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export { MySqlGrammar } from "./schema/grammars/MySqlGrammar.js";
|
|
|
14
14
|
export { PostgresGrammar } from "./schema/grammars/PostgresGrammar.js";
|
|
15
15
|
export { Builder } from "./query/Builder.js";
|
|
16
16
|
export { Model, HasMany, BelongsTo, HasOne, HasManyThrough, HasOneThrough } from "./model/Model.js";
|
|
17
|
-
export type { ModelAttributeInput, ModelAttributes, ModelColumn, ModelColumnValue, ModelConstructor, GlobalScope, CastDefinition, CastsAttributes, } from "./model/Model.js";
|
|
17
|
+
export type { ModelAttributeInput, ModelAttributes, ModelColumn, ModelColumnValue, ModelConstructor, ModelRelationName, GlobalScope, CastDefinition, CastsAttributes, } from "./model/Model.js";
|
|
18
18
|
export { ModelNotFoundError } from "./model/ModelNotFoundError.js";
|
|
19
19
|
export { ObserverRegistry, type ObserverContract } from "./model/Observer.js";
|
|
20
20
|
export { MorphMap } from "./model/MorphMap.js";
|
|
@@ -22,6 +22,7 @@ export { MorphTo, MorphOne, MorphMany, MorphToMany } from "./model/MorphRelation
|
|
|
22
22
|
export { BelongsToMany } from "./model/BelongsToMany.js";
|
|
23
23
|
export { Migration } from "./migration/Migration.js";
|
|
24
24
|
export { Migrator } from "./migration/Migrator.js";
|
|
25
|
+
export type { MigrationEvent, MigrationEventListener, MigrationEventPayload } from "./migration/Migrator.js";
|
|
25
26
|
export { MigrationCreator } from "./migration/MigrationCreator.js";
|
|
26
27
|
export { TypeGenerator } from "./typegen/TypeGenerator.js";
|
|
27
28
|
export { TypeMapper } from "./typegen/TypeMapper.js";
|
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
import { Connection } from "../connection/Connection.js";
|
|
2
2
|
import type { TypeGeneratorOptions } from "../typegen/TypeGenerator.js";
|
|
3
|
+
export type MigrationEvent = "migrating" | "migrated" | "rollingBack" | "rolledBack" | "schemaDumped" | "schemaSquashed";
|
|
4
|
+
export interface MigrationEventPayload {
|
|
5
|
+
migration?: string;
|
|
6
|
+
batch?: number;
|
|
7
|
+
path?: string;
|
|
8
|
+
}
|
|
9
|
+
export type MigrationEventListener = (payload: MigrationEventPayload) => void | Promise<void>;
|
|
3
10
|
export declare class Migrator {
|
|
4
11
|
private connection;
|
|
5
12
|
private path;
|
|
6
13
|
private typesOutDir?;
|
|
7
14
|
private typeGeneratorOptions;
|
|
15
|
+
private static listeners;
|
|
8
16
|
constructor(connection: Connection, path: string | string[], typesOutDir?: string | undefined, typeGeneratorOptions?: Omit<TypeGeneratorOptions, "outDir">);
|
|
9
17
|
private getPaths;
|
|
10
18
|
private ensureMigrationsTable;
|
|
19
|
+
static on(event: MigrationEvent, listener: MigrationEventListener): () => void;
|
|
20
|
+
static clearListeners(event?: MigrationEvent): void;
|
|
21
|
+
private emit;
|
|
11
22
|
private getLastBatchNumber;
|
|
12
23
|
private getMigrationFiles;
|
|
13
24
|
run(): Promise<void>;
|
|
@@ -17,6 +28,9 @@ export declare class Migrator {
|
|
|
17
28
|
migration: string;
|
|
18
29
|
status: string;
|
|
19
30
|
}[]>;
|
|
31
|
+
dumpSchema(path: string): Promise<string>;
|
|
32
|
+
squash(path: string): Promise<string>;
|
|
33
|
+
private getSchemaDumpSql;
|
|
20
34
|
private resolve;
|
|
21
35
|
private getRan;
|
|
22
36
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync } from "fs";
|
|
2
|
-
import { readdir } from "fs/promises";
|
|
2
|
+
import { mkdir, readdir, writeFile } from "fs/promises";
|
|
3
3
|
import { basename, join, relative, resolve } from "path";
|
|
4
4
|
import { Schema } from "../schema/Schema.js";
|
|
5
5
|
import { Builder } from "../query/Builder.js";
|
|
@@ -10,6 +10,7 @@ export class Migrator {
|
|
|
10
10
|
path;
|
|
11
11
|
typesOutDir;
|
|
12
12
|
typeGeneratorOptions;
|
|
13
|
+
static listeners = new Map();
|
|
13
14
|
constructor(connection, path, typesOutDir, typeGeneratorOptions = {}) {
|
|
14
15
|
this.connection = connection;
|
|
15
16
|
this.path = path;
|
|
@@ -30,6 +31,23 @@ export class Migrator {
|
|
|
30
31
|
});
|
|
31
32
|
}
|
|
32
33
|
}
|
|
34
|
+
static on(event, listener) {
|
|
35
|
+
const listeners = this.listeners.get(event) || new Set();
|
|
36
|
+
listeners.add(listener);
|
|
37
|
+
this.listeners.set(event, listeners);
|
|
38
|
+
return () => listeners.delete(listener);
|
|
39
|
+
}
|
|
40
|
+
static clearListeners(event) {
|
|
41
|
+
if (event)
|
|
42
|
+
this.listeners.delete(event);
|
|
43
|
+
else
|
|
44
|
+
this.listeners.clear();
|
|
45
|
+
}
|
|
46
|
+
async emit(event, payload) {
|
|
47
|
+
for (const listener of Migrator.listeners.get(event) || []) {
|
|
48
|
+
await listener(payload);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
33
51
|
async getLastBatchNumber() {
|
|
34
52
|
const result = await new Builder(this.connection, "migrations")
|
|
35
53
|
.select("MAX(batch) as batch")
|
|
@@ -69,11 +87,13 @@ export class Migrator {
|
|
|
69
87
|
for (const file of pending) {
|
|
70
88
|
const migration = await this.resolve(file.id);
|
|
71
89
|
console.log(`Migrating: ${file.id}`);
|
|
90
|
+
await this.emit("migrating", { migration: file.id, batch });
|
|
72
91
|
await migration.up();
|
|
73
92
|
await new Builder(this.connection, "migrations").insert({
|
|
74
93
|
migration: file.id,
|
|
75
94
|
batch,
|
|
76
95
|
});
|
|
96
|
+
await this.emit("migrated", { migration: file.id, batch });
|
|
77
97
|
console.log(`Migrated: ${file.id}`);
|
|
78
98
|
}
|
|
79
99
|
await this.connection.commit();
|
|
@@ -103,10 +123,12 @@ export class Migrator {
|
|
|
103
123
|
for (const record of records) {
|
|
104
124
|
const migration = await this.resolve(record.migration);
|
|
105
125
|
console.log(`Rolling back: ${record.migration}`);
|
|
126
|
+
await this.emit("rollingBack", { migration: record.migration, batch });
|
|
106
127
|
await migration.down();
|
|
107
128
|
await new Builder(this.connection, "migrations")
|
|
108
129
|
.where("id", record.id)
|
|
109
130
|
.delete();
|
|
131
|
+
await this.emit("rolledBack", { migration: record.migration, batch });
|
|
110
132
|
console.log(`Rolled back: ${record.migration}`);
|
|
111
133
|
}
|
|
112
134
|
await this.connection.commit();
|
|
@@ -139,6 +161,87 @@ export class Migrator {
|
|
|
139
161
|
status: ran.has(file.id) || ran.has(file.fileName) ? "Ran" : "Pending",
|
|
140
162
|
}));
|
|
141
163
|
}
|
|
164
|
+
async dumpSchema(path) {
|
|
165
|
+
const sql = await this.getSchemaDumpSql();
|
|
166
|
+
await mkdir(resolve(path, ".."), { recursive: true });
|
|
167
|
+
await writeFile(path, sql, "utf-8");
|
|
168
|
+
await this.emit("schemaDumped", { path });
|
|
169
|
+
return sql;
|
|
170
|
+
}
|
|
171
|
+
async squash(path) {
|
|
172
|
+
const sql = await this.dumpSchema(path);
|
|
173
|
+
const files = await this.getMigrationFiles();
|
|
174
|
+
await this.ensureMigrationsTable();
|
|
175
|
+
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
|
+
});
|
|
182
|
+
}
|
|
183
|
+
await this.emit("schemaSquashed", { path, batch });
|
|
184
|
+
return sql;
|
|
185
|
+
}
|
|
186
|
+
async getSchemaDumpSql() {
|
|
187
|
+
const driver = this.connection.getDriverName();
|
|
188
|
+
if (driver === "sqlite") {
|
|
189
|
+
const rows = await this.connection.query("SELECT sql FROM sqlite_master WHERE sql IS NOT NULL AND type IN ('table', 'index', 'trigger', 'view') AND name NOT LIKE 'sqlite_%' ORDER BY type = 'table' DESC, name");
|
|
190
|
+
return rows.map((row) => `${String(row.sql).trim()};`).join("\n\n") + "\n";
|
|
191
|
+
}
|
|
192
|
+
if (driver === "mysql") {
|
|
193
|
+
const tables = await this.connection.query("SHOW TABLES");
|
|
194
|
+
const key = Object.keys(tables[0] ?? {})[0];
|
|
195
|
+
const statements = [];
|
|
196
|
+
for (const row of tables) {
|
|
197
|
+
const table = row[key];
|
|
198
|
+
const createRows = await this.connection.query(`SHOW CREATE TABLE ${table}`);
|
|
199
|
+
statements.push(`${createRows[0]["Create Table"]};`);
|
|
200
|
+
}
|
|
201
|
+
return statements.join("\n\n") + "\n";
|
|
202
|
+
}
|
|
203
|
+
const schema = this.connection.getSchema() || "public";
|
|
204
|
+
const tables = await this.connection.query(`SELECT table_name FROM information_schema.tables WHERE table_schema = '${schema}' AND table_type = 'BASE TABLE' ORDER BY table_name`);
|
|
205
|
+
const statements = [];
|
|
206
|
+
for (const tableRow of tables) {
|
|
207
|
+
const table = tableRow.table_name;
|
|
208
|
+
const columns = await this.connection.query(`SELECT column_name, data_type, is_nullable, column_default, character_maximum_length, numeric_precision, numeric_scale
|
|
209
|
+
FROM information_schema.columns
|
|
210
|
+
WHERE table_schema = '${schema}' AND table_name = '${table}'
|
|
211
|
+
ORDER BY ordinal_position`);
|
|
212
|
+
const primaryKeys = await this.connection.query(`SELECT kcu.column_name
|
|
213
|
+
FROM information_schema.table_constraints tc
|
|
214
|
+
JOIN information_schema.key_column_usage kcu
|
|
215
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
216
|
+
AND tc.table_schema = kcu.table_schema
|
|
217
|
+
AND tc.table_name = kcu.table_name
|
|
218
|
+
WHERE tc.table_schema = '${schema}'
|
|
219
|
+
AND tc.table_name = '${table}'
|
|
220
|
+
AND tc.constraint_type = 'PRIMARY KEY'
|
|
221
|
+
ORDER BY kcu.ordinal_position`);
|
|
222
|
+
const pkColumns = primaryKeys.map((row) => row.column_name);
|
|
223
|
+
const columnSql = columns.map((column) => {
|
|
224
|
+
let type = String(column.data_type).toUpperCase();
|
|
225
|
+
if ((type === "CHARACTER VARYING" || type === "CHARACTER") && column.character_maximum_length) {
|
|
226
|
+
type = `${type}(${column.character_maximum_length})`;
|
|
227
|
+
}
|
|
228
|
+
else if ((type === "NUMERIC" || type === "DECIMAL") && column.numeric_precision) {
|
|
229
|
+
type = `${type}(${column.numeric_precision}${column.numeric_scale ? `, ${column.numeric_scale}` : ""})`;
|
|
230
|
+
}
|
|
231
|
+
let sql = ` "${column.column_name}" ${type}`;
|
|
232
|
+
if (column.is_nullable === "NO")
|
|
233
|
+
sql += " NOT NULL";
|
|
234
|
+
if (column.column_default !== null && column.column_default !== undefined)
|
|
235
|
+
sql += ` DEFAULT ${column.column_default}`;
|
|
236
|
+
return sql;
|
|
237
|
+
});
|
|
238
|
+
if (pkColumns.length > 0) {
|
|
239
|
+
columnSql.push(` PRIMARY KEY (${pkColumns.map((column) => `"${column}"`).join(", ")})`);
|
|
240
|
+
}
|
|
241
|
+
statements.push(`CREATE TABLE "${schema}"."${table}" (\n${columnSql.join(",\n")}\n);`);
|
|
242
|
+
}
|
|
243
|
+
return statements.join("\n\n") + "\n";
|
|
244
|
+
}
|
|
142
245
|
async resolve(file) {
|
|
143
246
|
const normalized = toPosixPath(file);
|
|
144
247
|
const candidates = new Set();
|
|
@@ -13,6 +13,10 @@ export type ModelAttributes<T> = T extends {
|
|
|
13
13
|
export type ModelColumn<T> = LiteralUnion<Extract<keyof ModelAttributes<T>, string>>;
|
|
14
14
|
export type ModelColumnValue<T, K> = K extends keyof ModelAttributes<T> ? ModelAttributes<T>[K] : any;
|
|
15
15
|
export type ModelAttributeInput<T> = Partial<ModelAttributes<T>> & Record<string, any>;
|
|
16
|
+
export type ModelRelationValue = Relation<any> | MorphTo<any> | MorphOne<any> | MorphMany<any> | MorphToMany<any> | BelongsToMany<any>;
|
|
17
|
+
export type ModelRelationName<T> = LiteralUnion<Extract<{
|
|
18
|
+
[K in keyof T]-?: T[K] extends (...args: any[]) => ModelRelationValue ? K : never;
|
|
19
|
+
}[keyof T], string>>;
|
|
16
20
|
export type CastDefinition = string | CastsAttributes | (new (...args: any[]) => CastsAttributes);
|
|
17
21
|
export interface CastsAttributes {
|
|
18
22
|
get(model: Model, key: string, value: any, attributes: Record<string, any>): any;
|
|
@@ -160,7 +164,7 @@ export declare class Model<T extends Record<string, any> = Record<string, any>>
|
|
|
160
164
|
static inRandomOrder<M extends ModelConstructor>(this: M): Builder<InstanceType<M>>;
|
|
161
165
|
static lockForUpdate<M extends ModelConstructor>(this: M): Builder<InstanceType<M>>;
|
|
162
166
|
static sharedLock<M extends ModelConstructor>(this: M): Builder<InstanceType<M>>;
|
|
163
|
-
static with<M extends ModelConstructor>(this: M, ...relations:
|
|
167
|
+
static with<M extends ModelConstructor>(this: M, ...relations: ModelRelationName<InstanceType<M>>[]): Builder<InstanceType<M>>;
|
|
164
168
|
static withTrashed<M extends ModelConstructor>(this: M): Builder<InstanceType<M>>;
|
|
165
169
|
static onlyTrashed<M extends ModelConstructor>(this: M): Builder<InstanceType<M>>;
|
|
166
170
|
static withoutGlobalScope<M extends ModelConstructor>(this: M, scope: string): Builder<InstanceType<M>>;
|
|
@@ -6,12 +6,18 @@ export declare class MorphTo<T extends Model = Model> {
|
|
|
6
6
|
protected typeColumn: string;
|
|
7
7
|
protected idColumn: string;
|
|
8
8
|
protected typeMap?: Record<string, ModelConstructor>;
|
|
9
|
+
protected eagerModels: Model[];
|
|
9
10
|
constructor(parent: Model, name: string, typeMap?: Record<string, ModelConstructor>);
|
|
10
11
|
getResults(): Promise<T | null>;
|
|
12
|
+
private resolveRelated;
|
|
11
13
|
private resolveAndFind;
|
|
14
|
+
private throwMissingMorph;
|
|
12
15
|
addEagerConstraints(models: Model[]): void;
|
|
13
16
|
getEager(): Promise<any[]>;
|
|
14
|
-
match(models: Model[],
|
|
17
|
+
match(models: Model[], results: Array<{
|
|
18
|
+
__morphType: string;
|
|
19
|
+
model: Model;
|
|
20
|
+
}>, relationName: string): void;
|
|
15
21
|
}
|
|
16
22
|
export declare class MorphOne<T extends Model = Model> {
|
|
17
23
|
protected builder: Builder<T>;
|
|
@@ -6,6 +6,7 @@ export class MorphTo {
|
|
|
6
6
|
typeColumn;
|
|
7
7
|
idColumn;
|
|
8
8
|
typeMap;
|
|
9
|
+
eagerModels = [];
|
|
9
10
|
constructor(parent, name, typeMap) {
|
|
10
11
|
this.parent = parent;
|
|
11
12
|
this.name = name;
|
|
@@ -20,7 +21,7 @@ export class MorphTo {
|
|
|
20
21
|
return null;
|
|
21
22
|
return this.resolveAndFind(type, id);
|
|
22
23
|
}
|
|
23
|
-
|
|
24
|
+
resolveRelated(type) {
|
|
24
25
|
let Related;
|
|
25
26
|
if (this.typeMap) {
|
|
26
27
|
Related = this.typeMap[type];
|
|
@@ -28,43 +29,55 @@ export class MorphTo {
|
|
|
28
29
|
if (!Related) {
|
|
29
30
|
Related = MorphMap.get(type);
|
|
30
31
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
return Related;
|
|
33
|
+
}
|
|
34
|
+
async resolveAndFind(type, id) {
|
|
35
|
+
const Related = this.resolveRelated(type);
|
|
36
|
+
if (!Related)
|
|
37
|
+
this.throwMissingMorph(type);
|
|
34
38
|
return Related.on(this.parent.getConnection()).find(id);
|
|
35
39
|
}
|
|
40
|
+
throwMissingMorph(type) {
|
|
41
|
+
throw new Error(`No morph mapping found for type: ${type}. Register it with MorphMap.register() or pass a typeMap.`);
|
|
42
|
+
}
|
|
36
43
|
addEagerConstraints(models) {
|
|
37
|
-
|
|
44
|
+
this.eagerModels = models;
|
|
38
45
|
}
|
|
39
46
|
async getEager() {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
// Group models by type
|
|
44
|
-
const typeGroups = {};
|
|
45
|
-
for (const model of models) {
|
|
47
|
+
const results = [];
|
|
48
|
+
const groups = {};
|
|
49
|
+
for (const model of this.eagerModels) {
|
|
46
50
|
const type = model.getAttribute(this.typeColumn);
|
|
47
51
|
if (!type)
|
|
48
52
|
continue;
|
|
49
|
-
if (!
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
if (!groups[type])
|
|
54
|
+
groups[type] = [];
|
|
55
|
+
groups[type].push(model);
|
|
52
56
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const ids = groupModels.map((m) => m.getAttribute(this.idColumn)).filter((id) => id !== null && id !== undefined);
|
|
56
|
-
if (ids.length === 0)
|
|
57
|
-
continue;
|
|
58
|
-
let Related;
|
|
59
|
-
if (this.typeMap)
|
|
60
|
-
Related = this.typeMap[type];
|
|
61
|
-
if (!Related)
|
|
62
|
-
Related = MorphMap.get(type);
|
|
57
|
+
for (const [type, models] of Object.entries(groups)) {
|
|
58
|
+
const Related = this.resolveRelated(type);
|
|
63
59
|
if (!Related)
|
|
60
|
+
this.throwMissingMorph(type);
|
|
61
|
+
const ids = [...new Set(models.map((model) => model.getAttribute(this.idColumn)).filter((id) => id !== null && id !== undefined))];
|
|
62
|
+
if (ids.length === 0)
|
|
64
63
|
continue;
|
|
65
|
-
const relatedModels = Related.whereIn(Related.primaryKey, ids).get();
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
const relatedModels = await Related.on(models[0].getConnection()).whereIn(Related.primaryKey, ids).get();
|
|
65
|
+
for (const model of relatedModels) {
|
|
66
|
+
results.push({ __morphType: type, model });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return results;
|
|
70
|
+
}
|
|
71
|
+
match(models, results, relationName) {
|
|
72
|
+
const dictionary = {};
|
|
73
|
+
for (const result of results) {
|
|
74
|
+
const key = result.model.getAttribute(result.model.constructor.primaryKey);
|
|
75
|
+
dictionary[`${result.__morphType}:${String(key)}`] = result.model;
|
|
76
|
+
}
|
|
77
|
+
for (const model of models) {
|
|
78
|
+
const type = model.getAttribute(this.typeColumn);
|
|
79
|
+
const id = model.getAttribute(this.idColumn);
|
|
80
|
+
model.setRelation(relationName, dictionary[`${type}:${String(id)}`] || null);
|
|
68
81
|
}
|
|
69
82
|
}
|
|
70
83
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Connection } from "../connection/Connection.js";
|
|
2
2
|
import type { WhereClause, OrderClause, HavingClause, UnionClause } from "../types/index.js";
|
|
3
|
-
import type { ModelAttributeInput, ModelColumn, ModelColumnValue, ModelConstructor } from "../model/Model.js";
|
|
3
|
+
import type { ModelAttributeInput, ModelColumn, ModelColumnValue, ModelConstructor, ModelRelationName } from "../model/Model.js";
|
|
4
4
|
type RelationConstraint = (query: Builder<any>) => void | Builder<any>;
|
|
5
5
|
export interface Paginator<T> {
|
|
6
6
|
data: T[];
|
|
@@ -30,6 +30,7 @@ export declare class Builder<T = Record<string, any>> {
|
|
|
30
30
|
unions: UnionClause[];
|
|
31
31
|
fromRaw?: string;
|
|
32
32
|
updateJoins: string[];
|
|
33
|
+
bindings: any[];
|
|
33
34
|
constructor(connection: Connection, table: string);
|
|
34
35
|
private get grammar();
|
|
35
36
|
setModel(model: ModelConstructor): this;
|
|
@@ -102,7 +103,7 @@ export declare class Builder<T = Record<string, any>> {
|
|
|
102
103
|
crossJoin(table: string): this;
|
|
103
104
|
union(query: Builder<T> | string, all?: boolean): this;
|
|
104
105
|
unionAll(query: Builder<T> | string): this;
|
|
105
|
-
with(...relations:
|
|
106
|
+
with(...relations: ModelRelationName<T>[]): this;
|
|
106
107
|
withoutGlobalScope(scope: string): this;
|
|
107
108
|
withoutGlobalScopes(): this;
|
|
108
109
|
withTrashed(): this;
|
|
@@ -157,7 +158,7 @@ export declare class Builder<T = Record<string, any>> {
|
|
|
157
158
|
paginate(perPage?: number, page?: number): Promise<Paginator<T>>;
|
|
158
159
|
chunk(count: number, callback: (items: T[]) => void | Promise<void>): Promise<void>;
|
|
159
160
|
each(count: number, callback: (item: T) => void | Promise<void>): Promise<void>;
|
|
160
|
-
cursor(): AsyncGenerator<T>;
|
|
161
|
+
cursor(keyset?: Record<string, any>): AsyncGenerator<T>;
|
|
161
162
|
lazy(count?: number): AsyncGenerator<T>;
|
|
162
163
|
insert(data: ModelAttributeInput<T> | ModelAttributeInput<T>[]): Promise<any>;
|
|
163
164
|
insertGetId(data: ModelAttributeInput<T>, idColumn?: ModelColumn<T>): Promise<any>;
|
|
@@ -18,6 +18,7 @@ export class Builder {
|
|
|
18
18
|
unions = [];
|
|
19
19
|
fromRaw;
|
|
20
20
|
updateJoins = [];
|
|
21
|
+
bindings = [];
|
|
21
22
|
constructor(connection, table) {
|
|
22
23
|
this.connection = connection;
|
|
23
24
|
this.tableName = table;
|
|
@@ -455,6 +456,7 @@ export class Builder {
|
|
|
455
456
|
cloned.unions = [...this.unions];
|
|
456
457
|
cloned.fromRaw = this.fromRaw;
|
|
457
458
|
cloned.updateJoins = [...this.updateJoins];
|
|
459
|
+
cloned.bindings = [...this.bindings];
|
|
458
460
|
return cloned;
|
|
459
461
|
}
|
|
460
462
|
wrapColumn(value) {
|
|
@@ -697,14 +699,34 @@ export class Builder {
|
|
|
697
699
|
}
|
|
698
700
|
});
|
|
699
701
|
}
|
|
700
|
-
async *cursor() {
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
702
|
+
async *cursor(keyset) {
|
|
703
|
+
const model = this.model;
|
|
704
|
+
const primaryKey = model ? model.primaryKey || "id" : "id";
|
|
705
|
+
const orderColumn = this.orders[0]?.column || primaryKey;
|
|
706
|
+
const orderDirection = this.orders[0]?.direction || "asc";
|
|
707
|
+
const builder = this.clone();
|
|
708
|
+
builder.orders = [{ column: orderColumn, direction: orderDirection }];
|
|
709
|
+
builder.offsetValue = undefined;
|
|
710
|
+
if (keyset) {
|
|
711
|
+
const op = orderDirection === "asc" ? ">" : "<";
|
|
712
|
+
builder.wheres.push({
|
|
713
|
+
type: "basic",
|
|
714
|
+
column: orderColumn,
|
|
715
|
+
operator: op,
|
|
716
|
+
value: keyset[orderColumn],
|
|
717
|
+
boolean: "and",
|
|
718
|
+
scope: undefined,
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
const items = await builder.limit(1).get();
|
|
722
|
+
if (items.length === 0)
|
|
723
|
+
return;
|
|
724
|
+
yield items[0];
|
|
725
|
+
const nextKeyset = items[0] && typeof items[0] === "object"
|
|
726
|
+
? { [orderColumn]: items[0][orderColumn] }
|
|
727
|
+
: undefined;
|
|
728
|
+
if (nextKeyset) {
|
|
729
|
+
yield* this.cursor(nextKeyset);
|
|
708
730
|
}
|
|
709
731
|
}
|
|
710
732
|
async *lazy(count = 1000) {
|
|
@@ -14,6 +14,10 @@ export declare class ForeignKeyBuilder {
|
|
|
14
14
|
on(table: string): this;
|
|
15
15
|
onDelete(action: string): this;
|
|
16
16
|
onUpdate(action: string): this;
|
|
17
|
+
cascadeOnDelete(): this;
|
|
18
|
+
restrictOnDelete(): this;
|
|
19
|
+
nullOnDelete(): this;
|
|
20
|
+
cascadeOnUpdate(): this;
|
|
17
21
|
}
|
|
18
22
|
export declare class Blueprint {
|
|
19
23
|
readonly table: string;
|
|
@@ -47,17 +51,29 @@ export declare class Blueprint {
|
|
|
47
51
|
jsonb(name: string): this;
|
|
48
52
|
binary(name: string): this;
|
|
49
53
|
uuid(name: string): this;
|
|
54
|
+
foreignId(name: string): this;
|
|
55
|
+
foreignUuid(name: string): this;
|
|
50
56
|
enum(name: string, values: string[]): this;
|
|
51
57
|
nullable(): this;
|
|
52
58
|
default(value: any): this;
|
|
53
59
|
unique(): this;
|
|
54
60
|
index(): this;
|
|
61
|
+
index(columns: string | string[], name?: string): this;
|
|
55
62
|
primary(): this;
|
|
56
63
|
unsigned(): this;
|
|
57
64
|
comment(text: string): this;
|
|
65
|
+
change(): void;
|
|
58
66
|
timestamps(): void;
|
|
59
67
|
softDeletes(): void;
|
|
68
|
+
morphs(name: string): void;
|
|
69
|
+
nullableMorphs(name: string): void;
|
|
70
|
+
uuidMorphs(name: string): void;
|
|
71
|
+
nullableUuidMorphs(name: string): void;
|
|
60
72
|
foreign(columns: string | string[], name?: string): ForeignKeyBuilder;
|
|
73
|
+
constrained(table?: string, column?: string): ForeignKeyBuilder;
|
|
74
|
+
cascadeOnDelete(): ForeignKeyBuilder;
|
|
75
|
+
uniqueIndex(columns: string | string[], name?: string): this;
|
|
76
|
+
private guessConstrainedTable;
|
|
61
77
|
dropColumn(column: string | string[]): void;
|
|
62
78
|
renameColumn(from: string, to: string): void;
|
|
63
79
|
dropIndex(name: string): void;
|
|
@@ -22,6 +22,18 @@ export class ForeignKeyBuilder {
|
|
|
22
22
|
this.fk.onUpdate = action;
|
|
23
23
|
return this;
|
|
24
24
|
}
|
|
25
|
+
cascadeOnDelete() {
|
|
26
|
+
return this.onDelete("cascade");
|
|
27
|
+
}
|
|
28
|
+
restrictOnDelete() {
|
|
29
|
+
return this.onDelete("restrict");
|
|
30
|
+
}
|
|
31
|
+
nullOnDelete() {
|
|
32
|
+
return this.onDelete("set null");
|
|
33
|
+
}
|
|
34
|
+
cascadeOnUpdate() {
|
|
35
|
+
return this.onUpdate("cascade");
|
|
36
|
+
}
|
|
25
37
|
}
|
|
26
38
|
export class Blueprint {
|
|
27
39
|
table;
|
|
@@ -126,6 +138,12 @@ export class Blueprint {
|
|
|
126
138
|
uuid(name) {
|
|
127
139
|
return this.addColumn("uuid", name);
|
|
128
140
|
}
|
|
141
|
+
foreignId(name) {
|
|
142
|
+
return this.bigInteger(name).unsigned();
|
|
143
|
+
}
|
|
144
|
+
foreignUuid(name) {
|
|
145
|
+
return this.uuid(name);
|
|
146
|
+
}
|
|
129
147
|
enum(name, values) {
|
|
130
148
|
this.addColumn("enum", name);
|
|
131
149
|
this.currentColumn.values = values;
|
|
@@ -147,10 +165,24 @@ export class Blueprint {
|
|
|
147
165
|
}
|
|
148
166
|
return this;
|
|
149
167
|
}
|
|
150
|
-
index() {
|
|
151
|
-
if (
|
|
152
|
-
this.currentColumn
|
|
168
|
+
index(columns, name) {
|
|
169
|
+
if (columns === undefined) {
|
|
170
|
+
if (this.currentColumn) {
|
|
171
|
+
this.currentColumn.index = true;
|
|
172
|
+
this.indexes.push({
|
|
173
|
+
name: `${this.table}_${this.currentColumn.name}_index`,
|
|
174
|
+
columns: [this.currentColumn.name],
|
|
175
|
+
unique: false,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return this;
|
|
153
179
|
}
|
|
180
|
+
const cols = Array.isArray(columns) ? columns : [columns];
|
|
181
|
+
this.indexes.push({
|
|
182
|
+
name: name || `${this.table}_${cols.join("_")}_index`,
|
|
183
|
+
columns: cols,
|
|
184
|
+
unique: false,
|
|
185
|
+
});
|
|
154
186
|
return this;
|
|
155
187
|
}
|
|
156
188
|
primary() {
|
|
@@ -171,6 +203,12 @@ export class Blueprint {
|
|
|
171
203
|
}
|
|
172
204
|
return this;
|
|
173
205
|
}
|
|
206
|
+
change() {
|
|
207
|
+
if (!this.currentColumn) {
|
|
208
|
+
throw new Error("change() must be called after a column definition.");
|
|
209
|
+
}
|
|
210
|
+
this.commands.push({ name: "change", parameters: { column: this.currentColumn } });
|
|
211
|
+
}
|
|
174
212
|
timestamps() {
|
|
175
213
|
this.timestamp("created_at").nullable();
|
|
176
214
|
this.timestamp("updated_at").nullable();
|
|
@@ -178,10 +216,54 @@ export class Blueprint {
|
|
|
178
216
|
softDeletes() {
|
|
179
217
|
this.timestamp("deleted_at").nullable();
|
|
180
218
|
}
|
|
219
|
+
morphs(name) {
|
|
220
|
+
this.string(`${name}_type`);
|
|
221
|
+
this.bigInteger(`${name}_id`).unsigned();
|
|
222
|
+
this.index([`${name}_type`, `${name}_id`], `${this.table}_${name}_type_${name}_id_index`);
|
|
223
|
+
}
|
|
224
|
+
nullableMorphs(name) {
|
|
225
|
+
this.string(`${name}_type`).nullable();
|
|
226
|
+
this.bigInteger(`${name}_id`).unsigned().nullable();
|
|
227
|
+
this.index([`${name}_type`, `${name}_id`], `${this.table}_${name}_type_${name}_id_index`);
|
|
228
|
+
}
|
|
229
|
+
uuidMorphs(name) {
|
|
230
|
+
this.string(`${name}_type`);
|
|
231
|
+
this.uuid(`${name}_id`);
|
|
232
|
+
this.index([`${name}_type`, `${name}_id`], `${this.table}_${name}_type_${name}_id_index`);
|
|
233
|
+
}
|
|
234
|
+
nullableUuidMorphs(name) {
|
|
235
|
+
this.string(`${name}_type`).nullable();
|
|
236
|
+
this.uuid(`${name}_id`).nullable();
|
|
237
|
+
this.index([`${name}_type`, `${name}_id`], `${this.table}_${name}_type_${name}_id_index`);
|
|
238
|
+
}
|
|
181
239
|
foreign(columns, name) {
|
|
182
240
|
const cols = Array.isArray(columns) ? columns : [columns];
|
|
183
241
|
return new ForeignKeyBuilder(this, cols, name);
|
|
184
242
|
}
|
|
243
|
+
constrained(table, column = "id") {
|
|
244
|
+
if (!this.currentColumn) {
|
|
245
|
+
throw new Error("constrained() must be called after a column definition.");
|
|
246
|
+
}
|
|
247
|
+
const localColumn = this.currentColumn.name;
|
|
248
|
+
const foreignTable = table || this.guessConstrainedTable(localColumn);
|
|
249
|
+
return this.foreign(localColumn).references(column).on(foreignTable);
|
|
250
|
+
}
|
|
251
|
+
cascadeOnDelete() {
|
|
252
|
+
return this.constrained().cascadeOnDelete();
|
|
253
|
+
}
|
|
254
|
+
uniqueIndex(columns, name) {
|
|
255
|
+
const cols = Array.isArray(columns) ? columns : [columns];
|
|
256
|
+
this.indexes.push({
|
|
257
|
+
name: name || `${this.table}_${cols.join("_")}_unique`,
|
|
258
|
+
columns: cols,
|
|
259
|
+
unique: true,
|
|
260
|
+
});
|
|
261
|
+
return this;
|
|
262
|
+
}
|
|
263
|
+
guessConstrainedTable(column) {
|
|
264
|
+
const base = column.endsWith("_id") ? column.slice(0, -3) : column;
|
|
265
|
+
return `${base}s`;
|
|
266
|
+
}
|
|
185
267
|
dropColumn(column) {
|
|
186
268
|
this.commands.push({ name: "dropColumn", parameters: { column: Array.isArray(column) ? column : [column] } });
|
|
187
269
|
}
|
|
@@ -40,6 +40,10 @@ export class Schema {
|
|
|
40
40
|
for (const indexSql of indexes) {
|
|
41
41
|
await this.getConnection().run(indexSql);
|
|
42
42
|
}
|
|
43
|
+
const fks = grammar.compileForeignKeys(blueprint, connection.qualifyTable(table));
|
|
44
|
+
for (const fkSql of fks) {
|
|
45
|
+
await this.getConnection().run(fkSql);
|
|
46
|
+
}
|
|
43
47
|
}
|
|
44
48
|
static async createIfNotExists(table, callback) {
|
|
45
49
|
const blueprint = new Blueprint(table);
|
|
@@ -52,6 +56,10 @@ export class Schema {
|
|
|
52
56
|
for (const indexSql of indexes) {
|
|
53
57
|
await this.getConnection().run(indexSql);
|
|
54
58
|
}
|
|
59
|
+
const fks = grammar.compileForeignKeys(blueprint, connection.qualifyTable(table));
|
|
60
|
+
for (const fkSql of fks) {
|
|
61
|
+
await this.getConnection().run(fkSql);
|
|
62
|
+
}
|
|
55
63
|
}
|
|
56
64
|
static async table(table, callback) {
|
|
57
65
|
const blueprint = new Blueprint(table);
|
|
@@ -83,6 +91,16 @@ export class Schema {
|
|
|
83
91
|
else if (command.name === "dropForeign") {
|
|
84
92
|
await this.getConnection().run(`ALTER TABLE ${grammar.wrap(table)} DROP CONSTRAINT ${grammar.wrap(command.parameters.name)}`);
|
|
85
93
|
}
|
|
94
|
+
else if (command.name === "change") {
|
|
95
|
+
const sql = grammar.compileChange(qualifiedTable, command.parameters.column);
|
|
96
|
+
if (Array.isArray(sql)) {
|
|
97
|
+
for (const s of sql)
|
|
98
|
+
await this.getConnection().run(s);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
await this.getConnection().run(sql);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
86
104
|
}
|
|
87
105
|
const addSqls = grammar.compileAdd(blueprint, qualifiedTable);
|
|
88
106
|
for (const sql of addSqls) {
|
|
@@ -23,4 +23,5 @@ export declare abstract class Grammar {
|
|
|
23
23
|
protected compileForeignKey(table: string, fk: ForeignKeyDefinition): string;
|
|
24
24
|
abstract compileColumnRename(table: string, from: string, to: string): string;
|
|
25
25
|
abstract compileDropColumn(table: string, columns: string[]): string | string[];
|
|
26
|
+
abstract compileChange(table: string, column: ColumnDefinition): string | string[];
|
|
26
27
|
}
|
|
@@ -84,7 +84,8 @@ export class Grammar {
|
|
|
84
84
|
return blueprint.foreignKeys.map((fk) => this.compileForeignKey(table, fk));
|
|
85
85
|
}
|
|
86
86
|
compileForeignKey(table, fk) {
|
|
87
|
-
const
|
|
87
|
+
const constraint = fk.name ? ` CONSTRAINT ${this.wrap(fk.name)}` : "";
|
|
88
|
+
const sql = `ALTER TABLE ${this.wrap(table)} ADD${constraint} FOREIGN KEY (${this.wrapArray(fk.columns).join(", ")}) REFERENCES ${this.wrap(fk.onTable)} (${this.wrapArray(fk.references).join(", ")})`;
|
|
88
89
|
let full = sql;
|
|
89
90
|
if (fk.onDelete)
|
|
90
91
|
full += ` ON DELETE ${fk.onDelete}`;
|
|
@@ -12,6 +12,7 @@ export declare class MySqlGrammar extends Grammar {
|
|
|
12
12
|
protected getColumn(_blueprint: any, column: ColumnDefinition): string;
|
|
13
13
|
compileColumnRename(table: string, from: string, to: string): string;
|
|
14
14
|
compileDropColumn(table: string, columns: string[]): string;
|
|
15
|
+
compileChange(table: string, column: ColumnDefinition): string;
|
|
15
16
|
compileIndex(table: string, index: any): string;
|
|
16
17
|
compileCreate(blueprint: any, table: string): string;
|
|
17
18
|
compileCreateIfNotExists(blueprint: any, table: string): string;
|
|
@@ -79,6 +79,9 @@ export class MySqlGrammar extends Grammar {
|
|
|
79
79
|
compileDropColumn(table, columns) {
|
|
80
80
|
return `ALTER TABLE ${this.wrap(table)} ${columns.map((col) => `DROP COLUMN ${this.wrap(col)}`).join(", ")}`;
|
|
81
81
|
}
|
|
82
|
+
compileChange(table, column) {
|
|
83
|
+
return `ALTER TABLE ${this.wrap(table)} MODIFY COLUMN ${this.getColumn({}, column)}`;
|
|
84
|
+
}
|
|
82
85
|
compileIndex(table, index) {
|
|
83
86
|
const type = index.unique ? "UNIQUE INDEX" : "INDEX";
|
|
84
87
|
return `ALTER TABLE ${this.wrap(table)} ADD ${type} ${this.wrap(index.name)} (${this.wrapArray(index.columns).join(", ")})`;
|
|
@@ -10,6 +10,7 @@ export declare class PostgresGrammar extends Grammar {
|
|
|
10
10
|
protected getColumn(_blueprint: any, column: ColumnDefinition): string;
|
|
11
11
|
compileColumnRename(table: string, from: string, to: string): string;
|
|
12
12
|
compileDropColumn(table: string, columns: string[]): string;
|
|
13
|
+
compileChange(table: string, column: ColumnDefinition): string[];
|
|
13
14
|
compileIndex(table: string, index: any): string;
|
|
14
15
|
compileCreate(blueprint: any, table: string): string;
|
|
15
16
|
compileCreateIfNotExists(blueprint: any, table: string): string;
|
|
@@ -71,6 +71,16 @@ export class PostgresGrammar extends Grammar {
|
|
|
71
71
|
compileDropColumn(table, columns) {
|
|
72
72
|
return `ALTER TABLE ${this.wrap(table)} ${columns.map((col) => `DROP COLUMN ${this.wrap(col)}`).join(", ")}`;
|
|
73
73
|
}
|
|
74
|
+
compileChange(table, column) {
|
|
75
|
+
const statements = [
|
|
76
|
+
`ALTER TABLE ${this.wrap(table)} ALTER COLUMN ${this.wrap(column.name)} TYPE ${this.getType(column)}`,
|
|
77
|
+
`ALTER TABLE ${this.wrap(table)} ALTER COLUMN ${this.wrap(column.name)} ${column.nullable ? "DROP" : "SET"} NOT NULL`,
|
|
78
|
+
];
|
|
79
|
+
if (column.default !== undefined) {
|
|
80
|
+
statements.push(`ALTER TABLE ${this.wrap(table)} ALTER COLUMN ${this.wrap(column.name)} SET DEFAULT ${this.getDefaultValue(column.default)}`);
|
|
81
|
+
}
|
|
82
|
+
return statements;
|
|
83
|
+
}
|
|
74
84
|
compileIndex(table, index) {
|
|
75
85
|
const type = index.unique ? "UNIQUE INDEX" : "INDEX";
|
|
76
86
|
return `CREATE ${type} ${this.wrap(index.name)} ON ${this.wrap(table)} (${this.wrapArray(index.columns).join(", ")})`;
|
|
@@ -10,6 +10,7 @@ export declare class SQLiteGrammar extends Grammar {
|
|
|
10
10
|
protected getColumn(_blueprint: any, column: ColumnDefinition): string;
|
|
11
11
|
compileColumnRename(table: string, from: string, to: string): string;
|
|
12
12
|
compileDropColumn(table: string, columns: string[]): string | string[];
|
|
13
|
+
compileChange(_table: string, _column: ColumnDefinition): string | string[];
|
|
13
14
|
compileForeignKeys(blueprint: any, table: string): string[];
|
|
14
15
|
compileCreate(blueprint: any, table: string): string;
|
|
15
16
|
compileCreateIfNotExists(blueprint: any, table: string): string;
|
|
@@ -65,6 +65,9 @@ export class SQLiteGrammar extends Grammar {
|
|
|
65
65
|
// SQLite 3.35.0+ supports dropping columns.
|
|
66
66
|
return columns.map((col) => `ALTER TABLE ${this.wrap(table)} DROP COLUMN ${this.wrap(col)}`);
|
|
67
67
|
}
|
|
68
|
+
compileChange(_table, _column) {
|
|
69
|
+
throw new Error("Changing existing columns is not supported by the SQLite grammar.");
|
|
70
|
+
}
|
|
68
71
|
compileForeignKeys(blueprint, table) {
|
|
69
72
|
// SQLite supports foreign keys inside CREATE TABLE only.
|
|
70
73
|
// For simplicity, ALTER TABLE ADD CONSTRAINT is not supported in SQLite.
|
package/package.json
CHANGED