@simplysm/orm-common 14.0.4 → 14.0.5

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/docs/queryable.md CHANGED
@@ -4,233 +4,258 @@ Type-safe query builder and stored procedure executor.
4
4
 
5
5
  ## Queryable
6
6
 
7
- Chainable query builder class for constructing SELECT, INSERT, UPDATE, DELETE, and UPSERT queries against tables/views.
8
-
9
7
  ```typescript
10
- class Queryable<TData extends DataRecord, TFrom extends TableBuilder | never> {
8
+ class Queryable<TData extends DataRecord, TFrom extends TableBuilder<any, any> | never> {
11
9
  constructor(readonly meta: QueryableMeta<TData>);
12
10
  }
13
11
  ```
14
12
 
15
- **Type Parameters:**
13
+ Fluent query builder for composing SELECT, INSERT, UPDATE, DELETE, and UPSERT queries. All methods return new Queryable instances (immutable chaining).
16
14
 
17
- | Parameter | Description |
18
- |---|---|
19
- | `TData` | Query result data type |
20
- | `TFrom` | Source table builder type. `never` for views/subqueries (no CUD operations). |
15
+ - `TData` - Query result data type
16
+ - `TFrom` - Source table (required for CUD operations, `never` for read-only)
21
17
 
22
- ### SELECT / Options
18
+ ### Option Methods (SELECT / DISTINCT / LOCK)
23
19
 
24
20
  | Method | Signature | Description |
25
- |---|---|---|
26
- | `select` | `(fn: (cols) => R) => Queryable<UnwrapQueryableRecord<R>, never>` | Project specific columns. Replaces the default column set. |
27
- | `distinct` | `() => Queryable<TData, never>` | Apply DISTINCT to remove duplicate rows. |
28
- | `lock` | `() => Queryable<TData, TFrom>` | Apply row lock (FOR UPDATE) within a transaction. |
21
+ |--------|-----------|-------------|
22
+ | `select` | `<R>(fn: (cols: QueryableRecord<TData>) => R) => Queryable<UnwrapQueryableRecord<R>, never>` | Specify columns to SELECT |
23
+ | `distinct` | `() => Queryable<TData, never>` | Apply DISTINCT |
24
+ | `lock` | `() => Queryable<TData, TFrom>` | Apply FOR UPDATE row lock |
29
25
 
30
- ### Filtering
26
+ ### Restrict Methods (TOP / LIMIT)
31
27
 
32
28
  | Method | Signature | Description |
33
- |---|---|---|
34
- | `where` | `(predicate: (cols) => WhereExprUnit[]) => Queryable<TData, TFrom>` | Add WHERE conditions. Multiple calls are combined with AND. |
35
- | `search` | `(fn: (cols) => ExprUnit<string>[], searchText: string) => Queryable<TData, TFrom>` | Full-text search across columns. Supports `+required`, `-excluded`, `"exact phrase"`, and `*` wildcards. See `parseSearchQuery`. |
29
+ |--------|-----------|-------------|
30
+ | `top` | `(count: number) => Queryable<TData, TFrom>` | Select top N rows |
31
+ | `limit` | `(skip: number, take: number) => Queryable<TData, TFrom>` | Pagination (requires orderBy) |
36
32
 
37
- ### Sorting / Pagination
33
+ ### Sorting (ORDER BY)
38
34
 
39
35
  | Method | Signature | Description |
40
- |---|---|---|
41
- | `orderBy` | `(fn: (cols) => ExprUnit, orderBy?: "ASC" \| "DESC") => Queryable<TData, TFrom>` | Add ORDER BY. Multiple calls append in order. Default: ASC. |
42
- | `top` | `(count: number) => Queryable<TData, TFrom>` | Limit to N rows (works without ORDER BY). |
43
- | `limit` | `(skip: number, take: number) => Queryable<TData, TFrom>` | Pagination (OFFSET/LIMIT). Requires `orderBy()` first. |
36
+ |--------|-----------|-------------|
37
+ | `orderBy` | `(fn: (cols) => ExprUnit, orderBy?: "ASC" \| "DESC") => Queryable<TData, TFrom>` | Add sort condition (stackable) |
44
38
 
45
- ### Grouping
39
+ ### Filtering (WHERE)
46
40
 
47
41
  | Method | Signature | Description |
48
- |---|---|---|
49
- | `groupBy` | `(fn: (cols) => ExprUnit[]) => Queryable<TData, never>` | GROUP BY columns. |
50
- | `having` | `(predicate: (cols) => WhereExprUnit[]) => Queryable<TData, never>` | HAVING filter (after GROUP BY). |
42
+ |--------|-----------|-------------|
43
+ | `where` | `(predicate: (cols) => WhereExprUnit[]) => Queryable<TData, TFrom>` | Add WHERE conditions (stackable, AND) |
44
+ | `search` | `(fn: (cols) => ExprUnit<string\|undefined>[], searchText: string) => Queryable<TData, TFrom>` | Full-text search across columns |
45
+
46
+ ### Grouping (GROUP BY / HAVING)
47
+
48
+ | Method | Signature | Description |
49
+ |--------|-----------|-------------|
50
+ | `groupBy` | `(fn: (cols) => ExprUnit[]) => Queryable<TData, never>` | Group by columns |
51
+ | `having` | `(predicate: (cols) => WhereExprUnit[]) => Queryable<TData, never>` | Filter groups |
51
52
 
52
53
  ### JOIN
53
54
 
54
55
  | Method | Signature | Description |
55
- |---|---|---|
56
- | `join` | `(as, fn: (qr, cols) => Queryable<R>) => Queryable<TData & { [as]?: R[] }, TFrom>` | LEFT OUTER JOIN (1:N). Result added as array property. |
57
- | `joinSingle` | `(as, fn: (qr, cols) => Queryable<R>) => Queryable<TData & { [as]?: R }, TFrom>` | LEFT OUTER JOIN (N:1 or 1:1). Result added as single object. |
58
- | `include` | `(fn: (item: PathProxy) => PathProxy) => Queryable<TData, TFrom>` | Auto-JOIN based on FK/relation definitions. Supports nested paths (e.g., `p.author.company`). |
56
+ |--------|-----------|-------------|
57
+ | `join` | `<A, R>(as: A, fn: (qr: JoinQueryable, cols) => Queryable<R, any>) => Queryable<TData & { [K in A]?: R[] }, TFrom>` | LEFT OUTER JOIN (1:N, result as array) |
58
+ | `joinSingle` | `<A, R>(as: A, fn: (qr: JoinQueryable, cols) => Queryable<R, any>) => Queryable<TData & { [K in A]?: R }, TFrom>` | LEFT OUTER JOIN (N:1 or 1:1, result as single object) |
59
+ | `include` | `(fn: (item: PathProxy<TData>) => PathProxy) => Queryable<TData, TFrom>` | Auto-JOIN based on TableBuilder FK/FKT relations |
60
+
61
+ ### Subquery / Union
62
+
63
+ | Method | Signature | Description |
64
+ |--------|-----------|-------------|
65
+ | `wrap` | `() => Queryable<TData, never>` | Wrap as subquery (needed for count after distinct/groupBy) |
66
+ | `Queryable.union` (static) | `(...queries: Queryable<TData, any>[]) => Queryable<TData, never>` | Combine queries with UNION (min 2) |
59
67
 
60
- ### Subquery / UNION / Recursive
68
+ ### Recursive CTE
61
69
 
62
70
  | Method | Signature | Description |
63
- |---|---|---|
64
- | `wrap` | `() => Queryable<TData, never>` | Wrap current query as subquery. Required before `count()` after `distinct()`/`groupBy()`. |
65
- | `static union` | `(...queries: Queryable[]) => Queryable<TData, never>` | Combine 2+ Queryables with UNION. |
66
- | `recursive` | `(fn: (cte: RecursiveQueryable) => Queryable) => Queryable<TData, never>` | Generate a recursive CTE (WITH RECURSIVE). For hierarchical data. |
71
+ |--------|-----------|-------------|
72
+ | `recursive` | `(fn: (cte: RecursiveQueryable<TData>) => Queryable<TData, any>) => Queryable<TData, never>` | Generate recursive CTE (WITH RECURSIVE) |
67
73
 
68
74
  ### Execution (SELECT)
69
75
 
70
76
  | Method | Signature | Description |
71
- |---|---|---|
72
- | `execute` | `() => Promise<TData[]>` | Execute SELECT and return result array. |
73
- | `single` | `() => Promise<TData \| undefined>` | Return single result. Throws if more than one. |
74
- | `first` | `() => Promise<TData \| undefined>` | Return first result (even if multiple exist). |
75
- | `count` | `(fn?) => Promise<number>` | Return row count. Throws after `distinct()`/`groupBy()` (use `wrap()` first). |
76
- | `exists` | `() => Promise<boolean>` | Check if any matching data exists. |
77
+ |--------|-----------|-------------|
78
+ | `execute` | `() => Promise<TData[]>` | Execute SELECT and return results |
79
+ | `single` | `() => Promise<TData \| undefined>` | Return single result (throws if > 1) |
80
+ | `first` | `() => Promise<TData \| undefined>` | Return first result |
81
+ | `count` | `(fn?: (cols) => ExprUnit) => Promise<number>` | Count rows (throws after distinct/groupBy without wrap) |
82
+ | `exists` | `() => Promise<boolean>` | Check if any matching data exists |
77
83
 
78
- ### Execution (INSERT)
84
+ ### QueryDef Getters (SELECT)
79
85
 
80
86
  | Method | Signature | Description |
81
- |---|---|---|
82
- | `insert` | `(records: TFrom["$inferInsert"][]) => Promise<void>` | Insert records. Auto-chunks at 1000 for MSSQL. |
83
- | `insert` | `(records, outputColumns: K[]) => Promise<Pick<...>[]>` | Insert and return specified output columns. |
84
- | `insertIfNotExists` | `(record: TFrom["$inferInsert"]) => Promise<void>` | Insert only if WHERE condition matches no rows. |
85
- | `insertIfNotExists` | `(record, outputColumns: K[]) => Promise<Pick<...>>` | Insert if not exists and return output columns. |
86
- | `insertInto` | `(targetTable: TableBuilder) => Promise<void>` | INSERT INTO ... SELECT from current query. |
87
- | `insertInto` | `(targetTable, outputColumns: K[]) => Promise<Pick<...>[]>` | INSERT INTO with output columns. |
87
+ |--------|-----------|-------------|
88
+ | `getSelectQueryDef` | `() => SelectQueryDef` | Get the SELECT QueryDef AST |
89
+ | `getResultMeta` | `(outputColumns?: string[]) => ResultMeta` | Get result parsing metadata |
88
90
 
89
- ### Execution (UPDATE / DELETE)
91
+ ### INSERT
90
92
 
91
93
  | Method | Signature | Description |
92
- |---|---|---|
93
- | `update` | `(recordFwd: (cols) => WriteRecord) => Promise<void>` | Update matching records. Use `expr.val()` for literal values. |
94
- | `update` | `(recordFwd, outputColumns: K[]) => Promise<Pick<...>[]>` | Update and return output columns. |
95
- | `delete` | `() => Promise<void>` | Delete matching records. |
96
- | `delete` | `(outputColumns: K[]) => Promise<Pick<...>[]>` | Delete and return output columns. |
94
+ |--------|-----------|-------------|
95
+ | `insert` | `(records: TFrom["$inferInsert"][]) => Promise<void>` | Insert records (auto-chunks per 1000) |
96
+ | `insert` | `(records, outputColumns: K[]) => Promise<Pick<...>[]>` | Insert with output columns |
97
+ | `insertIfNotExists` | `(record: TFrom["$inferInsert"]) => Promise<void>` | Insert if WHERE condition has no match |
98
+ | `insertIfNotExists` | `(record, outputColumns: K[]) => Promise<Pick<...>>` | Insert if not exists with output |
99
+ | `insertInto` | `(targetTable: TableBuilder) => Promise<void>` | INSERT INTO ... SELECT |
100
+ | `insertInto` | `(targetTable, outputColumns: K[]) => Promise<Pick<...>[]>` | INSERT INTO ... SELECT with output |
97
101
 
98
- ### Execution (UPSERT)
102
+ ### UPDATE / DELETE
99
103
 
100
104
  | Method | Signature | Description |
101
- |---|---|---|
102
- | `upsert` | `(updateFn: (cols) => WriteRecord) => Promise<void>` | Update if exists, insert otherwise (same data). |
103
- | `upsert` | `(updateFn, insertFn) => Promise<void>` | Update/insert with different data. |
104
- | `upsert` | `(updateFn, insertFn?, outputColumns?: K[]) => Promise<Pick<...>[] \| void>` | Upsert with optional output columns. |
105
+ |--------|-----------|-------------|
106
+ | `update` | `(recordFwd: (cols) => QueryableWriteRecord) => Promise<void>` | Update matching rows |
107
+ | `update` | `(recordFwd, outputColumns: K[]) => Promise<Pick<...>[]>` | Update with output |
108
+ | `delete` | `() => Promise<void>` | Delete matching rows |
109
+ | `delete` | `(outputColumns: K[]) => Promise<Pick<...>[]>` | Delete with output |
105
110
 
106
- ### QueryDef Generators
111
+ ### UPSERT
107
112
 
108
- Each execution method has a corresponding `get*QueryDef` method:
113
+ | Method | Signature | Description |
114
+ |--------|-----------|-------------|
115
+ | `upsert` | `(updateFn: (cols) => WriteRecord) => Promise<void>` | Update or insert (same data) |
116
+ | `upsert` | `(updateFn, insertFn) => Promise<void>` | Update or insert (different data) |
117
+ | `upsert` | `(updateFn, insertFn?, outputColumns?) => Promise<Pick<...>[] \| void>` | With output columns |
109
118
 
110
- - `getSelectQueryDef(): SelectQueryDef`
111
- - `getInsertQueryDef(records, outputColumns?): InsertQueryDef`
112
- - `getInsertIfNotExistsQueryDef(record, outputColumns?): InsertIfNotExistsQueryDef`
113
- - `getInsertIntoQueryDef(targetTable, outputColumns?): InsertIntoQueryDef`
114
- - `getUpdateQueryDef(recordFwd, outputColumns?): UpdateQueryDef`
115
- - `getDeleteQueryDef(outputColumns?): DeleteQueryDef`
116
- - `getUpsertQueryDef(updateRecordFn, insertRecordFn, outputColumns?): UpsertQueryDef`
117
- - `getResultMeta(outputColumns?): ResultMeta`
119
+ ### DDL Helper
118
120
 
119
- ## QueryableRecord
121
+ | Method | Signature | Description |
122
+ |--------|-----------|-------------|
123
+ | `switchFk` | `(enabled: boolean) => Promise<void>` | Enable/disable FK constraints on this table |
120
124
 
121
- Maps data record fields to `ExprUnit` wrappers for type-safe expression building. Nested objects and arrays are recursively mapped.
125
+ ## queryable (factory)
122
126
 
123
127
  ```typescript
124
- type QueryableRecord<TData extends DataRecord> = {
125
- [K in keyof TData]: TData[K] extends ColumnPrimitive
126
- ? ExprUnit<TData[K]>
127
- : TData[K] extends (infer U)[]
128
- ? U extends DataRecord ? QueryableRecord<U>[] : never
129
- : TData[K] extends DataRecord ? QueryableRecord<TData[K]> : never;
130
- };
128
+ function queryable<TBuilder extends TableBuilder<any, any> | ViewBuilder<any, any, any>>(
129
+ db: DbContextBase,
130
+ tableOrView: TBuilder,
131
+ as?: string,
132
+ ): () => Queryable<TBuilder["$inferSelect"], TBuilder extends TableBuilder ? TBuilder : never>
131
133
  ```
132
134
 
133
- ## QueryableWriteRecord
135
+ Creates a factory function that returns a new Queryable instance each time it is called. Each call allocates a fresh alias via `db.getNextAlias()`.
134
136
 
135
- Maps data fields to `ExprInput` for write operations (INSERT/UPDATE).
137
+ ## Executable
136
138
 
137
139
  ```typescript
138
- type QueryableWriteRecord<TData> = {
139
- [K in keyof TData]: TData[K] extends ColumnPrimitive ? ExprInput<TData[K]> : never;
140
- };
140
+ class Executable<TParams extends ColumnBuilderRecord, TReturns extends ColumnBuilderRecord> {
141
+ constructor(db: DbContextBase, builder: ProcedureBuilder<TParams, TReturns>);
142
+ getExecProcQueryDef(params?: InferColumnExprs<TParams>): { type: "execProc"; ... };
143
+ async execute(params: InferColumnExprs<TParams>): Promise<InferColumnExprs<TReturns>[][]>;
144
+ }
141
145
  ```
142
146
 
143
- ## NullableQueryableRecord
147
+ Stored procedure execution wrapper.
148
+
149
+ | Method | Signature | Description |
150
+ |--------|-----------|-------------|
151
+ | `getExecProcQueryDef` | `(params?) => ExecProcQueryDef` | Build procedure execution QueryDef |
152
+ | `execute` | `(params) => Promise<T[][]>` | Execute the procedure |
144
153
 
145
- Same as `QueryableRecord` but all primitive fields include `| undefined` (for LEFT JOIN null propagation).
154
+ ## executable (factory)
146
155
 
147
156
  ```typescript
148
- type NullableQueryableRecord<TData extends DataRecord> = {
149
- [K in keyof TData]: TData[K] extends ColumnPrimitive
150
- ? ExprUnit<TData[K] | undefined>
151
- : /* recursive for nested records */;
152
- };
157
+ function executable<TParams, TReturns>(
158
+ db: DbContextBase,
159
+ builder: ProcedureBuilder<TParams, TReturns>,
160
+ ): () => Executable<TParams, TReturns>
153
161
  ```
154
162
 
155
- ## getMatchedPrimaryKeys
163
+ Creates a factory function that returns a new Executable instance.
156
164
 
157
- Returns the primary key columns of a target table matched to foreign key columns.
165
+ ## parseSearchQuery
158
166
 
159
167
  ```typescript
160
- function getMatchedPrimaryKeys(
161
- fkCols: string[],
162
- targetTable: TableBuilder<any, any>,
163
- ): string[];
168
+ function parseSearchQuery(searchText: string): ParsedSearchQuery
164
169
  ```
165
170
 
166
- ## Executable
171
+ Parses a search query string into SQL LIKE patterns.
167
172
 
168
- Stored procedure execution wrapper class.
173
+ | Syntax | Meaning | Example |
174
+ |--------|---------|---------|
175
+ | `term1 term2` | OR (match any) | `apple banana` |
176
+ | `+term` | Required (AND) | `+apple +banana` |
177
+ | `-term` | Exclude (NOT) | `apple -banana` |
178
+ | `"exact phrase"` | Exact match (required) | `"delicious fruit"` |
179
+ | `*` | Wildcard | `app*` becomes `app%` |
169
180
 
170
- ```typescript
171
- class Executable<TParams extends ColumnBuilderRecord, TReturns extends ColumnBuilderRecord> {
172
- constructor(db: DbContextBase, builder: ProcedureBuilder<TParams, TReturns>);
181
+ Escape sequences: `\\` (literal `\`), `\*`, `\%`, `\"`, `\+`, `\-`.
182
+
183
+ ## ParsedSearchQuery
173
184
 
174
- getExecProcQueryDef(params?: InferColumnExprs<TParams>): ExecProcQueryDef;
175
- execute(params: InferColumnExprs<TParams>): Promise<InferColumnExprs<TReturns>[][]>;
185
+ ```typescript
186
+ interface ParsedSearchQuery {
187
+ or: string[]; // OR conditions - LIKE patterns
188
+ must: string[]; // AND conditions (+ prefix or quotes) - LIKE patterns
189
+ not: string[]; // NOT conditions (- prefix) - LIKE patterns
176
190
  }
177
191
  ```
178
192
 
179
- **Example:**
193
+ ## getMatchedPrimaryKeys
180
194
 
181
195
  ```typescript
182
- const result = await db.getUserById().execute({ userId: 1n });
196
+ function getMatchedPrimaryKeys(
197
+ fkCols: string[],
198
+ targetTable: TableBuilder<any, any>,
199
+ ): string[]
183
200
  ```
184
201
 
185
- ## executable
202
+ Match FK column array with the target table's PK. Returns PK column name array. Throws if column counts do not match.
186
203
 
187
- Factory function that creates an `Executable` accessor for a `ProcedureBuilder`.
204
+ ## QueryableRecord
188
205
 
189
206
  ```typescript
190
- function executable<TParams, TReturns>(
191
- db: DbContextBase,
192
- builder: ProcedureBuilder<TParams, TReturns>,
193
- ): () => Executable<TParams, TReturns>;
207
+ type QueryableRecord<TData extends DataRecord> = {
208
+ [K in keyof TData]: TData[K] extends ColumnPrimitive
209
+ ? ExprUnit<TData[K]>
210
+ : TData[K] extends (infer U)[]
211
+ ? U extends DataRecord ? QueryableRecord<U>[] : never
212
+ : TData[K] extends DataRecord ? QueryableRecord<TData[K]> : ...
213
+ }
194
214
  ```
195
215
 
196
- ## ParsedSearchQuery
216
+ Maps each field of a DataRecord to its corresponding ExprUnit proxy. Used as the column accessor type in Queryable callbacks.
197
217
 
198
- Result of `parseSearchQuery()`.
218
+ ## QueryableWriteRecord
199
219
 
200
220
  ```typescript
201
- interface ParsedSearchQuery {
202
- or: string[]; // OR conditions (LIKE patterns)
203
- must: string[]; // AND conditions (+ prefix or quoted)
204
- not: string[]; // NOT conditions (- prefix)
221
+ type QueryableWriteRecord<TData> = {
222
+ [K in keyof TData]: TData[K] extends ColumnPrimitive ? ExprInput<TData[K]> : never;
205
223
  }
206
224
  ```
207
225
 
208
- ## parseSearchQuery
226
+ Maps each field to ExprInput for write operations (UPDATE, UPSERT).
209
227
 
210
- Parses a search query string into SQL LIKE patterns.
228
+ ## NullableQueryableRecord
211
229
 
212
230
  ```typescript
213
- function parseSearchQuery(searchText: string): ParsedSearchQuery;
231
+ type NullableQueryableRecord<TData extends DataRecord> = { ... }
214
232
  ```
215
233
 
216
- **Search Syntax:**
234
+ Like QueryableRecord but all primitive fields become `ExprUnit<T | undefined>`. Used for LEFT JOIN results where all columns may be NULL.
217
235
 
218
- | Syntax | Meaning | Example |
219
- |---|---|---|
220
- | `term1 term2` | OR (match any) | `apple banana` |
221
- | `+term` | Required (AND) | `+apple +banana` |
222
- | `-term` | Excluded (NOT) | `apple -banana` |
223
- | `"exact phrase"` | Exact phrase match (required) | `"delicious fruit"` |
224
- | `*` | Wildcard | `app*` produces `app%` |
236
+ ## UnwrapQueryableRecord
225
237
 
226
- **Escape Sequences:** `\\` (literal `\`), `\*`, `\%`, `\"`, `\+`, `\-`
238
+ ```typescript
239
+ type UnwrapQueryableRecord<R> = {
240
+ [K in keyof R]: R[K] extends ExprUnit<infer T> ? T : ...
241
+ }
242
+ ```
227
243
 
228
- **Example:**
244
+ Reverse-maps QueryableRecord back to a plain DataRecord. Used internally by `select()` to infer the resulting data type.
245
+
246
+ ## PathProxy
229
247
 
230
248
  ```typescript
231
- parseSearchQuery('apple "delicious fruit" -banana +strawberry');
232
- // { or: ["%apple%"], must: ["%delicious fruit%", "%strawberry%"], not: ["%banana%"] }
249
+ type PathProxy<TObject> = {
250
+ [K in keyof TObject as TObject[K] extends ColumnPrimitive ? never : K]-?: PathProxy<UnwrapArray<TObject[K]>>;
251
+ } & { readonly [PATH_SYMBOL]: string[] }
252
+ ```
233
253
 
234
- parseSearchQuery('app* test');
235
- // { or: ["app%", "%test%"], must: [], not: [] }
254
+ Type-safe proxy for `include()` that only exposes non-primitive (relation) fields. Property access builds a path array internally.
255
+
256
+ ```typescript
257
+ // Only relation fields are accessible:
258
+ db.post().include((p) => p.author) // OK - author is a relation
259
+ db.post().include((p) => p.author.company) // OK - nested relation
260
+ // db.post().include((p) => p.title) // Compile error - title is string
236
261
  ```