@ts-awesome/orm 2.0.0-alpha.1 → 2.0.0-alpha.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.
Files changed (69) hide show
  1. package/README.md +633 -70
  2. package/dist/base.d.ts +3 -2
  3. package/dist/base.d.ts.map +1 -0
  4. package/dist/base.js +16 -13
  5. package/dist/base.js.map +1 -1
  6. package/dist/branded.d.ts +1 -0
  7. package/dist/branded.d.ts.map +1 -0
  8. package/dist/builder.d.ts +10 -8
  9. package/dist/builder.d.ts.map +1 -0
  10. package/dist/builder.js +132 -54
  11. package/dist/builder.js.map +1 -1
  12. package/dist/compiler.d.ts +12 -10
  13. package/dist/compiler.d.ts.map +1 -0
  14. package/dist/compiler.js +191 -83
  15. package/dist/compiler.js.map +1 -1
  16. package/dist/decorators.d.ts +3 -2
  17. package/dist/decorators.d.ts.map +1 -0
  18. package/dist/decorators.js +31 -20
  19. package/dist/decorators.js.map +1 -1
  20. package/dist/errors.d.ts +1 -0
  21. package/dist/errors.d.ts.map +1 -0
  22. package/dist/errors.js +1 -1
  23. package/dist/errors.js.map +1 -1
  24. package/dist/index.d.ts +2 -1
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +0 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/interfaces.d.ts +175 -50
  29. package/dist/interfaces.d.ts.map +1 -0
  30. package/dist/intermediate.d.ts +5 -4
  31. package/dist/intermediate.d.ts.map +1 -0
  32. package/dist/operators.d.ts +384 -27
  33. package/dist/operators.d.ts.map +1 -0
  34. package/dist/operators.js +489 -43
  35. package/dist/operators.js.map +1 -1
  36. package/dist/reader.d.ts +3 -2
  37. package/dist/reader.d.ts.map +1 -0
  38. package/dist/reader.js +38 -26
  39. package/dist/reader.js.map +1 -1
  40. package/dist/symbols.d.ts +1 -0
  41. package/dist/symbols.d.ts.map +1 -0
  42. package/dist/test-driver/compiler.d.ts +3 -2
  43. package/dist/test-driver/compiler.d.ts.map +1 -0
  44. package/dist/test-driver/compiler.js +128 -14
  45. package/dist/test-driver/compiler.js.map +1 -1
  46. package/dist/test-driver/driver.d.ts +3 -2
  47. package/dist/test-driver/driver.d.ts.map +1 -0
  48. package/dist/test-driver/driver.js +5 -3
  49. package/dist/test-driver/driver.js.map +1 -1
  50. package/dist/test-driver/executor.d.ts +15 -3
  51. package/dist/test-driver/executor.d.ts.map +1 -0
  52. package/dist/test-driver/executor.js +95 -6
  53. package/dist/test-driver/executor.js.map +1 -1
  54. package/dist/test-driver/index.d.ts +1 -0
  55. package/dist/test-driver/index.d.ts.map +1 -0
  56. package/dist/test-driver/interfaces.d.ts +31 -6
  57. package/dist/test-driver/interfaces.d.ts.map +1 -0
  58. package/dist/test-driver/transaction.d.ts +4 -3
  59. package/dist/test-driver/transaction.d.ts.map +1 -0
  60. package/dist/test-driver/transaction.js +5 -5
  61. package/dist/test-driver/transaction.js.map +1 -1
  62. package/dist/wrappers.d.ts +88 -194
  63. package/dist/wrappers.d.ts.map +1 -0
  64. package/dist/wrappers.js +179 -96
  65. package/dist/wrappers.js.map +1 -1
  66. package/eslint.config.js +49 -0
  67. package/jest.config.js +4 -0
  68. package/package.json +14 -12
  69. package/.eslintrc.json +0 -27
package/README.md CHANGED
@@ -1,51 +1,115 @@
1
1
  # @ts-awesome/orm
2
2
 
3
- TypeScript friendly minimalistic Object Relation Mapping library
3
+ TypeScript-friendly, minimalistic object relational mapping library.
4
4
 
5
5
  Key features:
6
6
 
7
7
  * strong object mapping with [@ts-awesome/model-reader](https://github.com/ts-awesome/model-reader)
8
- * no relation navigation - intentional
8
+ * no relation navigation (intentional)
9
9
  * heavy use of type checks and lambdas
10
- * support common subset of SQL
10
+ * supports a common subset of SQL
11
+ * built-in unit testing driver
12
+
13
+ No relation navigation is intentional: relationships are expressed in queries so you always control joins and payload shape.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ npm install @ts-awesome/orm @ts-awesome/orm-pg
19
+ ```
20
+
21
+ ## Quick start
22
+
23
+ ```ts
24
+ import {dbField, dbTable, IBuildableQuery, IQueryExecutor, Select} from '@ts-awesome/orm';
25
+ import {ISqlQuery, PgCompiler} from '@ts-awesome/orm-pg';
26
+
27
+ @dbTable('users')
28
+ class User {
29
+ @dbField({primaryKey: true, autoIncrement: true})
30
+ public id!: number;
31
+
32
+ @dbField
33
+ public name!: string;
34
+ }
35
+
36
+ const compiler = new PgCompiler();
37
+ const driver: IQueryExecutor<ISqlQuery> = /* your driver instance */;
38
+
39
+ const query: IBuildableQuery = Select(User).where({name: 'Alice'}).limit(1);
40
+ const compiled: ISqlQuery = compiler.compile(query);
41
+ const results: User[] = await driver.execute(compiled, User);
42
+ ```
43
+
44
+ ## Supported drivers
45
+
46
+ * PostgreSQL: `@ts-awesome/orm-pg`
47
+ * SQLite: `@ts-awesome/orm-sqlite`
48
+ * Firebird: `@ts-awesome/orm-firebird`
49
+ * Other drivers may be available on request
11
50
 
12
51
  ## Model declaration
13
52
 
14
- Each model metadata is defined with `dbTable` and `dbField` decorators
53
+ Model metadata is defined with `dbTable` and `dbField` decorators.
15
54
 
16
55
  ```ts
17
- import {dbField, dbField} from "@ts-awesome/orm";
18
- import {DB_JSON} from "@ts-awesome/orm-pg"; // or other driver
56
+ import {dbField, dbTable, Branded} from '@ts-awesome/orm';
57
+ import {DB_JSON} from '@ts-awesome/orm-pg'; // or other driver
58
+
59
+ type FirstModelId = Branded<number, 'FirstModelId'>;
60
+ type AuthorId = Branded<number, 'AuthorId'>;
61
+
62
+ class SubDocumentModel {
63
+ public title!: string;
64
+ }
65
+
66
+ const enum UserStatus {
67
+ Active = 1,
68
+ Inactive = 0,
69
+ }
70
+
71
+ enum UserRole {
72
+ Admin = 'admin',
73
+ User = 'user',
74
+ }
19
75
 
20
76
  @dbTable('first_table')
21
77
  class FirstModel {
22
78
  // numeric autoincrement primary key
23
79
  @dbField({primaryKey: true, autoIncrement: true})
24
- public id!: number;
80
+ public id!: FirstModelId;
25
81
 
26
82
  // just another field
27
83
  @dbField
28
84
  public title!: string;
29
85
 
30
- // lets map prop to different field
86
+ // let's map prop to different field
31
87
  @dbField({name: 'author_id'})
32
- public authorId!: number;
88
+ public authorId!: AuthorId;
33
89
 
34
90
  // nullable field requires explicit model and nullable
35
- // these are direct match to @ts-awesome/model-reader
91
+ // these are direct matches to @ts-awesome/model-reader
36
92
  @dbField({
37
93
  model: String,
38
94
  nullable: true,
39
95
  })
40
96
  public description!: string | null;
41
97
 
42
- // advanced use case
43
- @dbModel({
98
+ // JSON column with model conversion
99
+ @dbField({
44
100
  kind: DB_JSON, // data will be stored as JSON
45
101
  model: SubDocumentModel, // and will be converted to instance of SubDocumentModel
46
102
  nullable: true,
47
103
  })
48
- public document!: SubDocumentModel | null
104
+ public document!: SubDocumentModel | null;
105
+
106
+ // numeric enum support
107
+ @dbField({model: UserStatus})
108
+ public status!: UserStatus;
109
+
110
+ // string enum support
111
+ @dbField({model: UserRole})
112
+ public role!: UserRole;
49
113
 
50
114
  // readonly field with database default
51
115
  @dbField({name: 'created_at', readonly: true})
@@ -53,67 +117,231 @@ class FirstModel {
53
117
  }
54
118
  ```
55
119
 
120
+ ### Common decorator options
121
+
122
+ * `primaryKey`: marks a primary key column
123
+ * `autoIncrement`: enables auto-increment on numeric keys
124
+ * `name`: maps a property to a different column name
125
+ * `readonly`: marks a column as DB-managed (insert/update ignored)
126
+ * `nullable`: allows `null` in the model type
127
+ * `model`: overrides the inferred model type (enums, custom classes)
128
+ * `kind`: custom read/write hooks for DB-specific types
129
+ * `sensitive`: hides field values unless explicitly requested by reader/driver
130
+ * `default`: default DB value (used for metadata and inserts)
131
+
132
+ ### Table indexes for upsert
133
+
134
+ `@dbTable` can declare unique indexes used by `Upsert().conflict()`.
135
+
136
+ ```ts
137
+ import {dbField, dbTable} from '@ts-awesome/orm';
138
+
139
+ @dbTable('users', [
140
+ {name: 'users_email_unique', fields: ['email'], default: true},
141
+ ])
142
+ class User {
143
+ @dbField({primaryKey: true})
144
+ public id!: number;
145
+
146
+ @dbField
147
+ public email!: string;
148
+ }
149
+ ```
150
+
151
+ ### Custom field kinds
152
+
153
+ Use `kind` for custom read/write transformations and query wrapping.
154
+
155
+ ```ts
156
+ import {dbField, dbTable} from '@ts-awesome/orm';
157
+
158
+ const BoolAsNumber = {
159
+ reader: (raw: unknown) => raw === 1,
160
+ writer: (value: boolean) => (value ? 1 : 0),
161
+ };
162
+
163
+ @dbTable('flags')
164
+ class Flag {
165
+ @dbField({primaryKey: true})
166
+ public id!: number;
167
+
168
+ @dbField({kind: BoolAsNumber})
169
+ public enabled!: boolean;
170
+ }
171
+ ```
172
+
173
+ ### Derived fields
174
+
175
+ `@dbFilterField` lets you expose subquery-based fields without relation navigation.
176
+ It requires a single-column primary key on the source table.
177
+
178
+ ```ts
179
+ import {dbField, dbFilterField, dbTable, Select} from '@ts-awesome/orm';
180
+
181
+ @dbTable('roles')
182
+ class Role {
183
+ @dbField({primaryKey: true})
184
+ public id!: number;
185
+
186
+ @dbField
187
+ public userId!: number;
188
+
189
+ @dbField
190
+ public name!: string;
191
+ }
192
+
193
+ @dbTable('users')
194
+ class User {
195
+ @dbField({primaryKey: true})
196
+ public id!: number;
197
+
198
+ @dbFilterField((id, _table) => Select(Role)
199
+ .columns(({name}) => [name])
200
+ .where(({userId}) => userId.eq(id))
201
+ .limit(1))
202
+ public roleName!: string;
203
+ }
204
+ ```
205
+
206
+ `@dbManyField` exists but is deprecated; use `@dbFilterField` instead.
207
+
208
+ ### Sensitive fields
209
+
210
+ Fields marked as `sensitive` are omitted unless you pass `true` to the reader/driver.
211
+
212
+ ```ts
213
+ import {dbField, dbTable, Select} from '@ts-awesome/orm';
214
+ import {TestCompiler, TestDriver} from '@ts-awesome/orm/test-driver';
215
+
216
+ @dbTable('users')
217
+ class User {
218
+ @dbField({primaryKey: true})
219
+ public id!: number;
220
+
221
+ @dbField({sensitive: true})
222
+ public passwordHash!: string;
223
+ }
224
+
225
+ const driver = new TestDriver();
226
+ const compiler = new TestCompiler();
227
+ const results = await driver.execute(compiler.compile(Select(User)), User, true);
228
+ ```
229
+
56
230
  ## Vanilla select
57
231
 
58
232
  ```ts
59
- import {IBuildableQuery, IQueryExecutor, Select} from "@ts-awesome/orm";
60
- import {ISqlQuery, PgCompiler} from "@ts-awesome/orm-pg"; // or other driver
233
+ import {Branded, dbField, dbTable, IBuildableQuery, IQueryExecutor, Select} from '@ts-awesome/orm';
234
+ import {ISqlQuery, PgCompiler} from '@ts-awesome/orm-pg'; // or other driver
235
+
236
+ type AuthorId = Branded<number, 'AuthorId'>;
237
+
238
+ @dbTable('first_table')
239
+ class FirstModel {
240
+ @dbField({name: 'author_id'})
241
+ public authorId!: AuthorId;
242
+ }
61
243
 
62
244
  const compiler = new PgCompiler();
63
245
  const driver: IQueryExecutor<ISqlQuery>;
64
246
 
65
- const query: IBuildableQuery = Select(FirstModel).where({authorId: 5}).limit(10);
247
+ const query: IBuildableQuery = Select(FirstModel).where({authorId: 5 as AuthorId}).limit(10);
66
248
  const compiled: ISqlQuery = compiler.compile(query);
67
249
  const results: FirstModel[] = await driver.execute(compiled, FirstModel);
68
250
  ```
69
251
 
70
- For more streamlined use please check [@ts-awesome/entity](https://github.com/ts-awesome/model-reader)
252
+ For more streamlined use, please check [@ts-awesome/model-reader](https://github.com/ts-awesome/model-reader).
71
253
 
72
254
  ## Select builder
73
255
 
74
- ORM provides a way to use model declaration to your advantage: TypeScript will check is fields exists.
75
- And TypeScript will check operands for compatible types.
256
+ ORM provides a way to use model declaration to your advantage: TypeScript will check if fields exist,
257
+ and will check operands for compatible types.
76
258
 
77
259
  ```ts
260
+ import {Branded, dbField, dbTable, Select} from '@ts-awesome/orm';
261
+
262
+ type AuthorId = Branded<number, 'AuthorId'>;
263
+
264
+ @dbTable('first_table')
265
+ class FirstModel {
266
+ @dbField({name: 'author_id'})
267
+ public authorId!: AuthorId;
268
+ }
269
+
78
270
  const query = Select(FirstModel)
79
271
  // authorId = 5;
80
- .where({authorId: '5'}) // gives error, it can be number only
272
+ .where({authorId: '5'}) // gives error, it can be number (branded AuthorId) only
81
273
  .limit(10);
82
274
  ```
83
275
 
84
- For more complex logic ORM provides WhereBuilder
276
+ For more complex logic, ORM provides a WhereBuilder.
85
277
 
86
278
  ```ts
279
+ import {Branded, dbField, dbTable, Select} from '@ts-awesome/orm';
280
+
281
+ type AuthorId = Branded<number, 'AuthorId'>;
282
+
283
+ @dbTable('first_table')
284
+ class FirstModel {
285
+ @dbField({name: 'author_id'})
286
+ public authorId!: AuthorId;
287
+ }
288
+
87
289
  const query = Select(FirstModel)
88
290
  // authorId = 5;
89
- .where(({authorId}) => authorId.eq(5))
291
+ .where(({authorId}) => authorId.eq(5 as AuthorId))
90
292
  .limit(10);
91
293
  ```
92
294
 
93
295
  ```ts
296
+ import {Branded, dbField, dbTable, Select} from '@ts-awesome/orm';
297
+
298
+ type AuthorId = Branded<number, 'AuthorId'>;
299
+
300
+ @dbTable('first_table')
301
+ class FirstModel {
302
+ @dbField({name: 'author_id'})
303
+ public authorId!: AuthorId;
304
+
305
+ @dbField({model: String, nullable: true})
306
+ public description!: string | null;
307
+ }
308
+
94
309
  const query = Select(FirstModel)
95
310
  // authorId in (5, 6)
96
- .where(({authorId, description}) => authorId.in([5, 6]))
311
+ .where(({authorId, description}) => authorId.in([5 as AuthorId, 6 as AuthorId]))
97
312
  .limit(10);
98
313
  ```
99
314
 
100
315
  ```ts
316
+ import {and, Branded, dbField, dbTable, Select} from '@ts-awesome/orm';
317
+
318
+ type AuthorId = Branded<number, 'AuthorId'>;
319
+
320
+ @dbTable('first_table')
321
+ class FirstModel {
322
+ @dbField({name: 'author_id'})
323
+ public authorId!: AuthorId;
324
+
325
+ @dbField({model: String, nullable: true})
326
+ public description!: string | null;
327
+ }
328
+
101
329
  const query = Select(FirstModel)
102
330
  // authorId = 5 AND description LIKE 'some%';
103
- .where(({authorId, description}) => and(authorId.eq(5), description.like('some%')))
331
+ .where(({authorId, description}) => and(authorId.eq(5 as AuthorId), description.like('some%')))
104
332
  .limit(10);
105
333
  ```
106
334
 
107
335
  #### Overview of operators and functions:
108
336
 
109
- * Generic comparable:
337
+ * Generic comparable:
110
338
  * left.`eq`(right) equivalent to left `=` right or left `IS NULL` if right === null
111
339
  * left.`neq`(right) equivalent to left `<>` right or left `IS NOT NULL` if right === null
112
340
  * left.`gt`(right) equivalent to left `>` right
113
341
  * left.`gte`(right) equivalent to left `>=` right
114
342
  * left.`lt`(right) equivalent to left `<` right
115
- * left.`lte`(right) equivalent to left `<=` right
116
- * left.`between`(a, b) equivalent left BETWEEN (a, b)
343
+ * left.`lte`(right) equivalent to left `<=` right
344
+ * left.`between`(a, b) equivalent to left BETWEEN (a, b)
117
345
  * Strings
118
346
  * left.`like`(right) equivalent to left `LIKE` right
119
347
  * Arrays
@@ -142,154 +370,489 @@ const query = Select(FirstModel)
142
370
  * `max`(expr) equivalent to `MAX` (expr)
143
371
  * `min`(expr) equivalent to `MIN` (expr)
144
372
  * `sum`(expr) equivalent to `SUM` (expr)
145
- * `count`(expr) equivalent to `count` (expr)
373
+ * `count`(expr) equivalent to `COUNT` (expr)
374
+ * `count`(expr, true) equivalent to `COUNT(DISTINCT expr)`
375
+ * `stddev_pop`, `stddev_samp`, `var_pop`, `var_samp`
376
+ * Date & Time
377
+ * `now`(), `current_date`(), `current_timestamp`()
378
+ * `extract`(field, source) equivalent to `EXTRACT(field FROM source)`
379
+ * `date_trunc`(part, source) equivalent to `DATE_TRUNC(part, source)`
380
+ * String
381
+ * `concat`(s1, s2, ...), `lower`(s), `upper`(s), `length`(s)
382
+ * `trim`(s), `ltrim`(s), `rtrim`(s)
383
+ * `substring`(s, start, len), `position`(sub in str), `replace`(str, from, to)
384
+ * `lpad`(s, len, pad), `rpad`(s, len, pad), `repeat`(s, n)
385
+ * `left`(s, n), `right`(s, n), `reverse`(s)
386
+ * Math
387
+ * `abs`(x), `ceil`(x), `floor`(x), `round`(x, d)
388
+ * `power`(b, e), `sqrt`(x), `mod`(x, y)
389
+ * `exp`(x), `ln`(x), `log`(x, base), `trunc`(x, d), `pi`(), `sign`(x), `random`()
390
+ * Conditional
391
+ * `coalesce`(v1, v2, ...), `nullif`(v1, v2)
392
+ * `greatest`(v1, v2, ...), `least`(v1, v2, ...)
393
+ * `case_`({when: cond, then: val}, ..., {else: val})
394
+ * Casting
395
+ * `cast`(expr, type) equivalent to `CAST(expr AS type)`
396
+
397
+ ### Column references without a model
398
+
399
+ ```ts
400
+ import {of, Select} from '@ts-awesome/orm';
401
+
402
+ const query = Select(FirstModel)
403
+ .orderBy(() => [of(null, 'score')]);
404
+ ```
146
405
 
147
406
  ### Joining
148
407
 
149
- Sometimes you may need to perform some joins for filtering
408
+ Sometimes you may need to perform joins for filtering.
150
409
 
151
410
  ```ts
152
- import {dbTable, dbField} from "@ts-awesome/orm";
411
+ import {Branded, dbField, dbTable, of, Select} from '@ts-awesome/orm';
412
+
413
+ type AuthorId = Branded<number, 'AuthorId'>;
414
+
415
+ @dbTable('first_table')
416
+ class FirstModel {
417
+ @dbField({name: 'author_id'})
418
+ public authorId!: AuthorId;
419
+ }
153
420
 
154
421
  @dbTable('second_table')
155
422
  class SecondModel {
156
- @dbField({primatyKey: true, autoIncrement: true})
157
- public id!: number;
158
-
423
+ @dbField({primaryKey: true, autoIncrement: true})
424
+ public id!: AuthorId;
425
+
159
426
  @dbField
160
427
  public name!: string;
161
428
  }
162
429
 
163
430
  const query = Select(FirstModel)
164
- // lets join SecondModel by FK
431
+ // let's join SecondModel by FK
165
432
  .join(SecondModel, (root, other) => root.authorId.eq(other.id))
166
- // lets filter by author name
433
+ // let's filter by author name
167
434
  .where(() => of(SecondModel, 'name').like('John%'))
168
- .limit(10)
435
+ .limit(10);
169
436
  ```
170
437
 
171
- In some cases `TableRef` might be handy, especially of need to join same table multiple times
438
+ In some cases `TableRef` might be handy, especially if you need to join the same table multiple times.
172
439
 
173
440
  ```ts
174
- import {dbTable, dbField} from "@ts-awesome/orm";
441
+ import {dbField, dbTable, Branded, of, or, Select, TableRef} from '@ts-awesome/orm';
442
+
443
+ type AuthorId = Branded<number, 'AuthorId'>;
444
+ type ThirdModelId = Branded<number, 'ThirdModelId'>;
175
445
 
176
446
  @dbTable('second_table')
177
447
  class SecondModel {
178
- @dbField({primatyKey: true, autoIncrement: true})
179
- public id!: number;
180
-
448
+ @dbField({primaryKey: true, autoIncrement: true})
449
+ public id!: AuthorId;
450
+
181
451
  @dbField
182
452
  public name!: string;
183
453
  }
184
454
 
185
455
  @dbTable('third_table')
186
456
  class ThirdModel {
187
- @dbField({primatyKey: true, autoIncrement: true})
188
- public id!: number;
457
+ @dbField({primaryKey: true, autoIncrement: true})
458
+ public id!: ThirdModelId;
189
459
 
190
460
  @dbField
191
- public createdBy!: number;
192
-
461
+ public createdBy!: AuthorId;
462
+
193
463
  @dbField
194
- public ownedBy!: number;
464
+ public ownedBy!: AuthorId;
195
465
  }
196
466
 
197
467
  const ownerRef = new TableRef(SecondModel);
198
468
  const creatorRef = new TableRef(SecondModel);
199
469
  const query = Select(ThirdModel)
200
- // lets join SecondModel by FK
470
+ // let's join SecondModel by FK
201
471
  .join(SecondModel, ownerRef, (root, other) => root.ownedBy.eq(other.id))
202
- // lets join SecondModel by FK
472
+ // let's join SecondModel by FK
203
473
  .join(SecondModel, creatorRef, (root, other) => root.createdBy.eq(other.id))
204
- // lets filter by owner or creator name
474
+ // let's filter by owner or creator name
205
475
  .where(() => or(
206
476
  of(ownerRef, 'name').like('John%'),
207
477
  of(creatorRef, 'name').like('John%'),
208
478
  ))
209
- .limit(10)
479
+ .limit(10);
210
480
  ```
211
481
 
212
482
  ### Grouping
213
483
 
214
484
  ```ts
215
- import {Select, min, count, alias} from '@ts-awesome/orm'
485
+ import {alias, Branded, count, dbField, dbTable, min, Select} from '@ts-awesome/orm';
486
+
487
+ type AuthorId = Branded<number, 'AuthorId'>;
488
+
489
+ @dbTable('first_table')
490
+ class FirstModel {
491
+ @dbField({name: 'author_id'})
492
+ public authorId!: AuthorId;
493
+
494
+ @dbField
495
+ public title!: string;
216
496
 
217
- const ts: Date; // some timestamp in past
497
+ @dbField({name: 'created_at'})
498
+ public createdAt!: Date;
499
+ }
500
+
501
+ const ts: Date; // some timestamp in the past
218
502
  const query = Select(FirstModel)
219
503
  // we need titles to contain `key`
220
504
  .where(({title}) => title.like('%key%'))
221
505
  // group by authors
222
506
  .groupBy(['authorId'])
223
- // filter to have first publication not before ts
507
+ // filter to have first publication not before ts
224
508
  .having(({createdAt}) => min(createdAt).gte(ts))
225
509
  // result should have 2 columns: authorId and count
226
- .columns(({authorId}) => [authorId, alias(count(), 'count')])
510
+ .columns(({authorId}) => [authorId, alias(count(), 'count')]);
227
511
  ```
228
512
 
229
513
  ### Ordering
230
514
 
231
515
  ```ts
232
- import {Select, desc} from '@ts-awesome/orm'
516
+ import {Branded, dbField, dbTable, desc, of, Select} from '@ts-awesome/orm';
517
+
518
+ type AuthorId = Branded<number, 'AuthorId'>;
519
+
520
+ @dbTable('first_table')
521
+ class FirstModel {
522
+ @dbField({name: 'author_id'})
523
+ public authorId!: AuthorId;
524
+
525
+ @dbField
526
+ public title!: string;
527
+ }
528
+
529
+ @dbTable('second_table')
530
+ class SecondModel {
531
+ @dbField({primaryKey: true})
532
+ public id!: AuthorId;
533
+
534
+ @dbField
535
+ public name!: string;
536
+ }
537
+
538
+ const query = Select(FirstModel)
539
+ // let's join SecondModel by FK
540
+ .join(SecondModel, (root, other) => root.authorId.eq(other.id))
541
+ // let's sort by author and title reverse
542
+ .orderBy(({title}) => [of(SecondModel, 'name'), desc(title)])
543
+ .limit(10);
544
+ ```
545
+
546
+ ### Pagination
547
+
548
+ ```ts
549
+ import {Branded, dbField, dbTable, Select} from '@ts-awesome/orm';
550
+
551
+ type AuthorId = Branded<number, 'AuthorId'>;
552
+
553
+ @dbTable('first_table')
554
+ class FirstModel {
555
+ @dbField({name: 'author_id'})
556
+ public authorId!: AuthorId;
557
+ }
558
+
559
+ const query = Select(FirstModel)
560
+ .where(({authorId}) => authorId.eq(5 as AuthorId))
561
+ .limit(10)
562
+ .offset(20);
563
+ ```
564
+
565
+ ### Distinct and FOR UPDATE
566
+
567
+ ```ts
568
+ import {Branded, dbField, dbTable, Select} from '@ts-awesome/orm';
569
+
570
+ type FirstModelId = Branded<number, 'FirstModelId'>;
571
+
572
+ @dbTable('first_table')
573
+ class FirstModel {
574
+ @dbField({primaryKey: true})
575
+ public id!: FirstModelId;
576
+
577
+ @dbField({name: 'author_id'})
578
+ public authorId!: number;
579
+ }
580
+
581
+ const distinctQuery = Select(FirstModel, true)
582
+ .columns(({authorId}) => [authorId]);
583
+
584
+ const lockedQuery = Select(FirstModel, 'UPDATE')
585
+ .where(({id}) => id.eq(1 as FirstModelId));
586
+ ```
587
+
588
+ Supported `FOR` modes: `'UPDATE' | 'NO KEY UPDATE' | 'SHARE' | 'KEY SHARE'`.
589
+
590
+ ### Set operations
591
+
592
+ ```ts
593
+ import {dbField, dbTable, Select} from '@ts-awesome/orm';
594
+
595
+ enum UserStatus {
596
+ Active = 1,
597
+ Inactive = 0,
598
+ }
599
+
600
+ @dbTable('first_table')
601
+ class FirstModel {
602
+ @dbField({model: UserStatus})
603
+ public status!: UserStatus;
604
+ }
605
+
606
+ const query = Select(FirstModel)
607
+ .where(({status}) => status.eq(UserStatus.Active))
608
+ .union(true, Select(FirstModel).where(({status}) => status.eq(UserStatus.Inactive)));
609
+ ```
610
+
611
+ Set operations require compatible column lists; the boolean flag toggles `DISTINCT`.
612
+
613
+ ### Join types
614
+
615
+ ```ts
616
+ import {Branded, dbField, dbTable, Select} from '@ts-awesome/orm';
617
+
618
+ type AuthorId = Branded<number, 'AuthorId'>;
619
+
620
+ @dbTable('first_table')
621
+ class FirstModel {
622
+ @dbField({name: 'author_id'})
623
+ public authorId!: AuthorId;
624
+ }
625
+
626
+ @dbTable('second_table')
627
+ class SecondModel {
628
+ @dbField({primaryKey: true})
629
+ public id!: AuthorId;
630
+ }
233
631
 
234
632
  const query = Select(FirstModel)
235
- // lets join SecondModel by FK
236
- .join(SecondModel, (root, other) => root.authorId.eq(other.id))
237
- // lets sort by author and title reverse
238
- .orderby(({title}) => [of(SecondModel, 'name'), desc(title)])
239
- .limit(10)
633
+ .joinLeft(SecondModel, (root, other) => root.authorId.eq(other.id));
634
+ ```
635
+
636
+ Join variants include `joinLeft`, `joinRight`, and `joinFull`.
637
+
638
+ ### Scalar subqueries
639
+
640
+ ```ts
641
+ import {Select} from '@ts-awesome/orm';
642
+
643
+ const subquery = Select(FirstModel)
644
+ .columns(({authorId}) => [authorId])
645
+ .where(({id}) => id.eq(1 as FirstModelId))
646
+ .asScalar();
240
647
  ```
241
648
 
242
649
  ## Other builders
243
650
 
244
- ORM provides `Insert`, `Update`, `Upset` and `Delete` builders
651
+ ORM provides `Insert`, `Update`, `Upsert` and `Delete` builders.
245
652
 
246
653
  ### Insert
247
654
 
248
655
  ```ts
249
- import {Insert} from '@ts-awesome/orm';
656
+ import {dbField, dbTable, Insert} from '@ts-awesome/orm';
657
+
658
+ @dbTable('first_table')
659
+ class FirstModel {
660
+ @dbField
661
+ public title!: string;
662
+ }
250
663
 
251
664
  const query = Insert(FirstModel)
252
665
  .values({
253
666
  title: 'New book'
254
- })
667
+ });
255
668
  ```
256
669
 
257
670
  ### Update
258
671
 
259
672
  ```ts
260
- import {Update} from '@ts-awesome/orm';
673
+ import {Branded, dbField, dbTable, Update} from '@ts-awesome/orm';
674
+
675
+ type FirstModelId = Branded<number, 'FirstModelId'>;
676
+
677
+ @dbTable('first_table')
678
+ class FirstModel {
679
+ @dbField({primaryKey: true})
680
+ public id!: FirstModelId;
681
+
682
+ @dbField
683
+ public title!: string;
684
+ }
261
685
 
262
686
  const query = Update(FirstModel)
263
687
  .values({
264
688
  title: 'New book'
265
689
  })
266
- .where(({id}) => id.eq(2))
690
+ .where(({id}) => id.eq(2 as FirstModelId));
267
691
  ```
268
692
 
269
693
  ### Upsert
270
694
 
271
695
  ```ts
272
- import {Upsert} from '@ts-awesome/orm';
696
+ import {Branded, dbField, dbTable, Upsert} from '@ts-awesome/orm';
697
+
698
+ type FirstModelId = Branded<number, 'FirstModelId'>;
699
+
700
+ @dbTable('first_table')
701
+ class FirstModel {
702
+ @dbField({primaryKey: true})
703
+ public id!: FirstModelId;
704
+
705
+ @dbField
706
+ public title!: string;
707
+ }
273
708
 
274
709
  const query = Upsert(FirstModel)
275
710
  .values({
276
711
  title: 'New book'
277
712
  })
278
- .where(({id}) => id.eq(2))
713
+ .where(({id}) => id.eq(2 as FirstModelId))
279
714
  // conflict resolution index is defined in @dbTable decorator
280
- .conflict('index_name')
715
+ .conflict('index_name');
281
716
  ```
282
717
 
283
718
 
284
719
  ### Delete
285
720
 
286
721
  ```ts
287
- import {Delete} from '@ts-awesome/orm';
722
+ import {Branded, dbField, dbTable, Delete} from '@ts-awesome/orm';
723
+
724
+ type AuthorId = Branded<number, 'AuthorId'>;
725
+
726
+ @dbTable('first_table')
727
+ class FirstModel {
728
+ @dbField({name: 'author_id'})
729
+ public authorId!: AuthorId;
730
+ }
288
731
 
289
732
  const query = Delete(FirstModel)
290
- .where(({authorId}) => authorId.eq(2))
733
+ .where(({authorId}) => authorId.eq(2 as AuthorId));
734
+ ```
735
+
736
+
737
+ ### Window Functions
738
+
739
+ The ORM supports standard SQL window functions using the `Window` builder.
740
+
741
+ ```typescript
742
+ import {alias, dbField, dbTable, desc, row_number, Select, Window} from '@ts-awesome/orm';
743
+
744
+ @dbTable('first_table')
745
+ class FirstModel {
746
+ @dbField({primaryKey: true})
747
+ public id!: number;
748
+
749
+ @dbField({name: 'author_id'})
750
+ public authorId!: number;
751
+
752
+ @dbField({name: 'created_at'})
753
+ public createdAt!: Date;
754
+ }
755
+
756
+ // Define the window
757
+ const w = new Window(FirstModel)
758
+ .partitionBy(['authorId'])
759
+ .orderBy(desc('createdAt'));
760
+
761
+ const query = Select(FirstModel)
762
+ .columns(({id, authorId}) => [
763
+ id,
764
+ authorId,
765
+ // Use the window definition
766
+ alias(row_number(w), 'row_num')
767
+ ]);
768
+ ```
769
+
770
+ Supported functions: `row_number`, `rank`, `dense_rank`, `percent_rank`, `cume_dist`, `ntile`, `lag`, `lead`, `first_value`, `last_value`, `nth_value`.
771
+
772
+ Window framing is supported via `range()`, `rows()`, `groups()`, and `start()/end()/exclusion()`.
773
+
774
+ ```ts
775
+ const framed = new Window(FirstModel)
776
+ .partitionBy(['authorId'])
777
+ .orderBy(desc('createdAt'))
778
+ .rows()
779
+ .start(1, 'PRECEDING')
780
+ .end('CURRENT ROW');
781
+ ```
782
+
783
+
784
+ # Branded Types
785
+
786
+ To improve type safety for IDs, you can use `Branded<T, Brand>`.
787
+
788
+ ```typescript
789
+ import {Branded, dbField, dbTable} from '@ts-awesome/orm';
790
+
791
+ // Branded ID types (best practice)
792
+ type UserId = Branded<number, 'UserId'>;
793
+
794
+ @dbTable('users')
795
+ class User {
796
+ @dbField({primaryKey: true})
797
+ public id!: UserId;
798
+ }
799
+
800
+ // Now you can't accidentally pass a plain number or a different ID type
801
+ // const user: User = ...;
802
+ // const otherId: OrderId = 5;
803
+ // user.id = otherId; // Error
804
+ // user.id = 5 as UserId; // OK
805
+
806
+ // For string-based UIDs (UUID, NanoID, etc.)
807
+ type OrderUid = Branded<string, 'OrderUid'>;
808
+
809
+ @dbTable('orders')
810
+ class Order {
811
+ @dbField({primaryKey: true})
812
+ public uid!: OrderUid;
813
+ }
814
+
815
+ // const orderUid = 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11' as OrderUid;
816
+ ```
817
+
818
+ Array models are supported using `model: [Class]`.
819
+
820
+ ```ts
821
+ @dbTable('groups')
822
+ class Group {
823
+ @dbField({model: [String]})
824
+ public tags!: string[];
825
+ }
826
+ ```
827
+
828
+ # Test Driver
829
+
830
+ The `@ts-awesome/orm/test-driver` module provides a powerful mechanism to unit test your services without a real database. It allows you to mock query results and inspect executed queries.
831
+
832
+ ```typescript
833
+ import {dbField, dbTable, Select} from '@ts-awesome/orm';
834
+ import {TestCompiler, TestDriver} from '@ts-awesome/orm/test-driver';
835
+
836
+ @dbTable('users')
837
+ class User {
838
+ @dbField({primaryKey: true})
839
+ public id!: number;
840
+ }
841
+
842
+ const driver = new TestDriver();
843
+ const compiler = new TestCompiler();
844
+
845
+ // Mock results
846
+ driver.whenSelect('users').return([{ id: 1, name: 'Alice' }]);
847
+
848
+ // Assertions
849
+ expect(driver.executedQueries[0].tableName).toBe('users');
850
+
851
+ // Include sensitive fields when reading
852
+ const results = await driver.execute(compiler.compile(Select(User)), User, true);
291
853
  ```
292
854
 
855
+ For full documentation, please refer to the [Test Driver README](src/test-driver/README.md).
293
856
 
294
857
  # License
295
858
  May be freely distributed under the [MIT license](https://opensource.org/licenses/MIT).