@bunnykit/orm 0.1.0
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/LICENSE +21 -0
- package/README.md +904 -0
- package/dist/bin/bunny.d.ts +2 -0
- package/dist/bin/bunny.js +108 -0
- package/dist/src/connection/Connection.d.ts +13 -0
- package/dist/src/connection/Connection.js +49 -0
- package/dist/src/index.d.ts +20 -0
- package/dist/src/index.js +18 -0
- package/dist/src/migration/Migration.d.ts +4 -0
- package/dist/src/migration/Migration.js +2 -0
- package/dist/src/migration/MigrationCreator.d.ts +5 -0
- package/dist/src/migration/MigrationCreator.js +39 -0
- package/dist/src/migration/Migrator.d.ts +21 -0
- package/dist/src/migration/Migrator.js +137 -0
- package/dist/src/model/BelongsToMany.d.ts +27 -0
- package/dist/src/model/BelongsToMany.js +118 -0
- package/dist/src/model/Model.d.ts +166 -0
- package/dist/src/model/Model.js +763 -0
- package/dist/src/model/MorphMap.d.ts +7 -0
- package/dist/src/model/MorphMap.js +12 -0
- package/dist/src/model/MorphRelations.d.ts +81 -0
- package/dist/src/model/MorphRelations.js +296 -0
- package/dist/src/model/Observer.d.ts +19 -0
- package/dist/src/model/Observer.js +21 -0
- package/dist/src/query/Builder.d.ts +106 -0
- package/dist/src/query/Builder.js +466 -0
- package/dist/src/schema/Blueprint.d.ts +66 -0
- package/dist/src/schema/Blueprint.js +200 -0
- package/dist/src/schema/Schema.d.ts +16 -0
- package/dist/src/schema/Schema.js +135 -0
- package/dist/src/schema/grammars/Grammar.d.ts +26 -0
- package/dist/src/schema/grammars/Grammar.js +95 -0
- package/dist/src/schema/grammars/MySqlGrammar.d.ts +18 -0
- package/dist/src/schema/grammars/MySqlGrammar.js +96 -0
- package/dist/src/schema/grammars/PostgresGrammar.d.ts +16 -0
- package/dist/src/schema/grammars/PostgresGrammar.js +88 -0
- package/dist/src/schema/grammars/SQLiteGrammar.d.ts +16 -0
- package/dist/src/schema/grammars/SQLiteGrammar.js +108 -0
- package/dist/src/typegen/TypeGenerator.d.ts +29 -0
- package/dist/src/typegen/TypeGenerator.js +171 -0
- package/dist/src/typegen/TypeMapper.d.ts +4 -0
- package/dist/src/typegen/TypeMapper.js +27 -0
- package/dist/src/types/index.d.ts +53 -0
- package/dist/src/types/index.js +1 -0
- package/dist/src/utils.d.ts +1 -0
- package/dist/src/utils.js +6 -0
- package/package.json +62 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { Connection } from "../src/connection/Connection.js";
|
|
3
|
+
import { Migrator } from "../src/migration/Migrator.js";
|
|
4
|
+
import { MigrationCreator } from "../src/migration/MigrationCreator.js";
|
|
5
|
+
import { TypeGenerator } from "../src/typegen/TypeGenerator.js";
|
|
6
|
+
import { existsSync } from "fs";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
async function loadConfig() {
|
|
9
|
+
const configPath = join(process.cwd(), "bunny.config.ts");
|
|
10
|
+
if (existsSync(configPath)) {
|
|
11
|
+
const mod = await import(configPath);
|
|
12
|
+
return mod.default || mod;
|
|
13
|
+
}
|
|
14
|
+
const jsConfigPath = join(process.cwd(), "bunny.config.js");
|
|
15
|
+
if (existsSync(jsConfigPath)) {
|
|
16
|
+
const mod = await import(jsConfigPath);
|
|
17
|
+
return mod.default || mod;
|
|
18
|
+
}
|
|
19
|
+
// Fallback to environment variables
|
|
20
|
+
const url = process.env.DATABASE_URL;
|
|
21
|
+
if (url) {
|
|
22
|
+
return {
|
|
23
|
+
connection: { url },
|
|
24
|
+
migrationsPath: process.env.MIGRATIONS_PATH || "./database/migrations",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const driver = process.env.DB_CONNECTION;
|
|
28
|
+
if (driver) {
|
|
29
|
+
return {
|
|
30
|
+
connection: {
|
|
31
|
+
driver,
|
|
32
|
+
host: process.env.DB_HOST,
|
|
33
|
+
port: process.env.DB_PORT ? Number(process.env.DB_PORT) : undefined,
|
|
34
|
+
database: process.env.DB_DATABASE,
|
|
35
|
+
username: process.env.DB_USERNAME,
|
|
36
|
+
password: process.env.DB_PASSWORD,
|
|
37
|
+
filename: process.env.DB_DATABASE,
|
|
38
|
+
},
|
|
39
|
+
migrationsPath: process.env.MIGRATIONS_PATH || "./database/migrations",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
throw new Error("No database configuration found. Create bunny.config.ts or set DATABASE_URL / DB_CONNECTION environment variables.");
|
|
43
|
+
}
|
|
44
|
+
async function main() {
|
|
45
|
+
const args = process.argv.slice(2);
|
|
46
|
+
const command = args[0];
|
|
47
|
+
if (command === "migrate:make") {
|
|
48
|
+
const name = args[1];
|
|
49
|
+
if (!name) {
|
|
50
|
+
console.error("Usage: bun run bunny migrate:make <name>");
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
const config = await loadConfig();
|
|
54
|
+
const creator = new MigrationCreator();
|
|
55
|
+
const path = await creator.create(name, config.migrationsPath);
|
|
56
|
+
console.log(`Created migration: ${path}`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (command === "types:generate") {
|
|
60
|
+
const config = await loadConfig();
|
|
61
|
+
const connection = new Connection(config.connection);
|
|
62
|
+
const outDir = args[1] || config.typesOutDir || "./generated/models";
|
|
63
|
+
const generator = new TypeGenerator(connection, {
|
|
64
|
+
outDir,
|
|
65
|
+
stubs: config.typeStubs,
|
|
66
|
+
declarations: !config.typeStubs,
|
|
67
|
+
modelDeclarations: config.typeDeclarations,
|
|
68
|
+
modelDirectory: config.typeDeclarationModelsDir,
|
|
69
|
+
modelImportPrefix: config.typeDeclarationImportPrefix,
|
|
70
|
+
singularModels: config.typeDeclarationSingularModels,
|
|
71
|
+
});
|
|
72
|
+
await generator.generate();
|
|
73
|
+
console.log(`Generated model type declarations in ${outDir}`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const config = await loadConfig();
|
|
77
|
+
const connection = new Connection(config.connection);
|
|
78
|
+
const migrator = new Migrator(connection, config.migrationsPath, config.typesOutDir, {
|
|
79
|
+
declarations: !config.typeStubs,
|
|
80
|
+
stubs: config.typeStubs,
|
|
81
|
+
modelDeclarations: config.typeDeclarations,
|
|
82
|
+
modelDirectory: config.typeDeclarationModelsDir,
|
|
83
|
+
modelImportPrefix: config.typeDeclarationImportPrefix,
|
|
84
|
+
singularModels: config.typeDeclarationSingularModels,
|
|
85
|
+
});
|
|
86
|
+
if (command === "migrate") {
|
|
87
|
+
await migrator.run();
|
|
88
|
+
}
|
|
89
|
+
else if (command === "migrate:rollback") {
|
|
90
|
+
await migrator.rollback();
|
|
91
|
+
}
|
|
92
|
+
else if (command === "migrate:status") {
|
|
93
|
+
const status = await migrator.status();
|
|
94
|
+
console.table(status);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
console.log("Usage:");
|
|
98
|
+
console.log(" bun run bunny migrate Run pending migrations");
|
|
99
|
+
console.log(" bun run bunny migrate:make <name> Create a new migration");
|
|
100
|
+
console.log(" bun run bunny migrate:rollback Rollback the last batch");
|
|
101
|
+
console.log(" bun run bunny migrate:status Show migration status");
|
|
102
|
+
console.log(" bun run bunny types:generate [dir] Generate model type declarations from DB schema");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
main().catch((err) => {
|
|
106
|
+
console.error(err);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { SQL } from "bun";
|
|
2
|
+
import type { ConnectionConfig } from "../types/index.js";
|
|
3
|
+
export declare class Connection {
|
|
4
|
+
readonly driver: SQL;
|
|
5
|
+
private driverName;
|
|
6
|
+
constructor(config: ConnectionConfig);
|
|
7
|
+
getDriverName(): "sqlite" | "mysql" | "postgres";
|
|
8
|
+
query(sqlString: string): Promise<any[]>;
|
|
9
|
+
run(sqlString: string): Promise<any>;
|
|
10
|
+
beginTransaction(): Promise<void>;
|
|
11
|
+
commit(): Promise<void>;
|
|
12
|
+
rollback(): Promise<void>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { SQL } from "bun";
|
|
2
|
+
export class Connection {
|
|
3
|
+
driver;
|
|
4
|
+
driverName;
|
|
5
|
+
constructor(config) {
|
|
6
|
+
let url;
|
|
7
|
+
if ("url" in config && config.url) {
|
|
8
|
+
url = config.url;
|
|
9
|
+
}
|
|
10
|
+
else if ("driver" in config) {
|
|
11
|
+
const c = config;
|
|
12
|
+
if (c.driver === "sqlite") {
|
|
13
|
+
url = `sqlite://${c.filename || c.database || ":memory:"}`;
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
const protocol = c.driver === "mysql" ? "mysql" : "postgres";
|
|
17
|
+
url = `${protocol}://${c.username || ""}:${c.password || ""}@${c.host || "localhost"}:${c.port || (c.driver === "mysql" ? 3306 : 5432)}/${c.database || ""}`;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
throw new Error("Invalid connection configuration. Provide a url or driver config.");
|
|
22
|
+
}
|
|
23
|
+
this.driver = new SQL(url);
|
|
24
|
+
this.driverName = url.startsWith("sqlite")
|
|
25
|
+
? "sqlite"
|
|
26
|
+
: url.startsWith("mysql")
|
|
27
|
+
? "mysql"
|
|
28
|
+
: "postgres";
|
|
29
|
+
}
|
|
30
|
+
getDriverName() {
|
|
31
|
+
return this.driverName;
|
|
32
|
+
}
|
|
33
|
+
async query(sqlString) {
|
|
34
|
+
// Use unsafe for generated SQL strings
|
|
35
|
+
return (await this.driver.unsafe(sqlString));
|
|
36
|
+
}
|
|
37
|
+
async run(sqlString) {
|
|
38
|
+
return await this.driver.unsafe(sqlString);
|
|
39
|
+
}
|
|
40
|
+
async beginTransaction() {
|
|
41
|
+
await this.driver.unsafe("BEGIN");
|
|
42
|
+
}
|
|
43
|
+
async commit() {
|
|
44
|
+
await this.driver.unsafe("COMMIT");
|
|
45
|
+
}
|
|
46
|
+
async rollback() {
|
|
47
|
+
await this.driver.unsafe("ROLLBACK");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { Connection } from "./connection/Connection.js";
|
|
2
|
+
export type { ConnectionConfig } from "./types/index.js";
|
|
3
|
+
export { Schema } from "./schema/Schema.js";
|
|
4
|
+
export { Blueprint } from "./schema/Blueprint.js";
|
|
5
|
+
export { Grammar } from "./schema/grammars/Grammar.js";
|
|
6
|
+
export { SQLiteGrammar } from "./schema/grammars/SQLiteGrammar.js";
|
|
7
|
+
export { MySqlGrammar } from "./schema/grammars/MySqlGrammar.js";
|
|
8
|
+
export { PostgresGrammar } from "./schema/grammars/PostgresGrammar.js";
|
|
9
|
+
export { Builder } from "./query/Builder.js";
|
|
10
|
+
export { Model, HasMany, BelongsTo, HasOne, HasManyThrough, HasOneThrough } from "./model/Model.js";
|
|
11
|
+
export type { ModelConstructor, GlobalScope, CastDefinition, CastsAttributes } from "./model/Model.js";
|
|
12
|
+
export { ObserverRegistry, type ObserverContract } from "./model/Observer.js";
|
|
13
|
+
export { MorphMap } from "./model/MorphMap.js";
|
|
14
|
+
export { MorphTo, MorphOne, MorphMany, MorphToMany } from "./model/MorphRelations.js";
|
|
15
|
+
export { BelongsToMany } from "./model/BelongsToMany.js";
|
|
16
|
+
export { Migration } from "./migration/Migration.js";
|
|
17
|
+
export { Migrator } from "./migration/Migrator.js";
|
|
18
|
+
export { MigrationCreator } from "./migration/MigrationCreator.js";
|
|
19
|
+
export { TypeGenerator } from "./typegen/TypeGenerator.js";
|
|
20
|
+
export { TypeMapper } from "./typegen/TypeMapper.js";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { Connection } from "./connection/Connection.js";
|
|
2
|
+
export { Schema } from "./schema/Schema.js";
|
|
3
|
+
export { Blueprint } from "./schema/Blueprint.js";
|
|
4
|
+
export { Grammar } from "./schema/grammars/Grammar.js";
|
|
5
|
+
export { SQLiteGrammar } from "./schema/grammars/SQLiteGrammar.js";
|
|
6
|
+
export { MySqlGrammar } from "./schema/grammars/MySqlGrammar.js";
|
|
7
|
+
export { PostgresGrammar } from "./schema/grammars/PostgresGrammar.js";
|
|
8
|
+
export { Builder } from "./query/Builder.js";
|
|
9
|
+
export { Model, HasMany, BelongsTo, HasOne, HasManyThrough, HasOneThrough } from "./model/Model.js";
|
|
10
|
+
export { ObserverRegistry } from "./model/Observer.js";
|
|
11
|
+
export { MorphMap } from "./model/MorphMap.js";
|
|
12
|
+
export { MorphTo, MorphOne, MorphMany, MorphToMany } from "./model/MorphRelations.js";
|
|
13
|
+
export { BelongsToMany } from "./model/BelongsToMany.js";
|
|
14
|
+
export { Migration } from "./migration/Migration.js";
|
|
15
|
+
export { Migrator } from "./migration/Migrator.js";
|
|
16
|
+
export { MigrationCreator } from "./migration/MigrationCreator.js";
|
|
17
|
+
export { TypeGenerator } from "./typegen/TypeGenerator.js";
|
|
18
|
+
export { TypeMapper } from "./typegen/TypeMapper.js";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
export class MigrationCreator {
|
|
4
|
+
async create(name, path) {
|
|
5
|
+
await mkdir(path, { recursive: true });
|
|
6
|
+
const timestamp = new Date().toISOString().replace(/[-:T.Z]/g, "").slice(0, 14);
|
|
7
|
+
const filename = `${timestamp}_${this.snakeCase(name)}.ts`;
|
|
8
|
+
const filePath = join(path, filename);
|
|
9
|
+
const stub = `import { Migration } from "@bunnykit/orm";
|
|
10
|
+
import { Schema } from "@bunnykit/orm";
|
|
11
|
+
|
|
12
|
+
export default class ${this.toClassName(name)} extends Migration {
|
|
13
|
+
async up(): Promise<void> {
|
|
14
|
+
// Schema.create("table_name", (table) => {
|
|
15
|
+
// table.increments("id");
|
|
16
|
+
// table.timestamps();
|
|
17
|
+
// });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async down(): Promise<void> {
|
|
21
|
+
// Schema.dropIfExists("table_name");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
`;
|
|
25
|
+
await writeFile(filePath, stub, "utf-8");
|
|
26
|
+
return filePath;
|
|
27
|
+
}
|
|
28
|
+
snakeCase(str) {
|
|
29
|
+
return str
|
|
30
|
+
.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
|
|
31
|
+
.replace(/^_/g, "");
|
|
32
|
+
}
|
|
33
|
+
toClassName(name) {
|
|
34
|
+
return name
|
|
35
|
+
.split(/[_\-]/)
|
|
36
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
37
|
+
.join("");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Connection } from "../connection/Connection.js";
|
|
2
|
+
import type { TypeGeneratorOptions } from "../typegen/TypeGenerator.js";
|
|
3
|
+
export declare class Migrator {
|
|
4
|
+
private connection;
|
|
5
|
+
private path;
|
|
6
|
+
private typesOutDir?;
|
|
7
|
+
private typeGeneratorOptions;
|
|
8
|
+
constructor(connection: Connection, path: string, typesOutDir?: string | undefined, typeGeneratorOptions?: Omit<TypeGeneratorOptions, "outDir">);
|
|
9
|
+
private ensureMigrationsTable;
|
|
10
|
+
private getRan;
|
|
11
|
+
private getLastBatchNumber;
|
|
12
|
+
private getMigrationFiles;
|
|
13
|
+
run(): Promise<void>;
|
|
14
|
+
rollback(): Promise<void>;
|
|
15
|
+
private generateTypesIfNeeded;
|
|
16
|
+
status(): Promise<{
|
|
17
|
+
migration: string;
|
|
18
|
+
status: string;
|
|
19
|
+
}[]>;
|
|
20
|
+
private resolve;
|
|
21
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { readdir } from "fs/promises";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
import { Schema } from "../schema/Schema.js";
|
|
4
|
+
import { Builder } from "../query/Builder.js";
|
|
5
|
+
import { TypeGenerator } from "../typegen/TypeGenerator.js";
|
|
6
|
+
export class Migrator {
|
|
7
|
+
connection;
|
|
8
|
+
path;
|
|
9
|
+
typesOutDir;
|
|
10
|
+
typeGeneratorOptions;
|
|
11
|
+
constructor(connection, path, typesOutDir, typeGeneratorOptions = {}) {
|
|
12
|
+
this.connection = connection;
|
|
13
|
+
this.path = path;
|
|
14
|
+
this.typesOutDir = typesOutDir;
|
|
15
|
+
this.typeGeneratorOptions = typeGeneratorOptions;
|
|
16
|
+
Schema.setConnection(connection);
|
|
17
|
+
}
|
|
18
|
+
async ensureMigrationsTable() {
|
|
19
|
+
const exists = await Schema.hasTable("migrations");
|
|
20
|
+
if (!exists) {
|
|
21
|
+
await Schema.create("migrations", (table) => {
|
|
22
|
+
table.increments("id");
|
|
23
|
+
table.string("migration");
|
|
24
|
+
table.integer("batch");
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async getRan() {
|
|
29
|
+
await this.ensureMigrationsTable();
|
|
30
|
+
const results = await new Builder(this.connection, "migrations")
|
|
31
|
+
.orderBy("id", "asc")
|
|
32
|
+
.get();
|
|
33
|
+
return results.map((row) => row.migration);
|
|
34
|
+
}
|
|
35
|
+
async getLastBatchNumber() {
|
|
36
|
+
const result = await new Builder(this.connection, "migrations")
|
|
37
|
+
.select("MAX(batch) as batch")
|
|
38
|
+
.first();
|
|
39
|
+
return result?.batch || 0;
|
|
40
|
+
}
|
|
41
|
+
async getMigrationFiles() {
|
|
42
|
+
const files = await readdir(this.path);
|
|
43
|
+
return files
|
|
44
|
+
.filter((f) => f.endsWith(".ts") || f.endsWith(".js"))
|
|
45
|
+
.sort((a, b) => a.localeCompare(b));
|
|
46
|
+
}
|
|
47
|
+
async run() {
|
|
48
|
+
const ran = await this.getRan();
|
|
49
|
+
const files = await this.getMigrationFiles();
|
|
50
|
+
const pending = files.filter((f) => !ran.includes(f));
|
|
51
|
+
if (pending.length === 0) {
|
|
52
|
+
console.log("Nothing to migrate.");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const batch = (await this.getLastBatchNumber()) + 1;
|
|
56
|
+
await this.connection.beginTransaction();
|
|
57
|
+
try {
|
|
58
|
+
for (const file of pending) {
|
|
59
|
+
const migration = await this.resolve(file);
|
|
60
|
+
console.log(`Migrating: ${file}`);
|
|
61
|
+
await migration.up();
|
|
62
|
+
await new Builder(this.connection, "migrations").insert({
|
|
63
|
+
migration: file,
|
|
64
|
+
batch,
|
|
65
|
+
});
|
|
66
|
+
console.log(`Migrated: ${file}`);
|
|
67
|
+
}
|
|
68
|
+
await this.connection.commit();
|
|
69
|
+
await this.generateTypesIfNeeded();
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
await this.connection.rollback();
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async rollback() {
|
|
77
|
+
const batch = await this.getLastBatchNumber();
|
|
78
|
+
if (batch === 0) {
|
|
79
|
+
console.log("Nothing to rollback.");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const records = (await new Builder(this.connection, "migrations")
|
|
83
|
+
.where("batch", batch)
|
|
84
|
+
.orderBy("id", "desc")
|
|
85
|
+
.get());
|
|
86
|
+
if (records.length === 0) {
|
|
87
|
+
console.log("Nothing to rollback.");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
await this.connection.beginTransaction();
|
|
91
|
+
try {
|
|
92
|
+
for (const record of records) {
|
|
93
|
+
const migration = await this.resolve(record.migration);
|
|
94
|
+
console.log(`Rolling back: ${record.migration}`);
|
|
95
|
+
await migration.down();
|
|
96
|
+
await new Builder(this.connection, "migrations")
|
|
97
|
+
.where("id", record.id)
|
|
98
|
+
.delete();
|
|
99
|
+
console.log(`Rolled back: ${record.migration}`);
|
|
100
|
+
}
|
|
101
|
+
await this.connection.commit();
|
|
102
|
+
await this.generateTypesIfNeeded();
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
await this.connection.rollback();
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async generateTypesIfNeeded() {
|
|
110
|
+
if (!this.typesOutDir)
|
|
111
|
+
return;
|
|
112
|
+
const generator = new TypeGenerator(this.connection, {
|
|
113
|
+
declarations: true,
|
|
114
|
+
...this.typeGeneratorOptions,
|
|
115
|
+
outDir: this.typesOutDir,
|
|
116
|
+
});
|
|
117
|
+
await generator.generate();
|
|
118
|
+
console.log(`Regenerated types in ${this.typesOutDir}`);
|
|
119
|
+
}
|
|
120
|
+
async status() {
|
|
121
|
+
const ran = await this.getRan();
|
|
122
|
+
const files = await this.getMigrationFiles();
|
|
123
|
+
return files.map((file) => ({
|
|
124
|
+
migration: file,
|
|
125
|
+
status: ran.includes(file) ? "Ran" : "Pending",
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
async resolve(file) {
|
|
129
|
+
const fullPath = resolve(this.path, file);
|
|
130
|
+
const module = await import(fullPath);
|
|
131
|
+
const MigrationClass = module.default || Object.values(module)[0];
|
|
132
|
+
if (!MigrationClass) {
|
|
133
|
+
throw new Error(`Migration ${file} does not export a class.`);
|
|
134
|
+
}
|
|
135
|
+
return new MigrationClass();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Builder } from "../query/Builder.js";
|
|
2
|
+
import type { Model } from "./Model.js";
|
|
3
|
+
export declare class BelongsToMany<T extends Model = Model> {
|
|
4
|
+
protected parent: Model;
|
|
5
|
+
protected related: typeof Model;
|
|
6
|
+
protected table: string;
|
|
7
|
+
protected foreignPivotKey: string;
|
|
8
|
+
protected relatedPivotKey: string;
|
|
9
|
+
protected parentKey: string;
|
|
10
|
+
protected relatedKey: string;
|
|
11
|
+
protected builder: Builder<T>;
|
|
12
|
+
constructor(parent: Model, related: typeof Model, table?: string, foreignPivotKey?: string, relatedPivotKey?: string, parentKey?: string, relatedKey?: string);
|
|
13
|
+
protected addConstraints(): void;
|
|
14
|
+
getQuery(): Builder<T>;
|
|
15
|
+
addEagerConstraints(models: Model[]): void;
|
|
16
|
+
getEager(): Promise<any[]>;
|
|
17
|
+
match(models: Model[], results: any[], relationName: string): void;
|
|
18
|
+
getResults(): Promise<T[]>;
|
|
19
|
+
qualifyRelatedColumn(column: string): string;
|
|
20
|
+
protected newExistenceQuery(parentTable: string, aggregate: string, callback?: (query: Builder<any>) => void | Builder<any>): Builder<any>;
|
|
21
|
+
getRelationExistenceSql(parentQuery: Builder<any>, callback?: (query: Builder<any>) => void | Builder<any>): string;
|
|
22
|
+
getRelationCountSql(parentQuery: Builder<any>, callback?: (query: Builder<any>) => void | Builder<any>): string;
|
|
23
|
+
getRelationAggregateSql(parentQuery: Builder<any>, aggregate: string, callback?: (query: Builder<any>) => void | Builder<any>): string;
|
|
24
|
+
attach(ids: any | any[], attributes?: Record<string, any>): Promise<void>;
|
|
25
|
+
detach(ids?: any | any[]): Promise<void>;
|
|
26
|
+
sync(ids: any | any[], detachMissing?: boolean): Promise<void>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Builder } from "../query/Builder.js";
|
|
2
|
+
import { snakeCase } from "../utils.js";
|
|
3
|
+
function defaultPivotTable(parent, related) {
|
|
4
|
+
const names = [snakeCase(parent.constructor.name), snakeCase(related.name)].sort();
|
|
5
|
+
return `${names[0]}_${names[1]}`;
|
|
6
|
+
}
|
|
7
|
+
export class BelongsToMany {
|
|
8
|
+
parent;
|
|
9
|
+
related;
|
|
10
|
+
table;
|
|
11
|
+
foreignPivotKey;
|
|
12
|
+
relatedPivotKey;
|
|
13
|
+
parentKey;
|
|
14
|
+
relatedKey;
|
|
15
|
+
builder;
|
|
16
|
+
constructor(parent, related, table, foreignPivotKey, relatedPivotKey, parentKey, relatedKey) {
|
|
17
|
+
this.parent = parent;
|
|
18
|
+
this.related = related;
|
|
19
|
+
this.table = table || defaultPivotTable(parent, related);
|
|
20
|
+
this.parentKey = parentKey || parent.constructor.primaryKey;
|
|
21
|
+
this.relatedKey = relatedKey || related.primaryKey;
|
|
22
|
+
this.foreignPivotKey = foreignPivotKey || `${snakeCase(parent.constructor.name)}_id`;
|
|
23
|
+
this.relatedPivotKey = relatedPivotKey || `${snakeCase(related.name)}_id`;
|
|
24
|
+
this.builder = related.query();
|
|
25
|
+
this.addConstraints();
|
|
26
|
+
}
|
|
27
|
+
addConstraints() {
|
|
28
|
+
const relatedTable = this.related.getTable();
|
|
29
|
+
this.builder.select(`${relatedTable}.*`);
|
|
30
|
+
this.builder.join(this.table, `${this.table}.${this.relatedPivotKey}`, "=", `${relatedTable}.${this.relatedKey}`);
|
|
31
|
+
this.builder.where(`${this.table}.${this.foreignPivotKey}`, this.parent.getAttribute(this.parentKey));
|
|
32
|
+
}
|
|
33
|
+
getQuery() {
|
|
34
|
+
return this.builder;
|
|
35
|
+
}
|
|
36
|
+
addEagerConstraints(models) {
|
|
37
|
+
const keys = models.map((m) => m.getAttribute(this.parentKey));
|
|
38
|
+
this.builder = this.related.query();
|
|
39
|
+
const relatedTable = this.related.getTable();
|
|
40
|
+
this.builder.select(`${relatedTable}.*`, `${this.table}.${this.foreignPivotKey}`);
|
|
41
|
+
this.builder.join(this.table, `${this.table}.${this.relatedPivotKey}`, "=", `${relatedTable}.${this.relatedKey}`);
|
|
42
|
+
this.builder.whereIn(`${this.table}.${this.foreignPivotKey}`, keys);
|
|
43
|
+
}
|
|
44
|
+
async getEager() {
|
|
45
|
+
return this.builder.get();
|
|
46
|
+
}
|
|
47
|
+
match(models, results, relationName) {
|
|
48
|
+
const dictionary = {};
|
|
49
|
+
for (const result of results) {
|
|
50
|
+
const key = result.$attributes[this.foreignPivotKey];
|
|
51
|
+
if (!dictionary[key])
|
|
52
|
+
dictionary[key] = [];
|
|
53
|
+
delete result.$attributes[this.foreignPivotKey];
|
|
54
|
+
dictionary[key].push(result);
|
|
55
|
+
}
|
|
56
|
+
for (const model of models) {
|
|
57
|
+
const key = model.getAttribute(this.parentKey);
|
|
58
|
+
model.setRelation(relationName, dictionary[key] || []);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async getResults() {
|
|
62
|
+
return this.builder.get();
|
|
63
|
+
}
|
|
64
|
+
qualifyRelatedColumn(column) {
|
|
65
|
+
return column.includes(".") ? column : `${this.related.getTable()}.${column}`;
|
|
66
|
+
}
|
|
67
|
+
newExistenceQuery(parentTable, aggregate, callback) {
|
|
68
|
+
const relatedTable = this.related.getTable();
|
|
69
|
+
const query = this.related.query().select(aggregate);
|
|
70
|
+
query.join(this.table, `${this.table}.${this.relatedPivotKey}`, "=", `${relatedTable}.${this.relatedKey}`);
|
|
71
|
+
query.whereColumn(`${this.table}.${this.foreignPivotKey}`, "=", `${parentTable}.${this.parentKey}`);
|
|
72
|
+
if (callback)
|
|
73
|
+
callback(query);
|
|
74
|
+
return query;
|
|
75
|
+
}
|
|
76
|
+
getRelationExistenceSql(parentQuery, callback) {
|
|
77
|
+
return this.newExistenceQuery(parentQuery.tableName, "1", callback).toSql();
|
|
78
|
+
}
|
|
79
|
+
getRelationCountSql(parentQuery, callback) {
|
|
80
|
+
return this.getRelationAggregateSql(parentQuery, "COUNT(*)", callback);
|
|
81
|
+
}
|
|
82
|
+
getRelationAggregateSql(parentQuery, aggregate, callback) {
|
|
83
|
+
return this.newExistenceQuery(parentQuery.tableName, aggregate, callback).toSql();
|
|
84
|
+
}
|
|
85
|
+
async attach(ids, attributes) {
|
|
86
|
+
const idList = Array.isArray(ids) ? ids : [ids];
|
|
87
|
+
const records = idList.map((id) => ({
|
|
88
|
+
[this.foreignPivotKey]: this.parent.getAttribute(this.parentKey),
|
|
89
|
+
[this.relatedPivotKey]: id,
|
|
90
|
+
...attributes,
|
|
91
|
+
}));
|
|
92
|
+
await new Builder(this.related.getConnection(), this.table).insert(records);
|
|
93
|
+
}
|
|
94
|
+
async detach(ids) {
|
|
95
|
+
const builder = new Builder(this.related.getConnection(), this.table)
|
|
96
|
+
.where(this.foreignPivotKey, this.parent.getAttribute(this.parentKey));
|
|
97
|
+
if (ids !== undefined) {
|
|
98
|
+
builder.whereIn(this.relatedPivotKey, Array.isArray(ids) ? ids : [ids]);
|
|
99
|
+
}
|
|
100
|
+
await builder.delete();
|
|
101
|
+
}
|
|
102
|
+
async sync(ids, detachMissing = true) {
|
|
103
|
+
const idList = Array.isArray(ids) ? ids : [ids];
|
|
104
|
+
const current = await new Builder(this.related.getConnection(), this.table)
|
|
105
|
+
.where(this.foreignPivotKey, this.parent.getAttribute(this.parentKey))
|
|
106
|
+
.pluck(this.relatedPivotKey);
|
|
107
|
+
const currentSet = new Set(current);
|
|
108
|
+
const newSet = new Set(idList);
|
|
109
|
+
if (detachMissing) {
|
|
110
|
+
const toDetach = current.filter((id) => !newSet.has(id));
|
|
111
|
+
if (toDetach.length > 0)
|
|
112
|
+
await this.detach(toDetach);
|
|
113
|
+
}
|
|
114
|
+
const toAttach = idList.filter((id) => !currentSet.has(id));
|
|
115
|
+
if (toAttach.length > 0)
|
|
116
|
+
await this.attach(toAttach);
|
|
117
|
+
}
|
|
118
|
+
}
|