@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.
Files changed (174) hide show
  1. package/README.md +14 -14
  2. package/content/best-practices/api-usage-examples.md +39 -19
  3. package/content/best-practices/architectural-patterns.md +24 -13
  4. package/content/best-practices/architecture-decisions.md +29 -16
  5. package/content/best-practices/code-style-standards/advanced-patterns.md +43 -31
  6. package/content/best-practices/code-style-standards/constants-configuration.md +19 -4
  7. package/content/best-practices/code-style-standards/control-flow.md +33 -1
  8. package/content/best-practices/code-style-standards/documentation.md +28 -8
  9. package/content/best-practices/code-style-standards/function-patterns.md +15 -4
  10. package/content/best-practices/code-style-standards/index.md +7 -2
  11. package/content/best-practices/code-style-standards/naming-conventions.md +27 -3
  12. package/content/best-practices/code-style-standards/route-definitions.md +9 -5
  13. package/content/best-practices/code-style-standards/tooling.md +14 -1
  14. package/content/best-practices/code-style-standards/type-safety.md +39 -6
  15. package/content/best-practices/common-pitfalls.md +59 -35
  16. package/content/best-practices/contribution-workflow.md +8 -4
  17. package/content/best-practices/data-modeling.md +78 -62
  18. package/content/best-practices/deployment-strategies.md +56 -19
  19. package/content/best-practices/error-handling.md +247 -153
  20. package/content/best-practices/index.md +6 -0
  21. package/content/best-practices/performance-optimization.md +26 -13
  22. package/content/best-practices/security-guidelines.md +35 -9
  23. package/content/best-practices/testing-strategies.md +7 -2
  24. package/content/best-practices/troubleshooting-tips.md +27 -17
  25. package/content/extensions/components/api-reference.md +109 -323
  26. package/content/extensions/components/authentication/api.md +489 -602
  27. package/content/extensions/components/authentication/errors.md +135 -498
  28. package/content/extensions/components/authentication/index.md +89 -801
  29. package/content/extensions/components/authentication/usage.md +231 -955
  30. package/content/extensions/components/authorization/api.md +991 -644
  31. package/content/extensions/components/authorization/errors.md +204 -208
  32. package/content/extensions/components/authorization/getting-started.md +227 -0
  33. package/content/extensions/components/authorization/index.md +88 -795
  34. package/content/extensions/components/authorization/usage.md +219 -528
  35. package/content/extensions/components/health-check.md +77 -243
  36. package/content/extensions/components/index.md +24 -90
  37. package/content/extensions/components/mail/api.md +553 -285
  38. package/content/extensions/components/mail/errors.md +76 -62
  39. package/content/extensions/components/mail/index.md +111 -463
  40. package/content/extensions/components/mail/usage.md +139 -173
  41. package/content/extensions/components/request-tracker.md +70 -173
  42. package/content/extensions/components/socket-io/api.md +462 -785
  43. package/content/extensions/components/socket-io/errors.md +49 -51
  44. package/content/extensions/components/socket-io/index.md +69 -372
  45. package/content/extensions/components/socket-io/usage.md +212 -105
  46. package/content/extensions/components/static-asset/api.md +461 -141
  47. package/content/extensions/components/static-asset/errors.md +121 -53
  48. package/content/extensions/components/static-asset/index.md +84 -606
  49. package/content/extensions/components/static-asset/usage.md +184 -299
  50. package/content/extensions/components/template/index.md +3 -3
  51. package/content/extensions/components/websocket/api.md +311 -404
  52. package/content/extensions/components/websocket/errors.md +47 -56
  53. package/content/extensions/components/websocket/index.md +75 -407
  54. package/content/extensions/components/websocket/usage.md +120 -338
  55. package/content/extensions/helpers/cron/index.md +52 -160
  56. package/content/extensions/helpers/crypto/index.md +67 -480
  57. package/content/extensions/helpers/crypto/reference.md +528 -0
  58. package/content/extensions/helpers/env/index.md +64 -178
  59. package/content/extensions/helpers/error/index.md +286 -196
  60. package/content/extensions/helpers/index.md +61 -47
  61. package/content/extensions/helpers/inversion/index.md +76 -550
  62. package/content/extensions/helpers/inversion/reference.md +530 -0
  63. package/content/extensions/helpers/kafka/admin.md +23 -3
  64. package/content/extensions/helpers/kafka/compile-binary.md +83 -52
  65. package/content/extensions/helpers/kafka/consumer.md +65 -28
  66. package/content/extensions/helpers/kafka/examples.md +46 -253
  67. package/content/extensions/helpers/kafka/index.md +55 -617
  68. package/content/extensions/helpers/kafka/producer.md +156 -22
  69. package/content/extensions/helpers/kafka/schema-registry.md +59 -79
  70. package/content/extensions/helpers/logger/hf-logger.md +220 -0
  71. package/content/extensions/helpers/logger/index.md +78 -552
  72. package/content/extensions/helpers/logger/pino.md +105 -0
  73. package/content/extensions/helpers/logger/reference.md +937 -0
  74. package/content/extensions/helpers/network/api.md +276 -195
  75. package/content/extensions/helpers/network/index.md +87 -524
  76. package/content/extensions/helpers/queue/index.md +80 -897
  77. package/content/extensions/helpers/queue/reference.md +494 -0
  78. package/content/extensions/helpers/redis/index.md +86 -640
  79. package/content/extensions/helpers/redis/reference.md +784 -0
  80. package/content/extensions/helpers/secrets/index.md +136 -0
  81. package/content/extensions/helpers/socket-io/api.md +325 -204
  82. package/content/extensions/helpers/socket-io/index.md +79 -429
  83. package/content/extensions/helpers/storage/api.md +584 -464
  84. package/content/extensions/helpers/storage/index.md +80 -573
  85. package/content/extensions/helpers/types/index.md +75 -495
  86. package/content/extensions/helpers/types/reference.md +689 -0
  87. package/content/extensions/helpers/uid/index.md +176 -189
  88. package/content/extensions/helpers/websocket/api.md +366 -214
  89. package/content/extensions/helpers/websocket/index.md +68 -503
  90. package/content/extensions/helpers/worker-thread/index.md +62 -396
  91. package/content/extensions/helpers/worker-thread/reference.md +428 -0
  92. package/content/extensions/index.md +38 -39
  93. package/content/extensions/src-details/mcp-server.md +96 -548
  94. package/content/guides/core-concepts/persistent/datasources.md +2 -2
  95. package/content/guides/core-concepts/persistent/index.md +5 -1
  96. package/content/guides/core-concepts/persistent/models.md +1 -1
  97. package/content/guides/core-concepts/persistent/pglite.md +218 -0
  98. package/content/guides/core-concepts/persistent/postgres-drivers.md +21 -15
  99. package/content/guides/core-concepts/persistent/search-meilisearch.md +22 -12
  100. package/content/guides/core-concepts/persistent/search-typesense.md +28 -16
  101. package/content/guides/core-concepts/persistent/sqlite.md +296 -0
  102. package/content/guides/core-concepts/persistent/transactions.md +12 -11
  103. package/content/guides/core-concepts/secrets-vault.md +177 -0
  104. package/content/guides/core-concepts/services.md +1 -1
  105. package/content/guides/get-started/5-minute-quickstart.md +59 -206
  106. package/content/guides/get-started/philosophy.md +135 -670
  107. package/content/guides/get-started/setup.md +53 -74
  108. package/content/guides/migrations/redis-helpers-migration.md +5 -4
  109. package/content/guides/migrations/scoped-rbac-migration.md +5 -5
  110. package/content/guides/migrations/unified-connectors-migration.md +8 -7
  111. package/content/guides/tutorials/ecommerce-api.md +3 -8
  112. package/content/guides/tutorials/realtime-chat.md +1 -1
  113. package/content/references/base/application.md +64 -22
  114. package/content/references/base/components.md +3 -3
  115. package/content/references/base/connectors.md +79 -136
  116. package/content/references/base/controllers.md +12 -12
  117. package/content/references/base/datasources-reference.md +600 -0
  118. package/content/references/base/datasources.md +84 -444
  119. package/content/references/base/dependency-injection.md +25 -46
  120. package/content/references/base/filter-system/application-usage.md +95 -123
  121. package/content/references/base/filter-system/array-operators.md +33 -43
  122. package/content/references/base/filter-system/comparison-operators.md +55 -63
  123. package/content/references/base/filter-system/default-filter.md +144 -350
  124. package/content/references/base/filter-system/fields-order-pagination.md +116 -148
  125. package/content/references/base/filter-system/index.md +114 -253
  126. package/content/references/base/filter-system/json-filtering.md +52 -181
  127. package/content/references/base/filter-system/list-operators.md +28 -44
  128. package/content/references/base/filter-system/logical-operators.md +69 -114
  129. package/content/references/base/filter-system/null-operators.md +41 -98
  130. package/content/references/base/filter-system/pattern-matching.md +48 -51
  131. package/content/references/base/filter-system/quick-reference.md +93 -196
  132. package/content/references/base/filter-system/range-operators.md +22 -38
  133. package/content/references/base/filter-system/tips.md +70 -133
  134. package/content/references/base/filter-system/use-cases.md +173 -232
  135. package/content/references/base/grpc-controllers.md +53 -17
  136. package/content/references/base/index.md +5 -3
  137. package/content/references/base/middlewares.md +43 -28
  138. package/content/references/base/models-reference.md +886 -0
  139. package/content/references/base/models.md +81 -1452
  140. package/content/references/base/providers.md +8 -8
  141. package/content/references/base/repositories/advanced.md +281 -419
  142. package/content/references/base/repositories/index.md +84 -644
  143. package/content/references/base/repositories/mixins.md +25 -21
  144. package/content/references/base/repositories/relations.md +194 -375
  145. package/content/references/base/repositories/soft-deletable.md +67 -55
  146. package/content/references/base/secrets.md +267 -0
  147. package/content/references/base/services.md +8 -6
  148. package/content/references/configuration/environment-variables.md +73 -21
  149. package/content/references/configuration/index.md +51 -31
  150. package/content/references/index.md +1 -1
  151. package/content/references/quick-reference.md +3 -16
  152. package/content/references/utilities/crypto.md +35 -76
  153. package/content/references/utilities/date.md +33 -73
  154. package/content/references/utilities/duration.md +85 -0
  155. package/content/references/utilities/index.md +6 -2
  156. package/content/references/utilities/jsx-reference.md +298 -0
  157. package/content/references/utilities/jsx.md +82 -525
  158. package/content/references/utilities/module.md +81 -61
  159. package/content/references/utilities/parse.md +34 -64
  160. package/content/references/utilities/performance.md +33 -58
  161. package/content/references/utilities/promise.md +28 -62
  162. package/content/references/utilities/request.md +58 -218
  163. package/content/references/utilities/retry.md +139 -0
  164. package/content/references/utilities/schema.md +43 -137
  165. package/content/references/utilities/statuses-reference.md +361 -0
  166. package/content/references/utilities/statuses.md +63 -667
  167. package/dist/mcp-server/index.js +0 -0
  168. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
  169. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts.map +1 -1
  170. package/dist/mcp-server/tools/github/list-project-files.tool.js +3 -3
  171. package/dist/mcp-server/tools/github/search-code.tool.js +1 -1
  172. package/dist/mcp-server/tools/github/verify-dependencies.tool.js +1 -1
  173. package/dist/mcp-server/tools/github/view-source-file.tool.js +1 -1
  174. package/package.json +24 -23
@@ -1,19 +1,22 @@
1
- # Relations & Includes
2
-
3
- Fetch related data using `include` for eager loading. This guide covers one-to-one, one-to-many, and many-to-many relationships.
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
- ## Basic Include
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
- ### One-to-Many: User with Posts
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
- > When `include` is present in the filter, the repository uses the **Query API** (`connector.query`) instead of the Core API. This is handled automatically by the `canUseCoreAPI` check in `ReadableRepository`.
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
- ## Defining Relations
38
+ Each element of the `include` array accepts:
238
39
 
239
- Relations are declared on the model as a static `relations` resolver returning an array of `TRelationConfig`. The framework translates them to Drizzle ORM relations internally during schema discovery.
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
- ### Relation Config Type
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
- ```typescript
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
- ### In Your Model
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 { BasePostgresEntity, TRelationConfig } from '@venizia/ignis/postgres';
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 BasePostgresEntity<typeof User.schema> {
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
- The resolver form (`() => [...]`) defers evaluation until all `@model` classes are registered, avoiding circular-import ordering issues between related models.
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
- ### Relation Types
89
+ `metadata`'s shape comes straight from Drizzle's own `one()`/`many()` parameter types, not a hand-duplicated one.
291
90
 
292
- | Type | Drizzle Function | Description | Example |
293
- |------|------------------|-------------|---------|
294
- | `'one'` | `one()` | One-to-one or many-to-one | Post has one Author, User has one Profile |
295
- | `'many'` | `many()` | One-to-many | User has many Posts |
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
- > Unlike LoopBack 4's `hasMany`/`hasOne`/`belongsTo` terminology, IGNIS uses Drizzle ORM's relation model which has only `one` and `many` types. A "belongsTo" relationship is expressed as `type: 'one'` with `fields` (local FK) and `references` (remote PK) in the metadata.
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
- ### Example: Post Model with Both Types
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 BasePostgresEntity<typeof Post.schema> {
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
- ### How Configs Become Drizzle Relations
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
- ## Auto-Resolution
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
- ## Hidden Properties in Relations
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
- include: [{ relation: 'author' }]
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
- ## Type Safety with Generics
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
- // Define the expected return type
371
- type UserWithPosts = User & {
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: [{ relation: 'posts' }]
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
- ### Nested Relations Type
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
- type ProductWithChannels = Product & {
393
- saleChannelProducts: (SaleChannelProduct & {
394
- saleChannel: SaleChannel;
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
- const product = await productRepository.findOne<ProductWithChannels>({
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: 'prod1' },
198
+ where: { id: '123' },
401
199
  include: [{
402
- relation: 'saleChannelProducts',
403
- scope: {
404
- include: [{ relation: 'saleChannel' }]
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
- ## TInclusion Type Reference
210
+ ### Many-to-many through a junction table
416
211
 
417
- Each element in the `include` array has this shape:
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
- type TInclusion = {
421
- relation: string; // Name of the relation to include
422
- scope?: TFilter; // Optional nested filter (where, order, limit, fields, include)
423
- shouldSkipDefaultFilter?: boolean; // Skip the related model's default filter
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
- ## Common Patterns
237
+ ### Count relations without fetching them fully
429
238
 
430
- ### Find All with Count of Relations
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
- relation: 'posts',
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
- ### Conditional Include
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
- ## Error Handling
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
- // Error: [FilterBuilder][toInclude] Relation NOT FOUND | relation: 'nonExistent'
476
- await userRepository.find({
271
+ type UserWithPosts = User & {
272
+ posts: Post[];
273
+ };
274
+
275
+ const user = await userRepository.findOne<UserWithPosts>({
477
276
  filter: {
478
- include: [{ relation: 'nonExistent' }]
479
- }
277
+ where: { id: '123' },
278
+ include: [{ relation: 'posts' }],
279
+ },
480
280
  });
481
- ```
482
281
 
483
- **Fix:** Check your model's `relations` definition and ensure the relation name matches.
282
+ if (user) {
283
+ console.log(user.posts[0].title); // Fully typed
284
+ }
285
+ ```
484
286
 
485
- ### Invalid Include Format
287
+ The same pattern nests for a two-level include:
486
288
 
487
289
  ```typescript
488
- // Error: [FilterBuilder][toInclude] Invalid include format | include: ...
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
- **Fix:** Ensure each include element has a `relation` string property.
309
+ ## Hidden properties in relations
492
310
 
493
- ### Schema Key Mismatch
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
- Error: [UserRepository] Schema key mismatch | Entity name 'User' not found
497
- in connector.query | Available keys: [Post, Comment]
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
- **Fix:** Ensure your model's `TABLE_NAME` matches the schema registration.
322
+ See [Hidden Properties](./advanced#hidden-properties) for the top-level equivalent.
501
323
 
324
+ ## Errors
502
325
 
503
- ## Performance Tips
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
- 1. **Limit nesting depth** - Max 2 levels recommended
506
- 2. **Use `fields` in scope** - Only fetch needed columns
507
- 3. **Use `limit` in scope** - Don't fetch unbounded related data
508
- 4. **Consider separate queries** - For complex data needs, multiple simple queries often outperform one complex nested query
509
- 5. **Use `shouldSkipDefaultFilter` sparingly** - Only when you explicitly need filtered-out records
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
- ## Next Steps
543
-
544
- - [JSON Path Filtering](../filter-system/json-filtering) - Query JSONB columns
545
- - [Array Operators](../filter-system/array-operators) - PostgreSQL array queries
546
- - [Advanced Features](./advanced.md) - Transactions, hidden props
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
- - **Related Topics:**
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
- - **External Resources:**
560
- - [Drizzle ORM Relations](https://orm.drizzle.team/docs/rqb#relations) - Relation definition guide
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()`)