@ghom/orm 2.0.0 → 2.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/biome.json +7 -0
- package/dist/app/migration.d.ts +171 -0
- package/dist/app/migration.js +198 -0
- package/dist/app/orm.d.ts +21 -1
- package/dist/app/orm.js +51 -1
- package/dist/app/table.d.ts +75 -15
- package/dist/app/table.js +96 -10
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +2 -3
- package/readme.md +91 -19
- package/tests/orm.test.ts +144 -1
- package/tests/tables/d.ts +0 -4
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,171 @@
|
|
|
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
|
+
* Extract all "From" keys from a union of TypedMigration.
|
|
22
|
+
* These are the columns that will be removed/renamed.
|
|
23
|
+
*/
|
|
24
|
+
type ExtractFromKeys<M> = M extends TypedMigration<infer From, any> ? keyof From : never;
|
|
25
|
+
/**
|
|
26
|
+
* Extract all "To" types from a union of TypedMigration and intersect them.
|
|
27
|
+
* These are the columns that will be added.
|
|
28
|
+
*/
|
|
29
|
+
type ExtractToTypes<M> = M extends TypedMigration<any, infer To> ? To : never;
|
|
30
|
+
/**
|
|
31
|
+
* Convert a union to an intersection.
|
|
32
|
+
* Used to combine all "To" types from migrations.
|
|
33
|
+
*/
|
|
34
|
+
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
|
|
35
|
+
/**
|
|
36
|
+
* Apply all migrations to compute the final type.
|
|
37
|
+
* 1. Remove all columns specified in migration "From" types
|
|
38
|
+
* 2. Add all columns specified in migration "To" types
|
|
39
|
+
*/
|
|
40
|
+
export type ApplyMigrations<Base, Migrations extends Record<string, TypedMigration<any, any>>> = Migrations[keyof Migrations] extends infer M ? Omit<Base, ExtractFromKeys<M>> & UnionToIntersection<ExtractToTypes<M>> : Base;
|
|
41
|
+
/**
|
|
42
|
+
* Compute the final table type from base columns and migrations.
|
|
43
|
+
*/
|
|
44
|
+
export type FinalTableType<Columns extends Record<string, ColumnDef<any, any>>, Migrations extends Record<string, TypedMigration<any, any>> = {}> = ApplyMigrations<InferColumns<Columns>, Migrations>;
|
|
45
|
+
/**
|
|
46
|
+
* Migration helpers for creating typed migrations.
|
|
47
|
+
* Each helper returns a TypedMigration with appropriate type transformations.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* import { migrate, col } from "@ghom/orm"
|
|
52
|
+
*
|
|
53
|
+
* const userTable = new Table({
|
|
54
|
+
* name: "user",
|
|
55
|
+
* columns: (col) => ({
|
|
56
|
+
* id: col.increments(),
|
|
57
|
+
* name: col.string(),
|
|
58
|
+
* }),
|
|
59
|
+
* migrations: {
|
|
60
|
+
* "001_rename_name": migrate.renameColumn("name", "username"),
|
|
61
|
+
* "002_add_email": migrate.addColumn("email", col.string()),
|
|
62
|
+
* },
|
|
63
|
+
* })
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export declare const migrate: {
|
|
67
|
+
/**
|
|
68
|
+
* Add a new column to the table.
|
|
69
|
+
*
|
|
70
|
+
* @param name - The column name
|
|
71
|
+
* @param column - The column definition
|
|
72
|
+
* @returns A typed migration that adds the column
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* migrate.addColumn("email", col.string())
|
|
76
|
+
* // Adds: { email: string }
|
|
77
|
+
*/
|
|
78
|
+
addColumn<K extends string, C extends ColumnDef<any, any>>(name: K, column: C): TypedMigration<{}, { [P in K]: InferColumnType<C>; }>;
|
|
79
|
+
/**
|
|
80
|
+
* Drop a column from the table.
|
|
81
|
+
*
|
|
82
|
+
* @param name - The column name to drop
|
|
83
|
+
* @returns A typed migration that removes the column
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* migrate.dropColumn("oldField")
|
|
87
|
+
* // Removes: { oldField: any }
|
|
88
|
+
*/
|
|
89
|
+
dropColumn<K extends string>(name: K): TypedMigration<{ [P in K]: unknown; }, {}>;
|
|
90
|
+
/**
|
|
91
|
+
* Rename a column.
|
|
92
|
+
*
|
|
93
|
+
* @param oldName - The current column name
|
|
94
|
+
* @param newName - The new column name
|
|
95
|
+
* @returns A typed migration that renames the column
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* migrate.renameColumn("name", "username")
|
|
99
|
+
* // Removes: { name: any }, Adds: { username: any }
|
|
100
|
+
*/
|
|
101
|
+
renameColumn<Old extends string, New extends string>(oldName: Old, newName: New): TypedMigration<{ [P in Old]: unknown; }, { [P in New]: unknown; }>;
|
|
102
|
+
/**
|
|
103
|
+
* Alter a column's type or constraints.
|
|
104
|
+
*
|
|
105
|
+
* @param name - The column name
|
|
106
|
+
* @param column - The new column definition
|
|
107
|
+
* @returns A typed migration that alters the column
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* migrate.alterColumn("age", col.integer().nullable())
|
|
111
|
+
* // Changes type: { age: number | null }
|
|
112
|
+
*/
|
|
113
|
+
alterColumn<K extends string, C extends ColumnDef<any, any>>(name: K, column: C): TypedMigration<{ [P in K]: unknown; }, { [P in K]: InferColumnType<C>; }>;
|
|
114
|
+
/**
|
|
115
|
+
* Add an index on one or more columns.
|
|
116
|
+
*
|
|
117
|
+
* @param columns - Array of column names to index
|
|
118
|
+
* @param name - Optional index name
|
|
119
|
+
* @returns A typed migration (no type change)
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* migrate.addIndex(["email"], "idx_email")
|
|
123
|
+
*/
|
|
124
|
+
addIndex(columns: string[], name?: string): TypedMigration<{}, {}>;
|
|
125
|
+
/**
|
|
126
|
+
* Drop an index by name.
|
|
127
|
+
*
|
|
128
|
+
* @param name - The index name to drop
|
|
129
|
+
* @returns A typed migration (no type change)
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* migrate.dropIndex("idx_email")
|
|
133
|
+
*/
|
|
134
|
+
dropIndex(name: string): TypedMigration<{}, {}>;
|
|
135
|
+
/**
|
|
136
|
+
* Add a unique constraint on one or more columns.
|
|
137
|
+
*
|
|
138
|
+
* @param columns - Array of column names
|
|
139
|
+
* @param name - Optional constraint name
|
|
140
|
+
* @returns A typed migration (no type change)
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* migrate.addUnique(["email"], "uniq_email")
|
|
144
|
+
*/
|
|
145
|
+
addUnique(columns: string[], name?: string): TypedMigration<{}, {}>;
|
|
146
|
+
/**
|
|
147
|
+
* Drop a unique constraint by name.
|
|
148
|
+
*
|
|
149
|
+
* @param name - The constraint name to drop
|
|
150
|
+
* @returns A typed migration (no type change)
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* migrate.dropUnique("uniq_email")
|
|
154
|
+
*/
|
|
155
|
+
dropUnique(name: string): TypedMigration<{}, {}>;
|
|
156
|
+
/**
|
|
157
|
+
* Custom migration with a raw callback.
|
|
158
|
+
* Use this when the built-in helpers don't cover your use case.
|
|
159
|
+
*
|
|
160
|
+
* @param fn - The migration callback
|
|
161
|
+
* @returns A typed migration (no type change by default)
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* migrate.raw((builder) => {
|
|
165
|
+
* builder.dropColumn("temp")
|
|
166
|
+
* builder.string("new_col")
|
|
167
|
+
* })
|
|
168
|
+
*/
|
|
169
|
+
raw<From = {}, To = {}>(fn: (builder: Knex.AlterTableBuilder) => void): TypedMigration<From, To>;
|
|
170
|
+
};
|
|
171
|
+
export {};
|
|
@@ -0,0 +1,198 @@
|
|
|
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
|
+
};
|
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
|
}
|
package/dist/app/table.d.ts
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
import { CachedQuery } from "@ghom/query";
|
|
2
2
|
import type { Knex } from "knex";
|
|
3
3
|
import { type ColumnDef, type InferColumns } from "./column.js";
|
|
4
|
+
import type { FinalTableType, TypedMigration } from "./migration.js";
|
|
4
5
|
import type { ORM } from "./orm.js";
|
|
6
|
+
/**
|
|
7
|
+
* A migration can be either a callback function or a TypedMigration object.
|
|
8
|
+
*/
|
|
9
|
+
export type MigrationValue = ((builder: Knex.CreateTableBuilder) => void) | TypedMigration<any, any>;
|
|
5
10
|
export interface MigrationData {
|
|
6
11
|
table: string;
|
|
7
|
-
version:
|
|
12
|
+
version: string;
|
|
8
13
|
}
|
|
9
14
|
/**
|
|
10
|
-
* Table options with typed columns.
|
|
11
|
-
* Type is automatically inferred from the column definitions.
|
|
15
|
+
* Table options with typed columns and optional typed migrations.
|
|
16
|
+
* Type is automatically inferred from the column definitions and migrations.
|
|
17
|
+
*
|
|
18
|
+
* @template Columns - Record of column definitions
|
|
19
|
+
* @template Migrations - Record of migration definitions (optional)
|
|
12
20
|
*/
|
|
13
|
-
export interface TableOptions<Columns extends Record<string, ColumnDef<any, any
|
|
21
|
+
export interface TableOptions<Columns extends Record<string, ColumnDef<any, any>>, Migrations extends Record<string, MigrationValue> = {}> {
|
|
14
22
|
name: string;
|
|
15
23
|
description?: string;
|
|
16
24
|
priority?: number;
|
|
@@ -19,10 +27,30 @@ export interface TableOptions<Columns extends Record<string, ColumnDef<any, any>
|
|
|
19
27
|
* Default is `Infinity`.
|
|
20
28
|
*/
|
|
21
29
|
caching?: number;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Database migrations to apply.
|
|
32
|
+
*
|
|
33
|
+
* Supports three key patterns:
|
|
34
|
+
* - **Numeric keys** (`"1"`, `"2"`): Sorted numerically
|
|
35
|
+
* - **Numeric-prefixed keys** (`"001_init"`, `"002_add"`): Sorted by prefix
|
|
36
|
+
* - **Pure string keys** (`"init"`, `"add"`): Uses insertion order
|
|
37
|
+
*
|
|
38
|
+
* Can use either callback functions or TypedMigration objects.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* // Using callbacks
|
|
42
|
+
* migrations: {
|
|
43
|
+
* "1": (builder) => builder.dropColumn("oldField"),
|
|
44
|
+
* }
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* // Using typed migrations
|
|
48
|
+
* migrations: {
|
|
49
|
+
* "001_add_email": migrate.addColumn("email", col.string()),
|
|
50
|
+
* }
|
|
51
|
+
*/
|
|
52
|
+
migrations?: Migrations;
|
|
53
|
+
then?: (this: Table<Columns, Migrations>, table: Table<Columns, Migrations>) => unknown;
|
|
26
54
|
/**
|
|
27
55
|
* Typed columns definition with automatic type inference.
|
|
28
56
|
*
|
|
@@ -37,7 +65,7 @@ export interface TableOptions<Columns extends Record<string, ColumnDef<any, any>
|
|
|
37
65
|
columns: (col: typeof import("./column.js").col) => Columns;
|
|
38
66
|
}
|
|
39
67
|
/**
|
|
40
|
-
* Represents a database table with typed columns.
|
|
68
|
+
* Represents a database table with typed columns and migrations.
|
|
41
69
|
*
|
|
42
70
|
* @example
|
|
43
71
|
* const userTable = new Table({
|
|
@@ -49,15 +77,35 @@ export interface TableOptions<Columns extends Record<string, ColumnDef<any, any>
|
|
|
49
77
|
* }),
|
|
50
78
|
* })
|
|
51
79
|
* // Type is automatically inferred as { id: number; username: string; age: number | null }
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* // With typed migrations
|
|
83
|
+
* const userTable = new Table({
|
|
84
|
+
* name: "user",
|
|
85
|
+
* columns: (col) => ({
|
|
86
|
+
* id: col.increments(),
|
|
87
|
+
* name: col.string(),
|
|
88
|
+
* }),
|
|
89
|
+
* migrations: {
|
|
90
|
+
* "001_rename": migrate.renameColumn("name", "username"),
|
|
91
|
+
* "002_add_email": migrate.addColumn("email", col.string()),
|
|
92
|
+
* },
|
|
93
|
+
* })
|
|
94
|
+
* // Type includes migration transforms: { id: number; username: string; email: string }
|
|
52
95
|
*/
|
|
53
|
-
export declare class Table<Columns extends Record<string, ColumnDef<any, any>> = Record<string, ColumnDef<any, any
|
|
54
|
-
readonly options: TableOptions<Columns>;
|
|
96
|
+
export declare class Table<Columns extends Record<string, ColumnDef<any, any>> = Record<string, ColumnDef<any, any>>, Migrations extends Record<string, MigrationValue> = {}> {
|
|
97
|
+
readonly options: TableOptions<Columns, Migrations>;
|
|
55
98
|
orm?: ORM;
|
|
56
|
-
_whereCache?: CachedQuery<[
|
|
99
|
+
_whereCache?: CachedQuery<[
|
|
100
|
+
cb: (query: Table<Columns, Migrations>["query"]) => unknown
|
|
101
|
+
], unknown>;
|
|
57
102
|
_countCache?: CachedQuery<[where: string | null], number>;
|
|
58
|
-
constructor(options: TableOptions<Columns>);
|
|
59
|
-
/**
|
|
60
|
-
|
|
103
|
+
constructor(options: TableOptions<Columns, Migrations>);
|
|
104
|
+
/**
|
|
105
|
+
* The inferred TypeScript type for rows of this table.
|
|
106
|
+
* Includes base columns and all migration type transforms.
|
|
107
|
+
*/
|
|
108
|
+
readonly $type: Migrations extends Record<string, TypedMigration<any, any>> ? FinalTableType<Columns, Migrations> : InferColumns<Columns>;
|
|
61
109
|
private requireOrm;
|
|
62
110
|
get client(): Knex;
|
|
63
111
|
get query(): Knex.QueryBuilder<InferColumns<Columns>, {
|
|
@@ -82,5 +130,17 @@ export declare class Table<Columns extends Record<string, ColumnDef<any, any>> =
|
|
|
82
130
|
getColumnNames(): Promise<Array<keyof InferColumns<Columns> & string>>;
|
|
83
131
|
isEmpty(): Promise<boolean>;
|
|
84
132
|
make(orm: ORM): Promise<this>;
|
|
133
|
+
/**
|
|
134
|
+
* Get sorted migration keys based on their pattern.
|
|
135
|
+
* - Pure numeric keys ("1", "2", "10") are sorted numerically
|
|
136
|
+
* - Numeric-prefixed keys ("001_add", "010_rename") are sorted by prefix
|
|
137
|
+
* - Pure string keys ("add_email", "rename") use insertion order or alphabetical
|
|
138
|
+
*/
|
|
139
|
+
private getMigrationKeys;
|
|
140
|
+
/**
|
|
141
|
+
* Compare migration keys for determining if one is greater than another.
|
|
142
|
+
* Handles both numeric and string comparisons appropriately.
|
|
143
|
+
*/
|
|
144
|
+
private compareMigrationKeys;
|
|
85
145
|
private migrate;
|
|
86
146
|
}
|
package/dist/app/table.js
CHANGED
|
@@ -2,7 +2,7 @@ import { CachedQuery } from "@ghom/query";
|
|
|
2
2
|
import { buildColumnsSchema, col } from "./column.js";
|
|
3
3
|
import { styled } from "./util.js";
|
|
4
4
|
/**
|
|
5
|
-
* Represents a database table with typed columns.
|
|
5
|
+
* Represents a database table with typed columns and migrations.
|
|
6
6
|
*
|
|
7
7
|
* @example
|
|
8
8
|
* const userTable = new Table({
|
|
@@ -14,6 +14,21 @@ import { styled } from "./util.js";
|
|
|
14
14
|
* }),
|
|
15
15
|
* })
|
|
16
16
|
* // Type is automatically inferred as { id: number; username: string; age: number | null }
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* // With typed migrations
|
|
20
|
+
* const userTable = new Table({
|
|
21
|
+
* name: "user",
|
|
22
|
+
* columns: (col) => ({
|
|
23
|
+
* id: col.increments(),
|
|
24
|
+
* name: col.string(),
|
|
25
|
+
* }),
|
|
26
|
+
* migrations: {
|
|
27
|
+
* "001_rename": migrate.renameColumn("name", "username"),
|
|
28
|
+
* "002_add_email": migrate.addColumn("email", col.string()),
|
|
29
|
+
* },
|
|
30
|
+
* })
|
|
31
|
+
* // Type includes migration transforms: { id: number; username: string; email: string }
|
|
17
32
|
*/
|
|
18
33
|
export class Table {
|
|
19
34
|
options;
|
|
@@ -120,27 +135,98 @@ export class Table {
|
|
|
120
135
|
}
|
|
121
136
|
return this;
|
|
122
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Get sorted migration keys based on their pattern.
|
|
140
|
+
* - Pure numeric keys ("1", "2", "10") are sorted numerically
|
|
141
|
+
* - Numeric-prefixed keys ("001_add", "010_rename") are sorted by prefix
|
|
142
|
+
* - Pure string keys ("add_email", "rename") use insertion order or alphabetical
|
|
143
|
+
*/
|
|
144
|
+
getMigrationKeys() {
|
|
145
|
+
const keys = Object.keys(this.options.migrations ?? {});
|
|
146
|
+
if (keys.length === 0)
|
|
147
|
+
return [];
|
|
148
|
+
// Detect key type patterns
|
|
149
|
+
const allPureNumeric = keys.every((k) => /^\d+$/.test(k));
|
|
150
|
+
const allNumericPrefix = keys.every((k) => /^\d+/.test(k));
|
|
151
|
+
const allPureString = keys.every((k) => !/^\d/.test(k));
|
|
152
|
+
// Validate: no mixing allowed
|
|
153
|
+
if (!allPureNumeric && !allNumericPrefix && !allPureString) {
|
|
154
|
+
throw new Error(`Table "${this.options.name}": Cannot mix migration key patterns. ` +
|
|
155
|
+
`Use one of: pure numbers (1, 2), prefixed strings ("001_x", "002_y"), or pure strings ("add_x").`);
|
|
156
|
+
}
|
|
157
|
+
if (allPureNumeric) {
|
|
158
|
+
// Sort purely numeric keys numerically: 1, 2, 10, 20
|
|
159
|
+
return keys.sort((a, b) => Number(a) - Number(b));
|
|
160
|
+
}
|
|
161
|
+
if (allNumericPrefix) {
|
|
162
|
+
// Sort by numeric prefix: "001_x" < "002_y" < "010_z"
|
|
163
|
+
const getNumericPrefix = (key) => parseInt(key.match(/^(\d+)/)?.[1] ?? "0", 10);
|
|
164
|
+
return keys.sort((a, b) => getNumericPrefix(a) - getNumericPrefix(b));
|
|
165
|
+
}
|
|
166
|
+
// Pure strings: alphabetical order OR insertion order
|
|
167
|
+
// Get ORM config if available (and not false for unconnected ORM)
|
|
168
|
+
const ormConfig = this.orm?.config === false ? undefined : this.orm?.config;
|
|
169
|
+
if (ormConfig?.migrations?.alphabeticalOrder) {
|
|
170
|
+
return keys.sort((a, b) => a.localeCompare(b));
|
|
171
|
+
}
|
|
172
|
+
// Warning for insertion order (Git merge risks)
|
|
173
|
+
if (keys.length > 1 && ormConfig) {
|
|
174
|
+
ormConfig.logger?.warn?.(`Table "${this.options.name}": Using insertion order for string migration keys. ` +
|
|
175
|
+
`This may cause issues with Git merges. Consider using numeric prefixes (e.g., "001_init").`);
|
|
176
|
+
}
|
|
177
|
+
return keys; // Insertion order (ES2015+)
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Compare migration keys for determining if one is greater than another.
|
|
181
|
+
* Handles both numeric and string comparisons appropriately.
|
|
182
|
+
*/
|
|
183
|
+
compareMigrationKeys(a, b) {
|
|
184
|
+
const aIsNumeric = /^\d+$/.test(a);
|
|
185
|
+
const bIsNumeric = /^\d+$/.test(b);
|
|
186
|
+
if (aIsNumeric && bIsNumeric) {
|
|
187
|
+
return Number(a) - Number(b);
|
|
188
|
+
}
|
|
189
|
+
const aHasPrefix = /^\d+/.test(a);
|
|
190
|
+
const bHasPrefix = /^\d+/.test(b);
|
|
191
|
+
if (aHasPrefix && bHasPrefix) {
|
|
192
|
+
const aPrefix = parseInt(a.match(/^(\d+)/)?.[1] ?? "0", 10);
|
|
193
|
+
const bPrefix = parseInt(b.match(/^(\d+)/)?.[1] ?? "0", 10);
|
|
194
|
+
return aPrefix - bPrefix;
|
|
195
|
+
}
|
|
196
|
+
return a.localeCompare(b);
|
|
197
|
+
}
|
|
123
198
|
async migrate() {
|
|
124
199
|
if (!this.options.migrations)
|
|
125
200
|
return false;
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
201
|
+
const sortedKeys = this.getMigrationKeys();
|
|
202
|
+
if (sortedKeys.length === 0)
|
|
203
|
+
return false;
|
|
204
|
+
const migrations = this.options.migrations;
|
|
129
205
|
const fromDatabase = await this.client("migration")
|
|
130
206
|
.where("table", this.options.name)
|
|
131
207
|
.first();
|
|
132
208
|
const data = fromDatabase || {
|
|
133
209
|
table: this.options.name,
|
|
134
|
-
version:
|
|
210
|
+
version: "",
|
|
135
211
|
};
|
|
136
212
|
const baseVersion = data.version;
|
|
137
|
-
for (const
|
|
213
|
+
for (const key of sortedKeys) {
|
|
214
|
+
// Skip migrations that have already been applied
|
|
215
|
+
if (data.version !== "" && this.compareMigrationKeys(key, data.version) <= 0) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
const migration = migrations[key];
|
|
138
219
|
await this.client.schema.alterTable(this.options.name, (builder) => {
|
|
139
|
-
if (
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
220
|
+
if (typeof migration === "function") {
|
|
221
|
+
// Callback function migration
|
|
222
|
+
migration(builder);
|
|
223
|
+
}
|
|
224
|
+
else if (migration && typeof migration === "object" && "apply" in migration) {
|
|
225
|
+
// TypedMigration object
|
|
226
|
+
migration.apply(builder);
|
|
227
|
+
}
|
|
143
228
|
});
|
|
229
|
+
data.version = key;
|
|
144
230
|
}
|
|
145
231
|
await this.client("migration").insert(data).onConflict("table").merge();
|
|
146
232
|
return baseVersion === data.version ? false : data.version;
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ghom/orm",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
"description": "TypeScript KnexJS ORM & handler",
|
|
9
9
|
"homepage": "https://github.com/GhomKrosmonaute/orm",
|
|
10
10
|
"scripts": {
|
|
11
|
-
"format": "biome format --write
|
|
12
|
-
"lint": "biome lint .",
|
|
11
|
+
"format": "biome format --write .",
|
|
13
12
|
"check": "biome check --write .",
|
|
14
13
|
"build": "rimraf dist && tsc",
|
|
15
14
|
"test": "bun test",
|
package/readme.md
CHANGED
|
@@ -62,46 +62,117 @@ orm.raw("SELECT 1") // throws Error
|
|
|
62
62
|
|
|
63
63
|
## Add tables
|
|
64
64
|
|
|
65
|
-
The tables are automatically loaded from the `
|
|
65
|
+
The tables are automatically loaded from the `tableLocation` directory. Types are automatically inferred from the column definitions.
|
|
66
66
|
|
|
67
67
|
```typescript
|
|
68
68
|
// tables/user.ts
|
|
69
69
|
|
|
70
|
-
import { Table } from "@ghom/orm"
|
|
70
|
+
import { Table, col } from "@ghom/orm"
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
username: string
|
|
74
|
-
password: string
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export default new Table<User>({
|
|
72
|
+
export default new Table({
|
|
78
73
|
name: "user",
|
|
79
74
|
|
|
80
75
|
// the higher the priority, the earlier the table is compiled
|
|
81
76
|
priority: 0,
|
|
82
77
|
|
|
83
|
-
//
|
|
78
|
+
// typed columns definition with automatic type inference
|
|
79
|
+
columns: (col) => ({
|
|
80
|
+
id: col.increments(),
|
|
81
|
+
username: col.string().unique(),
|
|
82
|
+
password: col.string(),
|
|
83
|
+
age: col.integer().nullable(),
|
|
84
|
+
role: col.enum(["admin", "user"]).defaultTo("user"),
|
|
85
|
+
}),
|
|
86
|
+
|
|
87
|
+
// migrations are executed in order based on key pattern (see Migration Keys section)
|
|
84
88
|
migrations: {
|
|
85
|
-
1: (table) => {
|
|
89
|
+
"1": (table) => {
|
|
86
90
|
table.renameColumn("name", "username")
|
|
87
91
|
}
|
|
88
92
|
},
|
|
89
93
|
|
|
90
|
-
// the
|
|
91
|
-
setup: (table) => {
|
|
92
|
-
table.string("name").notNullable()
|
|
93
|
-
table.string("password").notNullable()
|
|
94
|
-
},
|
|
95
|
-
|
|
96
|
-
// the then is executed after the table is created and the migrations are runned
|
|
94
|
+
// then is executed after the table is created and the migrations are run (only if table is empty)
|
|
97
95
|
then: ({ query }) => {
|
|
98
|
-
query.insert({ username: "admin", password: "admin" })
|
|
96
|
+
query.insert({ username: "admin", password: "admin", role: "admin" })
|
|
99
97
|
},
|
|
100
98
|
|
|
101
99
|
caching: 10 * 60 * 1000 // The table cache. Default to the ORM cache or Infinity
|
|
102
100
|
})
|
|
101
|
+
|
|
102
|
+
// Type is automatically inferred:
|
|
103
|
+
// { id: number; username: string; password: string; age: number | null; role: "admin" | "user" }
|
|
104
|
+
type User = typeof userTable.$type
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Typed Migrations
|
|
108
|
+
|
|
109
|
+
You can also use typed migrations that automatically update the TypeScript type:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
import { Table, col, migrate } from "@ghom/orm"
|
|
113
|
+
|
|
114
|
+
export default new Table({
|
|
115
|
+
name: "user",
|
|
116
|
+
columns: (col) => ({
|
|
117
|
+
id: col.increments(),
|
|
118
|
+
name: col.string(), // will be renamed to username
|
|
119
|
+
}),
|
|
120
|
+
migrations: {
|
|
121
|
+
"001_rename_name": migrate.renameColumn("name", "username"),
|
|
122
|
+
"002_add_email": migrate.addColumn("email", col.string()),
|
|
123
|
+
"003_add_age": migrate.addColumn("age", col.integer().nullable()),
|
|
124
|
+
},
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// Final type: { id: number; username: string; email: string; age: number | null }
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Available migration helpers:
|
|
131
|
+
- `migrate.addColumn(name, columnDef)` - Add a new column
|
|
132
|
+
- `migrate.dropColumn(name)` - Remove a column
|
|
133
|
+
- `migrate.renameColumn(oldName, newName)` - Rename a column
|
|
134
|
+
- `migrate.alterColumn(name, newColumnDef)` - Change column type/constraints
|
|
135
|
+
- `migrate.addIndex(columns, name?)` - Add an index
|
|
136
|
+
- `migrate.dropIndex(name)` - Remove an index
|
|
137
|
+
- `migrate.addUnique(columns, name?)` - Add a unique constraint
|
|
138
|
+
- `migrate.dropUnique(name)` - Remove a unique constraint
|
|
139
|
+
- `migrate.raw(callback)` - Custom migration callback
|
|
140
|
+
|
|
141
|
+
## Migration Keys
|
|
142
|
+
|
|
143
|
+
The ORM supports three patterns for migration keys:
|
|
144
|
+
|
|
145
|
+
1. **Numeric keys** (`"1"`, `"2"`, `"10"`): Sorted numerically
|
|
146
|
+
2. **Numeric-prefixed keys** (`"001_init"`, `"002_add_users"`, `"010_fix"`): Sorted by numeric prefix
|
|
147
|
+
3. **Pure string keys** (`"init"`, `"add_users"`): Uses insertion order (ES2015+)
|
|
148
|
+
|
|
149
|
+
> **Warning**: Mixing key patterns is not allowed and will throw an error at runtime.
|
|
150
|
+
|
|
151
|
+
### Migration Configuration
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
const orm = new ORM({
|
|
155
|
+
tableLocation: "./tables",
|
|
156
|
+
migrations: {
|
|
157
|
+
/**
|
|
158
|
+
* NOT RECOMMENDED
|
|
159
|
+
* Force alphabetical sorting for string migration keys.
|
|
160
|
+
*
|
|
161
|
+
* If your keys start with numbers (e.g., "001_init"),
|
|
162
|
+
* they are automatically sorted by those numbers,
|
|
163
|
+
* not alphabetically.
|
|
164
|
+
*/
|
|
165
|
+
alphabeticalOrder: false // default
|
|
166
|
+
}
|
|
167
|
+
})
|
|
103
168
|
```
|
|
104
169
|
|
|
170
|
+
### ES2015+ Requirement
|
|
171
|
+
|
|
172
|
+
This ORM requires ES2015+ for guaranteed object key insertion order. Node.js 6+ and all modern browsers are supported.
|
|
173
|
+
|
|
174
|
+
The ORM performs a runtime check on initialization and will throw an error if the environment doesn't support ES2015+ key ordering.
|
|
175
|
+
|
|
105
176
|
## Launch a query
|
|
106
177
|
|
|
107
178
|
For more information about the query builder, see [knexjs.org](https://knexjs.org/).
|
|
@@ -192,8 +263,9 @@ The cache of the `<ORM>.cache.raw` method is automatically invalidated when the
|
|
|
192
263
|
|
|
193
264
|
- [x] Add timed caching system
|
|
194
265
|
- [x] Add backup option
|
|
266
|
+
- [x] Auto typings for tables from the column definitions
|
|
267
|
+
- [x] Typed migrations with automatic type inference
|
|
195
268
|
- [ ] Dependency management between tables
|
|
196
|
-
- [ ] Auto typings for tables from the column definitions
|
|
197
269
|
- [ ] Add specific methods for relations and joins
|
|
198
270
|
- [ ] Add admin panel
|
|
199
271
|
- [ ] Make possible to switch the data between all possible clients (pg, mysql, sqlite3)
|
package/tests/orm.test.ts
CHANGED
|
@@ -3,7 +3,7 @@ import fs from "fs"
|
|
|
3
3
|
import path from "path"
|
|
4
4
|
import { rimraf } from "rimraf"
|
|
5
5
|
|
|
6
|
-
import { col, ORM, type ORMConfig, Table } from "../src"
|
|
6
|
+
import { col, migrate, ORM, type ORMConfig, Table } from "../src"
|
|
7
7
|
|
|
8
8
|
import a from "./tables/a"
|
|
9
9
|
import b from "./tables/b"
|
|
@@ -57,6 +57,149 @@ describe("typed columns", () => {
|
|
|
57
57
|
})
|
|
58
58
|
})
|
|
59
59
|
|
|
60
|
+
describe("typed migrations", () => {
|
|
61
|
+
test("migrate.addColumn creates TypedMigration", () => {
|
|
62
|
+
const migration = migrate.addColumn("email", col.string())
|
|
63
|
+
|
|
64
|
+
expect(migration).toBeDefined()
|
|
65
|
+
expect(migration.apply).toBeInstanceOf(Function)
|
|
66
|
+
expect("_from" in migration).toBe(true)
|
|
67
|
+
expect("_to" in migration).toBe(true)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test("migrate.dropColumn creates TypedMigration", () => {
|
|
71
|
+
const migration = migrate.dropColumn("oldField")
|
|
72
|
+
|
|
73
|
+
expect(migration).toBeDefined()
|
|
74
|
+
expect(migration.apply).toBeInstanceOf(Function)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test("migrate.renameColumn creates TypedMigration", () => {
|
|
78
|
+
const migration = migrate.renameColumn("name", "username")
|
|
79
|
+
|
|
80
|
+
expect(migration).toBeDefined()
|
|
81
|
+
expect(migration.apply).toBeInstanceOf(Function)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test("migrate.alterColumn creates TypedMigration", () => {
|
|
85
|
+
const migration = migrate.alterColumn("age", col.integer().nullable())
|
|
86
|
+
|
|
87
|
+
expect(migration).toBeDefined()
|
|
88
|
+
expect(migration.apply).toBeInstanceOf(Function)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test("migrate.addIndex creates TypedMigration", () => {
|
|
92
|
+
const migration = migrate.addIndex(["email"], "idx_email")
|
|
93
|
+
|
|
94
|
+
expect(migration).toBeDefined()
|
|
95
|
+
expect(migration.apply).toBeInstanceOf(Function)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test("migrate.addUnique creates TypedMigration", () => {
|
|
99
|
+
const migration = migrate.addUnique(["email"], "uniq_email")
|
|
100
|
+
|
|
101
|
+
expect(migration).toBeDefined()
|
|
102
|
+
expect(migration.apply).toBeInstanceOf(Function)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test("migrate.raw creates TypedMigration", () => {
|
|
106
|
+
const migration = migrate.raw((builder) => {
|
|
107
|
+
builder.dropColumn("temp")
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
expect(migration).toBeDefined()
|
|
111
|
+
expect(migration.apply).toBeInstanceOf(Function)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test("Table with typed migrations has correct options", () => {
|
|
115
|
+
const userTable = new Table({
|
|
116
|
+
name: "test_typed_migrations",
|
|
117
|
+
columns: (col) => ({
|
|
118
|
+
id: col.increments(),
|
|
119
|
+
name: col.string(),
|
|
120
|
+
}),
|
|
121
|
+
migrations: {
|
|
122
|
+
"001_add_email": migrate.addColumn("email", col.string()),
|
|
123
|
+
"002_add_age": migrate.addColumn("age", col.integer().nullable()),
|
|
124
|
+
"003_rename_name": migrate.renameColumn("name", "username"),
|
|
125
|
+
},
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
expect(userTable).toBeInstanceOf(Table)
|
|
129
|
+
expect(userTable.options.migrations).toBeDefined()
|
|
130
|
+
expect(Object.keys(userTable.options.migrations!).length).toBe(3)
|
|
131
|
+
|
|
132
|
+
// Type inference check - final type includes base columns + migrations
|
|
133
|
+
// "name" is removed by renameColumn, "username" is added
|
|
134
|
+
type ExpectedType = typeof userTable.$type
|
|
135
|
+
const _typeCheck: ExpectedType = {
|
|
136
|
+
id: 1,
|
|
137
|
+
username: "test", // renamed from "name"
|
|
138
|
+
// @ts-expect-error - name is removed by renameColumn
|
|
139
|
+
name: "test",
|
|
140
|
+
email: "test@example.com",
|
|
141
|
+
age: null,
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
describe("migration key patterns", () => {
|
|
147
|
+
test("Table accepts pure numeric keys", () => {
|
|
148
|
+
const table = new Table({
|
|
149
|
+
name: "test_numeric_keys",
|
|
150
|
+
columns: (col) => ({
|
|
151
|
+
id: col.increments(),
|
|
152
|
+
}),
|
|
153
|
+
migrations: {
|
|
154
|
+
"1": (_builder) => {},
|
|
155
|
+
"2": (_builder) => {},
|
|
156
|
+
"10": (_builder) => {},
|
|
157
|
+
},
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
expect(table.options.migrations).toBeDefined()
|
|
161
|
+
expect(Object.keys(table.options.migrations!)).toEqual(["1", "2", "10"])
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test("Table accepts numeric-prefixed keys", () => {
|
|
165
|
+
const table = new Table({
|
|
166
|
+
name: "test_prefixed_keys",
|
|
167
|
+
columns: (col) => ({
|
|
168
|
+
id: col.increments(),
|
|
169
|
+
}),
|
|
170
|
+
migrations: {
|
|
171
|
+
"001_init": (_builder) => {},
|
|
172
|
+
"002_add_column": (_builder) => {},
|
|
173
|
+
"010_fix": (_builder) => {},
|
|
174
|
+
},
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
expect(table.options.migrations).toBeDefined()
|
|
178
|
+
expect(Object.keys(table.options.migrations!)).toEqual([
|
|
179
|
+
"001_init",
|
|
180
|
+
"002_add_column",
|
|
181
|
+
"010_fix",
|
|
182
|
+
])
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test("Table accepts pure string keys", () => {
|
|
186
|
+
const table = new Table({
|
|
187
|
+
name: "test_string_keys",
|
|
188
|
+
columns: (col) => ({
|
|
189
|
+
id: col.increments(),
|
|
190
|
+
}),
|
|
191
|
+
migrations: {
|
|
192
|
+
init: (_builder) => {},
|
|
193
|
+
add_column: (_builder) => {},
|
|
194
|
+
fix: (_builder) => {},
|
|
195
|
+
},
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
expect(table.options.migrations).toBeDefined()
|
|
199
|
+
expect(Object.keys(table.options.migrations!)).toEqual(["init", "add_column", "fix"])
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
|
|
60
203
|
describe("unconnected ORM", () => {
|
|
61
204
|
test("can be initialized with false", () => {
|
|
62
205
|
const unconnectedOrm = new ORM(false)
|