@ghom/orm 2.1.0 → 2.1.2
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/dist/app/migration.d.ts +91 -4
- package/dist/app/migration.js +31 -0
- package/dist/app/table.d.ts +19 -10
- package/dist/app/table.js +21 -10
- package/package.json +3 -3
- package/readme.md +173 -35
- package/tests/orm.test.ts +118 -9
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +2 -2
package/dist/app/migration.d.ts
CHANGED
|
@@ -17,31 +17,98 @@ export interface TypedMigration<From = {}, To = {}> {
|
|
|
17
17
|
*/
|
|
18
18
|
apply: (builder: Knex.AlterTableBuilder) => void;
|
|
19
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;
|
|
20
50
|
/**
|
|
21
51
|
* Extract all "From" keys from a union of TypedMigration.
|
|
22
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.
|
|
23
55
|
*/
|
|
24
|
-
type ExtractFromKeys<M> = M extends
|
|
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;
|
|
25
64
|
/**
|
|
26
65
|
* Extract all "To" types from a union of TypedMigration and intersect them.
|
|
27
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.
|
|
28
69
|
*/
|
|
29
|
-
type ExtractToTypes<M> = M extends
|
|
70
|
+
type ExtractToTypes<M> = M extends any ? UnwrapMigrationArray<M> extends infer U ? U extends any ? ExtractToTypesSingle<U> : never : never : never;
|
|
30
71
|
/**
|
|
31
72
|
* Convert a union to an intersection.
|
|
32
73
|
* Used to combine all "To" types from migrations.
|
|
33
74
|
*/
|
|
34
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>>>;
|
|
35
100
|
/**
|
|
36
101
|
* Apply all migrations to compute the final type.
|
|
37
102
|
* 1. Remove all columns specified in migration "From" types
|
|
38
103
|
* 2. Add all columns specified in migration "To" types
|
|
104
|
+
* Handles both single migrations and arrays of migrations.
|
|
39
105
|
*/
|
|
40
|
-
export type ApplyMigrations<Base, Migrations extends Record<string,
|
|
106
|
+
export type ApplyMigrations<Base, Migrations extends Record<string, any>> = Migrations[keyof Migrations] extends infer M ? Omit<Base, ExtractFromKeys<M>> & UnionToIntersection<ExtractToTypes<M>> : Base;
|
|
41
107
|
/**
|
|
42
108
|
* Compute the final table type from base columns and migrations.
|
|
109
|
+
* Supports both single migrations and arrays of migrations.
|
|
43
110
|
*/
|
|
44
|
-
export type FinalTableType<Columns extends Record<string, ColumnDef<any, any>>, Migrations extends Record<string,
|
|
111
|
+
export type FinalTableType<Columns extends Record<string, ColumnDef<any, any>>, Migrations extends Record<string, any> = {}> = ApplyMigrations<InferColumns<Columns>, Migrations>;
|
|
45
112
|
/**
|
|
46
113
|
* Migration helpers for creating typed migrations.
|
|
47
114
|
* Each helper returns a TypedMigration with appropriate type transformations.
|
|
@@ -167,5 +234,25 @@ export declare const migrate: {
|
|
|
167
234
|
* })
|
|
168
235
|
*/
|
|
169
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>;
|
|
170
257
|
};
|
|
171
258
|
export {};
|
package/dist/app/migration.js
CHANGED
|
@@ -195,4 +195,35 @@ export const migrate = {
|
|
|
195
195
|
apply: fn,
|
|
196
196
|
};
|
|
197
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
|
+
},
|
|
198
229
|
};
|
package/dist/app/table.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
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
|
+
import type { FinalTableType, TypedMigration, TypedMigrationSequence } from "./migration.js";
|
|
5
5
|
import type { ORM } from "./orm.js";
|
|
6
6
|
/**
|
|
7
|
-
* A migration
|
|
7
|
+
* A migration value is a TypedMigration or TypedMigrationSequence.
|
|
8
|
+
* Use migrate.sequence() to combine multiple migrations.
|
|
8
9
|
*/
|
|
9
|
-
export type MigrationValue =
|
|
10
|
+
export type MigrationValue = TypedMigration<any, any> | TypedMigrationSequence<any>;
|
|
10
11
|
export interface MigrationData {
|
|
11
12
|
table: string;
|
|
12
13
|
version: string;
|
|
@@ -28,25 +29,32 @@ export interface TableOptions<Columns extends Record<string, ColumnDef<any, any>
|
|
|
28
29
|
*/
|
|
29
30
|
caching?: number;
|
|
30
31
|
/**
|
|
31
|
-
* Database migrations to apply.
|
|
32
|
+
* Database migrations to apply using typed migrations.
|
|
32
33
|
*
|
|
33
34
|
* Supports three key patterns:
|
|
34
35
|
* - **Numeric keys** (`"1"`, `"2"`): Sorted numerically
|
|
35
36
|
* - **Numeric-prefixed keys** (`"001_init"`, `"002_add"`): Sorted by prefix
|
|
36
37
|
* - **Pure string keys** (`"init"`, `"add"`): Uses insertion order
|
|
37
38
|
*
|
|
38
|
-
*
|
|
39
|
+
* @example
|
|
40
|
+
* // Single migration
|
|
41
|
+
* migrations: {
|
|
42
|
+
* "001_add_email": migrate.addColumn("email", col.string()),
|
|
43
|
+
* }
|
|
39
44
|
*
|
|
40
45
|
* @example
|
|
41
|
-
* //
|
|
46
|
+
* // Multiple migrations in sequence
|
|
42
47
|
* migrations: {
|
|
43
|
-
* "
|
|
48
|
+
* "002_add_fields": migrate.sequence(
|
|
49
|
+
* migrate.addColumn("phone", col.string()),
|
|
50
|
+
* migrate.addColumn("address", col.string().nullable()),
|
|
51
|
+
* ),
|
|
44
52
|
* }
|
|
45
53
|
*
|
|
46
54
|
* @example
|
|
47
|
-
* //
|
|
55
|
+
* // Raw migration for advanced use cases
|
|
48
56
|
* migrations: {
|
|
49
|
-
* "
|
|
57
|
+
* "003_custom": migrate.raw((builder) => builder.dropColumn("oldField")),
|
|
50
58
|
* }
|
|
51
59
|
*/
|
|
52
60
|
migrations?: Migrations;
|
|
@@ -104,8 +112,9 @@ export declare class Table<Columns extends Record<string, ColumnDef<any, any>> =
|
|
|
104
112
|
/**
|
|
105
113
|
* The inferred TypeScript type for rows of this table.
|
|
106
114
|
* Includes base columns and all migration type transforms.
|
|
115
|
+
* Supports both single migrations and arrays of migrations.
|
|
107
116
|
*/
|
|
108
|
-
readonly $type:
|
|
117
|
+
readonly $type: FinalTableType<Columns, Migrations>;
|
|
109
118
|
private requireOrm;
|
|
110
119
|
get client(): Knex;
|
|
111
120
|
get query(): Knex.QueryBuilder<InferColumns<Columns>, {
|
package/dist/app/table.js
CHANGED
|
@@ -151,8 +151,26 @@ export class Table {
|
|
|
151
151
|
const allPureString = keys.every((k) => !/^\d/.test(k));
|
|
152
152
|
// Validate: no mixing allowed
|
|
153
153
|
if (!allPureNumeric && !allNumericPrefix && !allPureString) {
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
// Categorize keys for helpful error message
|
|
155
|
+
const numericKeys = keys.filter((k) => /^\d+$/.test(k));
|
|
156
|
+
const prefixedKeys = keys.filter((k) => /^\d+[_\-a-zA-Z]/.test(k));
|
|
157
|
+
const stringKeys = keys.filter((k) => !/^\d/.test(k));
|
|
158
|
+
const parts = [];
|
|
159
|
+
if (numericKeys.length > 0) {
|
|
160
|
+
parts.push(`numeric keys: ${numericKeys.map((k) => `"${k}"`).join(", ")}`);
|
|
161
|
+
}
|
|
162
|
+
if (prefixedKeys.length > 0) {
|
|
163
|
+
parts.push(`prefixed keys: ${prefixedKeys.map((k) => `"${k}"`).join(", ")}`);
|
|
164
|
+
}
|
|
165
|
+
if (stringKeys.length > 0) {
|
|
166
|
+
parts.push(`string keys: ${stringKeys.map((k) => `"${k}"`).join(", ")}`);
|
|
167
|
+
}
|
|
168
|
+
throw new Error(`Table "${this.options.name}": Migration keys use mixed patterns which prevents reliable ordering.\n\n` +
|
|
169
|
+
`Found: ${parts.join(" AND ")}\n\n` +
|
|
170
|
+
`Choose ONE pattern for all keys:\n` +
|
|
171
|
+
` - Pure numbers: "1", "2", "10" (sorted numerically)\n` +
|
|
172
|
+
` - Prefixed strings: "001_init", "002_add" (sorted by prefix)\n` +
|
|
173
|
+
` - Pure strings: "init", "add_email" (insertion order)`);
|
|
156
174
|
}
|
|
157
175
|
if (allPureNumeric) {
|
|
158
176
|
// Sort purely numeric keys numerically: 1, 2, 10, 20
|
|
@@ -217,14 +235,7 @@ export class Table {
|
|
|
217
235
|
}
|
|
218
236
|
const migration = migrations[key];
|
|
219
237
|
await this.client.schema.alterTable(this.options.name, (builder) => {
|
|
220
|
-
|
|
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
|
-
}
|
|
238
|
+
migration.apply(builder);
|
|
228
239
|
});
|
|
229
240
|
data.version = key;
|
|
230
241
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ghom/orm",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
"homepage": "https://github.com/GhomKrosmonaute/orm",
|
|
10
10
|
"scripts": {
|
|
11
11
|
"format": "biome format --write .",
|
|
12
|
-
"check": "biome check --write .",
|
|
13
|
-
"build": "rimraf dist && tsc",
|
|
12
|
+
"check": "biome check --write . && tsc --noEmit",
|
|
13
|
+
"build": "rimraf dist && tsc -p tsconfig.build.json",
|
|
14
14
|
"test": "bun test",
|
|
15
15
|
"prepublishOnly": "npm run check && npm run build && npm test"
|
|
16
16
|
},
|
package/readme.md
CHANGED
|
@@ -21,12 +21,12 @@ const orm = new ORM({
|
|
|
21
21
|
// tables directory
|
|
22
22
|
tableLocation: "./tables",
|
|
23
23
|
|
|
24
|
-
// knex config (sqlite3 by default)
|
|
25
|
-
database: {
|
|
24
|
+
// knex config (sqlite3 in-memory by default)
|
|
25
|
+
database: { client: "sqlite3", connection: { filename: ":memory:" } },
|
|
26
26
|
|
|
27
|
-
// custom logger (
|
|
27
|
+
// optional custom logger (must have log, error, warn methods)
|
|
28
28
|
logger: console,
|
|
29
|
-
|
|
29
|
+
loggerStyles: { highlight: "cyan", rawValue: "yellow", description: "dim" },
|
|
30
30
|
|
|
31
31
|
// caching options for all tables and rawCache queries (default to Infinity)
|
|
32
32
|
caching: 10 * 60 * 1000,
|
|
@@ -34,6 +34,12 @@ const orm = new ORM({
|
|
|
34
34
|
// configuration for the database backups
|
|
35
35
|
backups: {
|
|
36
36
|
location: "./backups",
|
|
37
|
+
chunkSize: 1000, // rows per backup file chunk
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
// migration behavior configuration
|
|
41
|
+
migrations: {
|
|
42
|
+
alphabeticalOrder: false // default
|
|
37
43
|
}
|
|
38
44
|
})
|
|
39
45
|
|
|
@@ -67,11 +73,14 @@ The tables are automatically loaded from the `tableLocation` directory. Types ar
|
|
|
67
73
|
```typescript
|
|
68
74
|
// tables/user.ts
|
|
69
75
|
|
|
70
|
-
import { Table, col } from "@ghom/orm"
|
|
76
|
+
import { Table, col, migrate } from "@ghom/orm"
|
|
71
77
|
|
|
72
78
|
export default new Table({
|
|
73
79
|
name: "user",
|
|
74
80
|
|
|
81
|
+
// optional description for logging
|
|
82
|
+
description: "User accounts",
|
|
83
|
+
|
|
75
84
|
// the higher the priority, the earlier the table is compiled
|
|
76
85
|
priority: 0,
|
|
77
86
|
|
|
@@ -81,32 +90,108 @@ export default new Table({
|
|
|
81
90
|
username: col.string().unique(),
|
|
82
91
|
password: col.string(),
|
|
83
92
|
age: col.integer().nullable(),
|
|
84
|
-
role: col.enum(["admin", "user"]).defaultTo("user"),
|
|
93
|
+
role: col.enum(["admin", "user"] as const).defaultTo("user"),
|
|
85
94
|
}),
|
|
86
95
|
|
|
87
96
|
// migrations are executed in order based on key pattern (see Migration Keys section)
|
|
88
97
|
migrations: {
|
|
89
|
-
"
|
|
90
|
-
table.renameColumn("name", "username")
|
|
91
|
-
}
|
|
98
|
+
"001_add_email": migrate.addColumn("email", col.string()),
|
|
92
99
|
},
|
|
93
100
|
|
|
94
101
|
// then is executed after the table is created and the migrations are run (only if table is empty)
|
|
95
|
-
then: (
|
|
96
|
-
query.insert({ username: "admin", password: "admin", role: "admin" })
|
|
102
|
+
then: (table) => {
|
|
103
|
+
table.query.insert({ username: "admin", password: "admin", role: "admin", email: "admin@admin.com" })
|
|
97
104
|
},
|
|
98
105
|
|
|
99
106
|
caching: 10 * 60 * 1000 // The table cache. Default to the ORM cache or Infinity
|
|
100
107
|
})
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The type is automatically inferred from columns + migrations:
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// { id: number; username: string; password: string; age: number | null; role: "admin" | "user"; email: string }
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
You can export and use the type from another file:
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
// somewhere else in your code
|
|
120
|
+
import userTable from "./tables/user"
|
|
101
121
|
|
|
102
|
-
// Type is automatically inferred:
|
|
103
|
-
// { id: number; username: string; password: string; age: number | null; role: "admin" | "user" }
|
|
104
122
|
type User = typeof userTable.$type
|
|
105
123
|
```
|
|
106
124
|
|
|
107
|
-
|
|
125
|
+
## Column Types
|
|
126
|
+
|
|
127
|
+
All available column types with their TypeScript types:
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
import { col } from "@ghom/orm"
|
|
131
|
+
|
|
132
|
+
// Numeric types
|
|
133
|
+
col.increments() // number - auto-incrementing primary key
|
|
134
|
+
col.bigIncrements() // bigint - big auto-incrementing primary key
|
|
135
|
+
col.integer() // number
|
|
136
|
+
col.bigInteger() // bigint
|
|
137
|
+
col.tinyint() // number (0-255)
|
|
138
|
+
col.smallint() // number
|
|
139
|
+
col.mediumint() // number
|
|
140
|
+
col.float(precision?, scale?) // number
|
|
141
|
+
col.double(precision?, scale?) // number
|
|
142
|
+
col.decimal(precision?, scale?) // number
|
|
143
|
+
|
|
144
|
+
// String types
|
|
145
|
+
col.string(length?) // string (default: 255)
|
|
146
|
+
col.text(textType?) // string - "text" | "mediumtext" | "longtext"
|
|
147
|
+
col.uuid() // string
|
|
148
|
+
|
|
149
|
+
// Boolean
|
|
150
|
+
col.boolean() // boolean
|
|
151
|
+
|
|
152
|
+
// Date/Time types
|
|
153
|
+
col.date() // Date
|
|
154
|
+
col.datetime(options?) // Date - { useTz?: boolean; precision?: number }
|
|
155
|
+
col.timestamp(options?) // Date - { useTz?: boolean; precision?: number }
|
|
156
|
+
col.time() // string
|
|
157
|
+
|
|
158
|
+
// Other types
|
|
159
|
+
col.binary(length?) // Buffer
|
|
160
|
+
col.enum(values) // union of values - col.enum(["a", "b"] as const) => "a" | "b"
|
|
161
|
+
col.json<T>() // T (default: unknown)
|
|
162
|
+
col.jsonb<T>() // T (PostgreSQL)
|
|
163
|
+
col.specificType<T>(type) // T - database-specific type
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Column Modifiers
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
col.string()
|
|
170
|
+
.nullable() // allows null values
|
|
171
|
+
.defaultTo(value) // sets default value
|
|
172
|
+
.unique() // adds unique constraint
|
|
173
|
+
.primary() // sets as primary key
|
|
174
|
+
.index(indexName?) // adds an index
|
|
175
|
+
.comment(comment) // adds a column comment
|
|
176
|
+
.collate(collation) // sets collation
|
|
177
|
+
|
|
178
|
+
col.integer()
|
|
179
|
+
.unsigned() // only positive values (numeric columns only)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Foreign Key References
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
col.integer()
|
|
186
|
+
.references("id") // column name in referenced table
|
|
187
|
+
.inTable("users") // referenced table name
|
|
188
|
+
.onDelete("CASCADE") // CASCADE | SET NULL | RESTRICT | NO ACTION
|
|
189
|
+
.onUpdate("CASCADE") // CASCADE | SET NULL | RESTRICT | NO ACTION
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Typed Migrations
|
|
108
193
|
|
|
109
|
-
|
|
194
|
+
Use typed migrations that automatically update the TypeScript type:
|
|
110
195
|
|
|
111
196
|
```typescript
|
|
112
197
|
import { Table, col, migrate } from "@ghom/orm"
|
|
@@ -127,7 +212,8 @@ export default new Table({
|
|
|
127
212
|
// Final type: { id: number; username: string; email: string; age: number | null }
|
|
128
213
|
```
|
|
129
214
|
|
|
130
|
-
|
|
215
|
+
### Migration Helpers
|
|
216
|
+
|
|
131
217
|
- `migrate.addColumn(name, columnDef)` - Add a new column
|
|
132
218
|
- `migrate.dropColumn(name)` - Remove a column
|
|
133
219
|
- `migrate.renameColumn(oldName, newName)` - Rename a column
|
|
@@ -136,7 +222,25 @@ Available migration helpers:
|
|
|
136
222
|
- `migrate.dropIndex(name)` - Remove an index
|
|
137
223
|
- `migrate.addUnique(columns, name?)` - Add a unique constraint
|
|
138
224
|
- `migrate.dropUnique(name)` - Remove a unique constraint
|
|
139
|
-
- `migrate.raw(callback)` - Custom migration callback
|
|
225
|
+
- `migrate.raw<From, To>(callback)` - Custom migration callback
|
|
226
|
+
- `migrate.sequence(...migrations)` - Combine multiple migrations
|
|
227
|
+
|
|
228
|
+
### Migration Sequences
|
|
229
|
+
|
|
230
|
+
Use `migrate.sequence()` to combine multiple migrations in a single migration key:
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
migrations: {
|
|
234
|
+
"001_user_updates": migrate.sequence(
|
|
235
|
+
migrate.addColumn("phone", col.string()),
|
|
236
|
+
migrate.addColumn("address", col.string().nullable()),
|
|
237
|
+
migrate.addIndex(["phone"], "idx_phone"),
|
|
238
|
+
migrate.renameColumn("name", "username"),
|
|
239
|
+
),
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Intermediate columns (added then removed in the sequence) are excluded from the final type automatically.
|
|
140
244
|
|
|
141
245
|
## Migration Keys
|
|
142
246
|
|
|
@@ -176,21 +280,44 @@ The ORM performs a runtime check on initialization and will throw an error if th
|
|
|
176
280
|
## Launch a query
|
|
177
281
|
|
|
178
282
|
For more information about the query builder, see [knexjs.org](https://knexjs.org/).
|
|
179
|
-
You can launch a SQL query on a table like
|
|
283
|
+
You can launch a SQL query on a table like this:
|
|
180
284
|
|
|
181
285
|
```typescript
|
|
182
|
-
import
|
|
286
|
+
import userTable from "./tables/user"
|
|
183
287
|
|
|
184
|
-
export async function compareHash(username, hash): Promise<boolean> {
|
|
185
|
-
const user = await
|
|
288
|
+
export async function compareHash(username: string, hash: string): Promise<boolean> {
|
|
289
|
+
const user = await userTable.query
|
|
186
290
|
.select()
|
|
187
291
|
.where("username", username)
|
|
188
292
|
.first()
|
|
189
293
|
|
|
190
|
-
return user && user.password === hash
|
|
294
|
+
return user !== undefined && user.password === hash
|
|
191
295
|
}
|
|
192
296
|
```
|
|
193
297
|
|
|
298
|
+
### Table Utilities
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
// Check if a column exists
|
|
302
|
+
await table.hasColumn("email") // boolean
|
|
303
|
+
|
|
304
|
+
// Get column info
|
|
305
|
+
await table.getColumn("email") // Knex.ColumnInfo
|
|
306
|
+
|
|
307
|
+
// Get all columns info
|
|
308
|
+
await table.getColumns() // Record<string, Knex.ColumnInfo>
|
|
309
|
+
|
|
310
|
+
// Get column names
|
|
311
|
+
await table.getColumnNames() // string[]
|
|
312
|
+
|
|
313
|
+
// Check if table is empty
|
|
314
|
+
await table.isEmpty() // boolean
|
|
315
|
+
|
|
316
|
+
// Count rows
|
|
317
|
+
await table.count() // number
|
|
318
|
+
await table.count("status = 'active'") // number with where clause
|
|
319
|
+
```
|
|
320
|
+
|
|
194
321
|
## Backup
|
|
195
322
|
|
|
196
323
|
You can backup the database by calling the `createBackup` and `restoreBackup` methods on the ORM instance. The backup is stored in the `config.backups.location` directory.
|
|
@@ -210,33 +337,38 @@ The cache is automatically managed by the ORM. When a table is requested from th
|
|
|
210
337
|
```typescript
|
|
211
338
|
// get the number of rows in the table with caching
|
|
212
339
|
await table.cache.count() // => 10
|
|
340
|
+
await table.cache.count("status = 'active'") // with where clause
|
|
213
341
|
|
|
214
|
-
// add a row with caching
|
|
342
|
+
// add a row with caching (automatically invalidates cache)
|
|
215
343
|
await table.cache.set((query) => {
|
|
216
344
|
return query.insert({ name: "test" })
|
|
217
345
|
})
|
|
218
346
|
|
|
219
347
|
await table.cache.count() // => 11
|
|
220
348
|
|
|
221
|
-
// Get
|
|
222
|
-
// After the first call, the
|
|
223
|
-
// the cache is
|
|
224
|
-
await table.cache.get("
|
|
225
|
-
return query.
|
|
226
|
-
}) // => { name: "test" }
|
|
349
|
+
// Get data with caching.
|
|
350
|
+
// After the first call, the result is cached until
|
|
351
|
+
// the cache is invalidated by a "cache.set" or "cache.invalidate" call
|
|
352
|
+
await table.cache.get("all users", (query) => {
|
|
353
|
+
return query.select("*")
|
|
354
|
+
}) // => [{ name: "test" }, ...]
|
|
227
355
|
|
|
228
356
|
// delete the row without caching
|
|
229
357
|
await table.query.delete().where("name", "test")
|
|
230
358
|
|
|
231
|
-
await table.cache.count() // => 11 (unchanged)
|
|
359
|
+
await table.cache.count() // => 11 (unchanged - cache not invalidated)
|
|
232
360
|
|
|
233
|
-
//
|
|
234
|
-
// and force the cache to be updated
|
|
361
|
+
// manually invalidate cache
|
|
235
362
|
table.cache.invalidate()
|
|
236
363
|
|
|
237
364
|
await table.cache.count() // => 10
|
|
238
365
|
await table.cache.count() // => 10 (no more query to the database)
|
|
239
366
|
|
|
367
|
+
// update with caching (automatically invalidates cache)
|
|
368
|
+
await table.cache.set((query) => {
|
|
369
|
+
return query.update({ status: "inactive" }).where("id", 1)
|
|
370
|
+
})
|
|
371
|
+
|
|
240
372
|
// remove all rows from a table with caching
|
|
241
373
|
await table.cache.set((query) => {
|
|
242
374
|
return query.truncate()
|
|
@@ -249,15 +381,21 @@ await table.cache.count() // => 0
|
|
|
249
381
|
|
|
250
382
|
### Raw cache
|
|
251
383
|
|
|
252
|
-
You can also cache raw queries with the `<ORM>.cache.raw`
|
|
384
|
+
You can also cache raw queries with the `<ORM>.cache.raw` method. The raw cache is useful when you have a complex query that you want to cache.
|
|
253
385
|
|
|
254
386
|
```typescript
|
|
255
387
|
const fooUser = await orm.cache.raw("select * from user where name = 'foo'") // query the database
|
|
256
388
|
const barUser = await orm.cache.raw("select * from user where name = 'bar'") // query the database
|
|
257
|
-
const fooUserCached = await orm.cache.raw("select * from user where name = 'foo'") // no query
|
|
389
|
+
const fooUserCached = await orm.cache.raw("select * from user where name = 'foo'") // cached - no query
|
|
390
|
+
|
|
391
|
+
// To invalidate the cache when you know data has changed externally:
|
|
392
|
+
const result = await orm.cache.raw("select * from user", true) // anyDataUpdated = true
|
|
258
393
|
```
|
|
259
394
|
|
|
260
|
-
The
|
|
395
|
+
The raw cache is invalidated when:
|
|
396
|
+
- You call `orm.cache.invalidate()`
|
|
397
|
+
- You use `table.cache.set()` to modify data
|
|
398
|
+
- You pass `true` as the second argument to `orm.cache.raw()`
|
|
261
399
|
|
|
262
400
|
## Future features
|
|
263
401
|
|
package/tests/orm.test.ts
CHANGED
|
@@ -141,6 +141,94 @@ describe("typed migrations", () => {
|
|
|
141
141
|
age: null,
|
|
142
142
|
}
|
|
143
143
|
})
|
|
144
|
+
|
|
145
|
+
test("Table accepts migrate.sequence for multiple typed migrations", () => {
|
|
146
|
+
const userTable = new Table({
|
|
147
|
+
name: "test_sequence_migrations",
|
|
148
|
+
columns: (col) => ({
|
|
149
|
+
id: col.increments(),
|
|
150
|
+
name: col.string(),
|
|
151
|
+
}),
|
|
152
|
+
migrations: {
|
|
153
|
+
"001_multiple_changes": migrate.sequence(
|
|
154
|
+
migrate.addColumn("phone", col.string()),
|
|
155
|
+
migrate.addColumn("address", col.string().nullable()),
|
|
156
|
+
migrate.addIndex(["phone"], "idx_phone"),
|
|
157
|
+
migrate.renameColumn("name", "username"),
|
|
158
|
+
migrate.renameColumn("username", "fullname"),
|
|
159
|
+
),
|
|
160
|
+
},
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
expect(userTable).toBeInstanceOf(Table)
|
|
164
|
+
expect(userTable.options.migrations).toBeDefined()
|
|
165
|
+
expect(Object.keys(userTable.options.migrations!).length).toBe(1)
|
|
166
|
+
|
|
167
|
+
// Type inference check - sequence migrations should infer types correctly
|
|
168
|
+
type ExpectedType = typeof userTable.$type
|
|
169
|
+
const _typeCheck: ExpectedType = {
|
|
170
|
+
id: 1,
|
|
171
|
+
// @ts-expect-error - name is removed by renameColumn
|
|
172
|
+
name: "test",
|
|
173
|
+
// @ts-expect-error - username is removed by renameColumn
|
|
174
|
+
username: "test",
|
|
175
|
+
fullname: "test",
|
|
176
|
+
phone: "123456789",
|
|
177
|
+
address: null,
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test("Table accepts mixed single and sequence migrations", () => {
|
|
182
|
+
const userTable = new Table({
|
|
183
|
+
name: "test_mixed_migrations",
|
|
184
|
+
columns: (col) => ({
|
|
185
|
+
id: col.increments(),
|
|
186
|
+
name: col.string(),
|
|
187
|
+
}),
|
|
188
|
+
migrations: {
|
|
189
|
+
"001_add_email": migrate.addColumn("email", col.string()),
|
|
190
|
+
"002_multiple_changes": migrate.sequence(
|
|
191
|
+
migrate.addColumn("phone", col.string()),
|
|
192
|
+
migrate.addColumn("age", col.integer().nullable()),
|
|
193
|
+
),
|
|
194
|
+
"003_add_active": migrate.addColumn("isActive", col.boolean().defaultTo(true)),
|
|
195
|
+
},
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
expect(userTable).toBeInstanceOf(Table)
|
|
199
|
+
expect(userTable.options.migrations).toBeDefined()
|
|
200
|
+
expect(Object.keys(userTable.options.migrations!).length).toBe(3)
|
|
201
|
+
|
|
202
|
+
// Type inference check - mixed migrations should infer all types
|
|
203
|
+
type ExpectedType = typeof userTable.$type
|
|
204
|
+
const _typeCheck: ExpectedType = {
|
|
205
|
+
id: 1,
|
|
206
|
+
name: "test",
|
|
207
|
+
email: "test@example.com",
|
|
208
|
+
phone: "123456789",
|
|
209
|
+
age: null,
|
|
210
|
+
isActive: true,
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
test("Table accepts migrate.sequence with raw migrations", () => {
|
|
215
|
+
const table = new Table({
|
|
216
|
+
name: "test_sequence_raw_migrations",
|
|
217
|
+
columns: (col) => ({
|
|
218
|
+
id: col.increments(),
|
|
219
|
+
}),
|
|
220
|
+
migrations: {
|
|
221
|
+
"001_multiple_raw": migrate.sequence(
|
|
222
|
+
migrate.raw((builder) => builder.string("field1")),
|
|
223
|
+
migrate.raw((builder) => builder.integer("field2")),
|
|
224
|
+
),
|
|
225
|
+
},
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
expect(table).toBeInstanceOf(Table)
|
|
229
|
+
expect(table.options.migrations).toBeDefined()
|
|
230
|
+
expect(table.options.migrations!["001_multiple_raw"]).toBeDefined()
|
|
231
|
+
})
|
|
144
232
|
})
|
|
145
233
|
|
|
146
234
|
describe("migration key patterns", () => {
|
|
@@ -151,9 +239,9 @@ describe("migration key patterns", () => {
|
|
|
151
239
|
id: col.increments(),
|
|
152
240
|
}),
|
|
153
241
|
migrations: {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
242
|
+
1: migrate.raw(() => {}),
|
|
243
|
+
2: migrate.raw(() => {}),
|
|
244
|
+
10: migrate.raw(() => {}),
|
|
157
245
|
},
|
|
158
246
|
})
|
|
159
247
|
|
|
@@ -168,9 +256,9 @@ describe("migration key patterns", () => {
|
|
|
168
256
|
id: col.increments(),
|
|
169
257
|
}),
|
|
170
258
|
migrations: {
|
|
171
|
-
"001_init": (
|
|
172
|
-
"002_add_column": (
|
|
173
|
-
"010_fix": (
|
|
259
|
+
"001_init": migrate.raw(() => {}),
|
|
260
|
+
"002_add_column": migrate.raw(() => {}),
|
|
261
|
+
"010_fix": migrate.raw(() => {}),
|
|
174
262
|
},
|
|
175
263
|
})
|
|
176
264
|
|
|
@@ -189,15 +277,36 @@ describe("migration key patterns", () => {
|
|
|
189
277
|
id: col.increments(),
|
|
190
278
|
}),
|
|
191
279
|
migrations: {
|
|
192
|
-
init: (
|
|
193
|
-
add_column: (
|
|
194
|
-
fix: (
|
|
280
|
+
init: migrate.raw(() => {}),
|
|
281
|
+
add_column: migrate.raw(() => {}),
|
|
282
|
+
fix: migrate.raw(() => {}),
|
|
195
283
|
},
|
|
196
284
|
})
|
|
197
285
|
|
|
198
286
|
expect(table.options.migrations).toBeDefined()
|
|
199
287
|
expect(Object.keys(table.options.migrations!)).toEqual(["init", "add_column", "fix"])
|
|
200
288
|
})
|
|
289
|
+
|
|
290
|
+
test("Table rejects mixed key patterns at migration time", () => {
|
|
291
|
+
const table = new Table({
|
|
292
|
+
name: "test_mixed_keys",
|
|
293
|
+
columns: (col) => ({
|
|
294
|
+
id: col.increments(),
|
|
295
|
+
}),
|
|
296
|
+
migrations: {
|
|
297
|
+
1: migrate.raw(() => {}),
|
|
298
|
+
"001_init": migrate.raw(() => {}),
|
|
299
|
+
init: migrate.raw(() => {}),
|
|
300
|
+
},
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
// The error is thrown when getMigrationKeys() is called during migration
|
|
304
|
+
// This happens during make(), not during construction
|
|
305
|
+
expect(() => {
|
|
306
|
+
// Access private method to test key validation
|
|
307
|
+
;(table as any).getMigrationKeys()
|
|
308
|
+
}).toThrow(/Migration keys use mixed patterns/)
|
|
309
|
+
})
|
|
201
310
|
})
|
|
202
311
|
|
|
203
312
|
describe("unconnected ORM", () => {
|
package/tsconfig.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
3
|
"strict": true,
|
|
4
|
-
"rootDir": "src",
|
|
5
4
|
"outDir": "dist",
|
|
6
5
|
"module": "NodeNext",
|
|
7
6
|
"target": "ESNext",
|
|
@@ -12,5 +11,6 @@
|
|
|
12
11
|
"skipLibCheck": true,
|
|
13
12
|
"typeRoots": ["./node_modules/@types", "./dist/typings"]
|
|
14
13
|
},
|
|
15
|
-
"include": ["src/**/*", "dist/typings/**/*"]
|
|
14
|
+
"include": ["src/**/*", "dist/typings/**/*"],
|
|
15
|
+
"exclude": ["tests/**/*"]
|
|
16
16
|
}
|