@taylordb/query-builder 0.16.3 → 0.17.0

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,111 @@
1
+ # Conditions (Filtering)
2
+
3
+ Use `.where()` and `.orWhere()` to filter records. Both methods are available on `selectFrom`, `update`, `deleteFrom`, and aggregation queries.
4
+
5
+ ## Simple condition
6
+
7
+ ```ts
8
+ .where(field, operator, value)
9
+ ```
10
+
11
+ ```ts
12
+ const users = await qb
13
+ .selectFrom('users')
14
+ .select(['id', 'name'])
15
+ .where('age', '>', 30)
16
+ .execute();
17
+ ```
18
+
19
+ The available operators and their accepted value types depend on the column type — see [field-types.md](./field-types.md) for the full operator table per type.
20
+
21
+ ## Multiple AND conditions
22
+
23
+ Chaining `.where()` adds each condition with AND logic.
24
+
25
+ ```ts
26
+ const users = await qb
27
+ .selectFrom('users')
28
+ .selectAll()
29
+ .where('age', '>', 18)
30
+ .where('role', '=', 'admin')
31
+ .execute();
32
+ // age > 18 AND role = 'admin'
33
+ ```
34
+
35
+ ## OR conditions
36
+
37
+ `.orWhere()` has the same signature as `.where()` but switches the conjunction to OR for all conditions in the current group.
38
+
39
+ ```ts
40
+ const users = await qb
41
+ .selectFrom('users')
42
+ .selectAll()
43
+ .where('name', '=', 'Alice')
44
+ .orWhere('name', '=', 'Bob')
45
+ .execute();
46
+ // name = 'Alice' OR name = 'Bob'
47
+ ```
48
+
49
+ ## Grouped conditions (nested logic)
50
+
51
+ Pass a callback to `.where()` to create a nested group. The callback receives a fresh builder scoped to the same table and only the conditions added inside it form the group.
52
+
53
+ ```ts
54
+ const users = await qb
55
+ .selectFrom('users')
56
+ .selectAll()
57
+ .where('status', '=', 'active')
58
+ .where(qb =>
59
+ qb
60
+ .where('role', '=', 'admin')
61
+ .orWhere('role', '=', 'editor')
62
+ )
63
+ .execute();
64
+ // status = 'active' AND (role = 'admin' OR role = 'editor')
65
+ ```
66
+
67
+ ## Cross-table filtering (link fields)
68
+
69
+ When filtering on a link field you can pass a callback instead of a plain value. The callback receives a builder scoped to the **linked** table, letting you filter based on properties of the related records.
70
+
71
+ ```ts
72
+ const users = await qb
73
+ .selectFrom('users')
74
+ .selectAll()
75
+ .where('posts', 'hasAnyOf', qb =>
76
+ qb.where('isPublished', '=', true)
77
+ )
78
+ .execute();
79
+ // users who have at least one published post
80
+ ```
81
+
82
+ The operator you choose on the link field still applies (`hasAnyOf`, `hasAllOf`, `isExactly`, `hasNoneOf`), but the value is resolved by running the inner filter against the linked table.
83
+
84
+ ## isEmpty / isNotEmpty
85
+
86
+ For operators that take no value (`isEmpty`, `isNotEmpty`), omit the third argument or pass `undefined`.
87
+
88
+ ```ts
89
+ .where('bio', 'isEmpty')
90
+ .where('avatar', 'isNotEmpty')
91
+ ```
92
+
93
+ ## Checkbox fields
94
+
95
+ Checkbox filters use numeric values — `1` for true, `0` for false.
96
+
97
+ ```ts
98
+ .where('isVerified', '=', 1)
99
+ ```
100
+
101
+ ## Date shorthand values
102
+
103
+ Date fields accept named shorthands in addition to exact values.
104
+
105
+ ```ts
106
+ .where('createdAt', '=', 'today')
107
+ .where('dueDate', '<', ['daysFromNow', 7])
108
+ .where('updatedAt', 'isWithIn', 'pastWeek')
109
+ ```
110
+
111
+ See [field-types.md](./field-types.md) for the full list of date filter values.
@@ -0,0 +1,50 @@
1
+ # Current User
2
+
3
+ The query builder exposes a built-in way to get the currently authenticated user's profile record.
4
+
5
+ ## Usage
6
+
7
+ ```ts
8
+ const user = await qb.auth.getUser();
9
+ ```
10
+
11
+ ## Return value
12
+
13
+ ```ts
14
+ {
15
+ id: number;
16
+ name: string;
17
+ email: string;
18
+ avatar: string;
19
+ } | null
20
+ ```
21
+
22
+ Returns `null` when no matching collaborator record is found.
23
+
24
+ ## How it works
25
+
26
+ `getUser()` pulls the user ID from the active WebSocket/HTTP connection, then looks up that ID in the `bambooCollaborators` internal table using a filter on `externalId`. Only active collaborators (those with `status = 'ACTIVE'`) are considered.
27
+
28
+ ## Requirement
29
+
30
+ Authentication must have been established at the connection level — i.e. the `apiKey` provided to `createQueryBuilder` must be a valid user token, not just a public API key. If the connection has no user ID attached, `getUser()` throws:
31
+
32
+ ```
33
+ Error: User ID not available from the connection
34
+ ```
35
+
36
+ ## Example
37
+
38
+ ```ts
39
+ const qb = createQueryBuilder<TaylorDatabase>({
40
+ baseUrl: 'https://your-instance.taylordb.ai',
41
+ baseId: 'your-base-id',
42
+ apiKey: 'user-bearer-token',
43
+ });
44
+
45
+ const me = await qb.auth.getUser();
46
+
47
+ if (me) {
48
+ console.log(`Hello ${me.name} (${me.email})`);
49
+ }
50
+ ```
@@ -0,0 +1,215 @@
1
+ # Field Types
2
+
3
+ Every column in a TaylorDB table is represented by a strongly-typed column type. These types encode what value you can write on insert/update, what value you read back on select, and which filter operators are valid for that field.
4
+
5
+ ## Column type anatomy
6
+
7
+ ```ts
8
+ ColumnType<Select, Update, Insert, IsRequired, Filters, Aggregations>
9
+ ```
10
+
11
+ | Slot | Meaning |
12
+ |------|---------|
13
+ | `Select` (`raw`) | The TypeScript type you receive when you read the field |
14
+ | `Update` | The type accepted by `.set({ field: value })` |
15
+ | `Insert` | The type accepted by `.values({ field: value })` |
16
+ | `IsRequired` | `true` — field must be provided on insert; `false` — optional |
17
+ | `Filters` | The operators available in `.where(field, operator, value)` |
18
+ | `Aggregations` | The aggregation functions available for this field |
19
+
20
+ ---
21
+
22
+ ## Text — `TextColumnType<IsRequired>`
23
+
24
+ Maps to field types: `singleLineText`, `text`, `longText`, `url`, `email`, `phoneNumber`, `json`.
25
+
26
+ - **Select / Insert / Update**: `string`
27
+
28
+ **Available filter operators**
29
+
30
+ | Operator | Value type |
31
+ |----------|-----------|
32
+ | `=` | `string` |
33
+ | `!=` | `string` |
34
+ | `caseEqual` | `string` |
35
+ | `contains` | `string` |
36
+ | `doesNotContain` | `string` |
37
+ | `startsWith` | `string` |
38
+ | `endsWith` | `string` |
39
+ | `hasAnyOf` | `string[]` |
40
+ | `isEmpty` | _(no value)_ |
41
+ | `isNotEmpty` | _(no value)_ |
42
+
43
+ ---
44
+
45
+ ## Number — `NumberColumnType<IsRequired>`
46
+
47
+ Maps to field types: `number`, `currency`, `percent`, `duration`, `serial`, `decimalSerial`, `position`.
48
+
49
+ - **Select / Insert / Update**: `number`
50
+
51
+ **Available filter operators**
52
+
53
+ | Operator | Value type |
54
+ |----------|-----------|
55
+ | `=` | `number` |
56
+ | `!=` | `number` |
57
+ | `>` | `number` |
58
+ | `>=` | `number` |
59
+ | `<` | `number` |
60
+ | `<=` | `number` |
61
+ | `hasAnyOf` | `number[]` |
62
+ | `hasNoneOf` | `number[]` |
63
+ | `isEmpty` | _(no value)_ |
64
+ | `isNotEmpty` | _(no value)_ |
65
+
66
+ **Available aggregations**: `sum`, `average`, `median`, `min`, `max`, `range`, `standardDeviation`, `histogram`, `empty`, `filled`, `unique`, `percentEmpty`, `percentFilled`, `percentUnique`
67
+
68
+ ---
69
+
70
+ ## Auto-generated number — `AutoGeneratedNumberColumnType`
71
+
72
+ Maps to field type: `autoNumber`, system field `id`.
73
+
74
+ - **Select**: `number`
75
+ - **Insert / Update**: `never` — cannot be written
76
+
77
+ Same filter operators and aggregations as `NumberColumnType`.
78
+
79
+ ---
80
+
81
+ ## Checkbox — `CheckboxColumnType<IsRequired>`
82
+
83
+ - **Select / Insert / Update**: `boolean`
84
+
85
+ **Available filter operators**
86
+
87
+ | Operator | Value type |
88
+ |----------|-----------|
89
+ | `=` | `number` (use `1` for true, `0` for false) |
90
+
91
+ ---
92
+
93
+ ## Date — `DateColumnType<IsRequired>`
94
+
95
+ - **Select / Insert / Update**: `string` (ISO 8601 date string)
96
+
97
+ **Available filter operators**
98
+
99
+ | Operator | Value type |
100
+ |----------|-----------|
101
+ | `=` | `DefaultDateFilterValue` |
102
+ | `!=` | `DefaultDateFilterValue` |
103
+ | `<` | `DefaultDateFilterValue` |
104
+ | `>` | `DefaultDateFilterValue` |
105
+ | `<=` | `DefaultDateFilterValue` |
106
+ | `>=` | `DefaultDateFilterValue` |
107
+ | `isWithIn` | `IsWithinOperatorValue` or `{ value: 'daysAgo' \| 'daysFromNow'; date: number }` |
108
+ | `isEmpty` | _(no value)_ |
109
+ | `isNotEmpty` | _(no value)_ |
110
+
111
+ `DefaultDateFilterValue` can be one of:
112
+ - A named shorthand string: `'today'`, `'tomorrow'`, `'yesterday'`, `'oneWeekAgo'`, `'oneWeekFromNow'`, `'oneMonthAgo'`, `'oneMonthFromNow'`
113
+ - A tuple `['exactDay' | 'exactTimestamp', string]` — pass an ISO date string as the second element
114
+ - A tuple `['daysAgo' | 'daysFromNow', number]`
115
+
116
+ `IsWithinOperatorValue`: `'pastWeek'`, `'pastMonth'`, `'pastYear'`, `'nextWeek'`, `'nextMonth'`, `'nextYear'`, `'daysFromNow'`, `'daysAgo'`, `'currentWeek'`, `'currentMonth'`, `'currentYear'`
117
+
118
+ **Available aggregations**: `empty`, `filled`, `unique`, `percentEmpty`, `percentFilled`, `percentUnique`, `min`, `max`, `daysRange`, `monthRange`
119
+
120
+ ---
121
+
122
+ ## Auto-generated date — `AutoGeneratedDateColumnType`
123
+
124
+ Maps to field types: `createdAt`, `updatedAt`, `modifiedAt`.
125
+
126
+ - **Select**: `string`
127
+ - **Insert / Update**: `never` — cannot be written
128
+
129
+ Same filter operators and aggregations as `DateColumnType`.
130
+
131
+ ---
132
+
133
+ ## Single select — `SingleSelectColumnType<Options, IsRequired>`
134
+
135
+ - **Select / Insert / Update**: `Options[number]` — one of the allowed string values
136
+ - `Options` is the `typeof YourTableFieldOptions` const generated by the CLI
137
+
138
+ **Available filter operators**
139
+
140
+ | Operator | Value type |
141
+ |----------|-----------|
142
+ | `=` | `Options[number]` |
143
+ | `hasAnyOf` | `Options[number][]` |
144
+ | `hasAllOf` | `Options[number][]` |
145
+ | `isExactly` | `Options[number][]` |
146
+ | `hasNoneOf` | `Options[number][]` |
147
+ | `contains` | `string` |
148
+ | `doesNotContain` | `string` |
149
+ | `isEmpty` | _(no value)_ |
150
+ | `isNotEmpty` | _(no value)_ |
151
+
152
+ ---
153
+
154
+ ## Multi select — `MultiSelectColumnType<Options, IsRequired>`
155
+
156
+ - **Select / Insert / Update**: `Options[number][]` — array of allowed string values
157
+
158
+ Same filter operators as `SingleSelectColumnType`.
159
+
160
+ ---
161
+
162
+ ## Link — `LinkColumnType<LinkedTable, IsRequired>`
163
+
164
+ Represents a relationship to another table.
165
+
166
+ - **Select**: `object` (resolved through `.with()`)
167
+ - **Insert**: `number[]` — array of IDs to link
168
+ - **Update**: `number[] | { newIds: number[]; deletedIds: number[] }`
169
+
170
+ **Available filter operators**
171
+
172
+ | Operator | Value type |
173
+ |----------|-----------|
174
+ | `=` | `number` |
175
+ | `hasAnyOf` | `number[]` |
176
+ | `hasAllOf` | `number[]` |
177
+ | `isExactly` | `number[]` |
178
+ | `hasNoneOf` | `number[]` |
179
+ | `contains` | `string` |
180
+ | `doesNotContain` | `string` |
181
+ | `isEmpty` | _(no value)_ |
182
+ | `isNotEmpty` | _(no value)_ |
183
+
184
+ Cross-table filtering is also supported — see [conditions.md](./conditions.md).
185
+
186
+ ---
187
+
188
+ ## Attachment — `AttachmentColumnType<IsRequired>`
189
+
190
+ - **Select**: `string[]` — array of absolute URLs (automatically resolved by the query builder)
191
+ - **Insert**: `Attachment[] | number[]`
192
+ - **Update**: `Attachment[] | number[] | { newAttachments: Attachment[]; deletedUrls: string[] }`
193
+
194
+ For how to produce `Attachment` instances, see [file-upload.md](./file-upload.md).
195
+
196
+ Same filter operators as `LinkColumnType`.
197
+
198
+ ---
199
+
200
+ ## Search — `SearchColumnType`
201
+
202
+ Read-only computed field.
203
+
204
+ - **Select**: `string`
205
+ - **Insert / Update**: `string`
206
+
207
+ **Available filter operators**
208
+
209
+ | Operator | Value type |
210
+ |----------|-----------|
211
+ | `search` | `string` |
212
+ | `contains` | `string` |
213
+ | `containsStrict` | `string` |
214
+ | `isEmpty` | _(no value)_ |
215
+ | `isNotEmpty` | _(no value)_ |
@@ -0,0 +1,89 @@
1
+ # File Upload (Attachments)
2
+
3
+ Attachment fields store files. The upload and the record write are two separate steps.
4
+
5
+ ## Step 1 — upload the files
6
+
7
+ Call `qb.uploadAttachments()` with an array of `{ file, name }` objects. This sends the files to the TaylorDB media service and returns an array of `Attachment` instances.
8
+
9
+ ```ts
10
+ const attachments = await qb.uploadAttachments([
11
+ { file: myBlob, name: 'invoice.pdf' },
12
+ { file: anotherBlob, name: 'receipt.png' },
13
+ ]);
14
+ ```
15
+
16
+ | Parameter | Type | Description |
17
+ |-----------|------|-------------|
18
+ | `file` | `Blob` | The file content (a browser `File` extends `Blob`) |
19
+ | `name` | `string` | The filename including extension |
20
+
21
+ Returns `Attachment[]`. Each `Attachment` object holds metadata: `collectionName`, `fileInformation`, `baseId`, `storageAdaptor`, `_id`.
22
+
23
+ The upload uses your `apiKey` and `baseId` from the query builder config for authentication.
24
+
25
+ ## Step 2 — write the record
26
+
27
+ Pass the `Attachment[]` array as the value for any `AttachmentColumnType` field. The builder automatically calls `.toColumnValue()` on each attachment to convert it to the format the API expects.
28
+
29
+ ```ts
30
+ const attachments = await qb.uploadAttachments([
31
+ { file: invoiceBlob, name: 'invoice.pdf' },
32
+ ]);
33
+
34
+ const record = await qb
35
+ .insertInto('expenses')
36
+ .values({
37
+ title: 'Office supplies',
38
+ receipt: attachments, // AttachmentColumnType field
39
+ })
40
+ .returning(['id', 'title'])
41
+ .executeTakeFirst();
42
+ ```
43
+
44
+ The same works for update:
45
+
46
+ ```ts
47
+ const newAttachments = await qb.uploadAttachments([
48
+ { file: newFileBlob, name: 'updated-receipt.pdf' },
49
+ ]);
50
+
51
+ await qb
52
+ .update('expenses')
53
+ .set({ receipt: newAttachments })
54
+ .where('id', '=', 42)
55
+ .execute();
56
+ ```
57
+
58
+ To replace some attachments while keeping others, use the `{ newAttachments, deletedUrls }` form that `AttachmentColumnType` update accepts:
59
+
60
+ ```ts
61
+ const uploadedAttachments = await qb.uploadAttachments([
62
+ { file: replacementBlob, name: 'replacement.jpg' },
63
+ ]);
64
+
65
+ await qb
66
+ .update('expenses')
67
+ .set({
68
+ receipt: {
69
+ newAttachments: uploadedAttachments,
70
+ deletedUrls: ['https://media.taylordb.ai/files/something.jpg'],
71
+ },
72
+ })
73
+ .where('id', '=', 42)
74
+ .execute();
75
+ ```
76
+
77
+ ## Reading attachments
78
+
79
+ When you select an attachment field the query builder automatically converts the raw storage path to a full absolute URL.
80
+
81
+ ```ts
82
+ const record = await qb
83
+ .selectFrom('expenses')
84
+ .select(['id', 'receipt'])
85
+ .executeTakeFirst();
86
+
87
+ // record.receipt is string[] — each entry is a full URL:
88
+ // 'https://media.taylordb.ai/files/...'
89
+ ```
package/docs/insert.md ADDED
@@ -0,0 +1,78 @@
1
+ # Insert
2
+
3
+ Use `insertInto` to create one or more records in a table.
4
+
5
+ ## Basic usage
6
+
7
+ ```ts
8
+ const record = await qb
9
+ .insertInto('users')
10
+ .values({ name: 'Alice', email: 'alice@example.com' })
11
+ .executeTakeFirst();
12
+ // returns { id: number } by default
13
+ ```
14
+
15
+ ## Methods
16
+
17
+ ### `.values(record | record[])`
18
+
19
+ Pass a single object or an array of objects. Each key must match a field name on the table. Required fields (typed as `IsRequired = true`) will cause a TypeScript error if omitted.
20
+
21
+ ```ts
22
+ // single
23
+ .values({ name: 'Alice' })
24
+
25
+ // batch
26
+ .values([{ name: 'Alice' }, { name: 'Bob' }])
27
+ ```
28
+
29
+ Attachment fields expect an `Attachment[]` value. Upload first with `qb.uploadAttachments()`, then pass the returned instances directly — the builder converts them to the correct column format automatically. See [file-upload.md](./file-upload.md).
30
+
31
+ Link fields expect `number[]` — the IDs of the records you want to associate.
32
+
33
+ ### `.returning(fields[])`
34
+
35
+ Specify which fields to include in the response. Without `.returning()` only `id` is returned.
36
+
37
+ ```ts
38
+ const user = await qb
39
+ .insertInto('users')
40
+ .values({ name: 'Alice' })
41
+ .returning(['id', 'name', 'email'])
42
+ .executeTakeFirst();
43
+ // user: { id: number; name: string; email: string }
44
+ ```
45
+
46
+ Only non-link fields can be passed to `.returning()`. To include related records, use an `UpdateQueryBuilder` after the insert.
47
+
48
+ ### `.execute()`
49
+
50
+ Runs the insert and returns an array of records matching the `.returning()` selection.
51
+
52
+ ```ts
53
+ const users = await qb
54
+ .insertInto('users')
55
+ .values([{ name: 'Alice' }, { name: 'Bob' }])
56
+ .returning(['id', 'name'])
57
+ .execute();
58
+ // users: Array<{ id: number; name: string }>
59
+ ```
60
+
61
+ ### `.executeTakeFirst()`
62
+
63
+ Same as `.execute()` but returns the first result or `null`.
64
+
65
+ ```ts
66
+ const user = await qb
67
+ .insertInto('users')
68
+ .values({ name: 'Alice' })
69
+ .returning(['id', 'name'])
70
+ .executeTakeFirst();
71
+ // user: { id: number; name: string } | null
72
+ ```
73
+
74
+ ## Restrictions
75
+
76
+ - `attachmentTable` and `collaborators` are blacklisted and cannot be inserted into.
77
+ - Auto-generated fields (`id`, `createdAt`, `updatedAt`, `autoNumber`) have insert type `never` — passing them causes a TypeScript error.
78
+ - Link fields on insert accept only `number[]` (adding IDs). To selectively add/remove links on an existing record use `.update().set({ field: { newIds: [], deletedIds: [] } })`.
@@ -0,0 +1,88 @@
1
+ # Pagination
2
+
3
+ Three methods control how many records are returned and from where in the result set.
4
+
5
+ ## `.limit(count)`
6
+
7
+ Sets the maximum number of records to return.
8
+
9
+ ```ts
10
+ const users = await qb
11
+ .selectFrom('users')
12
+ .selectAll()
13
+ .limit(20)
14
+ .execute();
15
+ ```
16
+
17
+ ## `.offset(count)`
18
+
19
+ Skips the first `count` records before returning results.
20
+
21
+ ```ts
22
+ const users = await qb
23
+ .selectFrom('users')
24
+ .selectAll()
25
+ .offset(40)
26
+ .execute();
27
+ ```
28
+
29
+ ## `.paginate(page, limit)`
30
+
31
+ Convenience wrapper around `.offset()` and `.limit()`. Pages are 1-indexed.
32
+
33
+ ```ts
34
+ .paginate(page, limit)
35
+ // equivalent to .offset((page - 1) * limit).limit(limit)
36
+ ```
37
+
38
+ ```ts
39
+ const page3 = await qb
40
+ .selectFrom('users')
41
+ .selectAll()
42
+ .orderBy('name', 'asc')
43
+ .paginate(3, 25) // rows 51–75
44
+ .execute();
45
+ ```
46
+
47
+ ## `.count()`
48
+
49
+ Returns the total number of records that match the current filters, ignoring `.limit()` and `.offset()`. Use this to calculate total pages.
50
+
51
+ ```ts
52
+ const total = await qb
53
+ .selectFrom('users')
54
+ .where('role', '=', 'admin')
55
+ .count();
56
+
57
+ const totalPages = Math.ceil(total / pageSize);
58
+ ```
59
+
60
+ `count()` can only be called on a root `selectFrom` query, not on a sub-query inside `.with()`.
61
+
62
+ ## Pagination on sub-queries
63
+
64
+ `.limit()` and `.offset()` are available inside the `.with()` object form to limit how many related records are returned per parent record.
65
+
66
+ ```ts
67
+ const users = await qb
68
+ .selectFrom('users')
69
+ .select(['id', 'name'])
70
+ .with({
71
+ posts: qb => qb.select(['id', 'title']).limit(3),
72
+ })
73
+ .execute();
74
+ // at most 3 posts per user
75
+ ```
76
+
77
+ ## Pagination on aggregation queries
78
+
79
+ All three methods — `.limit()`, `.offset()`, `.paginate()` — are available on `AggregationQueryBuilder` as well.
80
+
81
+ ```ts
82
+ const stats = await qb
83
+ .aggregateFrom('orders')
84
+ .groupBy('status')
85
+ .metrics({ total: count('id') })
86
+ .paginate(1, 10)
87
+ .execute();
88
+ ```
@@ -0,0 +1,75 @@
1
+ # Relationships (Loading Linked Records)
2
+
3
+ Use `.with()` to load related records through link fields. It is available on `selectFrom` queries.
4
+
5
+ ## What can be loaded
6
+
7
+ Only fields typed as `LinkColumnType` can be used with `.with()`. These are the fields the CLI generates when a TaylorDB field is of type `link`, `collaborators`, or `modifiedBy`.
8
+
9
+ Fields that use `AttachmentColumnType` are **not** loaded via `.with()` — attachment URLs are resolved automatically when the attachment field is included in `.select()` or `.selectAll()`.
10
+
11
+ ## Simple form — load all fields
12
+
13
+ Pass a relation name as a string or an array of relation names. All fields of the linked table are fetched.
14
+
15
+ ```ts
16
+ const users = await qb
17
+ .selectFrom('users')
18
+ .select(['id', 'name'])
19
+ .with('posts')
20
+ .execute();
21
+ // each user: { id, name, posts: Array<{ id, title, ... all post fields }> }
22
+ ```
23
+
24
+ Multiple relations at once:
25
+
26
+ ```ts
27
+ .with(['posts', 'team'])
28
+ ```
29
+
30
+ ## Object form — configure the sub-query
31
+
32
+ Pass an object where each key is a relation name and the value is a callback that receives a `QueryBuilder` scoped to the linked table. You can call `.select()`, `.where()`, `.orderBy()`, `.limit()`, `.offset()`, and further `.with()` on the sub-builder.
33
+
34
+ ```ts
35
+ const users = await qb
36
+ .selectFrom('users')
37
+ .select(['id', 'name'])
38
+ .with({
39
+ posts: qb =>
40
+ qb
41
+ .select(['id', 'title', 'createdAt'])
42
+ .where('isPublished', '=', true)
43
+ .orderBy('createdAt', 'desc')
44
+ .limit(5),
45
+ })
46
+ .execute();
47
+ // each user: { id, name, posts: Array<{ id, title, createdAt }> }
48
+ // only published posts, newest first, max 5 per user
49
+ ```
50
+
51
+ ## Nested relationships
52
+
53
+ You can nest `.with()` inside a sub-query to load relationships of related records.
54
+
55
+ ```ts
56
+ const users = await qb
57
+ .selectFrom('users')
58
+ .select(['id', 'name'])
59
+ .with({
60
+ posts: qb =>
61
+ qb
62
+ .select(['id', 'title'])
63
+ .with({
64
+ comments: cqb => cqb.select(['id', 'body']),
65
+ }),
66
+ })
67
+ .execute();
68
+ ```
69
+
70
+ ## What cannot be loaded
71
+
72
+ - **Attachment fields** (`AttachmentColumnType`) are not relationship fields and cannot be used with `.with()`. Select them directly — their URLs are resolved automatically.
73
+ - **Aggregated counts** of related records are not supported through `.with()`. Use `aggregateFrom` with a cross-table filter for that instead.
74
+ - **`returning()` on insert** does not support loading relationships. Perform a follow-up `selectFrom` query if you need related data after an insert.
75
+ - **`update` and `deleteFrom`** do not support `.with()`.