@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.
- package/README.md +633 -70
- package/dist/base.d.ts +3 -2
- package/dist/base.d.ts.map +1 -0
- package/dist/base.js +16 -13
- package/dist/base.js.map +1 -1
- package/dist/branded.d.ts +1 -0
- package/dist/branded.d.ts.map +1 -0
- package/dist/builder.d.ts +10 -8
- package/dist/builder.d.ts.map +1 -0
- package/dist/builder.js +132 -54
- package/dist/builder.js.map +1 -1
- package/dist/compiler.d.ts +12 -10
- package/dist/compiler.d.ts.map +1 -0
- package/dist/compiler.js +191 -83
- package/dist/compiler.js.map +1 -1
- package/dist/decorators.d.ts +3 -2
- package/dist/decorators.d.ts.map +1 -0
- package/dist/decorators.js +31 -20
- package/dist/decorators.js.map +1 -1
- package/dist/errors.d.ts +1 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/interfaces.d.ts +175 -50
- package/dist/interfaces.d.ts.map +1 -0
- package/dist/intermediate.d.ts +5 -4
- package/dist/intermediate.d.ts.map +1 -0
- package/dist/operators.d.ts +384 -27
- package/dist/operators.d.ts.map +1 -0
- package/dist/operators.js +489 -43
- package/dist/operators.js.map +1 -1
- package/dist/reader.d.ts +3 -2
- package/dist/reader.d.ts.map +1 -0
- package/dist/reader.js +38 -26
- package/dist/reader.js.map +1 -1
- package/dist/symbols.d.ts +1 -0
- package/dist/symbols.d.ts.map +1 -0
- package/dist/test-driver/compiler.d.ts +3 -2
- package/dist/test-driver/compiler.d.ts.map +1 -0
- package/dist/test-driver/compiler.js +128 -14
- package/dist/test-driver/compiler.js.map +1 -1
- package/dist/test-driver/driver.d.ts +3 -2
- package/dist/test-driver/driver.d.ts.map +1 -0
- package/dist/test-driver/driver.js +5 -3
- package/dist/test-driver/driver.js.map +1 -1
- package/dist/test-driver/executor.d.ts +15 -3
- package/dist/test-driver/executor.d.ts.map +1 -0
- package/dist/test-driver/executor.js +95 -6
- package/dist/test-driver/executor.js.map +1 -1
- package/dist/test-driver/index.d.ts +1 -0
- package/dist/test-driver/index.d.ts.map +1 -0
- package/dist/test-driver/interfaces.d.ts +31 -6
- package/dist/test-driver/interfaces.d.ts.map +1 -0
- package/dist/test-driver/transaction.d.ts +4 -3
- package/dist/test-driver/transaction.d.ts.map +1 -0
- package/dist/test-driver/transaction.js +5 -5
- package/dist/test-driver/transaction.js.map +1 -1
- package/dist/wrappers.d.ts +88 -194
- package/dist/wrappers.d.ts.map +1 -0
- package/dist/wrappers.js +179 -96
- package/dist/wrappers.js.map +1 -1
- package/eslint.config.js +49 -0
- package/jest.config.js +4 -0
- package/package.json +14 -12
- package/.eslintrc.json +0 -27
package/README.md
CHANGED
|
@@ -1,51 +1,115 @@
|
|
|
1
1
|
# @ts-awesome/orm
|
|
2
2
|
|
|
3
|
-
TypeScript
|
|
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
|
|
8
|
+
* no relation navigation (intentional)
|
|
9
9
|
* heavy use of type checks and lambdas
|
|
10
|
-
*
|
|
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
|
-
|
|
53
|
+
Model metadata is defined with `dbTable` and `dbField` decorators.
|
|
15
54
|
|
|
16
55
|
```ts
|
|
17
|
-
import {dbField,
|
|
18
|
-
import {DB_JSON} from
|
|
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!:
|
|
80
|
+
public id!: FirstModelId;
|
|
25
81
|
|
|
26
82
|
// just another field
|
|
27
83
|
@dbField
|
|
28
84
|
public title!: string;
|
|
29
85
|
|
|
30
|
-
//
|
|
86
|
+
// let's map prop to different field
|
|
31
87
|
@dbField({name: 'author_id'})
|
|
32
|
-
public authorId!:
|
|
88
|
+
public authorId!: AuthorId;
|
|
33
89
|
|
|
34
90
|
// nullable field requires explicit model and nullable
|
|
35
|
-
// these are direct
|
|
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
|
-
//
|
|
43
|
-
@
|
|
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
|
|
60
|
-
import {ISqlQuery, PgCompiler} from
|
|
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/
|
|
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
|
|
75
|
-
|
|
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 `
|
|
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
|
|
408
|
+
Sometimes you may need to perform joins for filtering.
|
|
150
409
|
|
|
151
410
|
```ts
|
|
152
|
-
import {dbTable,
|
|
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({
|
|
157
|
-
public id!:
|
|
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
|
-
//
|
|
431
|
+
// let's join SecondModel by FK
|
|
165
432
|
.join(SecondModel, (root, other) => root.authorId.eq(other.id))
|
|
166
|
-
//
|
|
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
|
|
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,
|
|
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({
|
|
179
|
-
public id!:
|
|
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({
|
|
188
|
-
public id!:
|
|
457
|
+
@dbField({primaryKey: true, autoIncrement: true})
|
|
458
|
+
public id!: ThirdModelId;
|
|
189
459
|
|
|
190
460
|
@dbField
|
|
191
|
-
public createdBy!:
|
|
192
|
-
|
|
461
|
+
public createdBy!: AuthorId;
|
|
462
|
+
|
|
193
463
|
@dbField
|
|
194
|
-
public ownedBy!:
|
|
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
|
-
//
|
|
470
|
+
// let's join SecondModel by FK
|
|
201
471
|
.join(SecondModel, ownerRef, (root, other) => root.ownedBy.eq(other.id))
|
|
202
|
-
//
|
|
472
|
+
// let's join SecondModel by FK
|
|
203
473
|
.join(SecondModel, creatorRef, (root, other) => root.createdBy.eq(other.id))
|
|
204
|
-
//
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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`, `
|
|
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).
|