@venizia/ignis-docs 0.2.0 → 0.2.1-1
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 +14 -14
- package/content/best-practices/api-usage-examples.md +39 -19
- package/content/best-practices/architectural-patterns.md +24 -13
- package/content/best-practices/architecture-decisions.md +29 -16
- package/content/best-practices/code-style-standards/advanced-patterns.md +43 -31
- package/content/best-practices/code-style-standards/constants-configuration.md +19 -4
- package/content/best-practices/code-style-standards/control-flow.md +33 -1
- package/content/best-practices/code-style-standards/documentation.md +28 -8
- package/content/best-practices/code-style-standards/function-patterns.md +15 -4
- package/content/best-practices/code-style-standards/index.md +7 -2
- package/content/best-practices/code-style-standards/naming-conventions.md +27 -3
- package/content/best-practices/code-style-standards/route-definitions.md +9 -5
- package/content/best-practices/code-style-standards/tooling.md +14 -1
- package/content/best-practices/code-style-standards/type-safety.md +39 -6
- package/content/best-practices/common-pitfalls.md +59 -35
- package/content/best-practices/contribution-workflow.md +8 -4
- package/content/best-practices/data-modeling.md +78 -62
- package/content/best-practices/deployment-strategies.md +56 -19
- package/content/best-practices/error-handling.md +247 -153
- package/content/best-practices/index.md +6 -0
- package/content/best-practices/performance-optimization.md +26 -13
- package/content/best-practices/security-guidelines.md +35 -9
- package/content/best-practices/testing-strategies.md +7 -2
- package/content/best-practices/troubleshooting-tips.md +27 -17
- package/content/extensions/components/api-reference.md +109 -323
- package/content/extensions/components/authentication/api.md +489 -602
- package/content/extensions/components/authentication/errors.md +135 -498
- package/content/extensions/components/authentication/index.md +89 -801
- package/content/extensions/components/authentication/usage.md +231 -955
- package/content/extensions/components/authorization/api.md +991 -644
- package/content/extensions/components/authorization/errors.md +204 -208
- package/content/extensions/components/authorization/getting-started.md +227 -0
- package/content/extensions/components/authorization/index.md +88 -795
- package/content/extensions/components/authorization/usage.md +219 -528
- package/content/extensions/components/health-check.md +77 -243
- package/content/extensions/components/index.md +24 -90
- package/content/extensions/components/mail/api.md +553 -285
- package/content/extensions/components/mail/errors.md +76 -62
- package/content/extensions/components/mail/index.md +111 -463
- package/content/extensions/components/mail/usage.md +139 -173
- package/content/extensions/components/request-tracker.md +70 -173
- package/content/extensions/components/socket-io/api.md +462 -785
- package/content/extensions/components/socket-io/errors.md +49 -51
- package/content/extensions/components/socket-io/index.md +69 -372
- package/content/extensions/components/socket-io/usage.md +212 -105
- package/content/extensions/components/static-asset/api.md +461 -141
- package/content/extensions/components/static-asset/errors.md +121 -53
- package/content/extensions/components/static-asset/index.md +84 -606
- package/content/extensions/components/static-asset/usage.md +184 -299
- package/content/extensions/components/template/index.md +3 -3
- package/content/extensions/components/websocket/api.md +311 -404
- package/content/extensions/components/websocket/errors.md +47 -56
- package/content/extensions/components/websocket/index.md +75 -407
- package/content/extensions/components/websocket/usage.md +120 -338
- package/content/extensions/helpers/cron/index.md +52 -160
- package/content/extensions/helpers/crypto/index.md +67 -480
- package/content/extensions/helpers/crypto/reference.md +528 -0
- package/content/extensions/helpers/env/index.md +64 -178
- package/content/extensions/helpers/error/index.md +286 -196
- package/content/extensions/helpers/index.md +61 -47
- package/content/extensions/helpers/inversion/index.md +76 -550
- package/content/extensions/helpers/inversion/reference.md +530 -0
- package/content/extensions/helpers/kafka/admin.md +23 -3
- package/content/extensions/helpers/kafka/compile-binary.md +83 -52
- package/content/extensions/helpers/kafka/consumer.md +65 -28
- package/content/extensions/helpers/kafka/examples.md +46 -253
- package/content/extensions/helpers/kafka/index.md +55 -617
- package/content/extensions/helpers/kafka/producer.md +156 -22
- package/content/extensions/helpers/kafka/schema-registry.md +59 -79
- package/content/extensions/helpers/logger/hf-logger.md +220 -0
- package/content/extensions/helpers/logger/index.md +78 -552
- package/content/extensions/helpers/logger/pino.md +105 -0
- package/content/extensions/helpers/logger/reference.md +937 -0
- package/content/extensions/helpers/network/api.md +276 -195
- package/content/extensions/helpers/network/index.md +87 -524
- package/content/extensions/helpers/queue/index.md +80 -897
- package/content/extensions/helpers/queue/reference.md +494 -0
- package/content/extensions/helpers/redis/index.md +86 -640
- package/content/extensions/helpers/redis/reference.md +784 -0
- package/content/extensions/helpers/secrets/index.md +136 -0
- package/content/extensions/helpers/socket-io/api.md +325 -204
- package/content/extensions/helpers/socket-io/index.md +79 -429
- package/content/extensions/helpers/storage/api.md +584 -464
- package/content/extensions/helpers/storage/index.md +80 -573
- package/content/extensions/helpers/types/index.md +75 -495
- package/content/extensions/helpers/types/reference.md +689 -0
- package/content/extensions/helpers/uid/index.md +176 -189
- package/content/extensions/helpers/websocket/api.md +366 -214
- package/content/extensions/helpers/websocket/index.md +68 -503
- package/content/extensions/helpers/worker-thread/index.md +62 -396
- package/content/extensions/helpers/worker-thread/reference.md +428 -0
- package/content/extensions/index.md +38 -39
- package/content/extensions/src-details/mcp-server.md +96 -548
- package/content/guides/core-concepts/persistent/datasources.md +2 -2
- package/content/guides/core-concepts/persistent/index.md +5 -1
- package/content/guides/core-concepts/persistent/models.md +1 -1
- package/content/guides/core-concepts/persistent/pglite.md +218 -0
- package/content/guides/core-concepts/persistent/postgres-drivers.md +21 -15
- package/content/guides/core-concepts/persistent/search-meilisearch.md +22 -12
- package/content/guides/core-concepts/persistent/search-typesense.md +28 -16
- package/content/guides/core-concepts/persistent/sqlite.md +296 -0
- package/content/guides/core-concepts/persistent/transactions.md +12 -11
- package/content/guides/core-concepts/secrets-vault.md +177 -0
- package/content/guides/core-concepts/services.md +1 -1
- package/content/guides/get-started/5-minute-quickstart.md +59 -206
- package/content/guides/get-started/philosophy.md +135 -670
- package/content/guides/get-started/setup.md +53 -74
- package/content/guides/migrations/redis-helpers-migration.md +5 -4
- package/content/guides/migrations/scoped-rbac-migration.md +5 -5
- package/content/guides/migrations/unified-connectors-migration.md +8 -7
- package/content/guides/tutorials/ecommerce-api.md +3 -8
- package/content/guides/tutorials/realtime-chat.md +1 -1
- package/content/references/base/application.md +64 -22
- package/content/references/base/components.md +3 -3
- package/content/references/base/connectors.md +79 -136
- package/content/references/base/controllers.md +12 -12
- package/content/references/base/datasources-reference.md +600 -0
- package/content/references/base/datasources.md +84 -444
- package/content/references/base/dependency-injection.md +25 -46
- package/content/references/base/filter-system/application-usage.md +95 -123
- package/content/references/base/filter-system/array-operators.md +33 -43
- package/content/references/base/filter-system/comparison-operators.md +55 -63
- package/content/references/base/filter-system/default-filter.md +144 -350
- package/content/references/base/filter-system/fields-order-pagination.md +116 -148
- package/content/references/base/filter-system/index.md +114 -253
- package/content/references/base/filter-system/json-filtering.md +52 -181
- package/content/references/base/filter-system/list-operators.md +28 -44
- package/content/references/base/filter-system/logical-operators.md +69 -114
- package/content/references/base/filter-system/null-operators.md +41 -98
- package/content/references/base/filter-system/pattern-matching.md +48 -51
- package/content/references/base/filter-system/quick-reference.md +93 -196
- package/content/references/base/filter-system/range-operators.md +22 -38
- package/content/references/base/filter-system/tips.md +70 -133
- package/content/references/base/filter-system/use-cases.md +173 -232
- package/content/references/base/grpc-controllers.md +53 -17
- package/content/references/base/index.md +5 -3
- package/content/references/base/middlewares.md +43 -28
- package/content/references/base/models-reference.md +886 -0
- package/content/references/base/models.md +81 -1452
- package/content/references/base/providers.md +8 -8
- package/content/references/base/repositories/advanced.md +281 -419
- package/content/references/base/repositories/index.md +84 -644
- package/content/references/base/repositories/mixins.md +25 -21
- package/content/references/base/repositories/relations.md +194 -375
- package/content/references/base/repositories/soft-deletable.md +67 -55
- package/content/references/base/secrets.md +267 -0
- package/content/references/base/services.md +8 -6
- package/content/references/configuration/environment-variables.md +73 -21
- package/content/references/configuration/index.md +51 -31
- package/content/references/index.md +1 -1
- package/content/references/quick-reference.md +3 -16
- package/content/references/utilities/crypto.md +35 -76
- package/content/references/utilities/date.md +33 -73
- package/content/references/utilities/duration.md +85 -0
- package/content/references/utilities/index.md +6 -2
- package/content/references/utilities/jsx-reference.md +298 -0
- package/content/references/utilities/jsx.md +82 -525
- package/content/references/utilities/module.md +81 -61
- package/content/references/utilities/parse.md +34 -64
- package/content/references/utilities/performance.md +33 -58
- package/content/references/utilities/promise.md +28 -62
- package/content/references/utilities/request.md +58 -218
- package/content/references/utilities/retry.md +139 -0
- package/content/references/utilities/schema.md +43 -137
- package/content/references/utilities/statuses-reference.md +361 -0
- package/content/references/utilities/statuses.md +63 -667
- package/dist/mcp-server/index.js +0 -0
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.js +3 -3
- package/dist/mcp-server/tools/github/search-code.tool.js +1 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.js +1 -1
- package/dist/mcp-server/tools/github/view-source-file.tool.js +1 -1
- package/package.json +24 -23
|
@@ -1,19 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
---
|
|
2
|
+
title: Relations & Includes
|
|
3
|
+
description: Declaring model relations and eager-loading them with include
|
|
4
|
+
difficulty: intermediate
|
|
5
|
+
---
|
|
4
6
|
|
|
7
|
+
# Relations & Includes
|
|
5
8
|
|
|
6
|
-
|
|
9
|
+
Declare `one`/`many` relations on a model, then eager-load them with `include` on any `find`/`findOne` call - one-to-one, one-to-many, and many-to-many. For CRUD basics, start with the [Repositories overview](/references/base/repositories/).
|
|
7
10
|
|
|
8
|
-
|
|
11
|
+
## In one example
|
|
9
12
|
|
|
10
13
|
```typescript
|
|
11
|
-
// Fetch user with their posts
|
|
14
|
+
// Fetch a user with their posts
|
|
12
15
|
const user = await userRepository.findOne({
|
|
13
16
|
filter: {
|
|
14
17
|
where: { id: '123' },
|
|
15
|
-
include: [{ relation: 'posts' }]
|
|
16
|
-
}
|
|
18
|
+
include: [{ relation: 'posts' }],
|
|
19
|
+
},
|
|
17
20
|
});
|
|
18
21
|
|
|
19
22
|
// Result:
|
|
@@ -27,247 +30,34 @@ const user = await userRepository.findOne({
|
|
|
27
30
|
// }
|
|
28
31
|
```
|
|
29
32
|
|
|
30
|
-
### One-to-One: Post with Author
|
|
31
|
-
|
|
32
|
-
```typescript
|
|
33
|
-
// Fetch post with its author
|
|
34
|
-
const post = await postRepository.findOne({
|
|
35
|
-
filter: {
|
|
36
|
-
where: { id: 'p1' },
|
|
37
|
-
include: [{ relation: 'author' }]
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
// Result:
|
|
42
|
-
// {
|
|
43
|
-
// id: 'p1',
|
|
44
|
-
// title: 'First Post',
|
|
45
|
-
// authorId: '123',
|
|
46
|
-
// author: { id: '123', name: 'John', email: 'john@example.com' }
|
|
47
|
-
// }
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
### Multiple Relations
|
|
51
|
-
|
|
52
|
-
```typescript
|
|
53
|
-
// Fetch post with author AND comments
|
|
54
|
-
const post = await postRepository.findOne({
|
|
55
|
-
filter: {
|
|
56
|
-
where: { id: 'p1' },
|
|
57
|
-
include: [
|
|
58
|
-
{ relation: 'author' },
|
|
59
|
-
{ relation: 'comments' }
|
|
60
|
-
]
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
```
|
|
64
|
-
|
|
65
33
|
> [!NOTE]
|
|
66
|
-
>
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
## Scoped Includes
|
|
70
|
-
|
|
71
|
-
Apply filters, ordering, and limits to included relations using `scope`:
|
|
72
|
-
|
|
73
|
-
### Filter Related Data
|
|
74
|
-
|
|
75
|
-
```typescript
|
|
76
|
-
// User with only published posts
|
|
77
|
-
const user = await userRepository.findOne({
|
|
78
|
-
filter: {
|
|
79
|
-
where: { id: '123' },
|
|
80
|
-
include: [{
|
|
81
|
-
relation: 'posts',
|
|
82
|
-
scope: {
|
|
83
|
-
where: { status: 'published' }
|
|
84
|
-
}
|
|
85
|
-
}]
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
### Order Related Data
|
|
91
|
-
|
|
92
|
-
```typescript
|
|
93
|
-
// User with posts ordered by date
|
|
94
|
-
const user = await userRepository.findOne({
|
|
95
|
-
filter: {
|
|
96
|
-
where: { id: '123' },
|
|
97
|
-
include: [{
|
|
98
|
-
relation: 'posts',
|
|
99
|
-
scope: {
|
|
100
|
-
order: ['createdAt DESC']
|
|
101
|
-
}
|
|
102
|
-
}]
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
### Limit Related Data
|
|
108
|
-
|
|
109
|
-
```typescript
|
|
110
|
-
// User with their 5 most recent posts
|
|
111
|
-
const user = await userRepository.findOne({
|
|
112
|
-
filter: {
|
|
113
|
-
where: { id: '123' },
|
|
114
|
-
include: [{
|
|
115
|
-
relation: 'posts',
|
|
116
|
-
scope: {
|
|
117
|
-
order: ['createdAt DESC'],
|
|
118
|
-
limit: 5
|
|
119
|
-
}
|
|
120
|
-
}]
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
### Combined Scope Options
|
|
126
|
-
|
|
127
|
-
```typescript
|
|
128
|
-
const user = await userRepository.findOne({
|
|
129
|
-
filter: {
|
|
130
|
-
where: { id: '123' },
|
|
131
|
-
include: [{
|
|
132
|
-
relation: 'posts',
|
|
133
|
-
scope: {
|
|
134
|
-
where: { status: 'published' },
|
|
135
|
-
order: ['createdAt DESC'],
|
|
136
|
-
limit: 10,
|
|
137
|
-
fields: ['id', 'title', 'createdAt']
|
|
138
|
-
}
|
|
139
|
-
}]
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
### Skip Default Filter on Includes
|
|
145
|
-
|
|
146
|
-
Each inclusion can independently bypass the related model's default filter:
|
|
147
|
-
|
|
148
|
-
```typescript
|
|
149
|
-
// Include soft-deleted posts that would normally be filtered out
|
|
150
|
-
const user = await userRepository.findOne({
|
|
151
|
-
filter: {
|
|
152
|
-
where: { id: '123' },
|
|
153
|
-
include: [{
|
|
154
|
-
relation: 'posts',
|
|
155
|
-
shouldSkipDefaultFilter: true
|
|
156
|
-
}]
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
## Nested Includes
|
|
163
|
-
|
|
164
|
-
Include relations of relations (up to 2 levels recommended):
|
|
165
|
-
|
|
166
|
-
### Two-Level Nesting
|
|
167
|
-
|
|
168
|
-
```typescript
|
|
169
|
-
// User -> Posts -> Comments
|
|
170
|
-
const user = await userRepository.findOne({
|
|
171
|
-
filter: {
|
|
172
|
-
where: { id: '123' },
|
|
173
|
-
include: [{
|
|
174
|
-
relation: 'posts',
|
|
175
|
-
scope: {
|
|
176
|
-
include: [{ relation: 'comments' }]
|
|
177
|
-
}
|
|
178
|
-
}]
|
|
179
|
-
}
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
// Result:
|
|
183
|
-
// {
|
|
184
|
-
// id: '123',
|
|
185
|
-
// name: 'John',
|
|
186
|
-
// posts: [
|
|
187
|
-
// {
|
|
188
|
-
// id: 'p1',
|
|
189
|
-
// title: 'First Post',
|
|
190
|
-
// comments: [
|
|
191
|
-
// { id: 'c1', text: 'Great post!' },
|
|
192
|
-
// { id: 'c2', text: 'Thanks for sharing' }
|
|
193
|
-
// ]
|
|
194
|
-
// }
|
|
195
|
-
// ]
|
|
196
|
-
// }
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
### Many-to-Many Through Junction
|
|
200
|
-
|
|
201
|
-
```typescript
|
|
202
|
-
// Product -> SaleChannelProduct (junction) -> SaleChannel
|
|
203
|
-
const product = await productRepository.findOne({
|
|
204
|
-
filter: {
|
|
205
|
-
where: { id: 'prod1' },
|
|
206
|
-
include: [{
|
|
207
|
-
relation: 'saleChannelProducts',
|
|
208
|
-
scope: {
|
|
209
|
-
include: [{ relation: 'saleChannel' }]
|
|
210
|
-
}
|
|
211
|
-
}]
|
|
212
|
-
}
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
// Result:
|
|
216
|
-
// {
|
|
217
|
-
// id: 'prod1',
|
|
218
|
-
// name: 'Widget',
|
|
219
|
-
// saleChannelProducts: [
|
|
220
|
-
// {
|
|
221
|
-
// productId: 'prod1',
|
|
222
|
-
// saleChannelId: 'ch1',
|
|
223
|
-
// saleChannel: { id: 'ch1', name: 'Online Store' }
|
|
224
|
-
// },
|
|
225
|
-
// {
|
|
226
|
-
// productId: 'prod1',
|
|
227
|
-
// saleChannelId: 'ch2',
|
|
228
|
-
// saleChannel: { id: 'ch2', name: 'Retail' }
|
|
229
|
-
// }
|
|
230
|
-
// ]
|
|
231
|
-
// }
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
> **Performance Warning:** Each nested `include` adds SQL complexity. **Maximum 2 levels recommended.** For deeper relationships, use multiple queries.
|
|
34
|
+
> An `include` in the filter routes the query through Drizzle's Query API (`connector.query`) instead of the Core API. The `canUseCoreAPI` check in `ReadableRelationalRepository` makes that choice automatically. Core API is ~15-20% faster but skips relations and field selection. See [Performance Optimization](./advanced#performance-optimization).
|
|
235
35
|
|
|
36
|
+
## `TInclusion` options
|
|
236
37
|
|
|
237
|
-
|
|
38
|
+
Each element of the `include` array accepts:
|
|
238
39
|
|
|
239
|
-
|
|
40
|
+
| Option | Type | Default | Meaning |
|
|
41
|
+
|---|---|---|---|
|
|
42
|
+
| `relation` | `string` | required | Name of the relation to include, matching an entry in the model's `relations` array |
|
|
43
|
+
| `scope` | `TFilter` | none | Nested filter on the related rows - `where`, `order`, `limit`, `fields`, `include` |
|
|
44
|
+
| `shouldSkipDefaultFilter` | `boolean` | `false` | Skip the related model's default filter for this inclusion only |
|
|
240
45
|
|
|
241
|
-
|
|
46
|
+
`scope` takes the same shape as a top-level filter - see the [Filter System](/references/base/filter-system/) reference for every `where` operator.
|
|
242
47
|
|
|
243
|
-
|
|
244
|
-
type TRelationConfig = {
|
|
245
|
-
name: string; // Relation name used in includes
|
|
246
|
-
} & (
|
|
247
|
-
| {
|
|
248
|
-
type: 'one'; // one-to-one or many-to-one
|
|
249
|
-
schema: TTableSchemaWithId;
|
|
250
|
-
metadata: { fields, references, relationName? };
|
|
251
|
-
}
|
|
252
|
-
| {
|
|
253
|
-
type: 'many'; // one-to-many
|
|
254
|
-
schema: TTableSchemaWithId;
|
|
255
|
-
metadata: { relationName? };
|
|
256
|
-
}
|
|
257
|
-
);
|
|
258
|
-
```
|
|
48
|
+
## Declaring relations on a model
|
|
259
49
|
|
|
260
|
-
|
|
50
|
+
Relations are declared as a static `relations` resolver on the model, returning an array of `TRelationConfig`. `MetadataRegistry` resolves the array during schema discovery and passes it to `createRelations`. `createRelations` builds the actual Drizzle `relations()` definition - application code never calls it directly.
|
|
261
51
|
|
|
262
52
|
```typescript
|
|
263
53
|
// src/models/user.model.ts
|
|
264
54
|
import { model, RelationTypes } from '@venizia/ignis';
|
|
265
|
-
import {
|
|
55
|
+
import { BaseEntity, TRelationConfig } from '@venizia/ignis/postgres';
|
|
266
56
|
import { pgTable, text } from 'drizzle-orm/pg-core';
|
|
267
57
|
import { Post } from './post.model';
|
|
268
58
|
|
|
269
59
|
@model({ type: 'entity' })
|
|
270
|
-
export class User extends
|
|
60
|
+
export class User extends BaseEntity<typeof User.schema> {
|
|
271
61
|
static override schema = pgTable('User', {
|
|
272
62
|
id: text('id').primaryKey(),
|
|
273
63
|
name: text('name').notNull(),
|
|
@@ -285,23 +75,36 @@ export class User extends BasePostgresEntity<typeof User.schema> {
|
|
|
285
75
|
}
|
|
286
76
|
```
|
|
287
77
|
|
|
288
|
-
|
|
78
|
+
Write the resolver as an arrow function (`() => [...]`), not a plain array. IGNIS defers evaluation until every `@model` class has registered, which avoids circular-import ordering issues between related models.
|
|
79
|
+
|
|
80
|
+
### `TRelationConfig` fields
|
|
81
|
+
|
|
82
|
+
| Field | Type | Meaning |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| `name` | `string` | Relation name used in `include` |
|
|
85
|
+
| `type` | `RelationTypes.ONE` \| `RelationTypes.MANY` | Which Drizzle relation helper to build |
|
|
86
|
+
| `schema` | `TTableSchemaWithId` | The related model's Drizzle table schema |
|
|
87
|
+
| `metadata` | inferred from Drizzle's `one()`/`many()` params | `{ fields, references, relationName? }` for `ONE`; `{ relationName? }` for `MANY` |
|
|
289
88
|
|
|
290
|
-
|
|
89
|
+
`metadata`'s shape comes straight from Drizzle's own `one()`/`many()` parameter types, not a hand-duplicated one.
|
|
291
90
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
|
295
|
-
|
|
91
|
+
### Relation types
|
|
92
|
+
|
|
93
|
+
| Type | Drizzle function | Description | Example |
|
|
94
|
+
|---|---|---|---|
|
|
95
|
+
| `RelationTypes.ONE` (`'one'`) | `one()` | One-to-one or many-to-one | Post has one Author, User has one Profile |
|
|
96
|
+
| `RelationTypes.MANY` (`'many'`) | `many()` | One-to-many | User has many Posts |
|
|
296
97
|
|
|
297
98
|
> [!NOTE]
|
|
298
|
-
>
|
|
99
|
+
> LoopBack 4 names these `hasMany`/`hasOne`/`belongsTo`. IGNIS uses Drizzle ORM's relation model instead, which has only `one` and `many`. A "belongsTo" relationship is `type: RelationTypes.ONE` with `fields` (the local foreign key) and `references` (the remote primary key) in `metadata`.
|
|
100
|
+
|
|
101
|
+
### A model with both types
|
|
299
102
|
|
|
300
|
-
|
|
103
|
+
This mirrors `examples/vert`'s `SaleChannelProduct` junction model: one `ONE` relation per foreign key, plus a `MANY` relation elsewhere.
|
|
301
104
|
|
|
302
105
|
```typescript
|
|
303
106
|
@model({ type: 'entity' })
|
|
304
|
-
export class Post extends
|
|
107
|
+
export class Post extends BaseEntity<typeof Post.schema> {
|
|
305
108
|
static override schema = postTable;
|
|
306
109
|
|
|
307
110
|
static override relations = (): TRelationConfig[] => [
|
|
@@ -324,14 +127,9 @@ export class Post extends BasePostgresEntity<typeof Post.schema> {
|
|
|
324
127
|
}
|
|
325
128
|
```
|
|
326
129
|
|
|
327
|
-
###
|
|
328
|
-
|
|
329
|
-
During schema discovery, `MetadataRegistry` resolves each model's `relations` array and passes it to the `createRelations` helper (`packages/core/src/connectors/postgres/repositories/operators/relation.ts`), which builds the actual Drizzle `relations()` definition registered on the DataSource schema. You do not call `createRelations` yourself in application code.
|
|
330
|
-
|
|
130
|
+
### Auto-resolution in the repository
|
|
331
131
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
Relations are automatically resolved from the entity's static `relations` property via `MetadataRegistry`. The `FilterBuilder.resolveRelations()` method reads them when building include queries. No need to pass them in the repository constructor:
|
|
132
|
+
The repository never receives relations through its constructor. `MetadataRegistry` resolves them from the entity's static `relations` property. `FilterBuilder.resolveRelations()` reads and caches the result in a `WeakMap` the first time an include query needs them.
|
|
335
133
|
|
|
336
134
|
```typescript
|
|
337
135
|
@repository({ model: User, dataSource: PostgresDataSource })
|
|
@@ -340,195 +138,220 @@ export class UserRepository extends DefaultCRUDRepository<typeof User.schema> {
|
|
|
340
138
|
}
|
|
341
139
|
```
|
|
342
140
|
|
|
141
|
+
## Recipes
|
|
343
142
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
When building include queries, the `FilterBuilder.toInclude()` method automatically:
|
|
347
|
-
|
|
348
|
-
1. Resolves hidden properties for each related model via `resolveHiddenProperties()`.
|
|
349
|
-
2. Resolves the default filter for each related model via `resolveDefaultFilter()`.
|
|
350
|
-
3. Merges the default filter with any user-provided `scope`.
|
|
351
|
-
4. Excludes hidden columns from the nested query's `columns` selection.
|
|
143
|
+
### Include multiple relations
|
|
352
144
|
|
|
353
145
|
```typescript
|
|
354
|
-
// User model has hiddenProperties: ['password']
|
|
355
146
|
const post = await postRepository.findOne({
|
|
356
147
|
filter: {
|
|
357
|
-
|
|
358
|
-
|
|
148
|
+
where: { id: 'p1' },
|
|
149
|
+
include: [{ relation: 'author' }, { relation: 'comments' }],
|
|
150
|
+
},
|
|
359
151
|
});
|
|
360
|
-
|
|
361
|
-
// post.author will NOT include password - excluded at SQL level
|
|
362
152
|
```
|
|
363
153
|
|
|
154
|
+
### Filter, order, and limit included rows
|
|
364
155
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
For queries with `include`, use generic type overrides for full type safety:
|
|
156
|
+
Combine `where`, `order`, `limit`, and `fields` inside `scope` the same way you would on a top-level filter:
|
|
368
157
|
|
|
369
158
|
```typescript
|
|
370
|
-
//
|
|
371
|
-
|
|
372
|
-
posts: Post[];
|
|
373
|
-
};
|
|
374
|
-
|
|
375
|
-
// Use generic override
|
|
376
|
-
const user = await userRepository.findOne<UserWithPosts>({
|
|
159
|
+
// User with their 5 most recent published posts, id and title only
|
|
160
|
+
const user = await userRepository.findOne({
|
|
377
161
|
filter: {
|
|
378
162
|
where: { id: '123' },
|
|
379
|
-
include: [{
|
|
380
|
-
|
|
163
|
+
include: [{
|
|
164
|
+
relation: 'posts',
|
|
165
|
+
scope: {
|
|
166
|
+
where: { status: 'published' },
|
|
167
|
+
order: ['createdAt DESC'],
|
|
168
|
+
limit: 5,
|
|
169
|
+
fields: ['id', 'title', 'createdAt'],
|
|
170
|
+
},
|
|
171
|
+
}],
|
|
172
|
+
},
|
|
381
173
|
});
|
|
382
|
-
|
|
383
|
-
// TypeScript knows the structure!
|
|
384
|
-
if (user) {
|
|
385
|
-
console.log(user.posts[0].title); // Fully typed
|
|
386
|
-
}
|
|
387
174
|
```
|
|
388
175
|
|
|
389
|
-
###
|
|
176
|
+
### Skip the default filter on one inclusion
|
|
177
|
+
|
|
178
|
+
Each inclusion can independently bypass the related model's default filter:
|
|
390
179
|
|
|
391
180
|
```typescript
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
}
|
|
181
|
+
// Include soft-deleted posts that would normally be filtered out
|
|
182
|
+
const user = await userRepository.findOne({
|
|
183
|
+
filter: {
|
|
184
|
+
where: { id: '123' },
|
|
185
|
+
include: [{ relation: 'posts', shouldSkipDefaultFilter: true }],
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
```
|
|
397
189
|
|
|
398
|
-
|
|
190
|
+
### Nest includes two levels deep
|
|
191
|
+
|
|
192
|
+
Put an `include` inside a `scope` to load a relation of a relation:
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
// User -> Posts -> Comments
|
|
196
|
+
const user = await userRepository.findOne({
|
|
399
197
|
filter: {
|
|
400
|
-
where: { id: '
|
|
198
|
+
where: { id: '123' },
|
|
401
199
|
include: [{
|
|
402
|
-
relation: '
|
|
403
|
-
scope: {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
}]
|
|
407
|
-
}
|
|
200
|
+
relation: 'posts',
|
|
201
|
+
scope: { include: [{ relation: 'comments' }] },
|
|
202
|
+
}],
|
|
203
|
+
},
|
|
408
204
|
});
|
|
409
|
-
|
|
410
|
-
// Fully typed access
|
|
411
|
-
product?.saleChannelProducts[0].saleChannel.name;
|
|
412
205
|
```
|
|
413
206
|
|
|
207
|
+
> [!WARNING] Performance
|
|
208
|
+
> Each nested `include` adds SQL complexity. **Maximum 2 levels recommended.** For deeper relationships, run multiple queries instead - see [Performance Tips](#performance-tips).
|
|
414
209
|
|
|
415
|
-
|
|
210
|
+
### Many-to-many through a junction table
|
|
416
211
|
|
|
417
|
-
|
|
212
|
+
This is the pattern `examples/vert` uses for `Product` <-> `SaleChannel` through the `SaleChannelProduct` junction table. Include the junction relation, then nest the far side inside its `scope`.
|
|
418
213
|
|
|
419
214
|
```typescript
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
215
|
+
// Product -> SaleChannelProduct (junction) -> SaleChannel
|
|
216
|
+
const product = await productRepository.findOne({
|
|
217
|
+
filter: {
|
|
218
|
+
where: { id: 'prod1' },
|
|
219
|
+
include: [{
|
|
220
|
+
relation: 'saleChannelProducts',
|
|
221
|
+
scope: { include: [{ relation: 'saleChannel' }] },
|
|
222
|
+
}],
|
|
223
|
+
},
|
|
224
|
+
});
|
|
426
225
|
|
|
226
|
+
// Result:
|
|
227
|
+
// {
|
|
228
|
+
// id: 'prod1',
|
|
229
|
+
// name: 'Widget',
|
|
230
|
+
// saleChannelProducts: [
|
|
231
|
+
// { productId: 'prod1', saleChannelId: 'ch1', saleChannel: { id: 'ch1', name: 'Online Store' } },
|
|
232
|
+
// { productId: 'prod1', saleChannelId: 'ch2', saleChannel: { id: 'ch2', name: 'Retail' } }
|
|
233
|
+
// ]
|
|
234
|
+
// }
|
|
235
|
+
```
|
|
427
236
|
|
|
428
|
-
|
|
237
|
+
### Count relations without fetching them fully
|
|
429
238
|
|
|
430
|
-
|
|
239
|
+
Fetch only `id` on the related rows to keep the payload small, then count the array client-side:
|
|
431
240
|
|
|
432
241
|
```typescript
|
|
433
|
-
// Get users with post count
|
|
434
242
|
const users = await userRepository.find({
|
|
435
243
|
filter: {
|
|
436
|
-
include: [{
|
|
437
|
-
|
|
438
|
-
scope: { fields: ['id'] } // Only fetch IDs to minimize data
|
|
439
|
-
}]
|
|
440
|
-
}
|
|
244
|
+
include: [{ relation: 'posts', scope: { fields: ['id'] } }],
|
|
245
|
+
},
|
|
441
246
|
});
|
|
442
247
|
|
|
443
|
-
// Calculate counts
|
|
444
248
|
const usersWithCounts = users.map(user => ({
|
|
445
249
|
...user,
|
|
446
|
-
postCount: (user as any).posts?.length ?? 0
|
|
250
|
+
postCount: (user as any).posts?.length ?? 0,
|
|
447
251
|
}));
|
|
448
252
|
```
|
|
449
253
|
|
|
450
|
-
###
|
|
254
|
+
### Include conditionally
|
|
451
255
|
|
|
452
256
|
```typescript
|
|
453
257
|
async function getUser(id: string, includePosts: boolean) {
|
|
454
|
-
const include = includePosts
|
|
455
|
-
? [{ relation: 'posts' }]
|
|
456
|
-
: [];
|
|
258
|
+
const include = includePosts ? [{ relation: 'posts' }] : [];
|
|
457
259
|
|
|
458
260
|
return userRepository.findOne({
|
|
459
|
-
filter: {
|
|
460
|
-
where: { id },
|
|
461
|
-
include
|
|
462
|
-
}
|
|
261
|
+
filter: { where: { id }, include },
|
|
463
262
|
});
|
|
464
263
|
}
|
|
465
264
|
```
|
|
466
265
|
|
|
266
|
+
### Type included results with a generic
|
|
467
267
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
### Relation Not Found
|
|
471
|
-
|
|
472
|
-
If you try to include a relation that doesn't exist:
|
|
268
|
+
`findOne`/`find` accept a type argument for the shape `include` produces, so the result is fully typed instead of falling back to the base entity:
|
|
473
269
|
|
|
474
270
|
```typescript
|
|
475
|
-
|
|
476
|
-
|
|
271
|
+
type UserWithPosts = User & {
|
|
272
|
+
posts: Post[];
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const user = await userRepository.findOne<UserWithPosts>({
|
|
477
276
|
filter: {
|
|
478
|
-
|
|
479
|
-
|
|
277
|
+
where: { id: '123' },
|
|
278
|
+
include: [{ relation: 'posts' }],
|
|
279
|
+
},
|
|
480
280
|
});
|
|
481
|
-
```
|
|
482
281
|
|
|
483
|
-
|
|
282
|
+
if (user) {
|
|
283
|
+
console.log(user.posts[0].title); // Fully typed
|
|
284
|
+
}
|
|
285
|
+
```
|
|
484
286
|
|
|
485
|
-
|
|
287
|
+
The same pattern nests for a two-level include:
|
|
486
288
|
|
|
487
289
|
```typescript
|
|
488
|
-
|
|
290
|
+
type ProductWithChannels = Product & {
|
|
291
|
+
saleChannelProducts: (SaleChannelProduct & {
|
|
292
|
+
saleChannel: SaleChannel;
|
|
293
|
+
})[];
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const product = await productRepository.findOne<ProductWithChannels>({
|
|
297
|
+
filter: {
|
|
298
|
+
where: { id: 'prod1' },
|
|
299
|
+
include: [{
|
|
300
|
+
relation: 'saleChannelProducts',
|
|
301
|
+
scope: { include: [{ relation: 'saleChannel' }] },
|
|
302
|
+
}],
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
product?.saleChannelProducts[0].saleChannel.name; // Fully typed access
|
|
489
307
|
```
|
|
490
308
|
|
|
491
|
-
|
|
309
|
+
## Hidden properties in relations
|
|
492
310
|
|
|
493
|
-
|
|
311
|
+
`FilterBuilder.toInclude()` excludes hidden columns from every included relation at the SQL level, the same way the top-level query does. For each inclusion it resolves the related model's `hiddenProperties` and default filter, and merges the default filter with your `scope` via `mergeFilter()`. It then drops hidden columns from the nested `columns` selection.
|
|
494
312
|
|
|
495
|
-
```
|
|
496
|
-
|
|
497
|
-
|
|
313
|
+
```typescript
|
|
314
|
+
// User model has hiddenProperties: ['password']
|
|
315
|
+
const post = await postRepository.findOne({
|
|
316
|
+
filter: { include: [{ relation: 'author' }] },
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// post.author will NOT include password - excluded at SQL level
|
|
498
320
|
```
|
|
499
321
|
|
|
500
|
-
|
|
322
|
+
See [Hidden Properties](./advanced#hidden-properties) for the top-level equivalent.
|
|
501
323
|
|
|
324
|
+
## Errors
|
|
502
325
|
|
|
503
|
-
|
|
326
|
+
| Error | Cause | Fix |
|
|
327
|
+
|---|---|---|
|
|
328
|
+
| `[FilterBuilder][toInclude] Relation NOT FOUND \| relation: 'x'` | `include` names a relation absent from the model's `relations` array | Check the model's `relations` definition and match the name exactly |
|
|
329
|
+
| `[FilterBuilder][toInclude] Invalid include format \| include: ...` | An `include` element has no `relation` string | Give every include element a `relation` field |
|
|
330
|
+
| `[<Repository>] Schema key mismatch \| Entity name 'X' not found in connector.query` | The model's `TABLE_NAME` doesn't match its schema registration | See [Query Interface Validation](./advanced#query-interface-validation) |
|
|
504
331
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
332
|
+
## Performance tips
|
|
333
|
+
|
|
334
|
+
1. **Limit nesting depth** - max 2 levels recommended.
|
|
335
|
+
2. **Use `fields` in scope** - fetch only the columns you need.
|
|
336
|
+
3. **Use `limit` in scope** - don't fetch unbounded related data.
|
|
337
|
+
4. **Consider separate queries** - for complex data needs, several simple queries often outperform one deeply nested one.
|
|
338
|
+
5. **Use `shouldSkipDefaultFilter` sparingly** - only when you explicitly need filtered-out records.
|
|
510
339
|
|
|
511
340
|
```typescript
|
|
512
341
|
// Instead of deep nesting, use separate queries
|
|
513
342
|
const user = await userRepository.findById({ id: '123' });
|
|
514
343
|
const posts = await postRepository.find({
|
|
515
|
-
filter: {
|
|
516
|
-
where: { authorId: '123' },
|
|
517
|
-
limit: 10
|
|
518
|
-
}
|
|
344
|
+
filter: { where: { authorId: '123' }, limit: 10 },
|
|
519
345
|
});
|
|
520
346
|
const comments = await commentRepository.find({
|
|
521
|
-
filter: {
|
|
522
|
-
where: { postId: { inq: posts.map(p => p.id) } }
|
|
523
|
-
}
|
|
347
|
+
filter: { where: { postId: { inq: posts.map(p => p.id) } } },
|
|
524
348
|
});
|
|
525
349
|
```
|
|
526
350
|
|
|
527
|
-
|
|
528
|
-
## Quick Reference
|
|
351
|
+
## Quick reference
|
|
529
352
|
|
|
530
353
|
| Want to... | Code |
|
|
531
|
-
|
|
354
|
+
|---|---|
|
|
532
355
|
| Include one relation | `include: [{ relation: 'posts' }]` |
|
|
533
356
|
| Include multiple | `include: [{ relation: 'posts' }, { relation: 'profile' }]` |
|
|
534
357
|
| Filter included | `include: [{ relation: 'posts', scope: { where: { status: 'active' } } }]` |
|
|
@@ -538,23 +361,19 @@ const comments = await commentRepository.find({
|
|
|
538
361
|
| Select fields | `include: [{ relation: 'posts', scope: { fields: ['id', 'title'] } }]` |
|
|
539
362
|
| Skip default filter | `include: [{ relation: 'posts', shouldSkipDefaultFilter: true }]` |
|
|
540
363
|
|
|
364
|
+
## See also
|
|
541
365
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
- [
|
|
545
|
-
- [
|
|
546
|
-
- [
|
|
547
|
-
|
|
548
|
-
## See Also
|
|
549
|
-
|
|
550
|
-
- **Related Concepts:**
|
|
551
|
-
- [Repositories Overview](./index) - Core repository operations
|
|
552
|
-
- [Models](/guides/core-concepts/persistent/models) - Defining model relationships
|
|
366
|
+
- [Repositories overview](/references/base/repositories/) - CRUD basics, common tasks
|
|
367
|
+
- [Advanced Features](./advanced) - transactions, hidden properties, performance
|
|
368
|
+
- [Repository Mixins (Removed)](./mixins) - where default-filter and fields-visibility behavior lives now
|
|
369
|
+
- [Filter System](/references/base/filter-system/) - every `where` operator, ordering, pagination
|
|
370
|
+
- [Models - Full Reference](/references/base/models-reference) - `@model`, entity hierarchy, schema enrichers
|
|
371
|
+
- [Drizzle ORM Relations](https://orm.drizzle.team/docs/rqb#relations) - relation definition guide
|
|
553
372
|
|
|
554
|
-
|
|
555
|
-
- [Advanced Features](./advanced) - Hidden properties, transactions
|
|
556
|
-
- [Repository Mixins (Removed)](./mixins) - Where default-filter and fields-visibility behavior lives now
|
|
557
|
-
- [Filter System](/references/base/filter-system/) - Query operators
|
|
373
|
+
**Files:**
|
|
558
374
|
|
|
559
|
-
-
|
|
560
|
-
|
|
375
|
+
- [`packages/filter/src/common/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/filter/src/common/types.ts) - `TFilter`, `TInclusion`
|
|
376
|
+
- [`packages/core-server/src/base/repositories/common/constants.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/base/repositories/common/constants.ts) - `RelationTypes`
|
|
377
|
+
- [`packages/core-server/src/connectors/postgres/repositories/common/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/connectors/postgres/repositories/common/types.ts) - `TRelationConfig`
|
|
378
|
+
- [`packages/core-server/src/connectors/relational/repositories/dialect/filter.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/connectors/relational/repositories/dialect/filter.ts) - `FilterBuilder` (`resolveRelations`, `toInclude`)
|
|
379
|
+
- [`packages/core-server/src/connectors/postgres/repositories/dialect/relation.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/connectors/postgres/repositories/dialect/relation.ts) - `createRelations` (config -> Drizzle `relations()`)
|