@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.
Files changed (180) hide show
  1. package/README.md +7 -7
  2. package/content/best-practices/api-usage-examples.md +15 -12
  3. package/content/best-practices/architectural-patterns.md +70 -78
  4. package/content/best-practices/architecture-decisions.md +91 -60
  5. package/content/best-practices/code-style-standards/advanced-patterns.md +56 -44
  6. package/content/best-practices/code-style-standards/constants-configuration.md +11 -11
  7. package/content/best-practices/code-style-standards/control-flow.md +5 -2
  8. package/content/best-practices/code-style-standards/documentation.md +13 -13
  9. package/content/best-practices/code-style-standards/function-patterns.md +9 -10
  10. package/content/best-practices/code-style-standards/index.md +1 -1
  11. package/content/best-practices/code-style-standards/naming-conventions.md +10 -8
  12. package/content/best-practices/code-style-standards/route-definitions.md +30 -12
  13. package/content/best-practices/code-style-standards/tooling.md +8 -5
  14. package/content/best-practices/code-style-standards/type-safety.md +13 -12
  15. package/content/best-practices/common-pitfalls.md +56 -37
  16. package/content/best-practices/contribution-workflow.md +13 -14
  17. package/content/best-practices/data-modeling.md +46 -22
  18. package/content/best-practices/deployment-strategies.md +28 -27
  19. package/content/best-practices/error-handling.md +48 -24
  20. package/content/best-practices/index.md +5 -5
  21. package/content/best-practices/performance-optimization.md +40 -31
  22. package/content/best-practices/security-guidelines.md +52 -23
  23. package/content/best-practices/testing-strategies.md +65 -51
  24. package/content/best-practices/troubleshooting-tips.md +24 -24
  25. package/content/extensions/components/{swagger.md → api-reference.md} +40 -31
  26. package/content/extensions/components/authentication/api.md +19 -19
  27. package/content/extensions/components/authentication/errors.md +7 -7
  28. package/content/extensions/components/authentication/index.md +10 -8
  29. package/content/extensions/components/authentication/usage.md +101 -6
  30. package/content/extensions/components/authorization/api.md +45 -25
  31. package/content/extensions/components/authorization/errors.md +6 -6
  32. package/content/extensions/components/authorization/index.md +11 -10
  33. package/content/extensions/components/authorization/usage.md +21 -21
  34. package/content/extensions/components/health-check.md +1 -1
  35. package/content/extensions/components/index.md +5 -5
  36. package/content/extensions/components/mail/errors.md +15 -15
  37. package/content/extensions/components/mail/index.md +1 -2
  38. package/content/extensions/components/mail/usage.md +1 -1
  39. package/content/extensions/components/request-tracker.md +1 -1
  40. package/content/extensions/components/socket-io/api.md +9 -9
  41. package/content/extensions/components/socket-io/errors.md +5 -5
  42. package/content/extensions/components/socket-io/index.md +8 -8
  43. package/content/extensions/components/socket-io/usage.md +1 -1
  44. package/content/extensions/components/static-asset/api.md +17 -4
  45. package/content/extensions/components/static-asset/errors.md +4 -4
  46. package/content/extensions/components/static-asset/index.md +26 -28
  47. package/content/extensions/components/static-asset/usage.md +13 -12
  48. package/content/extensions/components/template/index.md +2 -2
  49. package/content/extensions/components/template/setup-page.md +1 -1
  50. package/content/extensions/components/websocket/api.md +3 -3
  51. package/content/extensions/components/websocket/errors.md +5 -5
  52. package/content/extensions/components/websocket/index.md +5 -5
  53. package/content/extensions/components/websocket/usage.md +3 -3
  54. package/content/extensions/helpers/cron/index.md +2 -2
  55. package/content/extensions/helpers/crypto/index.md +1 -1
  56. package/content/extensions/helpers/env/index.md +27 -12
  57. package/content/extensions/helpers/error/index.md +81 -25
  58. package/content/extensions/helpers/index.md +2 -3
  59. package/content/extensions/helpers/inversion/index.md +15 -7
  60. package/content/extensions/helpers/kafka/compile-binary.md +92 -0
  61. package/content/extensions/helpers/kafka/examples.md +1 -1
  62. package/content/extensions/helpers/kafka/index.md +3 -0
  63. package/content/extensions/helpers/logger/index.md +32 -2
  64. package/content/extensions/helpers/network/index.md +6 -0
  65. package/content/extensions/helpers/queue/index.md +14 -17
  66. package/content/extensions/helpers/redis/index.md +548 -323
  67. package/content/extensions/helpers/socket-io/index.md +14 -10
  68. package/content/extensions/helpers/storage/api.md +44 -8
  69. package/content/extensions/helpers/storage/index.md +43 -7
  70. package/content/extensions/helpers/template/index.md +6 -3
  71. package/content/extensions/helpers/types/index.md +11 -8
  72. package/content/extensions/helpers/websocket/api.md +9 -9
  73. package/content/extensions/helpers/websocket/index.md +7 -7
  74. package/content/extensions/helpers/worker-thread/index.md +2 -2
  75. package/content/extensions/index.md +3 -4
  76. package/content/extensions/src-details/mcp-server.md +18 -24
  77. package/content/guides/core-concepts/application/bootstrapping.md +11 -14
  78. package/content/guides/core-concepts/application/index.md +3 -3
  79. package/content/guides/core-concepts/components.md +19 -10
  80. package/content/guides/core-concepts/dependency-injection.md +6 -3
  81. package/content/guides/core-concepts/grpc-controllers.md +6 -5
  82. package/content/guides/core-concepts/persistent/datasources.md +42 -43
  83. package/content/guides/core-concepts/persistent/index.md +16 -7
  84. package/content/guides/core-concepts/persistent/models.md +24 -20
  85. package/content/guides/core-concepts/persistent/postgres-drivers.md +201 -0
  86. package/content/guides/core-concepts/persistent/repositories.md +40 -23
  87. package/content/guides/core-concepts/persistent/search-meilisearch.md +185 -0
  88. package/content/guides/core-concepts/persistent/search-typesense.md +431 -0
  89. package/content/guides/core-concepts/persistent/transactions.md +61 -25
  90. package/content/guides/core-concepts/rest-controllers.md +12 -9
  91. package/content/guides/core-concepts/services.md +330 -60
  92. package/content/guides/get-started/5-minute-quickstart.md +15 -15
  93. package/content/guides/get-started/philosophy.md +36 -36
  94. package/content/guides/get-started/setup.md +3 -3
  95. package/content/guides/index.md +3 -3
  96. package/content/guides/migrations/redis-helpers-migration.md +177 -0
  97. package/content/guides/migrations/scoped-rbac-migration.md +17 -17
  98. package/content/guides/migrations/unified-connectors-migration.md +113 -0
  99. package/content/guides/reference/glossary.md +19 -12
  100. package/content/guides/reference/mcp-docs-server.md +22 -18
  101. package/content/guides/tutorials/building-a-crud-api.md +37 -44
  102. package/content/guides/tutorials/complete-installation.md +17 -17
  103. package/content/guides/tutorials/ecommerce-api.md +163 -124
  104. package/content/guides/tutorials/realtime-chat.md +181 -135
  105. package/content/guides/tutorials/testing.md +65 -523
  106. package/content/index.md +2 -180
  107. package/content/public/apple-touch-icon.png +0 -0
  108. package/content/public/og-image.png +0 -0
  109. package/content/public/site.webmanifest +11 -0
  110. package/content/references/base/application.md +4 -5
  111. package/content/references/base/bootstrapping.md +18 -5
  112. package/content/references/base/components.md +149 -120
  113. package/content/references/base/connectors.md +178 -0
  114. package/content/references/base/controllers.md +41 -30
  115. package/content/references/base/datasources.md +163 -92
  116. package/content/references/base/dependency-injection.md +34 -22
  117. package/content/references/base/filter-system/application-usage.md +17 -14
  118. package/content/references/base/filter-system/array-operators.md +7 -2
  119. package/content/references/base/filter-system/comparison-operators.md +3 -0
  120. package/content/references/base/filter-system/default-filter.md +89 -71
  121. package/content/references/base/filter-system/fields-order-pagination.md +22 -22
  122. package/content/references/base/filter-system/index.md +6 -3
  123. package/content/references/base/filter-system/json-filtering.md +20 -1
  124. package/content/references/base/filter-system/list-operators.md +1 -1
  125. package/content/references/base/filter-system/logical-operators.md +33 -1
  126. package/content/references/base/filter-system/null-operators.md +30 -1
  127. package/content/references/base/filter-system/quick-reference.md +23 -4
  128. package/content/references/base/filter-system/tips.md +5 -5
  129. package/content/references/base/filter-system/use-cases.md +12 -12
  130. package/content/references/base/grpc-controllers.md +13 -13
  131. package/content/references/base/index.md +24 -12
  132. package/content/references/base/middlewares.md +265 -327
  133. package/content/references/base/models.md +63 -49
  134. package/content/references/base/providers.md +136 -130
  135. package/content/references/base/repositories/advanced.md +59 -58
  136. package/content/references/base/repositories/index.md +115 -91
  137. package/content/references/base/repositories/mixins.md +55 -291
  138. package/content/references/base/repositories/relations.md +54 -64
  139. package/content/references/base/repositories/soft-deletable.md +31 -30
  140. package/content/references/base/services.md +296 -93
  141. package/content/references/configuration/environment-variables.md +49 -31
  142. package/content/references/configuration/index.md +6 -6
  143. package/content/references/index.md +17 -12
  144. package/content/references/quick-reference.md +65 -106
  145. package/content/references/utilities/crypto.md +65 -23
  146. package/content/references/utilities/index.md +3 -3
  147. package/content/references/utilities/jsx.md +6 -4
  148. package/content/references/utilities/module.md +68 -20
  149. package/content/references/utilities/parse.md +4 -14
  150. package/content/references/utilities/promise.md +9 -7
  151. package/content/references/utilities/schema.md +5 -3
  152. package/dist/mcp-server/common/guards.d.ts +8 -0
  153. package/dist/mcp-server/common/guards.d.ts.map +1 -0
  154. package/dist/mcp-server/common/guards.js +14 -0
  155. package/dist/mcp-server/common/guards.js.map +1 -0
  156. package/dist/mcp-server/common/index.d.ts +1 -0
  157. package/dist/mcp-server/common/index.d.ts.map +1 -1
  158. package/dist/mcp-server/common/index.js +1 -0
  159. package/dist/mcp-server/common/index.js.map +1 -1
  160. package/dist/mcp-server/helpers/docs.helper.d.ts.map +1 -1
  161. package/dist/mcp-server/helpers/docs.helper.js +4 -2
  162. package/dist/mcp-server/helpers/docs.helper.js.map +1 -1
  163. package/dist/mcp-server/helpers/github.helper.js +1 -1
  164. package/dist/mcp-server/index.js +7 -2
  165. package/dist/mcp-server/index.js.map +1 -1
  166. package/dist/mcp-server/tools/base.tool.d.ts +6 -2
  167. package/dist/mcp-server/tools/base.tool.d.ts.map +1 -1
  168. package/dist/mcp-server/tools/base.tool.js.map +1 -1
  169. package/dist/mcp-server/tools/docs/search-documents.tool.d.ts +1 -1
  170. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
  171. package/dist/mcp-server/tools/github/search-code.tool.d.ts +1 -1
  172. package/dist/mcp-server/tools/github/search-code.tool.d.ts.map +1 -1
  173. package/dist/mcp-server/tools/github/search-code.tool.js +4 -1
  174. package/dist/mcp-server/tools/github/search-code.tool.js.map +1 -1
  175. package/dist/mcp-server/tools/github/verify-dependencies.tool.d.ts.map +1 -1
  176. package/dist/mcp-server/tools/github/verify-dependencies.tool.js +3 -1
  177. package/dist/mcp-server/tools/github/verify-dependencies.tool.js.map +1 -1
  178. package/package.json +9 -9
  179. package/content/extensions/helpers/testing/index.md +0 -510
  180. 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
- | - DefaultFilterMixin merges default filter |
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/:id` | Query param: `?filter={...}` (for includes) |
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 _productRepo: ProductRepository,
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
- query: {
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._productRepo.find({ filter });
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 _productRepo: ProductRepository,
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._productRepo.find({ filter: enhancedFilter });
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._productRepo.find({ filter: isolatedFilter });
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 repo.find({
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 = repo.buildQuery({ filter: complexFilter });
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 | Empty Value `[]` | SQL Generated | Behavior |
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 userRepo.find({
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` | **Deep merge** (via lodash `merge`) -- user values override matching keys |
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
- ### Where Clause Merging
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
- The `where` clause uses deep merge with user values taking precedence:
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
- // Default filter
113
- { where: { isDeleted: false, status: 'pending' }, limit: 100 }
142
+ // { where: { createdAt: { lte: '2024-12-31' }, tenantId: { inq: ['t1', 't2'] } } }
143
+ ```
114
144
 
115
- // User filter
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
- // Merged result
147
+ ```typescript
119
148
  {
120
149
  where: {
121
- isDeleted: false, // From default (preserved)
122
- status: 'active', // User overrides default
123
- role: 'admin' // From user (added)
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 repo.find({
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 repo.find({
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 repo.find({ filter, options: { shouldSkipDefaultFilter: true } });
206
- await repo.findOne({ filter, options: { shouldSkipDefaultFilter: true } });
207
- await repo.findById({ id, options: { shouldSkipDefaultFilter: true } });
208
- await repo.count({ where, options: { shouldSkipDefaultFilter: true } });
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 repo.updateById({ id, data, options: { shouldSkipDefaultFilter: true } });
212
- await repo.updateAll({ where, data, options: { shouldSkipDefaultFilter: true } });
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 repo.deleteById({ id, options: { shouldSkipDefaultFilter: true } });
216
- await repo.deleteAll({ where, options: { shouldSkipDefaultFilter: true, force: true } });
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 postRepo.find({ filter: {} });
261
+ await postRepository.find({ filter: {} });
247
262
  // WHERE deletedAt IS NULL
248
263
 
249
264
  // Restore a deleted post
250
- await postRepo.updateById({
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 docRepo.find({ filter: { where: { type: 'invoice' } } });
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 docRepo.find({
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 logRepo.find({ filter: {} }); // LIMIT 1000
315
- await logRepo.find({ filter: { limit: 50 } }); // LIMIT 50
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 repo.find({
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 repo.beginTransaction();
379
+ const tx = await repository.beginTransaction();
365
380
 
366
381
  try {
367
382
  // Both transaction and shouldSkipDefaultFilter
368
- await repo.updateAll({
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 | --> | DefaultFilterMixin | --> | Repository Method |
392
- | defaultFilter | | applyDefaultFilter | | find/count/etc |
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
- ### DefaultFilterMixin
417
+ ### PostgresBaseRepository
403
418
 
404
- The `DefaultFilterMixin` provides:
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
- The default filter is resolved from `MetadataRegistry` on first access and cached for subsequent calls.
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 | `repo.hasDefaultFilter()` |
448
- | Get raw default filter | `repo.getDefaultFilter()` |
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) - Mixin architecture
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 repo.find({
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 repo.find({
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 repo.find({
49
+ await repository.find({
50
50
  filter: { order: ['createdAt DESC'] }
51
51
  });
52
52
 
53
53
  // Multiple columns
54
- await repo.find({
54
+ await repository.find({
55
55
  filter: { order: ['status ASC', 'createdAt DESC'] }
56
56
  });
57
57
 
58
58
  // Default direction is ASC
59
- await repo.find({
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 repo.find({
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 repo.find({
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 repo.find({
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 repo.find({
112
+ await repository.find({
113
113
  filter: { limit: 10, skip: 10 }
114
114
  });
115
115
 
116
116
  // Using offset (equivalent to skip)
117
- await repo.find({
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 repo.find({
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`** 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`.
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 default to 200 rows
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 countryRepo.find({ filter: {} }); // LIMIT 200
155
- await countryRepo.find({ filter: { limit: 10 } }); // LIMIT 10 (explicit wins)
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 to fetch more rows, pass an explicit `limit`.
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 repo.find({
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 repo.find({
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 repo.find({
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 repo.find({
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 Ignis filter system - operators, JSON filtering, array operators, default filters, and query patterns.
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 Ignis, start with:
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 Ignis. It provides a structured way to express complex queries without writing raw SQL.
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, Ignis wraps the extraction in a safe CASE expression. This prevents database errors when JSON fields contain mixed types:
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 repo.find({
67
+ const chunkResults = await repository.find({
68
68
  filter: { where: { id: { in: chunk } } }
69
69
  });
70
70
  results.push(...chunkResults);