@venizia/ignis-docs 0.2.0 → 0.2.1-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/README.md +14 -14
  2. package/content/best-practices/api-usage-examples.md +39 -19
  3. package/content/best-practices/architectural-patterns.md +24 -13
  4. package/content/best-practices/architecture-decisions.md +29 -16
  5. package/content/best-practices/code-style-standards/advanced-patterns.md +43 -31
  6. package/content/best-practices/code-style-standards/constants-configuration.md +19 -4
  7. package/content/best-practices/code-style-standards/control-flow.md +33 -1
  8. package/content/best-practices/code-style-standards/documentation.md +28 -8
  9. package/content/best-practices/code-style-standards/function-patterns.md +15 -4
  10. package/content/best-practices/code-style-standards/index.md +7 -2
  11. package/content/best-practices/code-style-standards/naming-conventions.md +27 -3
  12. package/content/best-practices/code-style-standards/route-definitions.md +9 -5
  13. package/content/best-practices/code-style-standards/tooling.md +14 -1
  14. package/content/best-practices/code-style-standards/type-safety.md +39 -6
  15. package/content/best-practices/common-pitfalls.md +59 -35
  16. package/content/best-practices/contribution-workflow.md +8 -4
  17. package/content/best-practices/data-modeling.md +78 -62
  18. package/content/best-practices/deployment-strategies.md +56 -19
  19. package/content/best-practices/error-handling.md +247 -153
  20. package/content/best-practices/index.md +6 -0
  21. package/content/best-practices/performance-optimization.md +26 -13
  22. package/content/best-practices/security-guidelines.md +35 -9
  23. package/content/best-practices/testing-strategies.md +7 -2
  24. package/content/best-practices/troubleshooting-tips.md +27 -17
  25. package/content/extensions/components/api-reference.md +109 -323
  26. package/content/extensions/components/authentication/api.md +489 -602
  27. package/content/extensions/components/authentication/errors.md +135 -498
  28. package/content/extensions/components/authentication/index.md +89 -801
  29. package/content/extensions/components/authentication/usage.md +231 -955
  30. package/content/extensions/components/authorization/api.md +991 -644
  31. package/content/extensions/components/authorization/errors.md +204 -208
  32. package/content/extensions/components/authorization/getting-started.md +227 -0
  33. package/content/extensions/components/authorization/index.md +88 -795
  34. package/content/extensions/components/authorization/usage.md +219 -528
  35. package/content/extensions/components/health-check.md +77 -243
  36. package/content/extensions/components/index.md +24 -90
  37. package/content/extensions/components/mail/api.md +553 -285
  38. package/content/extensions/components/mail/errors.md +76 -62
  39. package/content/extensions/components/mail/index.md +111 -463
  40. package/content/extensions/components/mail/usage.md +139 -173
  41. package/content/extensions/components/request-tracker.md +70 -173
  42. package/content/extensions/components/socket-io/api.md +462 -785
  43. package/content/extensions/components/socket-io/errors.md +49 -51
  44. package/content/extensions/components/socket-io/index.md +69 -372
  45. package/content/extensions/components/socket-io/usage.md +212 -105
  46. package/content/extensions/components/static-asset/api.md +461 -141
  47. package/content/extensions/components/static-asset/errors.md +121 -53
  48. package/content/extensions/components/static-asset/index.md +84 -606
  49. package/content/extensions/components/static-asset/usage.md +184 -299
  50. package/content/extensions/components/template/index.md +3 -3
  51. package/content/extensions/components/websocket/api.md +311 -404
  52. package/content/extensions/components/websocket/errors.md +47 -56
  53. package/content/extensions/components/websocket/index.md +75 -407
  54. package/content/extensions/components/websocket/usage.md +120 -338
  55. package/content/extensions/helpers/cron/index.md +52 -160
  56. package/content/extensions/helpers/crypto/index.md +67 -480
  57. package/content/extensions/helpers/crypto/reference.md +528 -0
  58. package/content/extensions/helpers/env/index.md +64 -178
  59. package/content/extensions/helpers/error/index.md +286 -196
  60. package/content/extensions/helpers/index.md +61 -47
  61. package/content/extensions/helpers/inversion/index.md +76 -550
  62. package/content/extensions/helpers/inversion/reference.md +530 -0
  63. package/content/extensions/helpers/kafka/admin.md +23 -3
  64. package/content/extensions/helpers/kafka/compile-binary.md +83 -52
  65. package/content/extensions/helpers/kafka/consumer.md +65 -28
  66. package/content/extensions/helpers/kafka/examples.md +46 -253
  67. package/content/extensions/helpers/kafka/index.md +55 -617
  68. package/content/extensions/helpers/kafka/producer.md +156 -22
  69. package/content/extensions/helpers/kafka/schema-registry.md +59 -79
  70. package/content/extensions/helpers/logger/hf-logger.md +220 -0
  71. package/content/extensions/helpers/logger/index.md +78 -552
  72. package/content/extensions/helpers/logger/pino.md +105 -0
  73. package/content/extensions/helpers/logger/reference.md +937 -0
  74. package/content/extensions/helpers/network/api.md +276 -195
  75. package/content/extensions/helpers/network/index.md +87 -524
  76. package/content/extensions/helpers/queue/index.md +80 -897
  77. package/content/extensions/helpers/queue/reference.md +494 -0
  78. package/content/extensions/helpers/redis/index.md +86 -640
  79. package/content/extensions/helpers/redis/reference.md +784 -0
  80. package/content/extensions/helpers/secrets/index.md +136 -0
  81. package/content/extensions/helpers/socket-io/api.md +325 -204
  82. package/content/extensions/helpers/socket-io/index.md +79 -429
  83. package/content/extensions/helpers/storage/api.md +584 -464
  84. package/content/extensions/helpers/storage/index.md +80 -573
  85. package/content/extensions/helpers/types/index.md +75 -495
  86. package/content/extensions/helpers/types/reference.md +689 -0
  87. package/content/extensions/helpers/uid/index.md +176 -189
  88. package/content/extensions/helpers/websocket/api.md +366 -214
  89. package/content/extensions/helpers/websocket/index.md +68 -503
  90. package/content/extensions/helpers/worker-thread/index.md +62 -396
  91. package/content/extensions/helpers/worker-thread/reference.md +428 -0
  92. package/content/extensions/index.md +38 -39
  93. package/content/extensions/src-details/mcp-server.md +96 -548
  94. package/content/guides/core-concepts/persistent/datasources.md +2 -2
  95. package/content/guides/core-concepts/persistent/index.md +5 -1
  96. package/content/guides/core-concepts/persistent/models.md +1 -1
  97. package/content/guides/core-concepts/persistent/pglite.md +218 -0
  98. package/content/guides/core-concepts/persistent/postgres-drivers.md +21 -15
  99. package/content/guides/core-concepts/persistent/search-meilisearch.md +22 -12
  100. package/content/guides/core-concepts/persistent/search-typesense.md +28 -16
  101. package/content/guides/core-concepts/persistent/sqlite.md +296 -0
  102. package/content/guides/core-concepts/persistent/transactions.md +12 -11
  103. package/content/guides/core-concepts/secrets-vault.md +177 -0
  104. package/content/guides/core-concepts/services.md +1 -1
  105. package/content/guides/get-started/5-minute-quickstart.md +59 -206
  106. package/content/guides/get-started/philosophy.md +135 -670
  107. package/content/guides/get-started/setup.md +53 -74
  108. package/content/guides/migrations/redis-helpers-migration.md +5 -4
  109. package/content/guides/migrations/scoped-rbac-migration.md +5 -5
  110. package/content/guides/migrations/unified-connectors-migration.md +8 -7
  111. package/content/guides/tutorials/ecommerce-api.md +3 -8
  112. package/content/guides/tutorials/realtime-chat.md +1 -1
  113. package/content/references/base/application.md +64 -22
  114. package/content/references/base/components.md +3 -3
  115. package/content/references/base/connectors.md +79 -136
  116. package/content/references/base/controllers.md +12 -12
  117. package/content/references/base/datasources-reference.md +600 -0
  118. package/content/references/base/datasources.md +84 -444
  119. package/content/references/base/dependency-injection.md +25 -46
  120. package/content/references/base/filter-system/application-usage.md +95 -123
  121. package/content/references/base/filter-system/array-operators.md +33 -43
  122. package/content/references/base/filter-system/comparison-operators.md +55 -63
  123. package/content/references/base/filter-system/default-filter.md +144 -350
  124. package/content/references/base/filter-system/fields-order-pagination.md +116 -148
  125. package/content/references/base/filter-system/index.md +114 -253
  126. package/content/references/base/filter-system/json-filtering.md +52 -181
  127. package/content/references/base/filter-system/list-operators.md +28 -44
  128. package/content/references/base/filter-system/logical-operators.md +69 -114
  129. package/content/references/base/filter-system/null-operators.md +41 -98
  130. package/content/references/base/filter-system/pattern-matching.md +48 -51
  131. package/content/references/base/filter-system/quick-reference.md +93 -196
  132. package/content/references/base/filter-system/range-operators.md +22 -38
  133. package/content/references/base/filter-system/tips.md +70 -133
  134. package/content/references/base/filter-system/use-cases.md +173 -232
  135. package/content/references/base/grpc-controllers.md +53 -17
  136. package/content/references/base/index.md +5 -3
  137. package/content/references/base/middlewares.md +43 -28
  138. package/content/references/base/models-reference.md +886 -0
  139. package/content/references/base/models.md +81 -1452
  140. package/content/references/base/providers.md +8 -8
  141. package/content/references/base/repositories/advanced.md +281 -419
  142. package/content/references/base/repositories/index.md +84 -644
  143. package/content/references/base/repositories/mixins.md +25 -21
  144. package/content/references/base/repositories/relations.md +194 -375
  145. package/content/references/base/repositories/soft-deletable.md +67 -55
  146. package/content/references/base/secrets.md +267 -0
  147. package/content/references/base/services.md +8 -6
  148. package/content/references/configuration/environment-variables.md +73 -21
  149. package/content/references/configuration/index.md +51 -31
  150. package/content/references/index.md +1 -1
  151. package/content/references/quick-reference.md +3 -16
  152. package/content/references/utilities/crypto.md +35 -76
  153. package/content/references/utilities/date.md +33 -73
  154. package/content/references/utilities/duration.md +85 -0
  155. package/content/references/utilities/index.md +6 -2
  156. package/content/references/utilities/jsx-reference.md +298 -0
  157. package/content/references/utilities/jsx.md +82 -525
  158. package/content/references/utilities/module.md +81 -61
  159. package/content/references/utilities/parse.md +34 -64
  160. package/content/references/utilities/performance.md +33 -58
  161. package/content/references/utilities/promise.md +28 -62
  162. package/content/references/utilities/request.md +58 -218
  163. package/content/references/utilities/retry.md +139 -0
  164. package/content/references/utilities/schema.md +43 -137
  165. package/content/references/utilities/statuses-reference.md +361 -0
  166. package/content/references/utilities/statuses.md +63 -667
  167. package/dist/mcp-server/index.js +0 -0
  168. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
  169. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts.map +1 -1
  170. package/dist/mcp-server/tools/github/list-project-files.tool.js +3 -3
  171. package/dist/mcp-server/tools/github/search-code.tool.js +1 -1
  172. package/dist/mcp-server/tools/github/verify-dependencies.tool.js +1 -1
  173. package/dist/mcp-server/tools/github/view-source-file.tool.js +1 -1
  174. package/package.json +24 -23
@@ -9,13 +9,13 @@ difficulty: advanced
9
9
  Technical reference for the DI system in IGNIS - managing resource lifecycles and dependency resolution.
10
10
 
11
11
  **Files:**
12
- - `packages/inversion/src/container.ts` - Base `Container` and `Binding` classes
13
- - `packages/inversion/src/registry.ts` - Base `MetadataRegistry`
14
- - `packages/inversion/src/metadata/injectors.ts` - Base `@inject` and `@injectable` decorators
12
+ - `packages/inversion/src/modules/container/index.ts` - Base `Container` and `Binding` classes
13
+ - `packages/inversion/src/modules/registry/index.ts` - Base `MetadataRegistry`
14
+ - `packages/inversion/src/modules/metadata/injectors.ts` - Base `@inject` decorator
15
15
  - `packages/inversion/src/common/types.ts` - `BindingScopes`, `BindingValueTypes`, `BindingKeys`, `IProvider`
16
- - `packages/core/src/helpers/inversion/container.ts` - Extended `Container` with `ApplicationLogger`
17
- - `packages/core/src/helpers/inversion/registry.ts` - Extended `MetadataRegistry` (singleton, with model/repository/datasource mixins)
18
- - `packages/core/src/base/metadata/injectors.ts` - Core `@inject` and `@injectable` (wired to extended registry)
16
+ - `packages/core-server/src/helpers/inversion/container.ts` - Extended `Container` with `ApplicationLogger`
17
+ - `packages/core-server/src/helpers/inversion/registry.ts` - Extended `MetadataRegistry` (singleton, with model/repository/datasource mixins)
18
+ - `packages/core-server/src/base/metadata/injectors.ts` - Core `@inject` (wired to extended registry)
19
19
 
20
20
  ## Quick Reference
21
21
 
@@ -24,7 +24,6 @@ Technical reference for the DI system in IGNIS - managing resource lifecycles an
24
24
  | **Container** | DI registry managing resource lifecycles | `bind()`, `get()`, `gets()`, `instantiate()`, `resolve()`, `findByTag()`, `isBound()`, `unbind()`, `clear()`, `reset()` |
25
25
  | **Binding** | Single registered dependency configuration | `toClass()`, `toValue()`, `toProvider()`, `setScope()`, `setTags()`, `getValue()`, `clearCache()` |
26
26
  | **@inject** | Decorator marking injection points | Applied to constructor parameters and class properties |
27
- | **@injectable** | Decorator marking a class as injectable | Stores scope and tag metadata |
28
27
  | **MetadataRegistry** | Stores decorator metadata | Singleton - base via `metadataRegistry` export, core via `MetadataRegistry.getInstance()` |
29
28
  | **BindingKeys** | Utility for building namespaced keys | `BindingKeys.build({ namespace, key })` |
30
29
  | **Boot System** | Automatic artifact discovery and binding | Integrates with Container via tags and bindings |
@@ -42,7 +41,7 @@ Before reading this document, you should understand:
42
41
 
43
42
  Heart of the DI system - registry managing all application resources.
44
43
 
45
- **File:** `packages/inversion/src/container.ts` (Base) & `packages/core/src/helpers/inversion/container.ts` (Extended)
44
+ **File:** `packages/inversion/src/modules/container/index.ts` (Base) & `packages/core-server/src/helpers/inversion/container.ts` (Extended)
46
45
 
47
46
  The base `Container` extends `BaseHelper` (which provides `scope` and `identifier` properties). The core `Container` extends the base and adds a `Logger` instance.
48
47
 
@@ -74,7 +73,8 @@ const container = new Container({ scope: 'MyApp' }); // scope is optional, defau
74
73
 
75
74
  When `container.instantiate(MyClass)` is called:
76
75
 
77
- 1. **Constructor injection** - Reads `@inject` metadata from the class by parameter index (the `Reflect`-stored array is already index-keyed, so there is no sort step), resolves each dependency from the container, and passes them as constructor arguments. If any index in range has no `@inject` metadata, `instantiate()` throws immediately rather than passing `undefined`.
76
+ 1. **Constructor injection** - Reads `@inject` metadata from the class by parameter index. The `Reflect`-stored array is already index-keyed, so there is no sort step; the container resolves each dependency and passes them as constructor arguments.
77
+ - If any index in range has no `@inject` metadata, `instantiate()` throws immediately rather than passing `undefined`.
78
78
  2. **Property injection** - After the instance is created, reads property metadata, resolves each dependency, and assigns them directly to the instance properties.
79
79
 
80
80
  ```typescript
@@ -91,22 +91,22 @@ class UserController {
91
91
  ```
92
92
 
93
93
  > [!IMPORTANT]
94
- > **Every constructor parameter of a container-instantiated class must carry `@inject`.** Mixing decorated and undecorated parameters is forbidden - `@inject` stores its metadata at the parameter's index, so an undecorated parameter leaves a hole in that array, and there is no channel through which the container could supply it anyway (it would resolve to `undefined`). `instantiate()` refuses the shape by class name and parameter index instead of silently dereferencing the hole:
94
+ > **Every constructor parameter of a container-instantiated class must carry `@inject`.** Mixing decorated and undecorated parameters is forbidden. `@inject` stores its metadata at the parameter's index, so an undecorated parameter leaves a hole in that array. There is no channel through which the container could supply it anyway - it would resolve to `undefined`. `instantiate()` refuses the shape by class name and parameter index instead of silently dereferencing the hole:
95
95
  >
96
96
  > ```
97
97
  > [NoteController] Constructor parameter 0 has no @inject | Every parameter of a container-instantiated
98
98
  > class must be decorated - the container cannot supply an undecorated one
99
99
  > ```
100
100
  >
101
- > The check lives in `instantiate()`, not in the `@inject` decorator itself: parameter decorators run right-to-left, so when `@inject` on parameter 1 runs, parameter 0 has not been visited yet and nothing at that point can know whether it will end up decorated.
101
+ > The check lives in `instantiate()`, not in the `@inject` decorator itself. Parameter decorators run right-to-left, so when `@inject` on parameter 1 runs, parameter 0 has not been visited yet - nothing at that point can know whether it will end up decorated.
102
102
  >
103
- > This does not apply to `@repository`-decorated classes whose constructor appears undecorated at index 0 - the `@repository` decorator programmatically writes that inject metadata (`registry.setInjectMetadata({ target, index: 0, ... })`) even though no literal `@inject` appears in source. See [Repositories](./repositories/).
103
+ > This does not apply to `@repository`-decorated classes whose constructor appears undecorated at index 0. The `@repository` decorator programmatically writes that inject metadata (`registry.setInjectMetadata({ target, index: 0, ... })`), even though no literal `@inject` appears in source. See [Repositories](./repositories/).
104
104
 
105
105
  ## `Binding` Class
106
106
 
107
107
  A `Binding` represents a single registered dependency in the container. It provides a fluent API to configure *how* a dependency should be created and managed.
108
108
 
109
- **File:** `packages/inversion/src/container.ts`
109
+ **File:** `packages/inversion/src/modules/container/index.ts`
110
110
 
111
111
  The `Binding` class extends `BaseHelper`.
112
112
 
@@ -206,7 +206,7 @@ This is also used internally by `container.get()` and `container.getBinding()` w
206
206
 
207
207
  The `@inject` decorator marks where dependencies should be injected - either on constructor parameters or class properties.
208
208
 
209
- **File:** `packages/inversion/src/metadata/injectors.ts` (base) & `packages/core/src/base/metadata/injectors.ts` (core wrapper)
209
+ **File:** `packages/inversion/src/modules/metadata/injectors.ts` (base) & `packages/core-server/src/base/metadata/injectors.ts` (core wrapper)
210
210
 
211
211
  ### Signature
212
212
 
@@ -257,30 +257,14 @@ The `@venizia/ignis-inversion` package exports base decorators that use the modu
257
257
 
258
258
  **Always import from `@venizia/ignis` in application code:**
259
259
  ```typescript
260
- import { inject, injectable } from '@venizia/ignis';
260
+ import { inject } from '@venizia/ignis';
261
261
  ```
262
262
 
263
- ## `@injectable` Decorator
263
+ ## Registering a Class
264
264
 
265
- Marks a class as injectable and attaches optional metadata.
266
-
267
- **File:** `packages/inversion/src/metadata/injectors.ts` (base) & `packages/core/src/base/metadata/injectors.ts` (core wrapper)
268
-
269
- ### Signature
270
-
271
- ```typescript
272
- @injectable({ scope?: TBindingScope; tags?: Record<string, any> })
273
- ```
274
-
275
- | Parameter | Type | Default | Description |
276
- | :--- | :--- | :--- | :--- |
277
- | `scope` | `'singleton' \| 'transient'` | - | Optional scope hint for the binding. |
278
- | `tags` | `Record<string, any>` | - | Optional metadata tags. |
279
-
280
- ### Example
265
+ No class decorator is needed to make a class injectable. A class becomes resolvable once a binding exists for it. That binding can come from boot auto-discovery, from a framework helper (`app.controller()`, `app.service()`, `@repository`), or explicitly from `container.bind()`. Scope is configured on the binding, never on the class:
281
266
 
282
267
  ```typescript
283
- @injectable({ scope: BindingScopes.SINGLETON })
284
268
  class UserService extends BaseService {
285
269
  constructor(
286
270
  @inject({ key: 'repositories.UserRepository' })
@@ -289,15 +273,19 @@ class UserService extends BaseService {
289
273
  super({ scope: UserService.name });
290
274
  }
291
275
  }
276
+
277
+ app.bind({ key: 'services.UserService' })
278
+ .toClass(UserService)
279
+ .setScope(BindingScopes.SINGLETON); // default is TRANSIENT
292
280
  ```
293
281
 
294
282
  ## `MetadataRegistry`
295
283
 
296
- The `MetadataRegistry` stores and retrieves all metadata attached by decorators (`@inject`, `@injectable`, `@controller`, `@model`, etc.).
284
+ The `MetadataRegistry` stores and retrieves all metadata attached by decorators (`@inject`, `@controller`, `@model`, etc.).
297
285
 
298
286
  ### Base MetadataRegistry
299
287
 
300
- **File:** `packages/inversion/src/registry.ts`
288
+ **File:** `packages/inversion/src/modules/registry/index.ts`
301
289
 
302
290
  A singleton exported as `metadataRegistry`. Extends `BaseHelper`.
303
291
 
@@ -315,12 +303,10 @@ A singleton exported as `metadataRegistry`. Extends `BaseHelper`.
315
303
  | `setPropertyMetadata({ target, propertyName, metadata })` | Stores property injection metadata (`IPropertyMetadata`). |
316
304
  | `getPropertiesMetadata({ target })` | Returns a `Map<string \| symbol, IPropertyMetadata>` for all injected properties. |
317
305
  | `getPropertyMetadata({ target, propertyName })` | Returns property metadata for a specific property. |
318
- | `setInjectableMetadata({ target, metadata })` | Stores `@injectable` metadata on a class. |
319
- | `getInjectableMetadata({ target })` | Returns `@injectable` metadata for a class. |
320
306
 
321
307
  ### Core MetadataRegistry
322
308
 
323
- **File:** `packages/core/src/helpers/inversion/registry.ts`
309
+ **File:** `packages/core-server/src/helpers/inversion/registry.ts`
324
310
 
325
311
  Extends the base with controller, repository, model, and datasource metadata support via mixins. Accessed as a singleton via `MetadataRegistry.getInstance()`.
326
312
 
@@ -333,12 +319,11 @@ Additional capabilities include:
333
319
 
334
320
  ### Metadata Keys
335
321
 
336
- Defined in `packages/inversion/src/common/keys.ts`:
322
+ Defined in `packages/inversion/src/modules/metadata/common/constants.ts`:
337
323
 
338
324
  ```typescript
339
325
  MetadataKeys.PROPERTIES = Symbol.for('ignis:properties')
340
326
  MetadataKeys.INJECT = Symbol.for('ignis:inject')
341
- MetadataKeys.INJECTABLE = Symbol.for('ignis:injectable')
342
327
  ```
343
328
 
344
329
  ### Key Types
@@ -355,11 +340,6 @@ interface IPropertyMetadata {
355
340
  isOptional?: boolean;
356
341
  [key: string]: any;
357
342
  }
358
-
359
- interface IInjectableMetadata {
360
- scope?: TBindingScope;
361
- tags?: Record<string, any>;
362
- }
363
343
  ```
364
344
 
365
345
  ## Boot System Integration
@@ -436,7 +416,6 @@ The boot system integrates into the application lifecycle:
436
416
  app.bind({ key: 'controllers.UserController' }).toClass(UserController);
437
417
 
438
418
  // 4. Later, when UserController is instantiated:
439
- @injectable()
440
419
  class UserController {
441
420
  constructor(
442
421
  @inject({ key: 'services.UserService' })
@@ -6,48 +6,25 @@ difficulty: intermediate
6
6
 
7
7
  # Using Filters in Your Application
8
8
 
9
- How filters flow through the application layers.
10
-
11
-
12
- ## Architecture Overview
9
+ A `filter` starts as a JSON-encoded query string on an HTTP request and ends as a Drizzle query. Here is where to hook into each layer in between.
13
10
 
14
11
  ```
15
- +-----------------------------------------------------------------+
16
- | HTTP Request |
17
- | GET /products?filter={"where":{"status":"active"},"limit":10} |
18
- +--------------------------------+--------------------------------+
19
- |
20
- v
21
- +-----------------------------------------------------------------+
22
- | Controller Layer |
23
- | - Validates filter via Zod schema |
24
- | - Parses JSON string -> Filter object |
25
- | - Passes to service/repository |
26
- +-----------------------------------------------------------------+
27
- |
28
- v
29
- +-----------------------------------------------------------------+
30
- | Service Layer (Optional) |
31
- | - Business logic, authorization |
32
- | - May modify filter before passing |
33
- +-----------------------------------------------------------------+
34
- |
35
- v
36
- +-----------------------------------------------------------------+
37
- | Repository Layer |
38
- | - applyDefaultFilter() merges the @model default filter |
39
- | - FilterBuilder transforms Filter -> Drizzle query options |
40
- | - Executes query via Drizzle ORM |
41
- | - Returns typed results |
42
- +-----------------------------------------------------------------+
12
+ HTTP Request GET /products?filter={"where":{"status":"active"},"limit":10}
13
+ |
14
+ v
15
+ Controller Layer Validates via Zod (FilterSchema), parses JSON string -> Filter object
16
+ |
17
+ v
18
+ Service Layer Optional - business logic, authorization, may edit the filter
19
+ |
20
+ v
21
+ Repository Layer applyDefaultFilter() merges the @model default filter,
22
+ FilterBuilder converts Filter -> Drizzle query options, executes
43
23
  ```
44
24
 
25
+ ## Generate a CRUD controller from an entity
45
26
 
46
- ## Controller Layer
47
-
48
- ### Using ControllerFactory (Recommended)
49
-
50
- The `ControllerFactory` automatically handles filter parsing and validation:
27
+ For a standard `GET`/`POST`/`PATCH`/`DELETE` resource, generate the controller instead of writing filter parsing by hand. `ControllerFactory.defineCrudController` builds it from an entity and a repository binding:
51
28
 
52
29
  ```typescript
53
30
  // src/controllers/product.controller.ts
@@ -77,10 +54,7 @@ const _Controller = ControllerFactory.defineCrudController({
77
54
  export class ProductController extends _Controller {
78
55
  constructor(
79
56
  @inject({
80
- key: BindingKeys.build({
81
- namespace: BindingNamespaces.REPOSITORY,
82
- key: ProductRepository.name,
83
- }),
57
+ key: BindingKeys.build({ namespace: BindingNamespaces.REPOSITORY, key: ProductRepository.name }),
84
58
  })
85
59
  repository: ProductRepository,
86
60
  ) {
@@ -89,20 +63,43 @@ export class ProductController extends _Controller {
89
63
  }
90
64
  ```
91
65
 
92
- **Generated Endpoints:**
66
+ This generates every filter-bearing endpoint the resource needs:
67
+
68
+ | Method | Endpoint | Query param |
69
+ |---|---|---|
70
+ | GET | `/products` | `filter` |
71
+ | GET | `/products/{id}` | `filter` (`where` is ignored - the id is the condition) |
72
+ | GET | `/products/find-one` | `filter` |
73
+ | GET | `/products/count` | `where` |
74
+
75
+ Set `isStrict.requestSchema: true` to make these query params Zod-required; set `isStrict.path: true` to reject trailing-slash variants of the route. Write endpoints (`POST /`, `PATCH /{id}`, `DELETE /{id}`, ...) come from the same factory call and take no filter.
76
+
77
+ ## Map a request query string to a parsed filter
78
+
79
+ `FilterSchema` and `WhereSchema` both accept a JSON string or a plain object, so the same schema validates a Hono query param (always a string) and a filter built in code:
80
+
81
+ | Request | Parsed filter |
82
+ |---|---|
83
+ | `GET /products?filter={"where":{"status":"active"}}` | `{ where: { status: 'active' } }` |
84
+ | `GET /products?filter={"limit":10,"skip":20}` | `{ limit: 10, skip: 20 }` |
85
+ | `GET /products?filter={"where":{"price":{"gte":100,"lte":500}},"order":["price ASC"]}` | `{ where: { price: { gte: 100, lte: 500 } }, order: ['price ASC'] }` |
86
+ | `GET /products/count?where={"role":"admin"}` | `{ role: 'admin' }` |
87
+
88
+ `WhereSchema` is independent of `FilterSchema` - it backs the `count` endpoint, which takes `where` directly rather than a full filter.
93
89
 
94
- | Method | Endpoint | Filter Location |
95
- |--------|----------|-----------------|
96
- | GET | `/products` | Query param: `?filter={...}` |
97
- | GET | `/products/{id}` | Query param: `?filter={...}` (for includes) |
98
- | GET | `/products/find-one` | Query param: `?filter={...}` |
99
- | GET | `/products/count` | Query param: `?where={...}` |
90
+ ## Write a custom route that accepts a filter
100
91
 
101
- ### Custom Controller with Manual Filter Handling
92
+ For a route outside the generated CRUD set, use `FilterQuerySchema` - the same query shape the generated `find` route takes:
102
93
 
103
94
  ```typescript
104
95
  import { z } from '@hono/zod-openapi';
105
- import { BaseRestController, controller, FilterSchema, inject, jsonResponse } from '@venizia/ignis';
96
+ import {
97
+ BaseRestController,
98
+ controller,
99
+ FilterQuerySchema,
100
+ inject,
101
+ jsonResponse,
102
+ } from '@venizia/ignis';
106
103
 
107
104
  @controller({ path: '/products' })
108
105
  export class ProductController extends BaseRestController {
@@ -118,12 +115,10 @@ export class ProductController extends BaseRestController {
118
115
  configs: {
119
116
  path: '/search',
120
117
  method: 'get',
121
- request: {
122
- query: z.object({ filter: FilterSchema }),
123
- },
118
+ request: { query: FilterQuerySchema },
124
119
  responses: jsonResponse({ schema: z.array(z.object({ id: z.string() })) }),
125
120
  },
126
- handler: async (context) => {
121
+ handler: async context => {
127
122
  const { filter = {} } = context.req.valid('query');
128
123
  const results = await this._productRepository.find({ filter });
129
124
  return context.json(results);
@@ -133,25 +128,29 @@ export class ProductController extends BaseRestController {
133
128
  }
134
129
  ```
135
130
 
131
+ ### The two query shapes
136
132
 
137
- ## Filter Schema Validation
133
+ Reach for these instead of rebuilding the same object at every route.
138
134
 
139
- The `FilterSchema` (Zod) accepts both object and JSON string formats:
135
+ | Schema | Query shape | Use it for |
136
+ |---|---|---|
137
+ | `FilterQuerySchema` | `{ filter?: TFilter }` | Any route that takes a full filter |
138
+ | `WhereQuerySchema` | `{ where?: TWhere }` | Any route that takes conditions and no pagination |
140
139
 
141
- ```typescript
142
- // Object format (from parsed query params)
143
- { where: { status: 'active' }, limit: 10 }
140
+ Both are plain Zod objects, so a route that takes more than one parameter extends rather than rebuilds:
144
141
 
145
- // JSON string format (from URL query string)
146
- '{"where":{"status":"active"},"limit":10}'
142
+ ```typescript
143
+ request: { query: WhereQuerySchema.extend({ q: z.string().max(255).optional() }) },
147
144
  ```
148
145
 
149
- The `WhereSchema` also accepts both formats independently, useful for the `count` endpoint which takes `where` directly.
146
+ Neither needs an extra `.optional()`. `FilterSchema` already carries one, so `FilterSchema.optional()` is the same schema written longer, and a trailing `.partial()` on a single optional key does nothing.
150
147
 
148
+ > [!NOTE]
149
+ > The generated `updateBy` and `deleteBy` routes deliberately do not use `WhereQuerySchema`. They require `where`, because a missing one rewrites or deletes every row in the table.
151
150
 
152
- ## Service Layer
151
+ ## Rewrite a filter before it reaches the repository
153
152
 
154
- Services can modify filters before passing to repositories:
153
+ Add a service between the controller and the repository when a constraint is a caller or session concern rather than a per-model constant - a tenant ID pulled from the request context, for example. Use [Default Filter](./default-filter) instead when the constraint applies to every caller of the model:
155
154
 
156
155
  ```typescript
157
156
  @service()
@@ -161,83 +160,56 @@ export class ProductService {
161
160
  private _productRepository: ProductRepository,
162
161
  ) {}
163
162
 
164
- async findProducts(filter: TFilter<TProductSchema> = {}) {
165
- // Merge user filter with soft-delete condition
166
- const enhancedFilter: TFilter<TProductSchema> = {
167
- ...filter,
168
- where: {
169
- ...filter.where,
170
- deletedAt: { is: null },
171
- },
172
- };
173
-
174
- return this._productRepository.find({ filter: enhancedFilter });
175
- }
176
-
177
- async findProductsForTenant(
178
- tenantId: string,
179
- filter: TFilter<TProductSchema> = {},
180
- ) {
181
- const isolatedFilter: TFilter<TProductSchema> = {
182
- ...filter,
183
- where: {
184
- ...filter.where,
185
- tenantId,
186
- },
187
- };
188
-
189
- return this._productRepository.find({ filter: isolatedFilter });
163
+ async findProductsForTenant(tenantId: string, filter: TFilter<TProductSchema> = {}) {
164
+ return this._productRepository.find({
165
+ filter: { ...filter, where: { ...filter.where, tenantId } },
166
+ });
190
167
  }
191
168
  }
192
169
  ```
193
170
 
171
+ ## Build a filter query string from a client
194
172
 
195
- ## HTTP Request Examples
173
+ Encode the filter as JSON and pass it as the `filter` query param, whichever HTTP client you use:
196
174
 
197
175
  **cURL:**
198
176
  ```bash
199
- # Simple filter
200
- curl "http://localhost:3000/products?filter=%7B%22where%22%3A%7B%22status%22%3A%22active%22%7D%2C%22limit%22%3A10%7D"
201
-
202
- # Decoded filter: {"where":{"status":"active"},"limit":10}
203
-
204
- # Complex filter with URL encoding
205
177
  curl -G "http://localhost:3000/products" \
206
178
  --data-urlencode 'filter={"where":{"price":{"gte":100,"lte":500},"tags":{"contains":["featured"]}},"order":["price ASC"],"limit":20}'
207
179
  ```
208
180
 
209
- **JavaScript/TypeScript:**
181
+ **Fetch/Axios:**
210
182
  ```typescript
211
- // Using fetch
212
- const filter = {
213
- where: { status: 'active', price: { lte: 100 } },
214
- order: ['createdAt DESC'],
215
- limit: 10,
216
- };
217
-
218
- const response = await fetch(
219
- `/api/products?filter=${encodeURIComponent(JSON.stringify(filter))}`
220
- );
221
-
222
- // Using axios
223
- const response = await axios.get('/api/products', {
224
- params: { filter: JSON.stringify(filter) },
225
- });
183
+ const filter = { where: { status: 'active', price: { lte: 100 } }, order: ['createdAt DESC'], limit: 10 };
184
+
185
+ // fetch
186
+ const response = await fetch(`/api/products?filter=${encodeURIComponent(JSON.stringify(filter))}`);
187
+
188
+ // axios
189
+ const response = await axios.get('/api/products', { params: { filter: JSON.stringify(filter) } });
226
190
  ```
227
191
 
192
+ ## Debug what a filter compiles to
228
193
 
229
- ## Debugging Filters
194
+ Call `buildQuery` to compile a filter into Drizzle query options without executing it - the fastest way to check what a filter actually resolves to:
230
195
 
231
196
  ```typescript
232
- // Enable logging to see generated SQL
233
- const result = await repository.find({
234
- filter: complexFilter,
235
- options: {
236
- log: { use: true, level: 'debug' },
237
- },
238
- });
239
-
240
- // Or use buildQuery to inspect without executing
241
197
  const queryOptions = repository.buildQuery({ filter: complexFilter });
242
198
  console.log('Generated query options:', queryOptions);
243
199
  ```
200
+
201
+ `options.log` also traces repository calls, but only on the write path (`create`/`updateById`/`updateAll`/`deleteById`/`deleteAll`) - `find`/`findOne`/`findById`/`count` never read it. See [Advanced Repository Features -> Log option](../repositories/advanced.md#log-option) for the write-side form.
202
+
203
+ ## See also
204
+
205
+ - [Filter System Overview](./) - the `filter` shape and every `where` operator family
206
+ - [Default Filter](./default-filter) - `shouldSkipDefaultFilter`, and the model-level alternative to the service-layer rewrite above
207
+ - [Use Case Gallery](./use-cases) - more filter shapes with their generated SQL
208
+ - [Advanced Repository Features](../repositories/advanced.md) - transactions, locking, and the full `log`/`lock` options
209
+
210
+ **Files:**
211
+
212
+ - [`packages/kernel/src/base/controllers/factory/controller.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/kernel/src/base/controllers/factory/controller.ts) - `ControllerFactory.defineCrudController`
213
+ - [`packages/kernel/src/base/repositories/query-schemas/index.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/kernel/src/base/repositories/query-schemas/index.ts) - `FilterSchema`, `WhereSchema`, `FilterQuerySchema`, `WhereQuerySchema`
214
+ - [`packages/filter/src/common/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/filter/src/common/types.ts) - `TFilter`, `TInclusion`
215
+ - [`packages/connectors/src/relational/core/repositories/core/base.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/connectors/src/relational/core/repositories/core/base.ts) - `RelationalBaseRepository.buildQuery`
@@ -23,44 +23,34 @@ Find rows where the array column contains **all** specified elements.
23
23
  // Schema: tags varchar(100)[]
24
24
  // Data: Product A has ['electronics', 'featured', 'sale']
25
25
 
26
- // Find products with BOTH 'electronics' AND 'featured'
27
26
  { where: { tags: { contains: ['electronics', 'featured'] } } }
28
27
  // SQL: "tags"::text[] @> ARRAY['electronics', 'featured']::text[]
29
-
30
- // Single element (can pass single value or array)
31
- { where: { tags: { contains: ['featured'] } } }
32
- { where: { tags: { contains: 'featured' } } } // Also works
33
- // Matches: ['featured'], ['featured', 'sale'], ['a', 'featured', 'b']
34
28
  ```
35
29
 
30
+ > [!NOTE]
31
+ > A single value is wrapped in an array automatically: `{ contains: 'featured' }` is treated as `{ contains: ['featured'] }`.
32
+
36
33
 
37
34
  ## containedBy (<@)
38
35
 
39
36
  Find rows where **all** array elements are within the specified set.
40
37
 
41
38
  ```typescript
42
- // Find products where ALL tags are in the allowed list
43
39
  { where: { tags: { containedBy: ['sale', 'featured', 'new', 'popular'] } } }
44
40
  // SQL: "tags"::text[] <@ ARRAY['sale', 'featured', 'new', 'popular']::text[]
45
-
46
- // Product A ['featured', 'sale'] -> matches (all in list)
47
- // Product B ['featured', 'clearance'] -> no match ('clearance' not in list)
48
- // Product C [] -> matches (empty is subset of everything)
49
41
  ```
50
42
 
43
+ > [!NOTE]
44
+ > An empty array is a subset of every set, so `tags: []` always matches `containedBy`.
45
+
51
46
 
52
47
  ## overlaps (&&)
53
48
 
54
49
  Find rows where the arrays share at least one common element.
55
50
 
56
51
  ```typescript
57
- // Find products with 'premium' OR 'sale' tag
58
52
  { where: { tags: { overlaps: ['premium', 'sale'] } } }
59
53
  // SQL: "tags"::text[] && ARRAY['premium', 'sale']::text[]
60
-
61
- // Product A ['featured', 'sale'] -> matches (has 'sale')
62
- // Product B ['premium', 'luxury'] -> matches (has 'premium')
63
- // Product C ['new', 'featured'] -> no match (no overlap)
64
54
  ```
65
55
 
66
56
 
@@ -84,52 +74,41 @@ Find rows where the arrays share at least one common element.
84
74
  | "Must have AT LEAST ONE of these tags" | `overlaps` |
85
75
 
86
76
 
87
- ## Empty Array Behavior
88
-
89
- | Operator | SQL Generated | Behavior |
90
- |----------|---------------|----------|
91
- | `contains: []` | `WHERE true` | Returns **ALL** rows |
92
- | `containedBy: []` | `WHERE "col" = '{}'` | Returns only rows with **empty arrays** |
93
- | `overlaps: []` | `WHERE false` | Returns **NO** rows |
94
-
95
- > [!NOTE]
96
- > Single values are automatically wrapped in an array: `{ contains: 'value' }` is treated as `{ contains: ['value'] }`.
97
-
98
-
99
77
  ## Type Handling
100
78
 
101
- **String Arrays** (`varchar[]`, `text[]`, `char[]`):
79
+ The element type of the array column decides the cast in the generated SQL.
80
+
102
81
  ```typescript
82
+ // String arrays (varchar[], text[], char[]) - both sides cast to text[]
103
83
  { where: { tags: { contains: ['a', 'b'] } } }
104
84
  // SQL: "tags"::text[] @> ARRAY['a', 'b']::text[]
105
- ```
106
85
 
107
- Both the column and the array literal are cast to `text[]` for compatibility.
108
-
109
- **Numeric Arrays** (`integer[]`, `numeric[]`):
110
- ```typescript
86
+ // Numeric arrays (integer[], numeric[]) - no cast needed
111
87
  { where: { scores: { contains: [100, 200] } } }
112
88
  // SQL: "scores" @> ARRAY[100, 200]
113
- ```
114
89
 
115
- No casting needed for numeric arrays.
116
-
117
- **Boolean Arrays**:
118
- ```typescript
90
+ // Boolean arrays - no cast needed
119
91
  { where: { flags: { contains: [true, false] } } }
120
92
  // SQL: "flags" @> ARRAY[true, false]
121
93
  ```
122
94
 
123
95
 
96
+ ## Empty Array Behavior
97
+
98
+ | Operator | SQL generated | Behavior |
99
+ |----------|---------------|----------|
100
+ | `contains: []` | `WHERE true` | Returns **ALL** rows |
101
+ | `containedBy: []` | `WHERE "col" = '{}'` | Returns only rows with **empty arrays** |
102
+ | `overlaps: []` | `WHERE false` | Returns **NO** rows |
103
+
104
+
124
105
  ## Security: Parameterized Values
125
106
 
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.
107
+ 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
108
 
128
109
 
129
110
  ## Defining Array Columns
130
111
 
131
- In your Drizzle schema:
132
-
133
112
  ```typescript
134
113
  import { pgTable, text, varchar, integer } from 'drizzle-orm/pg-core';
135
114
 
@@ -137,9 +116,20 @@ export const productTable = pgTable('Product', {
137
116
  id: text('id').primaryKey(),
138
117
  name: text('name').notNull(),
139
118
 
140
- // Array columns
141
119
  tags: varchar('tags', { length: 100 }).array(), // varchar(100)[]
142
120
  categories: text('categories').array(), // text[]
143
121
  scores: integer('scores').array(), // integer[]
144
122
  });
145
123
  ```
124
+
125
+ ## See also
126
+
127
+ - [Filter System Overview](./) - the `filter` shape and the full `where` operator table
128
+ - [List Operators](./list-operators) - `in`/`nin` match scalar values against an array, the operators these are not to be confused with
129
+ - [Quick Reference](./quick-reference) - every operator, one line each
130
+
131
+ **Files:**
132
+
133
+ - [`packages/core-server/src/connectors/postgres/repositories/dialect/query.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/connectors/postgres/repositories/dialect/query.ts) - `PostgresQueryOperators.FNS`, `buildPgArrayComparison`
134
+ - [`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`, translates `TFilter` to Drizzle/SQL
135
+ - [`packages/filter/src/common/operators.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/filter/src/common/operators.ts) - `QueryOperators` constants