@simplysm/orm-common 13.0.81 → 13.0.83

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.
@@ -0,0 +1,420 @@
1
+ # Queryable
2
+
3
+ Chainable query builder for constructing type-safe SELECT, INSERT, UPDATE, DELETE, JOIN, GROUP BY, UNION, and recursive CTE queries.
4
+
5
+ ## API Reference
6
+
7
+ ### `Queryable<TData, TFrom>`
8
+
9
+ The core query builder class. Created automatically from DbContext table/view accessors (e.g., `db.user()`).
10
+
11
+ - `TData` -- Data type of the query result
12
+ - `TFrom` -- Source table builder (required for CUD operations; `never` for views/subqueries)
13
+
14
+ ---
15
+
16
+ ### SELECT / Projection
17
+
18
+ #### `.select(fn)`
19
+
20
+ Specify columns to SELECT with optional transformations.
21
+
22
+ ```typescript
23
+ select<R>(fn: (columns) => R): Queryable<UnwrapQueryableRecord<R>, never>
24
+ ```
25
+
26
+ ```typescript
27
+ db.user().select((u) => ({
28
+ userName: u.name,
29
+ upperEmail: expr.upper(u.email),
30
+ }))
31
+ ```
32
+
33
+ #### `.distinct()`
34
+
35
+ Apply DISTINCT to remove duplicate rows.
36
+
37
+ ```typescript
38
+ db.user().select((u) => ({ name: u.name })).distinct()
39
+ ```
40
+
41
+ #### `.lock()`
42
+
43
+ Apply row lock (`FOR UPDATE`) within a transaction.
44
+
45
+ ```typescript
46
+ db.user().where((u) => [expr.eq(u.id, 1)]).lock()
47
+ ```
48
+
49
+ ---
50
+
51
+ ### Filtering
52
+
53
+ #### `.where(predicate)`
54
+
55
+ Add WHERE conditions. Multiple calls are combined with AND.
56
+
57
+ ```typescript
58
+ where(predicate: (columns) => WhereExprUnit[]): Queryable<TData, TFrom>
59
+ ```
60
+
61
+ ```typescript
62
+ db.user()
63
+ .where((u) => [expr.eq(u.status, "active")])
64
+ .where((u) => [expr.gte(u.age, 18)])
65
+ ```
66
+
67
+ #### `.search(fn, searchText)`
68
+
69
+ Full-text search using LIKE patterns. Supports `+required`, `-excluded`, `"exact phrase"`, and `*wildcard` syntax.
70
+
71
+ ```typescript
72
+ db.user().search((u) => [u.name, u.email], 'John +active -deleted')
73
+ ```
74
+
75
+ ---
76
+
77
+ ### Sorting and Pagination
78
+
79
+ #### `.orderBy(fn, direction?)`
80
+
81
+ Add ORDER BY. Multiple calls add additional sort columns.
82
+
83
+ ```typescript
84
+ db.user()
85
+ .orderBy((u) => u.name)
86
+ .orderBy((u) => u.createdAt, "DESC")
87
+ ```
88
+
89
+ #### `.top(count)`
90
+
91
+ Select only top N rows. Can be used without ORDER BY.
92
+
93
+ ```typescript
94
+ db.user().top(10)
95
+ ```
96
+
97
+ #### `.limit(skip, take)`
98
+
99
+ Set LIMIT/OFFSET for pagination. Requires `orderBy()` first.
100
+
101
+ ```typescript
102
+ db.user()
103
+ .orderBy((u) => u.createdAt)
104
+ .limit(0, 20) // first 20 rows
105
+ ```
106
+
107
+ ---
108
+
109
+ ### Grouping
110
+
111
+ #### `.groupBy(fn)`
112
+
113
+ Add GROUP BY clause.
114
+
115
+ ```typescript
116
+ db.order()
117
+ .select((o) => ({
118
+ userId: o.userId,
119
+ totalAmount: expr.sum(o.amount),
120
+ }))
121
+ .groupBy((o) => [o.userId])
122
+ ```
123
+
124
+ #### `.having(predicate)`
125
+
126
+ Add HAVING clause (filter after GROUP BY).
127
+
128
+ ```typescript
129
+ db.order()
130
+ .select((o) => ({
131
+ userId: o.userId,
132
+ totalAmount: expr.sum(o.amount),
133
+ }))
134
+ .groupBy((o) => [o.userId])
135
+ .having((o) => [expr.gte(o.totalAmount, 10000)])
136
+ ```
137
+
138
+ ---
139
+
140
+ ### JOIN
141
+
142
+ #### `.join(as, fn)` -- 1:N JOIN
143
+
144
+ LEFT OUTER JOIN that adds results as an array property.
145
+
146
+ ```typescript
147
+ db.user().join("posts", (qr, u) =>
148
+ qr.from(Post).where((p) => [expr.eq(p.authorId, u.id)])
149
+ )
150
+ // Result: { id, name, posts: [{ id, title }, ...] }
151
+ ```
152
+
153
+ #### `.joinSingle(as, fn)` -- N:1 / 1:1 JOIN
154
+
155
+ LEFT OUTER JOIN that adds results as a single object property.
156
+
157
+ ```typescript
158
+ db.post().joinSingle("author", (qr, p) =>
159
+ qr.from(User).where((u) => [expr.eq(u.id, p.authorId)])
160
+ )
161
+ // Result: { id, title, author: { id, name } | undefined }
162
+ ```
163
+
164
+ #### `.include(fn)` -- Automatic Relation JOIN
165
+
166
+ Automatically JOIN based on relations defined in `TableBuilder.relations()`.
167
+
168
+ ```typescript
169
+ // Single relation
170
+ db.post().include((p) => p.author)
171
+
172
+ // Nested relation
173
+ db.post().include((p) => p.author.company)
174
+
175
+ // Multiple relations (chain include calls)
176
+ db.user()
177
+ .include((u) => u.company)
178
+ .include((u) => u.posts)
179
+ ```
180
+
181
+ ---
182
+
183
+ ### Subquery and UNION
184
+
185
+ #### `.wrap()`
186
+
187
+ Wrap current Queryable as a subquery. Required before `count()` after `distinct()` or `groupBy()`.
188
+
189
+ ```typescript
190
+ const count = await db.user()
191
+ .select((u) => ({ name: u.name }))
192
+ .distinct()
193
+ .wrap()
194
+ .count();
195
+ ```
196
+
197
+ #### `Queryable.union(...queries)`
198
+
199
+ Combine multiple Queryables with UNION ALL.
200
+
201
+ ```typescript
202
+ const combined = Queryable.union(
203
+ db.user().where((u) => [expr.eq(u.type, "admin")]),
204
+ db.user().where((u) => [expr.eq(u.type, "manager")]),
205
+ );
206
+ ```
207
+
208
+ ---
209
+
210
+ ### Recursive CTE
211
+
212
+ #### `.recursive(fn)`
213
+
214
+ Build recursive Common Table Expressions for hierarchical data.
215
+
216
+ ```typescript
217
+ db.employee()
218
+ .where((e) => [expr.null(e.managerId)]) // base case: root nodes
219
+ .recursive((cte) =>
220
+ cte.from(Employee)
221
+ .where((e) => [expr.eq(e.managerId, e.self[0].id)])
222
+ )
223
+ ```
224
+
225
+ ---
226
+
227
+ ### Execution -- SELECT
228
+
229
+ #### `.execute()`
230
+
231
+ Execute SELECT and return result array.
232
+
233
+ ```typescript
234
+ const users: User[] = await db.user()
235
+ .where((u) => [expr.eq(u.isActive, true)])
236
+ .execute();
237
+ ```
238
+
239
+ #### `.single()`
240
+
241
+ Return single result. Throws if more than one result.
242
+
243
+ ```typescript
244
+ const user = await db.user()
245
+ .where((u) => [expr.eq(u.id, 1)])
246
+ .single();
247
+ // returns User | undefined
248
+ ```
249
+
250
+ #### `.first()`
251
+
252
+ Return first result (ignores additional results).
253
+
254
+ ```typescript
255
+ const latest = await db.user()
256
+ .orderBy((u) => u.createdAt, "DESC")
257
+ .first();
258
+ ```
259
+
260
+ #### `.count(fn?)`
261
+
262
+ Return row count. Cannot be used directly after `distinct()` or `groupBy()` (use `wrap()` first).
263
+
264
+ ```typescript
265
+ const count = await db.user()
266
+ .where((u) => [expr.eq(u.isActive, true)])
267
+ .count();
268
+ ```
269
+
270
+ #### `.exists()`
271
+
272
+ Check if any matching rows exist.
273
+
274
+ ```typescript
275
+ const hasAdmin = await db.user()
276
+ .where((u) => [expr.eq(u.role, "admin")])
277
+ .exists();
278
+ ```
279
+
280
+ ---
281
+
282
+ ### Execution -- INSERT
283
+
284
+ #### `.insert(records, outputColumns?)`
285
+
286
+ Insert records. Automatically chunks into batches of 1000 (MSSQL limit).
287
+
288
+ ```typescript
289
+ // Simple insert
290
+ await db.user().insert([
291
+ { name: "Alice", email: "alice@test.com" },
292
+ { name: "Bob" },
293
+ ]);
294
+
295
+ // Insert with output (get auto-generated IDs)
296
+ const inserted = await db.user().insert(
297
+ [{ name: "Alice" }],
298
+ ["id"],
299
+ );
300
+ // inserted[0].id
301
+ ```
302
+
303
+ #### `.insertIfNotExists(record, outputColumns?)`
304
+
305
+ Insert only if WHERE condition matches no rows.
306
+
307
+ ```typescript
308
+ await db.user()
309
+ .where((u) => [expr.eq(u.email, "test@test.com")])
310
+ .insertIfNotExists({ name: "Test", email: "test@test.com" });
311
+ ```
312
+
313
+ #### `.insertInto(targetTable, outputColumns?)`
314
+
315
+ INSERT INTO ... SELECT (copy current query results into another table).
316
+
317
+ ```typescript
318
+ await db.user()
319
+ .select((u) => ({ name: u.name, createdAt: u.createdAt }))
320
+ .where((u) => [expr.eq(u.isArchived, false)])
321
+ .insertInto(ArchivedUser);
322
+ ```
323
+
324
+ ---
325
+
326
+ ### Execution -- UPDATE
327
+
328
+ #### `.update(recordFn, outputColumns?)`
329
+
330
+ Update matching rows. The callback receives current column values for reference.
331
+
332
+ ```typescript
333
+ await db.user()
334
+ .where((u) => [expr.eq(u.id, 1)])
335
+ .update(() => ({
336
+ name: expr.val("string", "New Name"),
337
+ }));
338
+ ```
339
+
340
+ ---
341
+
342
+ ### Execution -- DELETE
343
+
344
+ #### `.delete(outputColumns?)`
345
+
346
+ Delete matching rows.
347
+
348
+ ```typescript
349
+ await db.user()
350
+ .where((u) => [expr.eq(u.status, "deleted")])
351
+ .delete();
352
+ ```
353
+
354
+ ---
355
+
356
+ ### Execution -- UPSERT
357
+
358
+ #### `.upsert(record, outputColumns?)`
359
+
360
+ INSERT or UPDATE (MERGE pattern). Requires a WHERE condition to check existence.
361
+
362
+ ```typescript
363
+ await db.user()
364
+ .where((u) => [expr.eq(u.email, "test@test.com")])
365
+ .upsert({
366
+ insert: { name: "Test", email: "test@test.com" },
367
+ update: { name: expr.val("string", "Updated") },
368
+ });
369
+ ```
370
+
371
+ ---
372
+
373
+ ### QueryDef Accessors
374
+
375
+ Each execution method has a corresponding `get*QueryDef()` method that returns the raw `QueryDef` without executing it:
376
+
377
+ - `getSelectQueryDef()` -- Returns `SelectQueryDef`
378
+ - `getInsertQueryDef(records, outputColumns?)` -- Returns `InsertQueryDef`
379
+ - `getInsertIfNotExistsQueryDef(record, outputColumns?)` -- Returns `InsertIfNotExistsQueryDef`
380
+ - `getInsertIntoQueryDef(targetTable, outputColumns?)` -- Returns `InsertIntoQueryDef`
381
+ - `getResultMeta(outputColumns?)` -- Returns `ResultMeta` for type parsing
382
+
383
+ ---
384
+
385
+ ## Usage Examples
386
+
387
+ ### Complex Query with Multiple Features
388
+
389
+ ```typescript
390
+ const result = await db.order()
391
+ .include((o) => o.customer)
392
+ .where((o) => [
393
+ expr.gte(o.createdAt, expr.val("DateTime", startDate)),
394
+ expr.eq(o.status, "completed"),
395
+ ])
396
+ .select((o) => ({
397
+ orderId: o.id,
398
+ customerName: o.customer.name,
399
+ total: o.totalAmount,
400
+ }))
401
+ .orderBy((o) => o.total, "DESC")
402
+ .limit(0, 50)
403
+ .execute();
404
+ ```
405
+
406
+ ### Aggregation with GROUP BY
407
+
408
+ ```typescript
409
+ const stats = await db.order()
410
+ .select((o) => ({
411
+ month: expr.month(o.createdAt),
412
+ year: expr.year(o.createdAt),
413
+ totalRevenue: expr.sum(o.amount),
414
+ orderCount: expr.count(),
415
+ }))
416
+ .groupBy((o) => [o.month, o.year])
417
+ .orderBy((o) => o.year)
418
+ .orderBy((o) => o.month)
419
+ .execute();
420
+ ```
@@ -0,0 +1,216 @@
1
+ # Schema Builders
2
+
3
+ Define database tables, views, and stored procedures with a fluent, type-safe API.
4
+
5
+ ## API Reference
6
+
7
+ ### `Table(name)`
8
+
9
+ Factory function that creates a `TableBuilder` for defining table schemas.
10
+
11
+ ```typescript
12
+ function Table(name: string): TableBuilder<{}, {}>
13
+ ```
14
+
15
+ #### TableBuilder Methods
16
+
17
+ | Method | Signature | Description |
18
+ |--------|-----------|-------------|
19
+ | `description(desc)` | `.description(desc: string)` | Set table description (DDL comment) |
20
+ | `database(db)` | `.database(db: string)` | Set database name |
21
+ | `schema(schema)` | `.schema(schema: string)` | Set schema name (MSSQL: `dbo`, PostgreSQL: `public`) |
22
+ | `columns(fn)` | `.columns((c) => ({...}))` | Define columns using column factory |
23
+ | `primaryKey(...cols)` | `.primaryKey("col1", "col2")` | Set primary key (single or composite) |
24
+ | `indexes(fn)` | `.indexes((i) => [...])` | Define indexes |
25
+ | `relations(fn)` | `.relations((r) => ({...}))` | Define relationships (FK, reverse FK, logical relations) |
26
+
27
+ #### Type Inference Properties
28
+
29
+ | Property | Description |
30
+ |----------|-------------|
31
+ | `$inferSelect` | Full type (columns + deep relations) |
32
+ | `$inferColumns` | Columns only |
33
+ | `$inferInsert` | Insert type (autoIncrement/nullable/default fields are optional) |
34
+ | `$inferUpdate` | Update type (all fields optional) |
35
+
36
+ ---
37
+
38
+ ### `View(name)`
39
+
40
+ Factory function that creates a `ViewBuilder` for defining database views.
41
+
42
+ ```typescript
43
+ function View(name: string): ViewBuilder<any, {}, {}>
44
+ ```
45
+
46
+ #### ViewBuilder Methods
47
+
48
+ | Method | Signature | Description |
49
+ |--------|-----------|-------------|
50
+ | `description(desc)` | `.description(desc: string)` | Set view description |
51
+ | `database(db)` | `.database(db: string)` | Set database name |
52
+ | `schema(schema)` | `.schema(schema: string)` | Set schema name |
53
+ | `query(viewFn)` | `.query((db) => db.table().select(...))` | Define view SELECT query |
54
+ | `relations(fn)` | `.relations((r) => ({...}))` | Define relationships (logical only, no FK) |
55
+
56
+ ---
57
+
58
+ ### `Procedure(name)`
59
+
60
+ Factory function that creates a `ProcedureBuilder` for defining stored procedures.
61
+
62
+ ```typescript
63
+ function Procedure(name: string): ProcedureBuilder<never, never>
64
+ ```
65
+
66
+ #### ProcedureBuilder Methods
67
+
68
+ | Method | Signature | Description |
69
+ |--------|-----------|-------------|
70
+ | `description(desc)` | `.description(desc: string)` | Set procedure description |
71
+ | `database(db)` | `.database(db: string)` | Set database name |
72
+ | `schema(schema)` | `.schema(schema: string)` | Set schema name |
73
+ | `params(fn)` | `.params((c) => ({...}))` | Define input parameters |
74
+ | `returns(fn)` | `.returns((c) => ({...}))` | Define return columns |
75
+ | `body(sql)` | `.body("SELECT ...")` | Set procedure body SQL |
76
+
77
+ ---
78
+
79
+ ### Column Factory
80
+
81
+ The column factory (`c`) is provided inside `.columns()` and `.params()`/`.returns()` callbacks.
82
+
83
+ #### Column Types
84
+
85
+ | Method | SQL Type | TypeScript Type | Notes |
86
+ |--------|----------|-----------------|-------|
87
+ | `c.int()` | INT | `number` | 4 bytes |
88
+ | `c.bigint()` | BIGINT | `number` | 8 bytes |
89
+ | `c.float()` | FLOAT | `number` | Single precision |
90
+ | `c.double()` | DOUBLE | `number` | Double precision |
91
+ | `c.decimal(p, s)` | DECIMAL(p,s) | `number` | Fixed-point |
92
+ | `c.varchar(len)` | VARCHAR(len) | `string` | Variable-length string |
93
+ | `c.char(len)` | CHAR(len) | `string` | Fixed-length string |
94
+ | `c.text()` | TEXT | `string` | Large text |
95
+ | `c.binary()` | BLOB/VARBINARY/BYTEA | `Bytes` | Binary data |
96
+ | `c.boolean()` | TINYINT(1)/BIT/BOOLEAN | `boolean` | |
97
+ | `c.datetime()` | DATETIME | `DateTime` | Date + time |
98
+ | `c.date()` | DATE | `DateOnly` | Date only |
99
+ | `c.time()` | TIME | `Time` | Time only |
100
+ | `c.uuid()` | BINARY(16)/UNIQUEIDENTIFIER/UUID | `Uuid` | |
101
+
102
+ #### Column Modifiers
103
+
104
+ | Method | Description |
105
+ |--------|-------------|
106
+ | `.autoIncrement()` | Auto-increment (optional in INSERT inference) |
107
+ | `.nullable()` | Allow NULL (adds `undefined` to type) |
108
+ | `.default(value)` | Set default value (optional in INSERT inference) |
109
+ | `.description(desc)` | Set column comment |
110
+
111
+ ---
112
+
113
+ ### Index Factory
114
+
115
+ The index factory (`i`) is provided inside `.indexes()` callbacks.
116
+
117
+ ```typescript
118
+ i.index("col1", "col2") // Create index on columns
119
+ .unique() // Make unique
120
+ .orderBy("ASC", "DESC") // Set sort order per column
121
+ .name("IX_Custom_Name") // Custom index name
122
+ .description("desc") // Description
123
+ ```
124
+
125
+ ---
126
+
127
+ ### Relation Factory
128
+
129
+ The relation factory (`r`) is provided inside `.relations()` callbacks.
130
+
131
+ | Method | Type | Description |
132
+ |--------|------|-------------|
133
+ | `r.foreignKey(cols, targetFn)` | N:1 | FK constraint created in DB |
134
+ | `r.foreignKeyTarget(targetFn, relName)` | 1:N | Reverse FK reference (array by default) |
135
+ | `r.relationKey(cols, targetFn)` | N:1 | Logical relation (no DB FK) |
136
+ | `r.relationKeyTarget(targetFn, relName)` | 1:N | Logical reverse reference |
137
+
138
+ Both `foreignKeyTarget` and `relationKeyTarget` support `.single()` to indicate a 1:1 relationship (returns single object instead of array).
139
+
140
+ ---
141
+
142
+ ## Usage Examples
143
+
144
+ ### Complete Table Definition
145
+
146
+ ```typescript
147
+ const User = Table("User")
148
+ .database("mydb")
149
+ .description("Application users")
150
+ .columns((c) => ({
151
+ id: c.bigint().autoIncrement(),
152
+ name: c.varchar(100),
153
+ email: c.varchar(200).nullable(),
154
+ status: c.varchar(20).default("active"),
155
+ createdAt: c.datetime().default("CURRENT_TIMESTAMP"),
156
+ }))
157
+ .primaryKey("id")
158
+ .indexes((i) => [
159
+ i.index("email").unique(),
160
+ i.index("status", "createdAt").orderBy("ASC", "DESC"),
161
+ ]);
162
+ ```
163
+
164
+ ### Table with Relations
165
+
166
+ ```typescript
167
+ const Post = Table("Post")
168
+ .columns((c) => ({
169
+ id: c.bigint().autoIncrement(),
170
+ authorId: c.bigint(),
171
+ title: c.varchar(200),
172
+ }))
173
+ .primaryKey("id")
174
+ .relations((r) => ({
175
+ author: r.foreignKey(["authorId"], () => User),
176
+ }));
177
+
178
+ const User = Table("User")
179
+ .columns((c) => ({
180
+ id: c.bigint().autoIncrement(),
181
+ name: c.varchar(100),
182
+ }))
183
+ .primaryKey("id")
184
+ .relations((r) => ({
185
+ posts: r.foreignKeyTarget(() => Post, "author"),
186
+ profile: r.foreignKeyTarget(() => Profile, "user").single(),
187
+ }));
188
+ ```
189
+
190
+ ### View Definition
191
+
192
+ ```typescript
193
+ const ActiveUsers = View("ActiveUsers")
194
+ .database("mydb")
195
+ .query((db: MyDb) =>
196
+ db.user()
197
+ .where((u) => [expr.eq(u.status, "active")])
198
+ .select((u) => ({ id: u.id, name: u.name, email: u.email }))
199
+ );
200
+ ```
201
+
202
+ ### Procedure Definition
203
+
204
+ ```typescript
205
+ const GetUserById = Procedure("GetUserById")
206
+ .database("mydb")
207
+ .params((c) => ({
208
+ userId: c.bigint(),
209
+ }))
210
+ .returns((c) => ({
211
+ id: c.bigint(),
212
+ name: c.varchar(100),
213
+ email: c.varchar(200),
214
+ }))
215
+ .body("SELECT id, name, email FROM User WHERE id = userId");
216
+ ```