@venizia/ignis-docs 0.0.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/content/best-practices/api-usage-examples.md +15 -12
- package/content/best-practices/architectural-patterns.md +70 -78
- package/content/best-practices/architecture-decisions.md +91 -60
- package/content/best-practices/code-style-standards/advanced-patterns.md +56 -44
- package/content/best-practices/code-style-standards/constants-configuration.md +11 -11
- package/content/best-practices/code-style-standards/control-flow.md +5 -2
- package/content/best-practices/code-style-standards/documentation.md +13 -13
- package/content/best-practices/code-style-standards/function-patterns.md +9 -10
- package/content/best-practices/code-style-standards/index.md +1 -1
- package/content/best-practices/code-style-standards/naming-conventions.md +10 -8
- package/content/best-practices/code-style-standards/route-definitions.md +30 -12
- package/content/best-practices/code-style-standards/tooling.md +8 -5
- package/content/best-practices/code-style-standards/type-safety.md +13 -12
- package/content/best-practices/common-pitfalls.md +56 -37
- package/content/best-practices/contribution-workflow.md +13 -14
- package/content/best-practices/data-modeling.md +46 -22
- package/content/best-practices/deployment-strategies.md +28 -27
- package/content/best-practices/error-handling.md +48 -24
- package/content/best-practices/index.md +5 -5
- package/content/best-practices/performance-optimization.md +40 -31
- package/content/best-practices/security-guidelines.md +52 -23
- package/content/best-practices/testing-strategies.md +65 -51
- package/content/best-practices/troubleshooting-tips.md +24 -24
- package/content/extensions/components/{swagger.md → api-reference.md} +40 -31
- package/content/extensions/components/authentication/api.md +19 -19
- package/content/extensions/components/authentication/errors.md +7 -7
- package/content/extensions/components/authentication/index.md +10 -8
- package/content/extensions/components/authentication/usage.md +101 -6
- package/content/extensions/components/authorization/api.md +45 -25
- package/content/extensions/components/authorization/errors.md +6 -6
- package/content/extensions/components/authorization/index.md +11 -10
- package/content/extensions/components/authorization/usage.md +21 -21
- package/content/extensions/components/health-check.md +1 -1
- package/content/extensions/components/index.md +5 -5
- package/content/extensions/components/mail/errors.md +15 -15
- package/content/extensions/components/mail/index.md +1 -2
- package/content/extensions/components/mail/usage.md +1 -1
- package/content/extensions/components/request-tracker.md +1 -1
- package/content/extensions/components/socket-io/api.md +9 -9
- package/content/extensions/components/socket-io/errors.md +5 -5
- package/content/extensions/components/socket-io/index.md +8 -8
- package/content/extensions/components/socket-io/usage.md +1 -1
- package/content/extensions/components/static-asset/api.md +17 -4
- package/content/extensions/components/static-asset/errors.md +4 -4
- package/content/extensions/components/static-asset/index.md +26 -28
- package/content/extensions/components/static-asset/usage.md +13 -12
- package/content/extensions/components/template/index.md +2 -2
- package/content/extensions/components/template/setup-page.md +1 -1
- package/content/extensions/components/websocket/api.md +3 -3
- package/content/extensions/components/websocket/errors.md +5 -5
- package/content/extensions/components/websocket/index.md +5 -5
- package/content/extensions/components/websocket/usage.md +3 -3
- package/content/extensions/helpers/cron/index.md +2 -2
- package/content/extensions/helpers/crypto/index.md +1 -1
- package/content/extensions/helpers/env/index.md +27 -12
- package/content/extensions/helpers/error/index.md +81 -25
- package/content/extensions/helpers/index.md +2 -3
- package/content/extensions/helpers/inversion/index.md +15 -7
- package/content/extensions/helpers/kafka/compile-binary.md +92 -0
- package/content/extensions/helpers/kafka/examples.md +1 -1
- package/content/extensions/helpers/kafka/index.md +3 -0
- package/content/extensions/helpers/logger/index.md +32 -2
- package/content/extensions/helpers/network/index.md +6 -0
- package/content/extensions/helpers/queue/index.md +14 -17
- package/content/extensions/helpers/redis/index.md +548 -323
- package/content/extensions/helpers/socket-io/index.md +14 -10
- package/content/extensions/helpers/storage/api.md +44 -8
- package/content/extensions/helpers/storage/index.md +43 -7
- package/content/extensions/helpers/template/index.md +6 -3
- package/content/extensions/helpers/types/index.md +11 -8
- package/content/extensions/helpers/websocket/api.md +9 -9
- package/content/extensions/helpers/websocket/index.md +7 -7
- package/content/extensions/helpers/worker-thread/index.md +2 -2
- package/content/extensions/index.md +3 -4
- package/content/extensions/src-details/mcp-server.md +18 -24
- package/content/guides/core-concepts/application/bootstrapping.md +11 -14
- package/content/guides/core-concepts/application/index.md +3 -3
- package/content/guides/core-concepts/components.md +19 -10
- package/content/guides/core-concepts/dependency-injection.md +6 -3
- package/content/guides/core-concepts/grpc-controllers.md +6 -5
- package/content/guides/core-concepts/persistent/datasources.md +42 -43
- package/content/guides/core-concepts/persistent/index.md +16 -7
- package/content/guides/core-concepts/persistent/models.md +24 -20
- package/content/guides/core-concepts/persistent/postgres-drivers.md +201 -0
- package/content/guides/core-concepts/persistent/repositories.md +40 -23
- package/content/guides/core-concepts/persistent/search-meilisearch.md +185 -0
- package/content/guides/core-concepts/persistent/search-typesense.md +431 -0
- package/content/guides/core-concepts/persistent/transactions.md +61 -25
- package/content/guides/core-concepts/rest-controllers.md +12 -9
- package/content/guides/core-concepts/services.md +330 -60
- package/content/guides/get-started/5-minute-quickstart.md +15 -15
- package/content/guides/get-started/philosophy.md +36 -36
- package/content/guides/get-started/setup.md +3 -3
- package/content/guides/index.md +3 -3
- package/content/guides/migrations/redis-helpers-migration.md +177 -0
- package/content/guides/migrations/scoped-rbac-migration.md +17 -17
- package/content/guides/migrations/unified-connectors-migration.md +113 -0
- package/content/guides/reference/glossary.md +19 -12
- package/content/guides/reference/mcp-docs-server.md +22 -18
- package/content/guides/tutorials/building-a-crud-api.md +37 -44
- package/content/guides/tutorials/complete-installation.md +17 -17
- package/content/guides/tutorials/ecommerce-api.md +163 -124
- package/content/guides/tutorials/realtime-chat.md +181 -135
- package/content/guides/tutorials/testing.md +65 -523
- package/content/index.md +2 -180
- package/content/public/apple-touch-icon.png +0 -0
- package/content/public/og-image.png +0 -0
- package/content/public/site.webmanifest +11 -0
- package/content/references/base/application.md +4 -5
- package/content/references/base/bootstrapping.md +18 -5
- package/content/references/base/components.md +149 -120
- package/content/references/base/connectors.md +178 -0
- package/content/references/base/controllers.md +41 -30
- package/content/references/base/datasources.md +163 -92
- package/content/references/base/dependency-injection.md +34 -22
- package/content/references/base/filter-system/application-usage.md +17 -14
- package/content/references/base/filter-system/array-operators.md +7 -2
- package/content/references/base/filter-system/comparison-operators.md +3 -0
- package/content/references/base/filter-system/default-filter.md +89 -71
- package/content/references/base/filter-system/fields-order-pagination.md +22 -22
- package/content/references/base/filter-system/index.md +6 -3
- package/content/references/base/filter-system/json-filtering.md +20 -1
- package/content/references/base/filter-system/list-operators.md +1 -1
- package/content/references/base/filter-system/logical-operators.md +33 -1
- package/content/references/base/filter-system/null-operators.md +30 -1
- package/content/references/base/filter-system/quick-reference.md +23 -4
- package/content/references/base/filter-system/tips.md +5 -5
- package/content/references/base/filter-system/use-cases.md +12 -12
- package/content/references/base/grpc-controllers.md +13 -13
- package/content/references/base/index.md +24 -12
- package/content/references/base/middlewares.md +265 -327
- package/content/references/base/models.md +63 -49
- package/content/references/base/providers.md +136 -130
- package/content/references/base/repositories/advanced.md +59 -58
- package/content/references/base/repositories/index.md +115 -91
- package/content/references/base/repositories/mixins.md +55 -291
- package/content/references/base/repositories/relations.md +54 -64
- package/content/references/base/repositories/soft-deletable.md +31 -30
- package/content/references/base/services.md +296 -93
- package/content/references/configuration/environment-variables.md +49 -31
- package/content/references/configuration/index.md +6 -6
- package/content/references/index.md +17 -12
- package/content/references/quick-reference.md +65 -106
- package/content/references/utilities/crypto.md +65 -23
- package/content/references/utilities/index.md +3 -3
- package/content/references/utilities/jsx.md +6 -4
- package/content/references/utilities/module.md +68 -20
- package/content/references/utilities/parse.md +4 -14
- package/content/references/utilities/promise.md +9 -7
- package/content/references/utilities/schema.md +5 -3
- package/dist/mcp-server/common/guards.d.ts +8 -0
- package/dist/mcp-server/common/guards.d.ts.map +1 -0
- package/dist/mcp-server/common/guards.js +14 -0
- package/dist/mcp-server/common/guards.js.map +1 -0
- package/dist/mcp-server/common/index.d.ts +1 -0
- package/dist/mcp-server/common/index.d.ts.map +1 -1
- package/dist/mcp-server/common/index.js +1 -0
- package/dist/mcp-server/common/index.js.map +1 -1
- package/dist/mcp-server/helpers/docs.helper.d.ts.map +1 -1
- package/dist/mcp-server/helpers/docs.helper.js +4 -2
- package/dist/mcp-server/helpers/docs.helper.js.map +1 -1
- package/dist/mcp-server/helpers/github.helper.js +1 -1
- package/dist/mcp-server/index.js +7 -2
- package/dist/mcp-server/index.js.map +1 -1
- package/dist/mcp-server/tools/base.tool.d.ts +6 -2
- package/dist/mcp-server/tools/base.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/base.tool.js.map +1 -1
- package/dist/mcp-server/tools/docs/search-documents.tool.d.ts +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
- package/dist/mcp-server/tools/github/search-code.tool.d.ts +1 -1
- package/dist/mcp-server/tools/github/search-code.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/github/search-code.tool.js +4 -1
- package/dist/mcp-server/tools/github/search-code.tool.js.map +1 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.js +3 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.js.map +1 -1
- package/package.json +9 -9
- package/content/extensions/helpers/testing/index.md +0 -510
- package/content/references/base/middleware.md +0 -347
|
@@ -35,7 +35,7 @@ How filters flow through the application layers.
|
|
|
35
35
|
v
|
|
36
36
|
+-----------------------------------------------------------------+
|
|
37
37
|
| Repository Layer |
|
|
38
|
-
| -
|
|
38
|
+
| - applyDefaultFilter() merges the @model default filter |
|
|
39
39
|
| - FilterBuilder transforms Filter -> Drizzle query options |
|
|
40
40
|
| - Executes query via Drizzle ORM |
|
|
41
41
|
| - Returns typed results |
|
|
@@ -68,8 +68,7 @@ const _Controller = ControllerFactory.defineCrudController({
|
|
|
68
68
|
controller: {
|
|
69
69
|
name: 'ProductController',
|
|
70
70
|
basePath: BASE_PATH,
|
|
71
|
-
isStrict: true,
|
|
72
|
-
defaultLimit: 20,
|
|
71
|
+
isStrict: { path: true, requestSchema: true },
|
|
73
72
|
},
|
|
74
73
|
entity: () => Product,
|
|
75
74
|
});
|
|
@@ -95,18 +94,21 @@ export class ProductController extends _Controller {
|
|
|
95
94
|
| Method | Endpoint | Filter Location |
|
|
96
95
|
|--------|----------|-----------------|
|
|
97
96
|
| GET | `/products` | Query param: `?filter={...}` |
|
|
98
|
-
| GET | `/products
|
|
99
|
-
| GET | `/products/one` | Query param: `?filter={...}` |
|
|
97
|
+
| GET | `/products/{id}` | Query param: `?filter={...}` (for includes) |
|
|
98
|
+
| GET | `/products/find-one` | Query param: `?filter={...}` |
|
|
100
99
|
| GET | `/products/count` | Query param: `?where={...}` |
|
|
101
100
|
|
|
102
101
|
### Custom Controller with Manual Filter Handling
|
|
103
102
|
|
|
104
103
|
```typescript
|
|
104
|
+
import { z } from '@hono/zod-openapi';
|
|
105
|
+
import { BaseRestController, controller, FilterSchema, inject, jsonResponse } from '@venizia/ignis';
|
|
106
|
+
|
|
105
107
|
@controller({ path: '/products' })
|
|
106
108
|
export class ProductController extends BaseRestController {
|
|
107
109
|
constructor(
|
|
108
110
|
@inject({ key: 'repositories.ProductRepository' })
|
|
109
|
-
private
|
|
111
|
+
private _productRepository: ProductRepository,
|
|
110
112
|
) {
|
|
111
113
|
super({ scope: 'ProductController', path: '/products' });
|
|
112
114
|
}
|
|
@@ -116,13 +118,14 @@ export class ProductController extends BaseRestController {
|
|
|
116
118
|
configs: {
|
|
117
119
|
path: '/search',
|
|
118
120
|
method: 'get',
|
|
119
|
-
|
|
120
|
-
filter: FilterSchema,
|
|
121
|
+
request: {
|
|
122
|
+
query: z.object({ filter: FilterSchema }),
|
|
121
123
|
},
|
|
124
|
+
responses: jsonResponse({ schema: z.array(z.object({ id: z.string() })) }),
|
|
122
125
|
},
|
|
123
126
|
handler: async (context) => {
|
|
124
127
|
const { filter = {} } = context.req.valid('query');
|
|
125
|
-
const results = await this.
|
|
128
|
+
const results = await this._productRepository.find({ filter });
|
|
126
129
|
return context.json(results);
|
|
127
130
|
},
|
|
128
131
|
});
|
|
@@ -155,7 +158,7 @@ Services can modify filters before passing to repositories:
|
|
|
155
158
|
export class ProductService {
|
|
156
159
|
constructor(
|
|
157
160
|
@inject({ key: 'repositories.ProductRepository' })
|
|
158
|
-
private
|
|
161
|
+
private _productRepository: ProductRepository,
|
|
159
162
|
) {}
|
|
160
163
|
|
|
161
164
|
async findProducts(filter: TFilter<TProductSchema> = {}) {
|
|
@@ -168,7 +171,7 @@ export class ProductService {
|
|
|
168
171
|
},
|
|
169
172
|
};
|
|
170
173
|
|
|
171
|
-
return this.
|
|
174
|
+
return this._productRepository.find({ filter: enhancedFilter });
|
|
172
175
|
}
|
|
173
176
|
|
|
174
177
|
async findProductsForTenant(
|
|
@@ -183,7 +186,7 @@ export class ProductService {
|
|
|
183
186
|
},
|
|
184
187
|
};
|
|
185
188
|
|
|
186
|
-
return this.
|
|
189
|
+
return this._productRepository.find({ filter: isolatedFilter });
|
|
187
190
|
}
|
|
188
191
|
}
|
|
189
192
|
```
|
|
@@ -227,7 +230,7 @@ const response = await axios.get('/api/products', {
|
|
|
227
230
|
|
|
228
231
|
```typescript
|
|
229
232
|
// Enable logging to see generated SQL
|
|
230
|
-
const result = await
|
|
233
|
+
const result = await repository.find({
|
|
231
234
|
filter: complexFilter,
|
|
232
235
|
options: {
|
|
233
236
|
log: { use: true, level: 'debug' },
|
|
@@ -235,6 +238,6 @@ const result = await repo.find({
|
|
|
235
238
|
});
|
|
236
239
|
|
|
237
240
|
// Or use buildQuery to inspect without executing
|
|
238
|
-
const queryOptions =
|
|
241
|
+
const queryOptions = repository.buildQuery({ filter: complexFilter });
|
|
239
242
|
console.log('Generated query options:', queryOptions);
|
|
240
243
|
```
|
|
@@ -86,8 +86,8 @@ Find rows where the arrays share at least one common element.
|
|
|
86
86
|
|
|
87
87
|
## Empty Array Behavior
|
|
88
88
|
|
|
89
|
-
| Operator |
|
|
90
|
-
|
|
89
|
+
| Operator | SQL Generated | Behavior |
|
|
90
|
+
|----------|---------------|----------|
|
|
91
91
|
| `contains: []` | `WHERE true` | Returns **ALL** rows |
|
|
92
92
|
| `containedBy: []` | `WHERE "col" = '{}'` | Returns only rows with **empty arrays** |
|
|
93
93
|
| `overlaps: []` | `WHERE false` | Returns **NO** rows |
|
|
@@ -121,6 +121,11 @@ No casting needed for numeric arrays.
|
|
|
121
121
|
```
|
|
122
122
|
|
|
123
123
|
|
|
124
|
+
## Security: Parameterized Values
|
|
125
|
+
|
|
126
|
+
Every element of `contains`/`containedBy`/`overlaps` is bound as a query parameter -- only the operator token (`@>`/`<@`/`&&`) is raw SQL. See [The Hardening Round](../../../changelogs/2026-07-13-hardening-round) for the prior injection this closed.
|
|
127
|
+
|
|
128
|
+
|
|
124
129
|
## Defining Array Columns
|
|
125
130
|
|
|
126
131
|
In your Drizzle schema:
|
|
@@ -56,6 +56,9 @@ Matches records where field does NOT equal the value. Both `ne` and `neq` are al
|
|
|
56
56
|
// SQL: WHERE "deleted_at" IS NOT NULL
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
+
> [!NOTE]
|
|
60
|
+
> When compared against a **real value** (not `null`), `ne`/`neq` follow SQL three-valued logic: a row whose field is `NULL` never matches `{ field: { neq: value } }`, because `NULL <> value` evaluates to UNKNOWN rather than TRUE. To include NULL rows, add an explicit branch: `{ or: [{ field: { neq: value } }, { field: null }] }`.
|
|
61
|
+
|
|
59
62
|
|
|
60
63
|
## gt - Greater Than
|
|
61
64
|
|
|
@@ -47,7 +47,7 @@ Now all queries automatically include the default filter:
|
|
|
47
47
|
|
|
48
48
|
```typescript
|
|
49
49
|
// Your code
|
|
50
|
-
await
|
|
50
|
+
await userRepository.find({
|
|
51
51
|
filter: { where: { status: 'active' } }
|
|
52
52
|
});
|
|
53
53
|
|
|
@@ -93,41 +93,83 @@ export class User extends BaseEntity<typeof User.schema> {}
|
|
|
93
93
|
|
|
94
94
|
## Merge Behavior
|
|
95
95
|
|
|
96
|
-
When a user provides a filter, it is merged with the default filter using `FilterBuilder.mergeFilter()
|
|
96
|
+
When a user provides a filter, it is merged with the default filter using `FilterBuilder.mergeFilter()`. Non-`where` properties are user-wins; the `where` clause follows a **narrowing** collision law so a default scope can never be widened or dropped.
|
|
97
97
|
|
|
98
98
|
| Property | Merge Strategy |
|
|
99
99
|
|----------|----------------|
|
|
100
|
-
| `where` | **
|
|
100
|
+
| `where` | **Per-key narrowing** -- non-colliding keys carry over; a colliding key is composed so the default condition always survives (see below) |
|
|
101
101
|
| `limit` | User replaces default (if provided) |
|
|
102
102
|
| `offset`/`skip` | User replaces default (if provided) |
|
|
103
103
|
| `order` | User replaces default (if provided) |
|
|
104
104
|
| `fields` | User replaces default (if provided) |
|
|
105
105
|
| `include` | User replaces default (if provided) |
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
A user value of `undefined` **never** overrides a defined default -- a caller cannot blow away a tenant or soft-delete scope by passing `undefined`.
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
### Where Clause Collision Law
|
|
110
|
+
|
|
111
|
+
Within `where`, keys present on only one side pass through untouched. When the **same key** appears in both the default and the user filter, the outcome depends on the shapes:
|
|
112
|
+
|
|
113
|
+
| Default | User | Result |
|
|
114
|
+
|---------|------|--------|
|
|
115
|
+
| scalar | scalar | **User wins** (the soft-delete opt-out -- e.g. `isDeleted: false` becomes `isDeleted: true`) |
|
|
116
|
+
| operator | operator | **AND-composed** into an `and: [...]` group (both conditions enforced) |
|
|
117
|
+
| scalar | operator | **AND-composed** |
|
|
118
|
+
| operator | scalar | **AND-composed** |
|
|
119
|
+
|
|
120
|
+
A colliding `and` key concatenates both conjunct lists (both survive). A colliding `or` key AND-composes the two disjunction groups as separate conjuncts -- the user's `or` cannot swallow the default's `or`. Every AND-composed pair is appended to any existing `and` group.
|
|
121
|
+
|
|
122
|
+
> [!IMPORTANT]
|
|
123
|
+
> Because operator collisions AND-compose rather than replace, a default scope (a `createdAt` floor, a tenant `inq`) can no longer be widened or dropped by a user filter. Only a bare scalar-over-scalar collision is a true override.
|
|
124
|
+
|
|
125
|
+
### Narrowing Example
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
// Default filter: a floor on createdAt plus a tenant scope
|
|
129
|
+
const defaultFilter = {
|
|
130
|
+
where: { createdAt: { gte: '2024-01-01' }, tenantId: { inq: ['t1', 't2'] } },
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// User filter: an upper bound on createdAt
|
|
134
|
+
const userFilter = {
|
|
135
|
+
where: { createdAt: { lte: '2024-12-31' } },
|
|
136
|
+
};
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Before** (old wholesale-replace law) -- the user key replaced the default's `createdAt`, dropping the floor:
|
|
110
140
|
|
|
111
141
|
```typescript
|
|
112
|
-
//
|
|
113
|
-
|
|
142
|
+
// { where: { createdAt: { lte: '2024-12-31' }, tenantId: { inq: ['t1', 't2'] } } }
|
|
143
|
+
```
|
|
114
144
|
|
|
115
|
-
|
|
116
|
-
{ where: { status: 'active', role: 'admin' }, limit: 10 }
|
|
145
|
+
**After** (narrowing law) -- operator over operator is AND-composed, so the floor survives:
|
|
117
146
|
|
|
118
|
-
|
|
147
|
+
```typescript
|
|
119
148
|
{
|
|
120
149
|
where: {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
150
|
+
tenantId: { inq: ['t1', 't2'] },
|
|
151
|
+
and: [
|
|
152
|
+
{ createdAt: { gte: '2024-01-01' } },
|
|
153
|
+
{ createdAt: { lte: '2024-12-31' } },
|
|
154
|
+
],
|
|
124
155
|
},
|
|
125
|
-
limit: 10 // User overrides default
|
|
126
156
|
}
|
|
127
157
|
```
|
|
128
158
|
|
|
159
|
+
### Scalar Override (soft-delete opt-out)
|
|
160
|
+
|
|
161
|
+
A plain scalar on both sides is the one case where the user still wins outright -- this is what lets an admin flip a soft-delete flag:
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
// Default: { where: { isDeleted: false } }
|
|
165
|
+
// User: { where: { isDeleted: true } }
|
|
166
|
+
// Result: { where: { isDeleted: true } }
|
|
167
|
+
```
|
|
168
|
+
|
|
129
169
|
### Complex Where Conditions
|
|
130
170
|
|
|
171
|
+
Keys that do not collide combine with an implicit AND:
|
|
172
|
+
|
|
131
173
|
```typescript
|
|
132
174
|
// Default: soft delete and tenant isolation
|
|
133
175
|
const defaultFilter = {
|
|
@@ -137,7 +179,7 @@ const defaultFilter = {
|
|
|
137
179
|
}
|
|
138
180
|
};
|
|
139
181
|
|
|
140
|
-
// User: OR conditions
|
|
182
|
+
// User: OR conditions (a distinct key, so it carries through)
|
|
141
183
|
const userFilter = {
|
|
142
184
|
where: {
|
|
143
185
|
or: [{ status: 'active' }, { priority: 'high' }]
|
|
@@ -149,33 +191,6 @@ const userFilter = {
|
|
|
149
191
|
// AND (status = 'active' OR priority = 'high')
|
|
150
192
|
```
|
|
151
193
|
|
|
152
|
-
### Operator Object Merging
|
|
153
|
-
|
|
154
|
-
Operator objects are deep merged, allowing range combinations:
|
|
155
|
-
|
|
156
|
-
```typescript
|
|
157
|
-
// Default: created after 2024
|
|
158
|
-
const defaultFilter = {
|
|
159
|
-
where: {
|
|
160
|
-
createdAt: { gte: '2024-01-01' }
|
|
161
|
-
}
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
// User: created before end of 2024
|
|
165
|
-
const userFilter = {
|
|
166
|
-
where: {
|
|
167
|
-
createdAt: { lte: '2024-12-31' }
|
|
168
|
-
}
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
// Result: date range
|
|
172
|
-
{
|
|
173
|
-
where: {
|
|
174
|
-
createdAt: { gte: '2024-01-01', lte: '2024-12-31' }
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
```
|
|
178
|
-
|
|
179
194
|
|
|
180
195
|
## Bypassing Default Filter
|
|
181
196
|
|
|
@@ -183,13 +198,13 @@ Use `shouldSkipDefaultFilter: true` to bypass the default filter:
|
|
|
183
198
|
|
|
184
199
|
```typescript
|
|
185
200
|
// Normal query - default filter applies
|
|
186
|
-
await
|
|
201
|
+
await repository.find({
|
|
187
202
|
filter: { where: { role: 'admin' } }
|
|
188
203
|
});
|
|
189
204
|
// WHERE isDeleted = false AND role = 'admin'
|
|
190
205
|
|
|
191
206
|
// Admin query - bypass default filter
|
|
192
|
-
await
|
|
207
|
+
await repository.find({
|
|
193
208
|
filter: { where: { role: 'admin' } },
|
|
194
209
|
options: { shouldSkipDefaultFilter: true }
|
|
195
210
|
});
|
|
@@ -202,18 +217,18 @@ await repo.find({
|
|
|
202
217
|
|
|
203
218
|
```typescript
|
|
204
219
|
// Read operations
|
|
205
|
-
await
|
|
206
|
-
await
|
|
207
|
-
await
|
|
208
|
-
await
|
|
220
|
+
await repository.find({ filter, options: { shouldSkipDefaultFilter: true } });
|
|
221
|
+
await repository.findOne({ filter, options: { shouldSkipDefaultFilter: true } });
|
|
222
|
+
await repository.findById({ id, options: { shouldSkipDefaultFilter: true } });
|
|
223
|
+
await repository.count({ where, options: { shouldSkipDefaultFilter: true } });
|
|
209
224
|
|
|
210
225
|
// Update operations
|
|
211
|
-
await
|
|
212
|
-
await
|
|
226
|
+
await repository.updateById({ id, data, options: { shouldSkipDefaultFilter: true } });
|
|
227
|
+
await repository.updateAll({ where, data, options: { shouldSkipDefaultFilter: true } });
|
|
213
228
|
|
|
214
229
|
// Delete operations
|
|
215
|
-
await
|
|
216
|
-
await
|
|
230
|
+
await repository.deleteById({ id, options: { shouldSkipDefaultFilter: true } });
|
|
231
|
+
await repository.deleteAll({ where, options: { shouldSkipDefaultFilter: true, force: true } });
|
|
217
232
|
```
|
|
218
233
|
|
|
219
234
|
### Use Cases for Bypassing
|
|
@@ -243,11 +258,11 @@ await repo.deleteAll({ where, options: { shouldSkipDefaultFilter: true, force: t
|
|
|
243
258
|
export class Post extends BaseEntity<typeof Post.schema> {}
|
|
244
259
|
|
|
245
260
|
// All queries exclude deleted posts
|
|
246
|
-
await
|
|
261
|
+
await postRepository.find({ filter: {} });
|
|
247
262
|
// WHERE deletedAt IS NULL
|
|
248
263
|
|
|
249
264
|
// Restore a deleted post
|
|
250
|
-
await
|
|
265
|
+
await postRepository.updateById({
|
|
251
266
|
id: postId,
|
|
252
267
|
data: { deletedAt: null },
|
|
253
268
|
options: { shouldSkipDefaultFilter: true }
|
|
@@ -268,11 +283,11 @@ await postRepo.updateById({
|
|
|
268
283
|
export class Document extends BaseEntity<typeof Document.schema> {}
|
|
269
284
|
|
|
270
285
|
// Queries scoped to tenant
|
|
271
|
-
await
|
|
286
|
+
await documentRepository.find({ filter: { where: { type: 'invoice' } } });
|
|
272
287
|
// WHERE tenantId = 'current-tenant' AND type = 'invoice'
|
|
273
288
|
|
|
274
289
|
// Cross-tenant admin query
|
|
275
|
-
await
|
|
290
|
+
await documentRepository.find({
|
|
276
291
|
filter: { where: { type: 'invoice' } },
|
|
277
292
|
options: { shouldSkipDefaultFilter: true }
|
|
278
293
|
});
|
|
@@ -311,8 +326,8 @@ Use the dedicated `settings.defaultLimit` to raise (or lower) the per-model defa
|
|
|
311
326
|
export class LogEntry extends BaseEntity<typeof LogEntry.schema> {}
|
|
312
327
|
|
|
313
328
|
// User can override limit, but there's always a sensible default
|
|
314
|
-
await
|
|
315
|
-
await
|
|
329
|
+
await logEntryRepository.find({ filter: {} }); // LIMIT 1000
|
|
330
|
+
await logEntryRepository.find({ filter: { limit: 50 } }); // LIMIT 50
|
|
316
331
|
```
|
|
317
332
|
|
|
318
333
|
> [!TIP]
|
|
@@ -324,7 +339,7 @@ await logRepo.find({ filter: { limit: 50 } }); // LIMIT 50
|
|
|
324
339
|
When using `include` to load relations, the default filter of the related model is also applied. You can bypass it per-relation:
|
|
325
340
|
|
|
326
341
|
```typescript
|
|
327
|
-
await
|
|
342
|
+
await repository.find({
|
|
328
343
|
filter: {
|
|
329
344
|
include: [
|
|
330
345
|
// Default filter of related model applies
|
|
@@ -361,11 +376,11 @@ interface IWithTransaction {
|
|
|
361
376
|
This allows combining with transactions:
|
|
362
377
|
|
|
363
378
|
```typescript
|
|
364
|
-
const tx = await
|
|
379
|
+
const tx = await repository.beginTransaction();
|
|
365
380
|
|
|
366
381
|
try {
|
|
367
382
|
// Both transaction and shouldSkipDefaultFilter
|
|
368
|
-
await
|
|
383
|
+
await repository.updateAll({
|
|
369
384
|
where: { status: 'archived' },
|
|
370
385
|
data: { isDeleted: true },
|
|
371
386
|
options: {
|
|
@@ -387,10 +402,10 @@ try {
|
|
|
387
402
|
### Architecture
|
|
388
403
|
|
|
389
404
|
```
|
|
390
|
-
+------------------+
|
|
391
|
-
| Model Settings | --> |
|
|
392
|
-
| defaultFilter | | applyDefaultFilter
|
|
393
|
-
+------------------+
|
|
405
|
+
+------------------+ +----------------------+ +------------------+
|
|
406
|
+
| Model Settings | --> | PostgresBaseRepository | --> | Repository Method |
|
|
407
|
+
| defaultFilter | | applyDefaultFilter() | | find/count/etc |
|
|
408
|
+
+------------------+ +----------------------+ +------------------+
|
|
394
409
|
|
|
|
395
410
|
v
|
|
396
411
|
+------------------+
|
|
@@ -399,9 +414,9 @@ try {
|
|
|
399
414
|
+------------------+
|
|
400
415
|
```
|
|
401
416
|
|
|
402
|
-
###
|
|
417
|
+
### PostgresBaseRepository
|
|
403
418
|
|
|
404
|
-
|
|
419
|
+
`PostgresBaseRepository` (`packages/core/src/connectors/postgres/repositories/core/base.ts`) implements the default-filter behavior directly as protected methods - no mixin is composed onto it:
|
|
405
420
|
|
|
406
421
|
```typescript
|
|
407
422
|
// Check if default filter is configured
|
|
@@ -417,7 +432,10 @@ applyDefaultFilter(opts: {
|
|
|
417
432
|
}): TFilter
|
|
418
433
|
```
|
|
419
434
|
|
|
420
|
-
|
|
435
|
+
`getDefaultFilter()` reads `this.modelSettings?.defaultFilter`, where `modelSettings` is a protected getter on `AbstractRepository` (`src/base/repositories/core/abstract.ts`) resolved from `MetadataRegistry` keyed by the entity's constructor (not by name string) on first access, and cached for subsequent calls.
|
|
436
|
+
|
|
437
|
+
> [!NOTE]
|
|
438
|
+
> An older `DefaultFilterMixin` implemented this same behavior via mixin composition. It is no longer composed onto any repository class - see [Repository Mixins (Legacy)](../repositories/mixins.md) for history.
|
|
421
439
|
|
|
422
440
|
### FilterBuilder.mergeFilter()
|
|
423
441
|
|
|
@@ -444,12 +462,12 @@ const merged = filterBuilder.mergeFilter({
|
|
|
444
462
|
| Bypass default filter | `options: { shouldSkipDefaultFilter: true }` |
|
|
445
463
|
| Bypass for relation | `include: [{ relation: 'x', shouldSkipDefaultFilter: true }]` |
|
|
446
464
|
| Combine with transaction | `options: { transaction: tx, shouldSkipDefaultFilter: true }` |
|
|
447
|
-
| Check if model has default | `
|
|
448
|
-
| Get raw default filter | `
|
|
465
|
+
| Check if model has default | `repository.hasDefaultFilter()` |
|
|
466
|
+
| Get raw default filter | `repository.getDefaultFilter()` |
|
|
449
467
|
|
|
450
468
|
|
|
451
469
|
## Next Steps
|
|
452
470
|
|
|
453
471
|
- [Filter System Overview](./index.md) - Filter structure and operators
|
|
454
|
-
- [Repository Mixins](../repositories/mixins.md) -
|
|
472
|
+
- [Repository Mixins (Legacy)](../repositories/mixins.md) - Historical mixin architecture
|
|
455
473
|
- [Advanced Features](../repositories/advanced.md) - Transactions, hidden properties
|
|
@@ -16,7 +16,7 @@ Control which fields are returned using `fields`:
|
|
|
16
16
|
### Array Format (Recommended)
|
|
17
17
|
|
|
18
18
|
```typescript
|
|
19
|
-
await
|
|
19
|
+
await repository.find({
|
|
20
20
|
filter: {
|
|
21
21
|
where: { status: 'active' },
|
|
22
22
|
fields: ['id', 'email', 'name']
|
|
@@ -29,7 +29,7 @@ await repo.find({
|
|
|
29
29
|
|
|
30
30
|
```typescript
|
|
31
31
|
// Include specific fields (only keys with `true` are selected)
|
|
32
|
-
await
|
|
32
|
+
await repository.find({
|
|
33
33
|
filter: {
|
|
34
34
|
fields: { id: true, email: true, name: true }
|
|
35
35
|
}
|
|
@@ -46,17 +46,17 @@ await repo.find({
|
|
|
46
46
|
|
|
47
47
|
```typescript
|
|
48
48
|
// Single column, descending
|
|
49
|
-
await
|
|
49
|
+
await repository.find({
|
|
50
50
|
filter: { order: ['createdAt DESC'] }
|
|
51
51
|
});
|
|
52
52
|
|
|
53
53
|
// Multiple columns
|
|
54
|
-
await
|
|
54
|
+
await repository.find({
|
|
55
55
|
filter: { order: ['status ASC', 'createdAt DESC'] }
|
|
56
56
|
});
|
|
57
57
|
|
|
58
58
|
// Default direction is ASC
|
|
59
|
-
await
|
|
59
|
+
await repository.find({
|
|
60
60
|
filter: { order: ['name'] } // Same as 'name ASC'
|
|
61
61
|
});
|
|
62
62
|
```
|
|
@@ -74,12 +74,12 @@ Error: Invalid direction: 'RANDOM' | Expected: 'ASC' or 'DESC'
|
|
|
74
74
|
Order by nested fields in JSON columns:
|
|
75
75
|
|
|
76
76
|
```typescript
|
|
77
|
-
await
|
|
77
|
+
await repository.find({
|
|
78
78
|
filter: { order: ['metadata.priority DESC'] }
|
|
79
79
|
});
|
|
80
80
|
// SQL: ORDER BY "metadata" #> '{priority}' DESC
|
|
81
81
|
|
|
82
|
-
await
|
|
82
|
+
await repository.find({
|
|
83
83
|
filter: { order: ['settings.display.theme ASC'] }
|
|
84
84
|
});
|
|
85
85
|
```
|
|
@@ -104,24 +104,24 @@ Both `skip` and `offset` are supported as aliases -- they both map to the SQL `O
|
|
|
104
104
|
|
|
105
105
|
```typescript
|
|
106
106
|
// First 10 results (default limit is 10)
|
|
107
|
-
await
|
|
107
|
+
await repository.find({
|
|
108
108
|
filter: { limit: 10 }
|
|
109
109
|
});
|
|
110
110
|
|
|
111
111
|
// Page 2 (skip first 10, get next 10)
|
|
112
|
-
await
|
|
112
|
+
await repository.find({
|
|
113
113
|
filter: { limit: 10, skip: 10 }
|
|
114
114
|
});
|
|
115
115
|
|
|
116
116
|
// Using offset (equivalent to skip)
|
|
117
|
-
await
|
|
117
|
+
await repository.find({
|
|
118
118
|
filter: { limit: 10, offset: 10 }
|
|
119
119
|
});
|
|
120
120
|
|
|
121
121
|
// Page N formula: skip = (page - 1) * limit
|
|
122
122
|
const page = 3;
|
|
123
123
|
const pageSize = 20;
|
|
124
|
-
await
|
|
124
|
+
await repository.find({
|
|
125
125
|
filter: {
|
|
126
126
|
limit: pageSize,
|
|
127
127
|
skip: (page - 1) * pageSize
|
|
@@ -140,23 +140,23 @@ When a query omits `limit`, the repository resolves one with this precedence:
|
|
|
140
140
|
query.limit ?? model settings.defaultLimit ?? DEFAULT_LIMIT (10)
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
-
- **`query.limit`**
|
|
144
|
-
- **`settings.defaultLimit`**
|
|
145
|
-
- **`DEFAULT_LIMIT`**
|
|
143
|
+
- **`query.limit`** - an explicit `limit` in the filter always wins.
|
|
144
|
+
- **`settings.defaultLimit`** - a per-model default set on the `@model` decorator. Must be a positive integer (validated at decoration time). Applies to top-level `find()` and to every to-many relation (using the related model's own `defaultLimit`).
|
|
145
|
+
- **`DEFAULT_LIMIT`** - the global fallback, `10`.
|
|
146
146
|
|
|
147
147
|
```typescript
|
|
148
148
|
@model({
|
|
149
149
|
type: 'entity',
|
|
150
|
-
settings: { defaultLimit: 200 }, // Small lookup table
|
|
150
|
+
settings: { defaultLimit: 200 }, // Small lookup table - default to 200 rows
|
|
151
151
|
})
|
|
152
152
|
export class Country extends BaseEntity<typeof Country.schema> {}
|
|
153
153
|
|
|
154
|
-
await
|
|
155
|
-
await
|
|
154
|
+
await countryRepository.find({ filter: {} }); // LIMIT 200
|
|
155
|
+
await countryRepository.find({ filter: { limit: 10 } }); // LIMIT 10 (explicit wins)
|
|
156
156
|
```
|
|
157
157
|
|
|
158
158
|
> [!NOTE]
|
|
159
|
-
> `defaultLimit` is independent of `defaultFilter`: passing `shouldSkipDefaultFilter` to bypass the default `where` clause does **not** drop the default limit. There is no "unbounded" sentinel
|
|
159
|
+
> `defaultLimit` is independent of `defaultFilter`: passing `shouldSkipDefaultFilter` to bypass the default `where` clause does **not** drop the default limit. There is no "unbounded" sentinel - to fetch more rows, pass an explicit `limit`.
|
|
160
160
|
|
|
161
161
|
### Pagination Helper
|
|
162
162
|
|
|
@@ -184,7 +184,7 @@ When building paginated APIs, you often need to return the total count alongside
|
|
|
184
184
|
### Basic Usage
|
|
185
185
|
|
|
186
186
|
```typescript
|
|
187
|
-
const result = await
|
|
187
|
+
const result = await repository.find({
|
|
188
188
|
filter: { limit: 10, skip: 20 },
|
|
189
189
|
options: { shouldQueryRange: true }
|
|
190
190
|
});
|
|
@@ -205,7 +205,7 @@ const result = await repo.find({
|
|
|
205
205
|
Use the range information to set standard HTTP headers:
|
|
206
206
|
|
|
207
207
|
```typescript
|
|
208
|
-
const { data, range } = await
|
|
208
|
+
const { data, range } = await repository.find({
|
|
209
209
|
filter: { limit: 10, skip: 20, where: { status: 'active' } },
|
|
210
210
|
options: { shouldQueryRange: true }
|
|
211
211
|
});
|
|
@@ -246,7 +246,7 @@ When `shouldQueryRange: true`, the repository executes the data query and count
|
|
|
246
246
|
## Combined Example
|
|
247
247
|
|
|
248
248
|
```typescript
|
|
249
|
-
await
|
|
249
|
+
await repository.find({
|
|
250
250
|
filter: {
|
|
251
251
|
where: { status: 'active' },
|
|
252
252
|
fields: ['id', 'name', 'price', 'createdAt'],
|
|
@@ -260,7 +260,7 @@ await repo.find({
|
|
|
260
260
|
### With Range Information
|
|
261
261
|
|
|
262
262
|
```typescript
|
|
263
|
-
const { data, range } = await
|
|
263
|
+
const { data, range } = await repository.find({
|
|
264
264
|
filter: {
|
|
265
265
|
where: { status: 'active' },
|
|
266
266
|
fields: ['id', 'name', 'price', 'createdAt'],
|
|
@@ -6,10 +6,10 @@ difficulty: intermediate
|
|
|
6
6
|
|
|
7
7
|
# Filter System
|
|
8
8
|
|
|
9
|
-
Complete reference for the
|
|
9
|
+
Complete reference for the IGNIS filter system - operators, JSON filtering, array operators, default filters, and query patterns.
|
|
10
10
|
|
|
11
11
|
> [!NOTE]
|
|
12
|
-
> If you're new to
|
|
12
|
+
> If you're new to IGNIS, start with:
|
|
13
13
|
> - [5-Minute Quickstart](/guides/get-started/5-minute-quickstart) - Get up and running
|
|
14
14
|
> - [Building a CRUD API](/guides/tutorials/building-a-crud-api) - Learn the basics
|
|
15
15
|
> - [Repositories](/references/base/repositories/) - Repository overview
|
|
@@ -45,7 +45,7 @@ Before reading this document, you should understand:
|
|
|
45
45
|
|
|
46
46
|
## Filter Structure
|
|
47
47
|
|
|
48
|
-
The `TFilter<T>` object is the core mechanism for querying data in
|
|
48
|
+
The `TFilter<T>` object is the core mechanism for querying data in IGNIS. It provides a structured way to express complex queries without writing raw SQL.
|
|
49
49
|
|
|
50
50
|
```typescript
|
|
51
51
|
type TFilter<T> = {
|
|
@@ -105,6 +105,9 @@ const filter = {
|
|
|
105
105
|
| Less or equal | `{ field: { lte: value } }` |
|
|
106
106
|
| Is null | `{ field: null }` or `{ field: { is: null } }` |
|
|
107
107
|
| Is not null | `{ field: { isn: null } }` or `{ field: { ne: null } }` |
|
|
108
|
+
| Field present (`IS NOT NULL`) | `{ field: { exists: true } }` (or `{ field: { notExists: false } }`) |
|
|
109
|
+
| Field missing/null (`IS NULL`) | `{ field: { exists: false } }` or `{ field: { notExists: true } }` |
|
|
110
|
+
| Negate a condition | `{ field: { not: value } }` (negates `eq`) or `{ field: { not: { gt: 10 } } }` |
|
|
108
111
|
| In list | `{ field: { in: [a, b, c] } }` or `{ field: { inq: [a, b, c] } }` |
|
|
109
112
|
| Not in list | `{ field: { nin: [a, b, c] } }` |
|
|
110
113
|
| Range | `{ field: { between: [min, max] } }` |
|
|
@@ -82,7 +82,7 @@ All standard operators work with JSON paths:
|
|
|
82
82
|
|
|
83
83
|
## Safe Numeric Casting
|
|
84
84
|
|
|
85
|
-
When a numeric comparison operator (`gt`, `gte`, `lt`, `lte`, `between`, `notBetween`) is used with a JSON path,
|
|
85
|
+
When a numeric comparison operator (`gt`, `gte`, `lt`, `lte`, `between`, `notBetween`) is used with a JSON path, IGNIS wraps the extraction in a safe CASE expression. This prevents database errors when JSON fields contain mixed types:
|
|
86
86
|
|
|
87
87
|
```typescript
|
|
88
88
|
// Data in database:
|
|
@@ -114,6 +114,25 @@ When a numeric comparison operator (`gt`, `gte`, `lt`, `lte`, `between`, `notBet
|
|
|
114
114
|
|
|
115
115
|
Non-numeric operators (`eq`, `ne`, `like`, `ilike`, `in`, etc.) use text comparison via `#>>` without numeric casting.
|
|
116
116
|
|
|
117
|
+
The cast is decided **per operator**, not once for the whole operator object -- a mixed object casts only the operators that need it:
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
// Mixed operator object: gte casts, like does not
|
|
121
|
+
{ where: { 'metadata.score': { gte: 1, like: '%a%' } } }
|
|
122
|
+
// SQL: CASE WHEN ("metadata" #>> '{score}') ~ '^-?[0-9]+(\.[0-9]+)?$'
|
|
123
|
+
// THEN ("metadata" #>> '{score}')::numeric ELSE NULL END >= 1
|
|
124
|
+
// AND "metadata" #>> '{score}' LIKE '%a%'
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`not` recurses into whatever it wraps, so a numeric operator nested under `not` still gets cast:
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
// not wrapping an operator - the nested gt still gets the numeric cast
|
|
131
|
+
{ where: { 'metadata.score': { not: { gt: 50 } } } }
|
|
132
|
+
// SQL: NOT (CASE WHEN ("metadata" #>> '{score}') ~ '^-?[0-9]+(\.[0-9]+)?$'
|
|
133
|
+
// THEN ("metadata" #>> '{score}')::numeric ELSE NULL END > 50)
|
|
134
|
+
```
|
|
135
|
+
|
|
117
136
|
|
|
118
137
|
## Numeric Value Equality
|
|
119
138
|
|
|
@@ -64,7 +64,7 @@ const chunkSize = 500;
|
|
|
64
64
|
const results = [];
|
|
65
65
|
for (let i = 0; i < allIds.length; i += chunkSize) {
|
|
66
66
|
const chunk = allIds.slice(i, i + chunkSize);
|
|
67
|
-
const chunkResults = await
|
|
67
|
+
const chunkResults = await repository.find({
|
|
68
68
|
filter: { where: { id: { in: chunk } } }
|
|
69
69
|
});
|
|
70
70
|
results.push(...chunkResults);
|