@ghom/orm 2.0.0 → 2.1.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/biome.json +7 -0
- package/dist/app/migration.d.ts +258 -0
- package/dist/app/migration.js +229 -0
- package/dist/app/orm.d.ts +21 -1
- package/dist/app/orm.js +51 -1
- package/dist/app/table.d.ts +84 -15
- package/dist/app/table.js +107 -10
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +4 -5
- package/readme.md +91 -19
- package/tests/orm.test.ts +253 -1
- package/tests/tables/d.ts +0 -4
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +2 -2
package/biome.json
CHANGED
|
@@ -24,6 +24,9 @@
|
|
|
24
24
|
"enabled": true,
|
|
25
25
|
"rules": {
|
|
26
26
|
"recommended": true,
|
|
27
|
+
"complexity": {
|
|
28
|
+
"noBannedTypes": "off"
|
|
29
|
+
},
|
|
27
30
|
"suspicious": {
|
|
28
31
|
"noExplicitAny": "off",
|
|
29
32
|
"noThenProperty": "off",
|
|
@@ -32,6 +35,10 @@
|
|
|
32
35
|
"style": {
|
|
33
36
|
"noNonNullAssertion": "off",
|
|
34
37
|
"useNodejsImportProtocol": "off"
|
|
38
|
+
},
|
|
39
|
+
"correctness": {
|
|
40
|
+
"noUnusedVariables": "off",
|
|
41
|
+
"noUnusedImports": "off"
|
|
35
42
|
}
|
|
36
43
|
}
|
|
37
44
|
},
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import type { Knex } from "knex";
|
|
2
|
+
import { type ColumnDef, type InferColumns, type InferColumnType } from "./column.js";
|
|
3
|
+
/**
|
|
4
|
+
* Represents a typed migration that transforms the table schema.
|
|
5
|
+
* Carries type information about what columns are removed and added.
|
|
6
|
+
*
|
|
7
|
+
* @template From - The type of columns being removed/modified
|
|
8
|
+
* @template To - The type of columns being added/modified
|
|
9
|
+
*/
|
|
10
|
+
export interface TypedMigration<From = {}, To = {}> {
|
|
11
|
+
/** @internal Type marker for columns being removed */
|
|
12
|
+
readonly _from: From;
|
|
13
|
+
/** @internal Type marker for columns being added */
|
|
14
|
+
readonly _to: To;
|
|
15
|
+
/**
|
|
16
|
+
* Apply the migration to the table builder.
|
|
17
|
+
*/
|
|
18
|
+
apply: (builder: Knex.AlterTableBuilder) => void;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Represents a sequence of typed migrations.
|
|
22
|
+
* Used internally by migrate.sequence() to preserve individual migration types.
|
|
23
|
+
*
|
|
24
|
+
* @template Migrations - Tuple of migrations in the sequence
|
|
25
|
+
*/
|
|
26
|
+
export interface TypedMigrationSequence<Migrations extends TypedMigration<any, any>[]> {
|
|
27
|
+
/** @internal The individual migrations in the sequence */
|
|
28
|
+
readonly __migrations__: Migrations;
|
|
29
|
+
/** @internal Type marker for columns being removed (computed from sequence) */
|
|
30
|
+
readonly _from: SequenceFromType<Migrations>;
|
|
31
|
+
/** @internal Type marker for columns being added (computed from sequence) */
|
|
32
|
+
readonly _to: SequenceToType<Migrations>;
|
|
33
|
+
/**
|
|
34
|
+
* Apply all migrations in sequence to the table builder.
|
|
35
|
+
*/
|
|
36
|
+
apply: (builder: Knex.AlterTableBuilder) => void;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Unwrap a migration array to get the individual migration types.
|
|
40
|
+
* If M is an array, extracts the element type; otherwise returns M as-is.
|
|
41
|
+
*/
|
|
42
|
+
type UnwrapMigrationArray<M> = M extends readonly (infer U)[] ? U : M;
|
|
43
|
+
/**
|
|
44
|
+
* Extract "From" keys from a single TypedMigration or TypedMigrationSequence.
|
|
45
|
+
* For sequences, excludes intermediate columns (those that are also in _to).
|
|
46
|
+
*/
|
|
47
|
+
type ExtractFromKeysSingle<M> = M extends {
|
|
48
|
+
__migrations__: infer Migrations;
|
|
49
|
+
} ? Migrations extends TypedMigration<any, any>[] ? Exclude<keyof UnionToIntersection<Migrations[number]["_from"]>, keyof UnionToIntersection<Migrations[number]["_to"]>> : never : M extends TypedMigration<infer From, any> ? keyof From : never;
|
|
50
|
+
/**
|
|
51
|
+
* Extract all "From" keys from a union of TypedMigration.
|
|
52
|
+
* These are the columns that will be removed/renamed.
|
|
53
|
+
* Handles both single migrations and arrays of migrations.
|
|
54
|
+
* Uses distributive conditional types to handle unions correctly.
|
|
55
|
+
*/
|
|
56
|
+
type ExtractFromKeys<M> = M extends any ? UnwrapMigrationArray<M> extends infer U ? U extends any ? ExtractFromKeysSingle<U> : never : never : never;
|
|
57
|
+
/**
|
|
58
|
+
* Extract "To" type from a single TypedMigration or TypedMigrationSequence.
|
|
59
|
+
* For sequences, excludes intermediate columns (those that are also in _from).
|
|
60
|
+
*/
|
|
61
|
+
type ExtractToTypesSingle<M> = M extends {
|
|
62
|
+
__migrations__: infer Migrations;
|
|
63
|
+
} ? Migrations extends TypedMigration<any, any>[] ? Omit<UnionToIntersection<Migrations[number]["_to"]>, keyof UnionToIntersection<Migrations[number]["_from"]>> : never : M extends TypedMigration<any, infer To> ? To : never;
|
|
64
|
+
/**
|
|
65
|
+
* Extract all "To" types from a union of TypedMigration and intersect them.
|
|
66
|
+
* These are the columns that will be added.
|
|
67
|
+
* Handles both single migrations and arrays of migrations.
|
|
68
|
+
* Uses distributive conditional types to handle unions correctly.
|
|
69
|
+
*/
|
|
70
|
+
type ExtractToTypes<M> = M extends any ? UnwrapMigrationArray<M> extends infer U ? U extends any ? ExtractToTypesSingle<U> : never : never : never;
|
|
71
|
+
/**
|
|
72
|
+
* Convert a union to an intersection.
|
|
73
|
+
* Used to combine all "To" types from migrations.
|
|
74
|
+
*/
|
|
75
|
+
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
|
|
76
|
+
/**
|
|
77
|
+
* Force TypeScript to evaluate a type (expands type aliases).
|
|
78
|
+
*/
|
|
79
|
+
type Simplify<T> = {
|
|
80
|
+
[K in keyof T]: T[K];
|
|
81
|
+
} & {};
|
|
82
|
+
/**
|
|
83
|
+
* Combine all "from" types from a tuple of migrations.
|
|
84
|
+
*/
|
|
85
|
+
type CombineFromTypes<T extends TypedMigration<any, any>[]> = UnionToIntersection<T[number]["_from"]>;
|
|
86
|
+
/**
|
|
87
|
+
* Combine all "to" types from a tuple of migrations.
|
|
88
|
+
*/
|
|
89
|
+
type CombineToTypes<T extends TypedMigration<any, any>[]> = UnionToIntersection<T[number]["_to"]>;
|
|
90
|
+
/**
|
|
91
|
+
* Compute the final "from" type for a sequence of migrations.
|
|
92
|
+
* Excludes columns that are also added (intermediate renames).
|
|
93
|
+
*/
|
|
94
|
+
type SequenceFromType<T extends TypedMigration<any, any>[]> = Simplify<Omit<CombineFromTypes<T>, keyof CombineToTypes<T>>>;
|
|
95
|
+
/**
|
|
96
|
+
* Compute the final "to" type for a sequence of migrations.
|
|
97
|
+
* Excludes columns that are also removed (intermediate renames).
|
|
98
|
+
*/
|
|
99
|
+
type SequenceToType<T extends TypedMigration<any, any>[]> = Simplify<Omit<CombineToTypes<T>, keyof CombineFromTypes<T>>>;
|
|
100
|
+
/**
|
|
101
|
+
* Apply all migrations to compute the final type.
|
|
102
|
+
* 1. Remove all columns specified in migration "From" types
|
|
103
|
+
* 2. Add all columns specified in migration "To" types
|
|
104
|
+
* Handles both single migrations and arrays of migrations.
|
|
105
|
+
*/
|
|
106
|
+
export type ApplyMigrations<Base, Migrations extends Record<string, any>> = Migrations[keyof Migrations] extends infer M ? Omit<Base, ExtractFromKeys<M>> & UnionToIntersection<ExtractToTypes<M>> : Base;
|
|
107
|
+
/**
|
|
108
|
+
* Compute the final table type from base columns and migrations.
|
|
109
|
+
* Supports both single migrations and arrays of migrations.
|
|
110
|
+
*/
|
|
111
|
+
export type FinalTableType<Columns extends Record<string, ColumnDef<any, any>>, Migrations extends Record<string, any> = {}> = ApplyMigrations<InferColumns<Columns>, Migrations>;
|
|
112
|
+
/**
|
|
113
|
+
* Migration helpers for creating typed migrations.
|
|
114
|
+
* Each helper returns a TypedMigration with appropriate type transformations.
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* ```typescript
|
|
118
|
+
* import { migrate, col } from "@ghom/orm"
|
|
119
|
+
*
|
|
120
|
+
* const userTable = new Table({
|
|
121
|
+
* name: "user",
|
|
122
|
+
* columns: (col) => ({
|
|
123
|
+
* id: col.increments(),
|
|
124
|
+
* name: col.string(),
|
|
125
|
+
* }),
|
|
126
|
+
* migrations: {
|
|
127
|
+
* "001_rename_name": migrate.renameColumn("name", "username"),
|
|
128
|
+
* "002_add_email": migrate.addColumn("email", col.string()),
|
|
129
|
+
* },
|
|
130
|
+
* })
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
export declare const migrate: {
|
|
134
|
+
/**
|
|
135
|
+
* Add a new column to the table.
|
|
136
|
+
*
|
|
137
|
+
* @param name - The column name
|
|
138
|
+
* @param column - The column definition
|
|
139
|
+
* @returns A typed migration that adds the column
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* migrate.addColumn("email", col.string())
|
|
143
|
+
* // Adds: { email: string }
|
|
144
|
+
*/
|
|
145
|
+
addColumn<K extends string, C extends ColumnDef<any, any>>(name: K, column: C): TypedMigration<{}, { [P in K]: InferColumnType<C>; }>;
|
|
146
|
+
/**
|
|
147
|
+
* Drop a column from the table.
|
|
148
|
+
*
|
|
149
|
+
* @param name - The column name to drop
|
|
150
|
+
* @returns A typed migration that removes the column
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* migrate.dropColumn("oldField")
|
|
154
|
+
* // Removes: { oldField: any }
|
|
155
|
+
*/
|
|
156
|
+
dropColumn<K extends string>(name: K): TypedMigration<{ [P in K]: unknown; }, {}>;
|
|
157
|
+
/**
|
|
158
|
+
* Rename a column.
|
|
159
|
+
*
|
|
160
|
+
* @param oldName - The current column name
|
|
161
|
+
* @param newName - The new column name
|
|
162
|
+
* @returns A typed migration that renames the column
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* migrate.renameColumn("name", "username")
|
|
166
|
+
* // Removes: { name: any }, Adds: { username: any }
|
|
167
|
+
*/
|
|
168
|
+
renameColumn<Old extends string, New extends string>(oldName: Old, newName: New): TypedMigration<{ [P in Old]: unknown; }, { [P in New]: unknown; }>;
|
|
169
|
+
/**
|
|
170
|
+
* Alter a column's type or constraints.
|
|
171
|
+
*
|
|
172
|
+
* @param name - The column name
|
|
173
|
+
* @param column - The new column definition
|
|
174
|
+
* @returns A typed migration that alters the column
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* migrate.alterColumn("age", col.integer().nullable())
|
|
178
|
+
* // Changes type: { age: number | null }
|
|
179
|
+
*/
|
|
180
|
+
alterColumn<K extends string, C extends ColumnDef<any, any>>(name: K, column: C): TypedMigration<{ [P in K]: unknown; }, { [P in K]: InferColumnType<C>; }>;
|
|
181
|
+
/**
|
|
182
|
+
* Add an index on one or more columns.
|
|
183
|
+
*
|
|
184
|
+
* @param columns - Array of column names to index
|
|
185
|
+
* @param name - Optional index name
|
|
186
|
+
* @returns A typed migration (no type change)
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* migrate.addIndex(["email"], "idx_email")
|
|
190
|
+
*/
|
|
191
|
+
addIndex(columns: string[], name?: string): TypedMigration<{}, {}>;
|
|
192
|
+
/**
|
|
193
|
+
* Drop an index by name.
|
|
194
|
+
*
|
|
195
|
+
* @param name - The index name to drop
|
|
196
|
+
* @returns A typed migration (no type change)
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* migrate.dropIndex("idx_email")
|
|
200
|
+
*/
|
|
201
|
+
dropIndex(name: string): TypedMigration<{}, {}>;
|
|
202
|
+
/**
|
|
203
|
+
* Add a unique constraint on one or more columns.
|
|
204
|
+
*
|
|
205
|
+
* @param columns - Array of column names
|
|
206
|
+
* @param name - Optional constraint name
|
|
207
|
+
* @returns A typed migration (no type change)
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* migrate.addUnique(["email"], "uniq_email")
|
|
211
|
+
*/
|
|
212
|
+
addUnique(columns: string[], name?: string): TypedMigration<{}, {}>;
|
|
213
|
+
/**
|
|
214
|
+
* Drop a unique constraint by name.
|
|
215
|
+
*
|
|
216
|
+
* @param name - The constraint name to drop
|
|
217
|
+
* @returns A typed migration (no type change)
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* migrate.dropUnique("uniq_email")
|
|
221
|
+
*/
|
|
222
|
+
dropUnique(name: string): TypedMigration<{}, {}>;
|
|
223
|
+
/**
|
|
224
|
+
* Custom migration with a raw callback.
|
|
225
|
+
* Use this when the built-in helpers don't cover your use case.
|
|
226
|
+
*
|
|
227
|
+
* @param fn - The migration callback
|
|
228
|
+
* @returns A typed migration (no type change by default)
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* migrate.raw((builder) => {
|
|
232
|
+
* builder.dropColumn("temp")
|
|
233
|
+
* builder.string("new_col")
|
|
234
|
+
* })
|
|
235
|
+
*/
|
|
236
|
+
raw<From = {}, To = {}>(fn: (builder: Knex.AlterTableBuilder) => void): TypedMigration<From, To>;
|
|
237
|
+
/**
|
|
238
|
+
* Combine multiple migrations into a single migration.
|
|
239
|
+
* All migrations are applied sequentially within the same alter table call.
|
|
240
|
+
* Type information from all migrations is preserved and combined.
|
|
241
|
+
*
|
|
242
|
+
* Intermediate columns (added then removed in the sequence) are excluded from the final type.
|
|
243
|
+
* For example: renameColumn("a", "b") + renameColumn("b", "c") results in only "c" being added.
|
|
244
|
+
*
|
|
245
|
+
* @param migrations - The migrations to combine
|
|
246
|
+
* @returns A typed migration sequence combining all type transformations
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* migrate.sequence(
|
|
250
|
+
* migrate.addColumn("phone", col.string()),
|
|
251
|
+
* migrate.addColumn("address", col.string().nullable()),
|
|
252
|
+
* migrate.addIndex(["phone"]),
|
|
253
|
+
* )
|
|
254
|
+
* // Combines: removes nothing, adds { phone: string; address: string | null }
|
|
255
|
+
*/
|
|
256
|
+
sequence<T extends TypedMigration<any, any>[]>(...migrations: T): TypedMigrationSequence<T>;
|
|
257
|
+
};
|
|
258
|
+
export {};
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { buildColumnsSchema, } from "./column.js";
|
|
2
|
+
/**
|
|
3
|
+
* Migration helpers for creating typed migrations.
|
|
4
|
+
* Each helper returns a TypedMigration with appropriate type transformations.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { migrate, col } from "@ghom/orm"
|
|
9
|
+
*
|
|
10
|
+
* const userTable = new Table({
|
|
11
|
+
* name: "user",
|
|
12
|
+
* columns: (col) => ({
|
|
13
|
+
* id: col.increments(),
|
|
14
|
+
* name: col.string(),
|
|
15
|
+
* }),
|
|
16
|
+
* migrations: {
|
|
17
|
+
* "001_rename_name": migrate.renameColumn("name", "username"),
|
|
18
|
+
* "002_add_email": migrate.addColumn("email", col.string()),
|
|
19
|
+
* },
|
|
20
|
+
* })
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export const migrate = {
|
|
24
|
+
/**
|
|
25
|
+
* Add a new column to the table.
|
|
26
|
+
*
|
|
27
|
+
* @param name - The column name
|
|
28
|
+
* @param column - The column definition
|
|
29
|
+
* @returns A typed migration that adds the column
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* migrate.addColumn("email", col.string())
|
|
33
|
+
* // Adds: { email: string }
|
|
34
|
+
*/
|
|
35
|
+
addColumn(name, column) {
|
|
36
|
+
return {
|
|
37
|
+
_from: {},
|
|
38
|
+
_to: {},
|
|
39
|
+
apply: (builder) => {
|
|
40
|
+
buildColumnsSchema(builder, { [name]: column });
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
/**
|
|
45
|
+
* Drop a column from the table.
|
|
46
|
+
*
|
|
47
|
+
* @param name - The column name to drop
|
|
48
|
+
* @returns A typed migration that removes the column
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* migrate.dropColumn("oldField")
|
|
52
|
+
* // Removes: { oldField: any }
|
|
53
|
+
*/
|
|
54
|
+
dropColumn(name) {
|
|
55
|
+
return {
|
|
56
|
+
_from: {},
|
|
57
|
+
_to: {},
|
|
58
|
+
apply: (builder) => {
|
|
59
|
+
builder.dropColumn(name);
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
/**
|
|
64
|
+
* Rename a column.
|
|
65
|
+
*
|
|
66
|
+
* @param oldName - The current column name
|
|
67
|
+
* @param newName - The new column name
|
|
68
|
+
* @returns A typed migration that renames the column
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* migrate.renameColumn("name", "username")
|
|
72
|
+
* // Removes: { name: any }, Adds: { username: any }
|
|
73
|
+
*/
|
|
74
|
+
renameColumn(oldName, newName) {
|
|
75
|
+
return {
|
|
76
|
+
_from: {},
|
|
77
|
+
_to: {},
|
|
78
|
+
apply: (builder) => {
|
|
79
|
+
builder.renameColumn(oldName, newName);
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
/**
|
|
84
|
+
* Alter a column's type or constraints.
|
|
85
|
+
*
|
|
86
|
+
* @param name - The column name
|
|
87
|
+
* @param column - The new column definition
|
|
88
|
+
* @returns A typed migration that alters the column
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* migrate.alterColumn("age", col.integer().nullable())
|
|
92
|
+
* // Changes type: { age: number | null }
|
|
93
|
+
*/
|
|
94
|
+
alterColumn(name, column) {
|
|
95
|
+
return {
|
|
96
|
+
_from: {},
|
|
97
|
+
_to: {},
|
|
98
|
+
apply: (builder) => {
|
|
99
|
+
builder.dropColumn(name);
|
|
100
|
+
buildColumnsSchema(builder, { [name]: column });
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
/**
|
|
105
|
+
* Add an index on one or more columns.
|
|
106
|
+
*
|
|
107
|
+
* @param columns - Array of column names to index
|
|
108
|
+
* @param name - Optional index name
|
|
109
|
+
* @returns A typed migration (no type change)
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* migrate.addIndex(["email"], "idx_email")
|
|
113
|
+
*/
|
|
114
|
+
addIndex(columns, name) {
|
|
115
|
+
return {
|
|
116
|
+
_from: {},
|
|
117
|
+
_to: {},
|
|
118
|
+
apply: (builder) => {
|
|
119
|
+
builder.index(columns, name);
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
/**
|
|
124
|
+
* Drop an index by name.
|
|
125
|
+
*
|
|
126
|
+
* @param name - The index name to drop
|
|
127
|
+
* @returns A typed migration (no type change)
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* migrate.dropIndex("idx_email")
|
|
131
|
+
*/
|
|
132
|
+
dropIndex(name) {
|
|
133
|
+
return {
|
|
134
|
+
_from: {},
|
|
135
|
+
_to: {},
|
|
136
|
+
apply: (builder) => {
|
|
137
|
+
builder.dropIndex([], name);
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
/**
|
|
142
|
+
* Add a unique constraint on one or more columns.
|
|
143
|
+
*
|
|
144
|
+
* @param columns - Array of column names
|
|
145
|
+
* @param name - Optional constraint name
|
|
146
|
+
* @returns A typed migration (no type change)
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* migrate.addUnique(["email"], "uniq_email")
|
|
150
|
+
*/
|
|
151
|
+
addUnique(columns, name) {
|
|
152
|
+
return {
|
|
153
|
+
_from: {},
|
|
154
|
+
_to: {},
|
|
155
|
+
apply: (builder) => {
|
|
156
|
+
builder.unique(columns, { indexName: name });
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
/**
|
|
161
|
+
* Drop a unique constraint by name.
|
|
162
|
+
*
|
|
163
|
+
* @param name - The constraint name to drop
|
|
164
|
+
* @returns A typed migration (no type change)
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* migrate.dropUnique("uniq_email")
|
|
168
|
+
*/
|
|
169
|
+
dropUnique(name) {
|
|
170
|
+
return {
|
|
171
|
+
_from: {},
|
|
172
|
+
_to: {},
|
|
173
|
+
apply: (builder) => {
|
|
174
|
+
builder.dropUnique([], name);
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
/**
|
|
179
|
+
* Custom migration with a raw callback.
|
|
180
|
+
* Use this when the built-in helpers don't cover your use case.
|
|
181
|
+
*
|
|
182
|
+
* @param fn - The migration callback
|
|
183
|
+
* @returns A typed migration (no type change by default)
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* migrate.raw((builder) => {
|
|
187
|
+
* builder.dropColumn("temp")
|
|
188
|
+
* builder.string("new_col")
|
|
189
|
+
* })
|
|
190
|
+
*/
|
|
191
|
+
raw(fn) {
|
|
192
|
+
return {
|
|
193
|
+
_from: {},
|
|
194
|
+
_to: {},
|
|
195
|
+
apply: fn,
|
|
196
|
+
};
|
|
197
|
+
},
|
|
198
|
+
/**
|
|
199
|
+
* Combine multiple migrations into a single migration.
|
|
200
|
+
* All migrations are applied sequentially within the same alter table call.
|
|
201
|
+
* Type information from all migrations is preserved and combined.
|
|
202
|
+
*
|
|
203
|
+
* Intermediate columns (added then removed in the sequence) are excluded from the final type.
|
|
204
|
+
* For example: renameColumn("a", "b") + renameColumn("b", "c") results in only "c" being added.
|
|
205
|
+
*
|
|
206
|
+
* @param migrations - The migrations to combine
|
|
207
|
+
* @returns A typed migration sequence combining all type transformations
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* migrate.sequence(
|
|
211
|
+
* migrate.addColumn("phone", col.string()),
|
|
212
|
+
* migrate.addColumn("address", col.string().nullable()),
|
|
213
|
+
* migrate.addIndex(["phone"]),
|
|
214
|
+
* )
|
|
215
|
+
* // Combines: removes nothing, adds { phone: string; address: string | null }
|
|
216
|
+
*/
|
|
217
|
+
sequence(...migrations) {
|
|
218
|
+
return {
|
|
219
|
+
__migrations__: migrations,
|
|
220
|
+
_from: {},
|
|
221
|
+
_to: {},
|
|
222
|
+
apply: (builder) => {
|
|
223
|
+
for (const migration of migrations) {
|
|
224
|
+
migration.apply(builder);
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
},
|
|
229
|
+
};
|
package/dist/app/orm.d.ts
CHANGED
|
@@ -43,6 +43,21 @@ export interface ORMConfig {
|
|
|
43
43
|
* Default is `Infinity`.
|
|
44
44
|
*/
|
|
45
45
|
caching?: number;
|
|
46
|
+
/**
|
|
47
|
+
* Configuration for migration behavior.
|
|
48
|
+
*/
|
|
49
|
+
migrations?: {
|
|
50
|
+
/**
|
|
51
|
+
* Force alphabetical sorting for string migration keys instead of insertion order.
|
|
52
|
+
*
|
|
53
|
+
* **NOT RECOMMENDED**: If your keys start with numbers (e.g., "001_init", "002_add_users"),
|
|
54
|
+
* they are automatically sorted by those numbers, not alphabetically.
|
|
55
|
+
* Prefer insertion order or purely numeric keys instead.
|
|
56
|
+
*
|
|
57
|
+
* @default false
|
|
58
|
+
*/
|
|
59
|
+
alphabeticalOrder?: boolean;
|
|
60
|
+
};
|
|
46
61
|
}
|
|
47
62
|
/**
|
|
48
63
|
* The main ORM class that manages database connections, tables, and caching.
|
|
@@ -85,7 +100,7 @@ export declare class ORM {
|
|
|
85
100
|
* Returns true if the ORM has a database client connected.
|
|
86
101
|
*/
|
|
87
102
|
get isConnected(): boolean;
|
|
88
|
-
get cachedTables(): Table<any>[];
|
|
103
|
+
get cachedTables(): Table<any, {}>[];
|
|
89
104
|
get cachedTableNames(): string[];
|
|
90
105
|
hasCachedTable(name: string): boolean;
|
|
91
106
|
hasTable(name: string): Promise<boolean>;
|
|
@@ -109,4 +124,9 @@ export declare class ORM {
|
|
|
109
124
|
* @warning This will delete all the data in the tables.
|
|
110
125
|
*/
|
|
111
126
|
restoreBackup(dirname?: string): Promise<void>;
|
|
127
|
+
/**
|
|
128
|
+
* Upgrade the migration table from integer version to string version.
|
|
129
|
+
* This is needed for projects that were using the old migration system.
|
|
130
|
+
*/
|
|
131
|
+
private upgradeMigrationTableIfNeeded;
|
|
112
132
|
}
|
package/dist/app/orm.js
CHANGED
|
@@ -5,6 +5,16 @@ import { default as knex } from "knex";
|
|
|
5
5
|
import { backupTable, disableForeignKeys, enableForeignKeys, restoreBackup } from "./backup.js";
|
|
6
6
|
import { Table } from "./table.js";
|
|
7
7
|
import { isCJS } from "./util.js";
|
|
8
|
+
/**
|
|
9
|
+
* Verify that the environment supports ES2015+ object key ordering.
|
|
10
|
+
* In ES2015+, integer keys are sorted numerically first, then string keys
|
|
11
|
+
* maintain their insertion order.
|
|
12
|
+
*/
|
|
13
|
+
function checkES2015KeyOrder() {
|
|
14
|
+
const test = { "2": "a", "1": "b", c: "d" };
|
|
15
|
+
const keys = Object.keys(test);
|
|
16
|
+
return keys[0] === "1" && keys[1] === "2" && keys[2] === "c";
|
|
17
|
+
}
|
|
8
18
|
/**
|
|
9
19
|
* The main ORM class that manages database connections, tables, and caching.
|
|
10
20
|
*
|
|
@@ -41,6 +51,9 @@ export class ORM {
|
|
|
41
51
|
*/
|
|
42
52
|
constructor(config) {
|
|
43
53
|
this.config = config;
|
|
54
|
+
if (!checkES2015KeyOrder()) {
|
|
55
|
+
throw new Error("@ghom/orm requires ES2015+ environment for guaranteed object key ordering");
|
|
56
|
+
}
|
|
44
57
|
if (config === false)
|
|
45
58
|
return;
|
|
46
59
|
this._client = knex(config.database ?? {
|
|
@@ -105,9 +118,11 @@ export class ORM {
|
|
|
105
118
|
priority: Infinity,
|
|
106
119
|
columns: (col) => ({
|
|
107
120
|
table: col.string().unique(),
|
|
108
|
-
version: col.
|
|
121
|
+
version: col.string(),
|
|
109
122
|
}),
|
|
110
123
|
}));
|
|
124
|
+
// Auto-migrate version column from integer to string for existing projects
|
|
125
|
+
await this.upgradeMigrationTableIfNeeded();
|
|
111
126
|
const sortedTables = this.cachedTables.toSorted((a, b) => (b.options.priority ?? 0) - (a.options.priority ?? 0));
|
|
112
127
|
for (const table of sortedTables) {
|
|
113
128
|
await table.make(this);
|
|
@@ -162,4 +177,39 @@ export class ORM {
|
|
|
162
177
|
});
|
|
163
178
|
console.log("Database restored from backup.");
|
|
164
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* Upgrade the migration table from integer version to string version.
|
|
182
|
+
* This is needed for projects that were using the old migration system.
|
|
183
|
+
*/
|
|
184
|
+
async upgradeMigrationTableIfNeeded() {
|
|
185
|
+
this.requireClient();
|
|
186
|
+
const hasMigrationTable = await this._client.schema.hasTable("migration");
|
|
187
|
+
if (!hasMigrationTable)
|
|
188
|
+
return;
|
|
189
|
+
const columnInfo = await this._client("migration").columnInfo("version");
|
|
190
|
+
const columnType = columnInfo?.type ?? "";
|
|
191
|
+
// Check if version column is integer type (varies by database)
|
|
192
|
+
const isIntegerType = columnType.includes("int") ||
|
|
193
|
+
columnType.includes("INT") ||
|
|
194
|
+
columnType === "integer" ||
|
|
195
|
+
columnType === "INTEGER";
|
|
196
|
+
if (!isIntegerType)
|
|
197
|
+
return;
|
|
198
|
+
// Migrate: convert integer versions to strings
|
|
199
|
+
// SQLite doesn't support column alterations well, so we use a temp column approach
|
|
200
|
+
await this._client.schema.alterTable("migration", (t) => {
|
|
201
|
+
t.string("version_new");
|
|
202
|
+
});
|
|
203
|
+
await this._client("migration").update({
|
|
204
|
+
version_new: this._client.raw("CAST(version AS TEXT)"),
|
|
205
|
+
});
|
|
206
|
+
await this._client.schema.alterTable("migration", (t) => {
|
|
207
|
+
t.dropColumn("version");
|
|
208
|
+
});
|
|
209
|
+
await this._client.schema.alterTable("migration", (t) => {
|
|
210
|
+
t.renameColumn("version_new", "version");
|
|
211
|
+
});
|
|
212
|
+
this.config !== false &&
|
|
213
|
+
this.config.logger?.log("Upgraded migration table: version column converted to string");
|
|
214
|
+
}
|
|
165
215
|
}
|