@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.
@@ -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 TypedMigration<infer From, any> ? keyof From : never;
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 TypedMigration<any, infer To> ? To : never;
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, TypedMigration<any, any>>> = Migrations[keyof Migrations] extends infer M ? Omit<Base, ExtractFromKeys<M>> & UnionToIntersection<ExtractToTypes<M>> : Base;
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, TypedMigration<any, any>> = {}> = ApplyMigrations<InferColumns<Columns>, Migrations>;
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 {};
@@ -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
  };
@@ -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 can be either a callback function or a TypedMigration object.
7
+ * A migration value is a TypedMigration or TypedMigrationSequence.
8
+ * Use migrate.sequence() to combine multiple migrations.
8
9
  */
9
- export type MigrationValue = ((builder: Knex.CreateTableBuilder) => void) | TypedMigration<any, any>;
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
- * Can use either callback functions or TypedMigration objects.
39
+ * @example
40
+ * // Single migration
41
+ * migrations: {
42
+ * "001_add_email": migrate.addColumn("email", col.string()),
43
+ * }
39
44
  *
40
45
  * @example
41
- * // Using callbacks
46
+ * // Multiple migrations in sequence
42
47
  * migrations: {
43
- * "1": (builder) => builder.dropColumn("oldField"),
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
- * // Using typed migrations
55
+ * // Raw migration for advanced use cases
48
56
  * migrations: {
49
- * "001_add_email": migrate.addColumn("email", col.string()),
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: Migrations extends Record<string, TypedMigration<any, any>> ? FinalTableType<Columns, Migrations> : InferColumns<Columns>;
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
- 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").`);
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
- 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
- }
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.0",
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 (console by default)
27
+ // optional custom logger (must have log, error, warn methods)
28
28
  logger: console,
29
- loggerColors: { ... },
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
- "1": (table) => {
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: ({ query }) => {
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
- ### Typed Migrations
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
- You can also use typed migrations that automatically update the TypeScript type:
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
- Available migration helpers:
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 that
283
+ You can launch a SQL query on a table like this:
180
284
 
181
285
  ```typescript
182
- import user from "./tables/user"
286
+ import userTable from "./tables/user"
183
287
 
184
- export async function compareHash(username, hash): Promise<boolean> {
185
- const user = await user.query
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 the row with caching.
222
- // After the first call, the row is cached until
223
- // the cache is invalidate by a "cache.set" or "cache.invalidate" call
224
- await table.cache.get("named test", (query) => {
225
- return query.where("name", "test").first()
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
- // indicate that the cache is invalidate
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` property. The raw cache is useful when you have a complex query that you want to cache.
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 to the database
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 cache of the `<ORM>.cache.raw` method is automatically invalidated when the database is updated.
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
- "1": (_builder) => {},
155
- "2": (_builder) => {},
156
- "10": (_builder) => {},
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": (_builder) => {},
172
- "002_add_column": (_builder) => {},
173
- "010_fix": (_builder) => {},
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: (_builder) => {},
193
- add_column: (_builder) => {},
194
- fix: (_builder) => {},
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", () => {
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src"
5
+ },
6
+ "include": ["src/**/*"],
7
+ "exclude": ["tests/**/*"]
8
+ }
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
  }