@fragno-dev/db 0.0.1
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/.turbo/turbo-build.log +17 -0
- package/.turbo/turbo-types$colon$check.log +1 -0
- package/README.md +13 -0
- package/dist/cuid.js +3 -0
- package/dist/mod.d.ts +5 -0
- package/dist/mod.d.ts.map +1 -0
- package/dist/mod.js +6 -0
- package/dist/mod.js.map +1 -0
- package/dist/schema/create.d.ts +282 -0
- package/dist/schema/create.d.ts.map +1 -0
- package/dist/schema/create.js +437 -0
- package/dist/schema/create.js.map +1 -0
- package/package.json +34 -0
- package/src/cuid.ts +3 -0
- package/src/migration-engine/auto-from-schema.test.ts +238 -0
- package/src/migration-engine/auto-from-schema.ts +106 -0
- package/src/migration-engine/create.test.ts +349 -0
- package/src/migration-engine/create.ts +199 -0
- package/src/migration-engine/shared.ts +102 -0
- package/src/mod.ts +1 -0
- package/src/schema/create.test.ts +287 -0
- package/src/schema/create.ts +809 -0
- package/src/shared/config.ts +18 -0
- package/src/shared/prisma.ts +45 -0
- package/src/shared/providers.ts +10 -0
- package/src/util/deep-equal.ts +29 -0
- package/tsconfig.json +10 -0
- package/tsdown.config.ts +7 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { type MigrationOperation } from "./shared";
|
|
2
|
+
import type { Provider } from "../shared/providers";
|
|
3
|
+
import type { AnySchema } from "../schema/create";
|
|
4
|
+
import { generateMigrationFromSchema as defaultGenerateMigrationFromSchema } from "./auto-from-schema";
|
|
5
|
+
|
|
6
|
+
type Awaitable<T> = T | Promise<T>;
|
|
7
|
+
|
|
8
|
+
interface MigrationContext {
|
|
9
|
+
auto: () => Promise<MigrationOperation[]>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type CustomMigrationFn = (context: MigrationContext) => Awaitable<MigrationOperation[]>;
|
|
13
|
+
|
|
14
|
+
export interface MigrateOptions {
|
|
15
|
+
/**
|
|
16
|
+
* Update internal settings, it's true by default.
|
|
17
|
+
* We don't recommend to disable it other than testing purposes.
|
|
18
|
+
*/
|
|
19
|
+
updateSettings?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface MigrationResult {
|
|
23
|
+
operations: MigrationOperation[];
|
|
24
|
+
getSQL?: () => string;
|
|
25
|
+
execute: () => Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface Migrator {
|
|
29
|
+
/**
|
|
30
|
+
* Get current version (returns 0 if not initialized)
|
|
31
|
+
*/
|
|
32
|
+
getVersion: () => Promise<number>;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Migrate to the latest schema version
|
|
36
|
+
*/
|
|
37
|
+
migrate: (options?: MigrateOptions) => Promise<MigrationResult>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Migrate to a specific version (only forward migrations allowed)
|
|
41
|
+
*/
|
|
42
|
+
migrateTo: (version: number, options?: MigrateOptions) => Promise<MigrationResult>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface MigrationEngineOptions {
|
|
46
|
+
/**
|
|
47
|
+
* The target schema to migrate to
|
|
48
|
+
*/
|
|
49
|
+
schema: AnySchema;
|
|
50
|
+
|
|
51
|
+
userConfig: {
|
|
52
|
+
provider: Provider;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
executor: (operations: MigrationOperation[]) => Promise<void>;
|
|
56
|
+
|
|
57
|
+
generateMigrationFromSchema?: typeof defaultGenerateMigrationFromSchema;
|
|
58
|
+
|
|
59
|
+
settings: {
|
|
60
|
+
/**
|
|
61
|
+
* Get current version from database (0 if not initialized)
|
|
62
|
+
*/
|
|
63
|
+
getVersion: () => Promise<number>;
|
|
64
|
+
|
|
65
|
+
updateSettingsInMigration: (version: number) => Awaitable<MigrationOperation[]>;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
sql?: {
|
|
69
|
+
toSql: (operations: MigrationOperation[]) => string;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
transformers?: MigrationTransformer[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface MigrationTransformer {
|
|
76
|
+
/**
|
|
77
|
+
* Run after auto-generating migration operations
|
|
78
|
+
*/
|
|
79
|
+
afterAuto?: (
|
|
80
|
+
operations: MigrationOperation[],
|
|
81
|
+
context: {
|
|
82
|
+
options: MigrateOptions;
|
|
83
|
+
fromVersion: number;
|
|
84
|
+
toVersion: number;
|
|
85
|
+
schema: AnySchema;
|
|
86
|
+
},
|
|
87
|
+
) => MigrationOperation[];
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Run on all migration operations
|
|
91
|
+
*/
|
|
92
|
+
afterAll?: (
|
|
93
|
+
operations: MigrationOperation[],
|
|
94
|
+
context: {
|
|
95
|
+
fromVersion: number;
|
|
96
|
+
toVersion: number;
|
|
97
|
+
schema: AnySchema;
|
|
98
|
+
},
|
|
99
|
+
) => MigrationOperation[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function createMigrator({
|
|
103
|
+
settings,
|
|
104
|
+
generateMigrationFromSchema = defaultGenerateMigrationFromSchema,
|
|
105
|
+
schema: targetSchema,
|
|
106
|
+
userConfig,
|
|
107
|
+
executor,
|
|
108
|
+
sql: sqlConfig,
|
|
109
|
+
transformers = [],
|
|
110
|
+
}: MigrationEngineOptions): Migrator {
|
|
111
|
+
const instance: Migrator = {
|
|
112
|
+
getVersion() {
|
|
113
|
+
return settings.getVersion();
|
|
114
|
+
},
|
|
115
|
+
async migrate(options = {}) {
|
|
116
|
+
return this.migrateTo(targetSchema.version, options);
|
|
117
|
+
},
|
|
118
|
+
async migrateTo(toVersion, options = {}) {
|
|
119
|
+
const { updateSettings: updateVersion = true } = options;
|
|
120
|
+
const fromVersion = await settings.getVersion();
|
|
121
|
+
|
|
122
|
+
if (toVersion < 0) {
|
|
123
|
+
throw new Error(`Cannot migrate to negative version: ${toVersion}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (toVersion < fromVersion) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Cannot migrate backwards: current version is ${fromVersion}, target is ${toVersion}. Only forward migrations are supported.`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (toVersion > targetSchema.version) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`Cannot migrate to version ${toVersion}: schema only has version ${targetSchema.version}`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (toVersion === fromVersion) {
|
|
139
|
+
// Already at target version, return empty migration
|
|
140
|
+
return {
|
|
141
|
+
operations: [],
|
|
142
|
+
getSQL: sqlConfig ? () => sqlConfig.toSql([]) : undefined,
|
|
143
|
+
execute: async () => {},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const context: MigrationContext = {
|
|
148
|
+
async auto() {
|
|
149
|
+
let generated = generateMigrationFromSchema(
|
|
150
|
+
targetSchema,
|
|
151
|
+
fromVersion,
|
|
152
|
+
toVersion,
|
|
153
|
+
userConfig,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
for (const transformer of transformers) {
|
|
157
|
+
if (!transformer.afterAuto) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
generated = transformer.afterAuto(generated, {
|
|
162
|
+
fromVersion,
|
|
163
|
+
toVersion,
|
|
164
|
+
schema: targetSchema,
|
|
165
|
+
options,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return generated;
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
let operations = await context.auto();
|
|
174
|
+
|
|
175
|
+
if (updateVersion) {
|
|
176
|
+
operations.push(...(await settings.updateSettingsInMigration(toVersion)));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
for (const transformer of transformers) {
|
|
180
|
+
if (!transformer.afterAll) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
operations = transformer.afterAll(operations, {
|
|
184
|
+
fromVersion,
|
|
185
|
+
toVersion,
|
|
186
|
+
schema: targetSchema,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
operations,
|
|
192
|
+
getSQL: sqlConfig ? () => sqlConfig.toSql(operations) : undefined,
|
|
193
|
+
execute: () => executor(operations),
|
|
194
|
+
};
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
return instance;
|
|
199
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { AnyColumn, AnyTable } from "../schema/create";
|
|
2
|
+
|
|
3
|
+
export interface ForeignKeyInfo {
|
|
4
|
+
name: string;
|
|
5
|
+
columns: string[];
|
|
6
|
+
referencedTable: string;
|
|
7
|
+
referencedColumns: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type MigrationOperation =
|
|
11
|
+
| TableOperation
|
|
12
|
+
| {
|
|
13
|
+
// warning: not supported by SQLite
|
|
14
|
+
type: "add-foreign-key";
|
|
15
|
+
table: string;
|
|
16
|
+
value: ForeignKeyInfo;
|
|
17
|
+
}
|
|
18
|
+
| {
|
|
19
|
+
// warning: not supported by SQLite
|
|
20
|
+
type: "drop-foreign-key";
|
|
21
|
+
table: string;
|
|
22
|
+
name: string;
|
|
23
|
+
}
|
|
24
|
+
| {
|
|
25
|
+
type: "drop-index";
|
|
26
|
+
table: string;
|
|
27
|
+
name: string;
|
|
28
|
+
}
|
|
29
|
+
| {
|
|
30
|
+
type: "add-index";
|
|
31
|
+
table: string;
|
|
32
|
+
columns: string[];
|
|
33
|
+
name: string;
|
|
34
|
+
unique: boolean;
|
|
35
|
+
}
|
|
36
|
+
| CustomOperation;
|
|
37
|
+
|
|
38
|
+
export type CustomOperation = {
|
|
39
|
+
type: "custom";
|
|
40
|
+
} & Record<string, unknown>;
|
|
41
|
+
|
|
42
|
+
export type TableOperation =
|
|
43
|
+
| {
|
|
44
|
+
type: "create-table";
|
|
45
|
+
value: AnyTable;
|
|
46
|
+
}
|
|
47
|
+
| {
|
|
48
|
+
type: "drop-table";
|
|
49
|
+
name: string;
|
|
50
|
+
}
|
|
51
|
+
| {
|
|
52
|
+
type: "update-table";
|
|
53
|
+
name: string;
|
|
54
|
+
value: ColumnOperation[];
|
|
55
|
+
}
|
|
56
|
+
| {
|
|
57
|
+
type: "rename-table";
|
|
58
|
+
from: string;
|
|
59
|
+
to: string;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type ColumnOperation =
|
|
63
|
+
| {
|
|
64
|
+
type: "rename-column";
|
|
65
|
+
from: string;
|
|
66
|
+
to: string;
|
|
67
|
+
}
|
|
68
|
+
| {
|
|
69
|
+
type: "drop-column";
|
|
70
|
+
name: string;
|
|
71
|
+
}
|
|
72
|
+
| {
|
|
73
|
+
/**
|
|
74
|
+
* Note: unique constraints are not created, please use dedicated operations like `add-index` instead
|
|
75
|
+
*/
|
|
76
|
+
type: "create-column";
|
|
77
|
+
value: AnyColumn;
|
|
78
|
+
}
|
|
79
|
+
| {
|
|
80
|
+
/**
|
|
81
|
+
* warning: Not supported by SQLite
|
|
82
|
+
*/
|
|
83
|
+
type: "update-column";
|
|
84
|
+
name: string;
|
|
85
|
+
/**
|
|
86
|
+
* For databases like MySQL, it requires the full definition for any modify column statement.
|
|
87
|
+
* Hence, you need to specify the full information of your column here.
|
|
88
|
+
*
|
|
89
|
+
* Then, opt-in for in-detail modification for other databases that supports changing data type/nullable/default separately, such as PostgreSQL.
|
|
90
|
+
*
|
|
91
|
+
* Note: unique constraints are not updated, please use dedicated operations like `add-index` instead
|
|
92
|
+
*/
|
|
93
|
+
value: AnyColumn;
|
|
94
|
+
|
|
95
|
+
updateNullable: boolean;
|
|
96
|
+
updateDefault: boolean;
|
|
97
|
+
updateDataType: boolean;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export function isUpdated(op: Extract<ColumnOperation, { type: "update-column" }>): boolean {
|
|
101
|
+
return op.updateDataType || op.updateDefault || op.updateNullable;
|
|
102
|
+
}
|
package/src/mod.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const hello = "world";
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { column, compileForeignKey, idColumn, referenceColumn, schema, table } from "./create";
|
|
3
|
+
|
|
4
|
+
describe("create", () => {
|
|
5
|
+
it("should create a table with columns using callback pattern", () => {
|
|
6
|
+
const userTable = table("users", (t) => {
|
|
7
|
+
return t
|
|
8
|
+
.addColumn("id", idColumn())
|
|
9
|
+
.addColumn("name", column("string"))
|
|
10
|
+
.addColumn("email", column("string"))
|
|
11
|
+
.createIndex("unique_email", ["email"], { unique: true })
|
|
12
|
+
.addColumn("age", column("integer").nullable());
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
expect(userTable.columns.id).toBeDefined();
|
|
16
|
+
expect(userTable.columns.name).toBeDefined();
|
|
17
|
+
expect(userTable.columns.email).toBeDefined();
|
|
18
|
+
expect(userTable.columns.age).toBeDefined();
|
|
19
|
+
expect(userTable.columns.age.isNullable).toBe(true);
|
|
20
|
+
expect(userTable.indexes).toEqual([
|
|
21
|
+
{
|
|
22
|
+
name: "unique_email",
|
|
23
|
+
columns: [userTable.columns.email],
|
|
24
|
+
unique: true,
|
|
25
|
+
},
|
|
26
|
+
]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should create a schema with multiple tables using callback pattern", () => {
|
|
30
|
+
const userSchema = schema((s) => {
|
|
31
|
+
return s
|
|
32
|
+
.addTable("users", (t) => {
|
|
33
|
+
return t.addColumn("id", idColumn()).addColumn("name", column("string"));
|
|
34
|
+
})
|
|
35
|
+
.addTable("posts", (t) => {
|
|
36
|
+
return t
|
|
37
|
+
.addColumn("id", idColumn())
|
|
38
|
+
.addColumn("title", column("string"))
|
|
39
|
+
.addColumn("content", column("string"));
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
expect(userSchema.version).toBe(2); // Two addTable calls
|
|
44
|
+
expect(userSchema.tables.users).toBeDefined();
|
|
45
|
+
expect(userSchema.tables.posts).toBeDefined();
|
|
46
|
+
expect(userSchema.tables.users.ormName).toBe("users");
|
|
47
|
+
expect(userSchema.tables.posts.ormName).toBe("posts");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should generate default values for columns", () => {
|
|
51
|
+
const testTable = table("test", (t) => {
|
|
52
|
+
return t
|
|
53
|
+
.addColumn("id", idColumn())
|
|
54
|
+
.addColumn("createdAt", column("timestamp").defaultTo$("now"))
|
|
55
|
+
.addColumn("status", column("string").defaultTo("active"));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const idValue = testTable.columns.id.generateDefaultValue();
|
|
59
|
+
expect(typeof idValue).toBe("string");
|
|
60
|
+
expect(idValue?.length).toBeGreaterThan(0);
|
|
61
|
+
|
|
62
|
+
const createdAtValue = testTable.columns.createdAt.generateDefaultValue();
|
|
63
|
+
expect(createdAtValue).toBeInstanceOf(Date);
|
|
64
|
+
|
|
65
|
+
const statusValue = testTable.columns.status.generateDefaultValue();
|
|
66
|
+
expect(statusValue).toBe("active");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("should increment version on each builder operation", () => {
|
|
70
|
+
const userSchema = schema((s) => {
|
|
71
|
+
return s
|
|
72
|
+
.addTable("users", (t) => {
|
|
73
|
+
return t
|
|
74
|
+
.addColumn("id", idColumn())
|
|
75
|
+
.addColumn("name", column("string"))
|
|
76
|
+
.addColumn("age", column("integer"));
|
|
77
|
+
})
|
|
78
|
+
.addTable("posts", (t) => {
|
|
79
|
+
return t.addColumn("id", idColumn());
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(userSchema.version).toBe(2); // Two addTable calls
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("should support unique constraints on tables via unique method", () => {
|
|
87
|
+
const userTable = table("users", (t) => {
|
|
88
|
+
return t
|
|
89
|
+
.addColumn("id", idColumn())
|
|
90
|
+
.addColumn("email", column("string"))
|
|
91
|
+
.addColumn("username", column("string"))
|
|
92
|
+
.createIndex("unique_email_username", ["email", "username"], { unique: true });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const uniqueIndexes = userTable.indexes.filter((idx) => idx.unique);
|
|
96
|
+
expect(uniqueIndexes).toHaveLength(1);
|
|
97
|
+
expect(uniqueIndexes[0].name).toBe("unique_email_username");
|
|
98
|
+
expect(uniqueIndexes[0].columns).toHaveLength(2);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should support creating indexes on tables", () => {
|
|
102
|
+
const userTable = table("users", (t) => {
|
|
103
|
+
return t
|
|
104
|
+
.addColumn("id", idColumn())
|
|
105
|
+
.addColumn("email", column("string"))
|
|
106
|
+
.addColumn("username", column("string"))
|
|
107
|
+
.createIndex("idx_email", ["email"])
|
|
108
|
+
.createIndex("idx_username_unique", ["username"], { unique: true });
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
expect(userTable.indexes).toHaveLength(2);
|
|
112
|
+
expect(userTable.indexes[0]).toEqual({
|
|
113
|
+
name: "idx_email",
|
|
114
|
+
columns: [userTable.columns.email],
|
|
115
|
+
unique: false,
|
|
116
|
+
});
|
|
117
|
+
expect(userTable.indexes[1]).toEqual({
|
|
118
|
+
name: "idx_username_unique",
|
|
119
|
+
columns: [userTable.columns.username],
|
|
120
|
+
unique: true,
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("should demonstrate manual many-to-many relation setup", () => {
|
|
125
|
+
// For many-to-many, create a junction table manually
|
|
126
|
+
const userSchema = schema((s) => {
|
|
127
|
+
return s
|
|
128
|
+
.addTable("users", (t) => {
|
|
129
|
+
return t.addColumn("id", idColumn()).addColumn("name", column("string"));
|
|
130
|
+
})
|
|
131
|
+
.addTable("tags", (t) => {
|
|
132
|
+
return t.addColumn("id", idColumn()).addColumn("name", column("string"));
|
|
133
|
+
})
|
|
134
|
+
.addTable("user_tags", (t) => {
|
|
135
|
+
return t
|
|
136
|
+
.addColumn("id", idColumn())
|
|
137
|
+
.addColumn("userId", referenceColumn())
|
|
138
|
+
.addColumn("tagId", referenceColumn());
|
|
139
|
+
})
|
|
140
|
+
.addReference("user_tags", "user", {
|
|
141
|
+
columns: ["userId"],
|
|
142
|
+
targetTable: "users",
|
|
143
|
+
targetColumns: ["id"],
|
|
144
|
+
})
|
|
145
|
+
.addReference("user_tags", "tag", {
|
|
146
|
+
columns: ["tagId"],
|
|
147
|
+
targetTable: "tags",
|
|
148
|
+
targetColumns: ["id"],
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const junctionTable = userSchema.tables.user_tags;
|
|
153
|
+
|
|
154
|
+
// Verify the junction table has both relations
|
|
155
|
+
expect(junctionTable.relations["user"]).toBeDefined();
|
|
156
|
+
expect(junctionTable.relations["tag"]).toBeDefined();
|
|
157
|
+
|
|
158
|
+
// Verify both foreign keys were created
|
|
159
|
+
expect(junctionTable.foreignKeys).toHaveLength(2);
|
|
160
|
+
expect(junctionTable.foreignKeys[0].referencedTable).toBe(userSchema.tables.users);
|
|
161
|
+
expect(junctionTable.foreignKeys[1].referencedTable).toBe(userSchema.tables.tags);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("should create a foreign key reference using addReference", () => {
|
|
165
|
+
const userSchema = schema((s) => {
|
|
166
|
+
return s
|
|
167
|
+
.addTable("users", (t) => {
|
|
168
|
+
return t.addColumn("id", idColumn()).addColumn("name", column("string"));
|
|
169
|
+
})
|
|
170
|
+
.addTable("posts", (t) => {
|
|
171
|
+
return t
|
|
172
|
+
.addColumn("id", idColumn())
|
|
173
|
+
.addColumn("title", column("string"))
|
|
174
|
+
.addColumn("authorId", referenceColumn());
|
|
175
|
+
})
|
|
176
|
+
.addReference("posts", "author", {
|
|
177
|
+
columns: ["authorId"],
|
|
178
|
+
targetTable: "users",
|
|
179
|
+
targetColumns: ["id"],
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const postsTable = userSchema.tables.posts;
|
|
184
|
+
|
|
185
|
+
// Verify the authorId column is marked as a reference
|
|
186
|
+
expect(postsTable.columns.authorId.isReference).toBe(true);
|
|
187
|
+
|
|
188
|
+
// Verify the relation exists
|
|
189
|
+
const authorRelation = postsTable.relations["author"];
|
|
190
|
+
expect(authorRelation).toBeDefined();
|
|
191
|
+
expect(authorRelation.type).toBe("one");
|
|
192
|
+
expect(authorRelation.table).toBe(userSchema.tables.users);
|
|
193
|
+
expect(authorRelation.on).toEqual([["authorId", "id"]]);
|
|
194
|
+
|
|
195
|
+
// Verify the foreign key was created
|
|
196
|
+
expect(postsTable.foreignKeys).toHaveLength(1);
|
|
197
|
+
const fk = postsTable.foreignKeys[0];
|
|
198
|
+
expect(fk.table).toBe(postsTable);
|
|
199
|
+
expect(fk.referencedTable).toBe(userSchema.tables.users);
|
|
200
|
+
expect(fk.columns).toEqual([postsTable.columns.authorId]);
|
|
201
|
+
expect(fk.referencedColumns).toEqual([userSchema.tables.users.columns.id]);
|
|
202
|
+
|
|
203
|
+
// Verify the compiled foreign key format
|
|
204
|
+
const compiledFk = compileForeignKey(fk);
|
|
205
|
+
expect(compiledFk).toEqual({
|
|
206
|
+
name: "posts_users_author_fk",
|
|
207
|
+
table: "posts",
|
|
208
|
+
referencedTable: "users",
|
|
209
|
+
columns: ["authorId"],
|
|
210
|
+
referencedColumns: ["id"],
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("should support multiple references by calling addReference multiple times", () => {
|
|
215
|
+
const userSchema = schema((s) => {
|
|
216
|
+
return s
|
|
217
|
+
.addTable("users", (t) => {
|
|
218
|
+
return t.addColumn("id", idColumn()).addColumn("name", column("string"));
|
|
219
|
+
})
|
|
220
|
+
.addTable("categories", (t) => {
|
|
221
|
+
return t.addColumn("id", idColumn()).addColumn("name", column("string"));
|
|
222
|
+
})
|
|
223
|
+
.addTable("posts", (t) => {
|
|
224
|
+
return t
|
|
225
|
+
.addColumn("id", idColumn())
|
|
226
|
+
.addColumn("title", column("string"))
|
|
227
|
+
.addColumn("authorId", referenceColumn())
|
|
228
|
+
.addColumn("categoryId", referenceColumn());
|
|
229
|
+
})
|
|
230
|
+
.addReference("posts", "author", {
|
|
231
|
+
columns: ["authorId"],
|
|
232
|
+
targetTable: "users",
|
|
233
|
+
targetColumns: ["id"],
|
|
234
|
+
})
|
|
235
|
+
.addReference("posts", "category", {
|
|
236
|
+
columns: ["categoryId"],
|
|
237
|
+
targetTable: "categories",
|
|
238
|
+
targetColumns: ["id"],
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const postsTable = userSchema.tables.posts;
|
|
243
|
+
|
|
244
|
+
// Verify both relations exist
|
|
245
|
+
expect(postsTable.relations["author"]).toBeDefined();
|
|
246
|
+
expect(postsTable.relations["category"]).toBeDefined();
|
|
247
|
+
|
|
248
|
+
// Verify both foreign keys were created
|
|
249
|
+
expect(postsTable.foreignKeys).toHaveLength(2);
|
|
250
|
+
expect(postsTable.foreignKeys[0].referencedTable).toBe(userSchema.tables.users);
|
|
251
|
+
expect(postsTable.foreignKeys[1].referencedTable).toBe(userSchema.tables.categories);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("should support self-referencing foreign keys", () => {
|
|
255
|
+
const userSchema = schema((s) => {
|
|
256
|
+
return s
|
|
257
|
+
.addTable("users", (t) => {
|
|
258
|
+
return t
|
|
259
|
+
.addColumn("id", idColumn())
|
|
260
|
+
.addColumn("name", column("string"))
|
|
261
|
+
.addColumn("invitedBy", referenceColumn().nullable());
|
|
262
|
+
})
|
|
263
|
+
.addReference("users", "inviter", {
|
|
264
|
+
columns: ["invitedBy"],
|
|
265
|
+
targetTable: "users",
|
|
266
|
+
targetColumns: ["id"],
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const usersTable = userSchema.tables.users;
|
|
271
|
+
|
|
272
|
+
// Verify the self-referencing relation exists
|
|
273
|
+
const inviterRelation = usersTable.relations["inviter"];
|
|
274
|
+
expect(inviterRelation).toBeDefined();
|
|
275
|
+
expect(inviterRelation.type).toBe("one");
|
|
276
|
+
expect(inviterRelation.table).toBe(usersTable);
|
|
277
|
+
expect(inviterRelation.on).toEqual([["invitedBy", "id"]]);
|
|
278
|
+
|
|
279
|
+
// Verify the foreign key was created
|
|
280
|
+
expect(usersTable.foreignKeys).toHaveLength(1);
|
|
281
|
+
const fk = usersTable.foreignKeys[0];
|
|
282
|
+
expect(fk.table).toBe(usersTable);
|
|
283
|
+
expect(fk.referencedTable).toBe(usersTable);
|
|
284
|
+
expect(fk.columns).toEqual([usersTable.columns.invitedBy]);
|
|
285
|
+
expect(fk.referencedColumns).toEqual([usersTable.columns.id]);
|
|
286
|
+
});
|
|
287
|
+
});
|