@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.
@@ -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
+ });