@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.
- package/content/best-practices/architectural-patterns.md +3 -3
- package/content/best-practices/code-style-standards/naming-conventions.md +1 -1
- package/content/best-practices/contribution-workflow.md +2 -2
- package/content/best-practices/error-handling.md +94 -89
- package/content/best-practices/security-guidelines.md +5 -5
- package/content/extensions/components/api-reference.md +22 -21
- package/content/extensions/components/authentication/api.md +64 -28
- package/content/extensions/components/authentication/errors.md +19 -5
- package/content/extensions/components/authentication/index.md +19 -18
- package/content/extensions/components/authentication/usage.md +55 -30
- package/content/extensions/components/authorization/api.md +351 -84
- package/content/extensions/components/authorization/errors.md +51 -17
- package/content/extensions/components/authorization/getting-started.md +227 -0
- package/content/extensions/components/authorization/index.md +24 -16
- package/content/extensions/components/authorization/usage.md +45 -21
- package/content/extensions/components/health-check.md +15 -9
- package/content/extensions/components/index.md +24 -90
- package/content/extensions/components/mail/api.md +105 -54
- package/content/extensions/components/mail/errors.md +12 -10
- package/content/extensions/components/mail/index.md +32 -13
- package/content/extensions/components/mail/usage.md +26 -18
- package/content/extensions/components/request-tracker.md +18 -14
- package/content/extensions/components/socket-io/api.md +377 -882
- package/content/extensions/components/socket-io/errors.md +49 -51
- package/content/extensions/components/socket-io/index.md +72 -88
- package/content/extensions/components/socket-io/usage.md +107 -117
- package/content/extensions/components/static-asset/api.md +83 -31
- package/content/extensions/components/static-asset/errors.md +18 -7
- package/content/extensions/components/static-asset/index.md +18 -11
- package/content/extensions/components/static-asset/usage.md +11 -6
- package/content/extensions/components/template/index.md +3 -3
- package/content/extensions/components/websocket/api.md +58 -27
- package/content/extensions/components/websocket/errors.md +3 -3
- package/content/extensions/components/websocket/index.md +8 -7
- package/content/extensions/components/websocket/usage.md +21 -8
- package/content/extensions/helpers/cron/index.md +8 -7
- package/content/extensions/helpers/crypto/index.md +16 -8
- package/content/extensions/helpers/crypto/reference.md +96 -24
- package/content/extensions/helpers/env/index.md +14 -10
- package/content/extensions/helpers/error/index.md +99 -23
- package/content/extensions/helpers/index.md +61 -47
- package/content/extensions/helpers/inversion/index.md +23 -6
- package/content/extensions/helpers/inversion/reference.md +30 -22
- package/content/extensions/helpers/kafka/admin.md +3 -2
- package/content/extensions/helpers/kafka/compile-binary.md +69 -44
- package/content/extensions/helpers/kafka/consumer.md +24 -21
- package/content/extensions/helpers/kafka/examples.md +22 -234
- package/content/extensions/helpers/kafka/index.md +30 -62
- package/content/extensions/helpers/kafka/producer.md +31 -26
- package/content/extensions/helpers/kafka/schema-registry.md +19 -14
- package/content/extensions/helpers/logger/hf-logger.md +49 -22
- package/content/extensions/helpers/logger/index.md +37 -12
- package/content/extensions/helpers/logger/pino.md +31 -11
- package/content/extensions/helpers/logger/reference.md +243 -52
- package/content/extensions/helpers/network/api.md +65 -30
- package/content/extensions/helpers/network/index.md +32 -11
- package/content/extensions/helpers/queue/index.md +17 -6
- package/content/extensions/helpers/queue/reference.md +52 -25
- package/content/extensions/helpers/redis/index.md +29 -11
- package/content/extensions/helpers/redis/reference.md +85 -28
- package/content/extensions/helpers/secrets/index.md +82 -12
- package/content/extensions/helpers/socket-io/api.md +40 -21
- package/content/extensions/helpers/socket-io/index.md +26 -10
- package/content/extensions/helpers/storage/api.md +42 -24
- package/content/extensions/helpers/storage/index.md +10 -7
- package/content/extensions/helpers/types/index.md +20 -7
- package/content/extensions/helpers/types/reference.md +53 -14
- package/content/extensions/helpers/uid/index.md +166 -10
- package/content/extensions/helpers/websocket/api.md +52 -13
- package/content/extensions/helpers/websocket/index.md +7 -4
- package/content/extensions/helpers/worker-thread/index.md +9 -5
- package/content/extensions/helpers/worker-thread/reference.md +20 -20
- 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/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 +26 -14
- package/content/guides/core-concepts/persistent/sqlite.md +296 -0
- package/content/guides/core-concepts/persistent/transactions.md +12 -11
- 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 +4 -3
- package/content/guides/migrations/scoped-rbac-migration.md +5 -5
- package/content/guides/migrations/unified-connectors-migration.md +7 -6
- package/content/guides/tutorials/realtime-chat.md +1 -1
- package/content/references/base/application.md +63 -21
- package/content/references/base/components.md +3 -3
- package/content/references/base/connectors.md +14 -14
- package/content/references/base/controllers.md +12 -12
- package/content/references/base/datasources-reference.md +32 -31
- package/content/references/base/datasources.md +6 -6
- package/content/references/base/dependency-injection.md +12 -11
- package/content/references/base/filter-system/application-usage.md +54 -30
- package/content/references/base/filter-system/array-operators.md +24 -46
- package/content/references/base/filter-system/comparison-operators.md +47 -67
- package/content/references/base/filter-system/default-filter.md +59 -53
- package/content/references/base/filter-system/fields-order-pagination.md +92 -146
- package/content/references/base/filter-system/index.md +25 -13
- package/content/references/base/filter-system/json-filtering.md +45 -184
- package/content/references/base/filter-system/list-operators.md +23 -53
- package/content/references/base/filter-system/logical-operators.md +63 -121
- package/content/references/base/filter-system/null-operators.md +34 -104
- package/content/references/base/filter-system/pattern-matching.md +40 -55
- package/content/references/base/filter-system/quick-reference.md +86 -198
- package/content/references/base/filter-system/range-operators.md +18 -46
- package/content/references/base/filter-system/tips.md +6 -6
- package/content/references/base/filter-system/use-cases.md +33 -15
- package/content/references/base/grpc-controllers.md +53 -17
- package/content/references/base/index.md +5 -3
- package/content/references/base/middlewares.md +11 -10
- package/content/references/base/models-reference.md +17 -17
- package/content/references/base/models.md +4 -3
- package/content/references/base/providers.md +8 -8
- package/content/references/base/repositories/advanced.md +224 -326
- package/content/references/base/repositories/index.md +19 -6
- package/content/references/base/repositories/mixins.md +5 -5
- package/content/references/base/repositories/relations.md +160 -293
- package/content/references/base/repositories/soft-deletable.md +16 -6
- package/content/references/base/secrets.md +17 -13
- package/content/references/base/services.md +6 -4
- package/content/references/configuration/environment-variables.md +31 -23
- package/content/references/configuration/index.md +6 -4
- package/content/references/index.md +1 -1
- package/content/references/utilities/duration.md +85 -0
- package/content/references/utilities/index.md +5 -1
- package/content/references/utilities/jsx-reference.md +11 -11
- package/content/references/utilities/jsx.md +2 -2
- package/content/references/utilities/module.md +78 -25
- package/content/references/utilities/request.md +2 -1
- package/content/references/utilities/retry.md +139 -0
- package/content/references/utilities/schema.md +2 -2
- package/content/references/utilities/statuses-reference.md +4 -4
- package/content/references/utilities/statuses.md +5 -5
- 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 +17 -16
|
@@ -6,22 +6,12 @@ difficulty: intermediate
|
|
|
6
6
|
|
|
7
7
|
# Relations & Includes
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
89
|
+
`metadata`'s shape comes straight from Drizzle's own `one()`/`many()` parameter types, not a hand-duplicated one.
|
|
81
90
|
|
|
82
|
-
###
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
141
|
+
## Recipes
|
|
142
|
+
|
|
143
|
+
### Include multiple relations
|
|
107
144
|
|
|
108
145
|
```typescript
|
|
109
|
-
|
|
110
|
-
const user = await userRepository.findOne({
|
|
146
|
+
const post = await postRepository.findOne({
|
|
111
147
|
filter: {
|
|
112
|
-
where: { id: '
|
|
113
|
-
include: [{ relation: '
|
|
148
|
+
where: { id: 'p1' },
|
|
149
|
+
include: [{ relation: 'author' }, { relation: 'comments' }],
|
|
114
150
|
},
|
|
115
151
|
});
|
|
116
152
|
```
|
|
117
153
|
|
|
118
|
-
###
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
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
|
-
|
|
295
|
-
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
// Relations auto-resolved from User.relations!
|
|
260
|
+
return userRepository.findOne({
|
|
261
|
+
filter: { where: { id }, include },
|
|
262
|
+
});
|
|
333
263
|
}
|
|
334
264
|
```
|
|
335
265
|
|
|
336
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
//
|
|
449
|
-
await
|
|
450
|
-
filter: { include: [{ relation: '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
332
|
+
## Performance tips
|
|
474
333
|
|
|
475
334
|
1. **Limit nesting depth** - max 2 levels recommended.
|
|
476
|
-
2. **Use `fields` in scope** - only
|
|
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,
|
|
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
|
|
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
|
|
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.**
|
|
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 }`
|
|
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
|
|