@venizia/ignis-docs 0.2.1-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 (145) hide show
  1. package/content/best-practices/architectural-patterns.md +3 -3
  2. package/content/best-practices/code-style-standards/naming-conventions.md +1 -1
  3. package/content/best-practices/contribution-workflow.md +2 -2
  4. package/content/best-practices/error-handling.md +94 -89
  5. package/content/best-practices/security-guidelines.md +5 -5
  6. package/content/extensions/components/api-reference.md +22 -21
  7. package/content/extensions/components/authentication/api.md +64 -28
  8. package/content/extensions/components/authentication/errors.md +19 -5
  9. package/content/extensions/components/authentication/index.md +19 -18
  10. package/content/extensions/components/authentication/usage.md +55 -30
  11. package/content/extensions/components/authorization/api.md +351 -84
  12. package/content/extensions/components/authorization/errors.md +51 -17
  13. package/content/extensions/components/authorization/getting-started.md +227 -0
  14. package/content/extensions/components/authorization/index.md +24 -16
  15. package/content/extensions/components/authorization/usage.md +45 -21
  16. package/content/extensions/components/health-check.md +15 -9
  17. package/content/extensions/components/index.md +24 -90
  18. package/content/extensions/components/mail/api.md +105 -54
  19. package/content/extensions/components/mail/errors.md +12 -10
  20. package/content/extensions/components/mail/index.md +32 -13
  21. package/content/extensions/components/mail/usage.md +26 -18
  22. package/content/extensions/components/request-tracker.md +18 -14
  23. package/content/extensions/components/socket-io/api.md +377 -882
  24. package/content/extensions/components/socket-io/errors.md +49 -51
  25. package/content/extensions/components/socket-io/index.md +72 -88
  26. package/content/extensions/components/socket-io/usage.md +107 -117
  27. package/content/extensions/components/static-asset/api.md +83 -31
  28. package/content/extensions/components/static-asset/errors.md +18 -7
  29. package/content/extensions/components/static-asset/index.md +18 -11
  30. package/content/extensions/components/static-asset/usage.md +11 -6
  31. package/content/extensions/components/template/index.md +3 -3
  32. package/content/extensions/components/websocket/api.md +58 -27
  33. package/content/extensions/components/websocket/errors.md +3 -3
  34. package/content/extensions/components/websocket/index.md +8 -7
  35. package/content/extensions/components/websocket/usage.md +21 -8
  36. package/content/extensions/helpers/cron/index.md +8 -7
  37. package/content/extensions/helpers/crypto/index.md +16 -8
  38. package/content/extensions/helpers/crypto/reference.md +96 -24
  39. package/content/extensions/helpers/env/index.md +14 -10
  40. package/content/extensions/helpers/error/index.md +99 -23
  41. package/content/extensions/helpers/index.md +61 -47
  42. package/content/extensions/helpers/inversion/index.md +23 -6
  43. package/content/extensions/helpers/inversion/reference.md +30 -22
  44. package/content/extensions/helpers/kafka/admin.md +3 -2
  45. package/content/extensions/helpers/kafka/compile-binary.md +69 -44
  46. package/content/extensions/helpers/kafka/consumer.md +24 -21
  47. package/content/extensions/helpers/kafka/examples.md +22 -234
  48. package/content/extensions/helpers/kafka/index.md +30 -62
  49. package/content/extensions/helpers/kafka/producer.md +31 -26
  50. package/content/extensions/helpers/kafka/schema-registry.md +19 -14
  51. package/content/extensions/helpers/logger/hf-logger.md +49 -22
  52. package/content/extensions/helpers/logger/index.md +37 -12
  53. package/content/extensions/helpers/logger/pino.md +31 -11
  54. package/content/extensions/helpers/logger/reference.md +243 -52
  55. package/content/extensions/helpers/network/api.md +65 -30
  56. package/content/extensions/helpers/network/index.md +32 -11
  57. package/content/extensions/helpers/queue/index.md +17 -6
  58. package/content/extensions/helpers/queue/reference.md +52 -25
  59. package/content/extensions/helpers/redis/index.md +29 -11
  60. package/content/extensions/helpers/redis/reference.md +85 -28
  61. package/content/extensions/helpers/secrets/index.md +82 -12
  62. package/content/extensions/helpers/socket-io/api.md +40 -21
  63. package/content/extensions/helpers/socket-io/index.md +26 -10
  64. package/content/extensions/helpers/storage/api.md +42 -24
  65. package/content/extensions/helpers/storage/index.md +10 -7
  66. package/content/extensions/helpers/types/index.md +20 -7
  67. package/content/extensions/helpers/types/reference.md +53 -14
  68. package/content/extensions/helpers/uid/index.md +166 -10
  69. package/content/extensions/helpers/websocket/api.md +52 -13
  70. package/content/extensions/helpers/websocket/index.md +7 -4
  71. package/content/extensions/helpers/worker-thread/index.md +9 -5
  72. package/content/extensions/helpers/worker-thread/reference.md +20 -20
  73. package/content/extensions/index.md +38 -39
  74. package/content/extensions/src-details/mcp-server.md +96 -548
  75. package/content/guides/core-concepts/persistent/datasources.md +2 -2
  76. package/content/guides/core-concepts/persistent/index.md +5 -1
  77. package/content/guides/core-concepts/persistent/pglite.md +218 -0
  78. package/content/guides/core-concepts/persistent/postgres-drivers.md +21 -15
  79. package/content/guides/core-concepts/persistent/search-meilisearch.md +22 -12
  80. package/content/guides/core-concepts/persistent/search-typesense.md +26 -14
  81. package/content/guides/core-concepts/persistent/sqlite.md +296 -0
  82. package/content/guides/core-concepts/persistent/transactions.md +12 -11
  83. package/content/guides/get-started/5-minute-quickstart.md +59 -206
  84. package/content/guides/get-started/philosophy.md +135 -670
  85. package/content/guides/get-started/setup.md +53 -74
  86. package/content/guides/migrations/redis-helpers-migration.md +4 -3
  87. package/content/guides/migrations/scoped-rbac-migration.md +5 -5
  88. package/content/guides/migrations/unified-connectors-migration.md +7 -6
  89. package/content/guides/tutorials/realtime-chat.md +1 -1
  90. package/content/references/base/application.md +63 -21
  91. package/content/references/base/components.md +3 -3
  92. package/content/references/base/connectors.md +14 -14
  93. package/content/references/base/controllers.md +12 -12
  94. package/content/references/base/datasources-reference.md +32 -31
  95. package/content/references/base/datasources.md +6 -6
  96. package/content/references/base/dependency-injection.md +12 -11
  97. package/content/references/base/filter-system/application-usage.md +54 -30
  98. package/content/references/base/filter-system/array-operators.md +24 -46
  99. package/content/references/base/filter-system/comparison-operators.md +47 -67
  100. package/content/references/base/filter-system/default-filter.md +59 -53
  101. package/content/references/base/filter-system/fields-order-pagination.md +92 -146
  102. package/content/references/base/filter-system/index.md +25 -13
  103. package/content/references/base/filter-system/json-filtering.md +45 -184
  104. package/content/references/base/filter-system/list-operators.md +23 -53
  105. package/content/references/base/filter-system/logical-operators.md +63 -121
  106. package/content/references/base/filter-system/null-operators.md +34 -104
  107. package/content/references/base/filter-system/pattern-matching.md +40 -55
  108. package/content/references/base/filter-system/quick-reference.md +86 -198
  109. package/content/references/base/filter-system/range-operators.md +18 -46
  110. package/content/references/base/filter-system/tips.md +6 -6
  111. package/content/references/base/filter-system/use-cases.md +33 -15
  112. package/content/references/base/grpc-controllers.md +53 -17
  113. package/content/references/base/index.md +5 -3
  114. package/content/references/base/middlewares.md +11 -10
  115. package/content/references/base/models-reference.md +17 -17
  116. package/content/references/base/models.md +4 -3
  117. package/content/references/base/providers.md +8 -8
  118. package/content/references/base/repositories/advanced.md +224 -326
  119. package/content/references/base/repositories/index.md +19 -6
  120. package/content/references/base/repositories/mixins.md +5 -5
  121. package/content/references/base/repositories/relations.md +160 -293
  122. package/content/references/base/repositories/soft-deletable.md +16 -6
  123. package/content/references/base/secrets.md +17 -13
  124. package/content/references/base/services.md +6 -4
  125. package/content/references/configuration/environment-variables.md +31 -23
  126. package/content/references/configuration/index.md +6 -4
  127. package/content/references/index.md +1 -1
  128. package/content/references/utilities/duration.md +85 -0
  129. package/content/references/utilities/index.md +5 -1
  130. package/content/references/utilities/jsx-reference.md +11 -11
  131. package/content/references/utilities/jsx.md +2 -2
  132. package/content/references/utilities/module.md +78 -25
  133. package/content/references/utilities/request.md +2 -1
  134. package/content/references/utilities/retry.md +139 -0
  135. package/content/references/utilities/schema.md +2 -2
  136. package/content/references/utilities/statuses-reference.md +4 -4
  137. package/content/references/utilities/statuses.md +5 -5
  138. package/dist/mcp-server/index.js +0 -0
  139. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
  140. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts.map +1 -1
  141. package/dist/mcp-server/tools/github/list-project-files.tool.js +3 -3
  142. package/dist/mcp-server/tools/github/search-code.tool.js +1 -1
  143. package/dist/mcp-server/tools/github/verify-dependencies.tool.js +1 -1
  144. package/dist/mcp-server/tools/github/view-source-file.tool.js +1 -1
  145. package/package.json +17 -16
@@ -6,22 +6,12 @@ difficulty: intermediate
6
6
 
7
7
  # Relations & Includes
8
8
 
9
- Exhaustive reference for declaring `one`/`many` relations on a model and eager-loading them via `include` - one-to-one, one-to-many, and many-to-many. For the common tasks, start with the [Repositories overview](/references/base/repositories/).
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/).
10
10
 
11
- **Files:**
12
-
13
- - [`packages/core/src/base/repositories/query-schemas/filter.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/base/repositories/query-schemas/filter.ts) - `TFilter`, `TInclusion`
14
- - [`packages/core/src/base/repositories/common/constants.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/base/repositories/common/constants.ts) - `RelationTypes`
15
- - [`packages/core/src/connectors/postgres/repositories/common/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/connectors/postgres/repositories/common/types.ts) - `TRelationConfig`
16
- - [`packages/core/src/connectors/postgres/repositories/dialect/filter.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/connectors/postgres/repositories/dialect/filter.ts) - `FilterBuilder` (`resolveRelations`, `toInclude`)
17
- - [`packages/core/src/connectors/postgres/repositories/dialect/relation.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/connectors/postgres/repositories/dialect/relation.ts) - `createRelations` (config -> Drizzle `relations()`)
18
-
19
- ## Basic Include
20
-
21
- ### One-to-many: user with posts
11
+ ## In one example
22
12
 
23
13
  ```typescript
24
- // Fetch user with their posts
14
+ // Fetch a user with their posts
25
15
  const user = await userRepository.findOne({
26
16
  filter: {
27
17
  where: { id: '123' },
@@ -40,84 +30,133 @@ const user = await userRepository.findOne({
40
30
  // }
41
31
  ```
42
32
 
43
- ### One-to-one: post with author
33
+ > [!NOTE]
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).
44
35
 
45
- ```typescript
46
- // Fetch post with its author
47
- const post = await postRepository.findOne({
48
- filter: {
49
- where: { id: 'p1' },
50
- include: [{ relation: 'author' }],
51
- },
52
- });
36
+ ## `TInclusion` options
53
37
 
54
- // Result:
55
- // {
56
- // id: 'p1',
57
- // title: 'First Post',
58
- // authorId: '123',
59
- // author: { id: '123', name: 'John', email: 'john@example.com' }
60
- // }
61
- ```
38
+ Each element of the `include` array accepts:
39
+
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 |
45
+
46
+ `scope` takes the same shape as a top-level filter - see the [Filter System](/references/base/filter-system/) reference for every `where` operator.
62
47
 
63
- ### Multiple relations
48
+ ## Declaring relations on a model
49
+
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.
64
51
 
65
52
  ```typescript
66
- // Fetch post with author AND comments
67
- const post = await postRepository.findOne({
68
- filter: {
69
- where: { id: 'p1' },
70
- include: [{ relation: 'author' }, { relation: 'comments' }],
71
- },
72
- });
53
+ // src/models/user.model.ts
54
+ import { model, RelationTypes } from '@venizia/ignis';
55
+ import { BaseEntity, TRelationConfig } from '@venizia/ignis/postgres';
56
+ import { pgTable, text } from 'drizzle-orm/pg-core';
57
+ import { Post } from './post.model';
58
+
59
+ @model({ type: 'entity' })
60
+ export class User extends BaseEntity<typeof User.schema> {
61
+ static override schema = pgTable('User', {
62
+ id: text('id').primaryKey(),
63
+ name: text('name').notNull(),
64
+ email: text('email').notNull(),
65
+ });
66
+
67
+ static override relations = (): TRelationConfig[] => [
68
+ {
69
+ name: 'posts',
70
+ type: RelationTypes.MANY,
71
+ schema: Post.schema,
72
+ metadata: { relationName: 'posts' },
73
+ },
74
+ ];
75
+ }
73
76
  ```
74
77
 
75
- > [!NOTE]
76
- > 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 `ReadableRelationalRepository` - see [Performance Optimization](./advanced#performance-optimization).
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
77
81
 
78
- ## Scoped Includes
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` |
79
88
 
80
- Apply filters, ordering, and limits to included relations using `scope` (itself a `TFilter`).
89
+ `metadata`'s shape comes straight from Drizzle's own `one()`/`many()` parameter types, not a hand-duplicated one.
81
90
 
82
- ### Filter related data
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 |
97
+
98
+ > [!NOTE]
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
102
+
103
+ This mirrors `examples/vert`'s `SaleChannelProduct` junction model: one `ONE` relation per foreign key, plus a `MANY` relation elsewhere.
83
104
 
84
105
  ```typescript
85
- // User with only published posts
86
- const user = await userRepository.findOne({
87
- filter: {
88
- where: { id: '123' },
89
- include: [{ relation: 'posts', scope: { where: { status: 'published' } } }],
90
- },
91
- });
106
+ @model({ type: 'entity' })
107
+ export class Post extends BaseEntity<typeof Post.schema> {
108
+ static override schema = postTable;
109
+
110
+ static override relations = (): TRelationConfig[] => [
111
+ {
112
+ name: 'author',
113
+ type: RelationTypes.ONE,
114
+ schema: User.schema,
115
+ metadata: {
116
+ fields: [Post.schema.authorId],
117
+ references: [User.schema.id],
118
+ },
119
+ },
120
+ {
121
+ name: 'comments',
122
+ type: RelationTypes.MANY,
123
+ schema: Comment.schema,
124
+ metadata: { relationName: 'comments' },
125
+ },
126
+ ];
127
+ }
92
128
  ```
93
129
 
94
- ### Order related data
130
+ ### Auto-resolution in the repository
131
+
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.
95
133
 
96
134
  ```typescript
97
- // User with posts ordered by date
98
- const user = await userRepository.findOne({
99
- filter: {
100
- where: { id: '123' },
101
- include: [{ relation: 'posts', scope: { order: ['createdAt DESC'] } }],
102
- },
103
- });
135
+ @repository({ model: User, dataSource: PostgresDataSource })
136
+ export class UserRepository extends DefaultCRUDRepository<typeof User.schema> {
137
+ // Relations auto-resolved from User.relations!
138
+ }
104
139
  ```
105
140
 
106
- ### Limit related data
141
+ ## Recipes
142
+
143
+ ### Include multiple relations
107
144
 
108
145
  ```typescript
109
- // User with their 5 most recent posts
110
- const user = await userRepository.findOne({
146
+ const post = await postRepository.findOne({
111
147
  filter: {
112
- where: { id: '123' },
113
- include: [{ relation: 'posts', scope: { order: ['createdAt DESC'], limit: 5 } }],
148
+ where: { id: 'p1' },
149
+ include: [{ relation: 'author' }, { relation: 'comments' }],
114
150
  },
115
151
  });
116
152
  ```
117
153
 
118
- ### Combined scope options
154
+ ### Filter, order, and limit included rows
155
+
156
+ Combine `where`, `order`, `limit`, and `fields` inside `scope` the same way you would on a top-level filter:
119
157
 
120
158
  ```typescript
159
+ // User with their 5 most recent published posts, id and title only
121
160
  const user = await userRepository.findOne({
122
161
  filter: {
123
162
  where: { id: '123' },
@@ -126,7 +165,7 @@ const user = await userRepository.findOne({
126
165
  scope: {
127
166
  where: { status: 'published' },
128
167
  order: ['createdAt DESC'],
129
- limit: 10,
168
+ limit: 5,
130
169
  fields: ['id', 'title', 'createdAt'],
131
170
  },
132
171
  }],
@@ -134,7 +173,7 @@ const user = await userRepository.findOne({
134
173
  });
135
174
  ```
136
175
 
137
- ### Skip default filter on includes
176
+ ### Skip the default filter on one inclusion
138
177
 
139
178
  Each inclusion can independently bypass the related model's default filter:
140
179
 
@@ -148,11 +187,9 @@ const user = await userRepository.findOne({
148
187
  });
149
188
  ```
150
189
 
151
- ## Nested Includes
152
-
153
- Include relations of relations by nesting `include` inside `scope` (2 levels recommended, see [Performance Tips](#performance-tips)).
190
+ ### Nest includes two levels deep
154
191
 
155
- ### Two-level nesting
192
+ Put an `include` inside a `scope` to load a relation of a relation:
156
193
 
157
194
  ```typescript
158
195
  // User -> Posts -> Comments
@@ -165,27 +202,14 @@ const user = await userRepository.findOne({
165
202
  }],
166
203
  },
167
204
  });
168
-
169
- // Result:
170
- // {
171
- // id: '123',
172
- // name: 'John',
173
- // posts: [
174
- // {
175
- // id: 'p1',
176
- // title: 'First Post',
177
- // comments: [
178
- // { id: 'c1', text: 'Great post!' },
179
- // { id: 'c2', text: 'Thanks for sharing' }
180
- // ]
181
- // }
182
- // ]
183
- // }
184
205
  ```
185
206
 
186
- ### Many-to-many through junction
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).
209
+
210
+ ### Many-to-many through a junction table
187
211
 
188
- This is the pattern `examples/vert` uses for `Product` <-> `SaleChannel` through the `SaleChannelProduct` junction table:
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`.
189
213
 
190
214
  ```typescript
191
215
  // Product -> SaleChannelProduct (junction) -> SaleChannel
@@ -204,158 +228,44 @@ const product = await productRepository.findOne({
204
228
  // id: 'prod1',
205
229
  // name: 'Widget',
206
230
  // saleChannelProducts: [
207
- // {
208
- // productId: 'prod1',
209
- // saleChannelId: 'ch1',
210
- // saleChannel: { id: 'ch1', name: 'Online Store' }
211
- // },
212
- // {
213
- // productId: 'prod1',
214
- // saleChannelId: 'ch2',
215
- // saleChannel: { id: 'ch2', name: 'Retail' }
216
- // }
231
+ // { productId: 'prod1', saleChannelId: 'ch1', saleChannel: { id: 'ch1', name: 'Online Store' } },
232
+ // { productId: 'prod1', saleChannelId: 'ch2', saleChannel: { id: 'ch2', name: 'Retail' } }
217
233
  // ]
218
234
  // }
219
235
  ```
220
236
 
221
- > [!WARNING] Performance
222
- > Each nested `include` adds SQL complexity. **Maximum 2 levels recommended.** For deeper relationships, use multiple queries - see [Performance Tips](#performance-tips).
223
-
224
- ## Defining Relations
225
-
226
- 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.
227
-
228
- ### Relation config type
229
-
230
- ```typescript
231
- type TRelationConfig = {
232
- name: string; // Relation name used in includes
233
- } & (
234
- | {
235
- type: 'one'; // one-to-one or many-to-one
236
- schema: TTableSchemaWithId;
237
- metadata: { fields, references, relationName? }; // Drizzle one() params
238
- }
239
- | {
240
- type: 'many'; // one-to-many
241
- schema: TTableSchemaWithId;
242
- metadata: { relationName? }; // Drizzle many() params
243
- }
244
- );
245
- ```
246
-
247
- `metadata`'s shape is inferred directly from Drizzle's own `one()`/`many()` relation-helper parameter types, not hand-duplicated.
237
+ ### Count relations without fetching them fully
248
238
 
249
- ### In your model
239
+ Fetch only `id` on the related rows to keep the payload small, then count the array client-side:
250
240
 
251
241
  ```typescript
252
- // src/models/user.model.ts
253
- import { model, RelationTypes } from '@venizia/ignis';
254
- import { BaseEntity, TRelationConfig } from '@venizia/ignis/postgres';
255
- import { pgTable, text } from 'drizzle-orm/pg-core';
256
- import { Post } from './post.model';
257
-
258
- @model({ type: 'entity' })
259
- export class User extends BaseEntity<typeof User.schema> {
260
- static override schema = pgTable('User', {
261
- id: text('id').primaryKey(),
262
- name: text('name').notNull(),
263
- email: text('email').notNull(),
264
- });
242
+ const users = await userRepository.find({
243
+ filter: {
244
+ include: [{ relation: 'posts', scope: { fields: ['id'] } }],
245
+ },
246
+ });
265
247
 
266
- static override relations = (): TRelationConfig[] => [
267
- {
268
- name: 'posts',
269
- type: RelationTypes.MANY,
270
- schema: Post.schema,
271
- metadata: { relationName: 'posts' },
272
- },
273
- ];
274
- }
248
+ const usersWithCounts = users.map(user => ({
249
+ ...user,
250
+ postCount: (user as any).posts?.length ?? 0,
251
+ }));
275
252
  ```
276
253
 
277
- The resolver form (`() => [...]`) defers evaluation until all `@model` classes are registered, avoiding circular-import ordering issues between related models.
278
-
279
- ### Relation types
280
-
281
- | Type | Drizzle function | Description | Example |
282
- |------|------------------|-------------|---------|
283
- | `RelationTypes.ONE` (`'one'`) | `one()` | One-to-one or many-to-one | Post has one Author, User has one Profile |
284
- | `RelationTypes.MANY` (`'many'`) | `many()` | One-to-many | User has many Posts |
285
-
286
- > [!NOTE]
287
- > 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: RelationTypes.ONE` with `fields` (local FK) and `references` (remote PK) in `metadata`.
288
-
289
- ### Example: model with both types
290
-
291
- This mirrors `examples/vert`'s `SaleChannelProduct` junction model - one `ONE` relation per foreign key, plus a `MANY` relation elsewhere:
254
+ ### Include conditionally
292
255
 
293
256
  ```typescript
294
- @model({ type: 'entity' })
295
- export class Post extends BaseEntity<typeof Post.schema> {
296
- static override schema = postTable;
297
-
298
- static override relations = (): TRelationConfig[] => [
299
- {
300
- name: 'author',
301
- type: RelationTypes.ONE,
302
- schema: User.schema,
303
- metadata: {
304
- fields: [Post.schema.authorId],
305
- references: [User.schema.id],
306
- },
307
- },
308
- {
309
- name: 'comments',
310
- type: RelationTypes.MANY,
311
- schema: Comment.schema,
312
- metadata: { relationName: 'comments' },
313
- },
314
- ];
315
- }
316
- ```
317
-
318
- ### How configs become Drizzle relations
319
-
320
- - **`MetadataRegistry` resolves the configs.** During schema discovery, it resolves each model's `relations` array and passes it to the `createRelations` helper ([`packages/core/src/connectors/postgres/repositories/dialect/relation.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/connectors/postgres/repositories/dialect/relation.ts)).
321
- - **`createRelations` builds the Drizzle definition.** It produces the actual Drizzle `relations()` definition, registered on the DataSource schema.
322
- - **Application code never calls it.** `createRelations` is internal - you only declare the `relations` resolver on the model.
323
-
324
- ## Auto-Resolution
325
-
326
- - **Resolved from the model, not the constructor.** Relations are automatically resolved from the entity's static `relations` property via `MetadataRegistry` - no need to pass them in the repository constructor.
327
- - **Memoized per schema.** `FilterBuilder.resolveRelations()` reads and caches them in a `WeakMap` when building include queries.
257
+ async function getUser(id: string, includePosts: boolean) {
258
+ const include = includePosts ? [{ relation: 'posts' }] : [];
328
259
 
329
- ```typescript
330
- @repository({ model: User, dataSource: PostgresDataSource })
331
- export class UserRepository extends DefaultCRUDRepository<typeof User.schema> {
332
- // Relations auto-resolved from User.relations!
260
+ return userRepository.findOne({
261
+ filter: { where: { id }, include },
262
+ });
333
263
  }
334
264
  ```
335
265
 
336
- ## Hidden Properties in Relations
337
-
338
- When building include queries, `FilterBuilder.toInclude()` automatically:
339
-
340
- 1. Resolves hidden properties for each related model via `resolveHiddenProperties()`.
341
- 2. Resolves the default filter for each related model via `resolveDefaultFilter()` (unless `shouldSkipDefaultFilter` is set on that inclusion).
342
- 3. Merges the default filter with any user-provided `scope` via `mergeFilter()`.
343
- 4. Excludes hidden columns from the nested query's `columns` selection.
344
-
345
- ```typescript
346
- // User model has hiddenProperties: ['password']
347
- const post = await postRepository.findOne({
348
- filter: { include: [{ relation: 'author' }] },
349
- });
350
-
351
- // post.author will NOT include password - excluded at SQL level
352
- ```
353
-
354
- See [Hidden Properties](./advanced#hidden-properties) for the top-level equivalent.
355
-
356
- ## Type Safety with Generics
266
+ ### Type included results with a generic
357
267
 
358
- For queries with `include`, use generic type overrides for full type safety:
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:
359
269
 
360
270
  ```typescript
361
271
  type UserWithPosts = User & {
@@ -374,7 +284,7 @@ if (user) {
374
284
  }
375
285
  ```
376
286
 
377
- ### Nested relations type
287
+ The same pattern nests for a two-level include:
378
288
 
379
289
  ```typescript
380
290
  type ProductWithChannels = Product & {
@@ -396,86 +306,35 @@ const product = await productRepository.findOne<ProductWithChannels>({
396
306
  product?.saleChannelProducts[0].saleChannel.name; // Fully typed access
397
307
  ```
398
308
 
399
- ## TInclusion Type Reference
400
-
401
- Each element in the `include` array has this shape:
402
-
403
- ```typescript
404
- type TInclusion = {
405
- relation: string; // Name of the relation to include
406
- scope?: TFilter; // Optional nested filter (where, order, limit, fields, include)
407
- shouldSkipDefaultFilter?: boolean; // Skip the related model's default filter
408
- };
409
- ```
410
-
411
- ## Common Patterns
412
-
413
- ### Find all with count of relations
414
-
415
- ```typescript
416
- // Get users with post count
417
- const users = await userRepository.find({
418
- filter: {
419
- include: [{ relation: 'posts', scope: { fields: ['id'] } }], // Only fetch IDs to minimize data
420
- },
421
- });
422
-
423
- const usersWithCounts = users.map(user => ({
424
- ...user,
425
- postCount: (user as any).posts?.length ?? 0,
426
- }));
427
- ```
428
-
429
- ### Conditional include
430
-
431
- ```typescript
432
- async function getUser(id: string, includePosts: boolean) {
433
- const include = includePosts ? [{ relation: 'posts' }] : [];
434
-
435
- return userRepository.findOne({
436
- filter: { where: { id }, include },
437
- });
438
- }
439
- ```
440
-
441
- ## Error Handling
309
+ ## Hidden properties in relations
442
310
 
443
- ### Relation not found
444
-
445
- If you try to include a relation that doesn't exist:
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.
446
312
 
447
313
  ```typescript
448
- // Error: [FilterBuilder][toInclude] Relation NOT FOUND | relation: 'nonExistent'
449
- await userRepository.find({
450
- filter: { include: [{ relation: 'nonExistent' }] },
314
+ // User model has hiddenProperties: ['password']
315
+ const post = await postRepository.findOne({
316
+ filter: { include: [{ relation: 'author' }] },
451
317
  });
452
- ```
453
-
454
- **Fix:** check your model's `relations` definition and ensure the relation name matches.
455
318
 
456
- ### Invalid include format
457
-
458
- ```typescript
459
- // Error: [FilterBuilder][toInclude] Invalid include format | include: ...
319
+ // post.author will NOT include password - excluded at SQL level
460
320
  ```
461
321
 
462
- **Fix:** ensure each include element has a `relation` string property.
463
-
464
- ### Schema key mismatch
322
+ See [Hidden Properties](./advanced#hidden-properties) for the top-level equivalent.
465
323
 
466
- ```
467
- Error: [UserRepository] Schema key mismatch | Entity name 'User' not found
468
- in connector.query | Available keys: [Post, Comment]
469
- ```
324
+ ## Errors
470
325
 
471
- **Fix:** ensure your model's `TABLE_NAME` matches the schema registration - see [Query Interface Validation](./advanced#query-interface-validation).
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) |
472
331
 
473
- ## Performance Tips
332
+ ## Performance tips
474
333
 
475
334
  1. **Limit nesting depth** - max 2 levels recommended.
476
- 2. **Use `fields` in scope** - only fetch needed columns.
335
+ 2. **Use `fields` in scope** - fetch only the columns you need.
477
336
  3. **Use `limit` in scope** - don't fetch unbounded related data.
478
- 4. **Consider separate queries** - for complex data needs, multiple simple queries often outperform one complex nested query.
337
+ 4. **Consider separate queries** - for complex data needs, several simple queries often outperform one deeply nested one.
479
338
  5. **Use `shouldSkipDefaultFilter` sparingly** - only when you explicitly need filtered-out records.
480
339
 
481
340
  ```typescript
@@ -489,10 +348,10 @@ const comments = await commentRepository.find({
489
348
  });
490
349
  ```
491
350
 
492
- ## Quick Reference
351
+ ## Quick reference
493
352
 
494
353
  | Want to... | Code |
495
- |------------|------|
354
+ |---|---|
496
355
  | Include one relation | `include: [{ relation: 'posts' }]` |
497
356
  | Include multiple | `include: [{ relation: 'posts' }, { relation: 'profile' }]` |
498
357
  | Filter included | `include: [{ relation: 'posts', scope: { where: { status: 'active' } } }]` |
@@ -510,3 +369,11 @@ const comments = await commentRepository.find({
510
369
  - [Filter System](/references/base/filter-system/) - every `where` operator, ordering, pagination
511
370
  - [Models - Full Reference](/references/base/models-reference) - `@model`, entity hierarchy, schema enrichers
512
371
  - [Drizzle ORM Relations](https://orm.drizzle.team/docs/rqb#relations) - relation definition guide
372
+
373
+ **Files:**
374
+
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()`)
@@ -10,9 +10,9 @@ Reference for `SoftDeletableRepository` - delete methods set a `deletedAt` times
10
10
 
11
11
  **Files:**
12
12
 
13
- - [`packages/core/src/connectors/postgres/repositories/core/soft-deletable.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/connectors/postgres/repositories/core/soft-deletable.ts) - `SoftDeletableRelationalRepository` - delete/restore overrides, `isStrict` findById
14
- - [`packages/core/src/connectors/postgres/repositories/core/index.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/connectors/postgres/repositories/core/index.ts) - compatibility alias `SoftDeletableRepository`
15
- - [`packages/core/src/connectors/postgres/models/enrichers/tz.enricher.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/connectors/postgres/models/enrichers/tz.enricher.ts) - `generateTzColumnDefs` - adds the `deletedAt` column
13
+ - [`packages/core-server/src/connectors/postgres/repositories/core/soft-deletable.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/connectors/postgres/repositories/core/soft-deletable.ts) - `SoftDeletableRelationalRepository` - delete/restore overrides, `isStrict` findById
14
+ - [`packages/core-server/src/connectors/postgres/repositories/core/index.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/connectors/postgres/repositories/core/index.ts) - compatibility alias `SoftDeletableRepository`
15
+ - [`packages/core-server/src/connectors/postgres/models/enrichers/tz.enricher.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/connectors/postgres/models/enrichers/tz.enricher.ts) - `generateTzColumnDefs` - adds the `deletedAt` column
16
16
 
17
17
  ## Setup
18
18
 
@@ -91,7 +91,7 @@ const result = await repository.deleteById({
91
91
 
92
92
  ### deleteAll
93
93
 
94
- `where` only needs `options.force: true` when it is empty - a non-empty condition never needs it.
94
+ `where` only needs `options.force: true` when `where` is empty - a non-empty condition never needs `force`.
95
95
 
96
96
  ```typescript
97
97
  // Soft delete matching records - non-empty where, no force needed
@@ -132,7 +132,7 @@ const result = await repository.deleteBy({
132
132
  ## Restore Operations
133
133
 
134
134
  - **Restore clears the timestamp.** Restore methods set `deletedAt = null`.
135
- - **Default filter is bypassed automatically.** They internally pass `shouldSkipDefaultFilter: true` so they can find the soft-deleted records the default filter would otherwise hide - you do not need to set this yourself.
135
+ - **Default filter is bypassed automatically.** Restore methods internally pass `shouldSkipDefaultFilter: true`, so they can find the soft-deleted records the default filter would otherwise hide. You don't need to set this yourself.
136
136
 
137
137
  ### restoreById
138
138
 
@@ -189,8 +189,18 @@ const category = await repository.findById({
189
189
  // Throws: [CategoryRepository][findById] Entity with id 123 not found (HTTP 404)
190
190
  ```
191
191
 
192
+ - **`isStrict` is the only thing the override adds.** Every other `findById` option - `retry`, `transaction`, `log`, `shouldSkipDefaultFilter`, `lock` - behaves exactly as on `DefaultCRUDRepository`. So does the `filter` argument.
192
193
  - **Everything else is inherited unchanged.** `find`, `findOne`, `count`, `existsWith` behave exactly as on `DefaultCRUDRepository`.
193
- - **The default filter excludes soft-deleted rows.** `{ deletedAt: null }` is applied automatically - pass `options: { shouldSkipDefaultFilter: true }` to include them.
194
+ - **The default filter excludes soft-deleted rows.** IGNIS applies `{ deletedAt: null }` automatically - pass `options: { shouldSkipDefaultFilter: true }` to include them.
195
+
196
+ You can combine `isStrict` with [read retry](/references/base/repositories/advanced#read-retry-replica-lag). The retry loop runs first, so a strict read waits out replica lag before it throws:
197
+
198
+ ```typescript
199
+ const category = await repository.findById({
200
+ id: '123',
201
+ options: { retry: { maxAttempts: 4 }, isStrict: true },
202
+ });
203
+ ```
194
204
 
195
205
  ## Options Reference
196
206