@venizia/ignis-docs 0.0.6-3 → 0.0.7-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 (113) hide show
  1. package/README.md +125 -388
  2. package/dist/mcp-server/common/config.d.ts +0 -21
  3. package/dist/mcp-server/common/config.d.ts.map +1 -1
  4. package/dist/mcp-server/common/config.js +1 -36
  5. package/dist/mcp-server/common/config.js.map +1 -1
  6. package/dist/mcp-server/helpers/docs.helper.d.ts +0 -24
  7. package/dist/mcp-server/helpers/docs.helper.d.ts.map +1 -1
  8. package/dist/mcp-server/helpers/docs.helper.js +0 -25
  9. package/dist/mcp-server/helpers/docs.helper.js.map +1 -1
  10. package/dist/mcp-server/helpers/github.helper.d.ts +0 -13
  11. package/dist/mcp-server/helpers/github.helper.d.ts.map +1 -1
  12. package/dist/mcp-server/helpers/github.helper.js +3 -20
  13. package/dist/mcp-server/helpers/github.helper.js.map +1 -1
  14. package/dist/mcp-server/index.js +0 -20
  15. package/dist/mcp-server/index.js.map +1 -1
  16. package/dist/mcp-server/tools/base.tool.d.ts +2 -79
  17. package/dist/mcp-server/tools/base.tool.d.ts.map +1 -1
  18. package/dist/mcp-server/tools/base.tool.js +1 -38
  19. package/dist/mcp-server/tools/base.tool.js.map +1 -1
  20. package/dist/mcp-server/tools/docs/get-document-content.tool.d.ts.map +1 -1
  21. package/dist/mcp-server/tools/docs/get-document-content.tool.js +0 -9
  22. package/dist/mcp-server/tools/docs/get-document-content.tool.js.map +1 -1
  23. package/dist/mcp-server/tools/docs/get-document-metadata.tool.d.ts.map +1 -1
  24. package/dist/mcp-server/tools/docs/get-document-metadata.tool.js +0 -9
  25. package/dist/mcp-server/tools/docs/get-document-metadata.tool.js.map +1 -1
  26. package/dist/mcp-server/tools/docs/get-package-overview.tool.d.ts +0 -6
  27. package/dist/mcp-server/tools/docs/get-package-overview.tool.d.ts.map +1 -1
  28. package/dist/mcp-server/tools/docs/get-package-overview.tool.js +1 -24
  29. package/dist/mcp-server/tools/docs/get-package-overview.tool.js.map +1 -1
  30. package/dist/mcp-server/tools/docs/list-categories.tool.d.ts.map +1 -1
  31. package/dist/mcp-server/tools/docs/list-categories.tool.js +0 -9
  32. package/dist/mcp-server/tools/docs/list-categories.tool.js.map +1 -1
  33. package/dist/mcp-server/tools/docs/list-documents.tool.d.ts.map +1 -1
  34. package/dist/mcp-server/tools/docs/list-documents.tool.js +0 -9
  35. package/dist/mcp-server/tools/docs/list-documents.tool.js.map +1 -1
  36. package/dist/mcp-server/tools/docs/search-documents.tool.d.ts.map +1 -1
  37. package/dist/mcp-server/tools/docs/search-documents.tool.js +0 -9
  38. package/dist/mcp-server/tools/docs/search-documents.tool.js.map +1 -1
  39. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts.map +1 -1
  40. package/dist/mcp-server/tools/github/list-project-files.tool.js +0 -9
  41. package/dist/mcp-server/tools/github/list-project-files.tool.js.map +1 -1
  42. package/dist/mcp-server/tools/github/search-code.tool.d.ts.map +1 -1
  43. package/dist/mcp-server/tools/github/search-code.tool.js +1 -13
  44. package/dist/mcp-server/tools/github/search-code.tool.js.map +1 -1
  45. package/dist/mcp-server/tools/github/verify-dependencies.tool.d.ts +0 -4
  46. package/dist/mcp-server/tools/github/verify-dependencies.tool.d.ts.map +1 -1
  47. package/dist/mcp-server/tools/github/verify-dependencies.tool.js +1 -18
  48. package/dist/mcp-server/tools/github/verify-dependencies.tool.js.map +1 -1
  49. package/dist/mcp-server/tools/github/view-source-file.tool.d.ts.map +1 -1
  50. package/dist/mcp-server/tools/github/view-source-file.tool.js +0 -9
  51. package/dist/mcp-server/tools/github/view-source-file.tool.js.map +1 -1
  52. package/dist/mcp-server/tools/index.d.ts.map +1 -1
  53. package/dist/mcp-server/tools/index.js +0 -2
  54. package/dist/mcp-server/tools/index.js.map +1 -1
  55. package/package.json +1 -1
  56. package/wiki/best-practices/api-usage-examples.md +7 -5
  57. package/wiki/best-practices/code-style-standards/advanced-patterns.md +1 -1
  58. package/wiki/best-practices/code-style-standards/constants-configuration.md +1 -1
  59. package/wiki/best-practices/code-style-standards/control-flow.md +1 -1
  60. package/wiki/best-practices/code-style-standards/function-patterns.md +1 -1
  61. package/wiki/best-practices/common-pitfalls.md +1 -1
  62. package/wiki/best-practices/data-modeling.md +33 -1
  63. package/wiki/best-practices/error-handling.md +7 -4
  64. package/wiki/best-practices/performance-optimization.md +1 -1
  65. package/wiki/best-practices/security-guidelines.md +5 -4
  66. package/wiki/guides/core-concepts/components-guide.md +1 -1
  67. package/wiki/guides/core-concepts/controllers.md +14 -8
  68. package/wiki/guides/core-concepts/persistent/models.md +32 -0
  69. package/wiki/guides/core-concepts/services.md +2 -1
  70. package/wiki/guides/get-started/5-minute-quickstart.md +1 -1
  71. package/wiki/guides/tutorials/building-a-crud-api.md +2 -1
  72. package/wiki/guides/tutorials/complete-installation.md +2 -2
  73. package/wiki/guides/tutorials/ecommerce-api.md +3 -3
  74. package/wiki/guides/tutorials/realtime-chat.md +7 -6
  75. package/wiki/index.md +2 -1
  76. package/wiki/references/base/application.md +28 -0
  77. package/wiki/references/base/components.md +2 -1
  78. package/wiki/references/base/controllers.md +31 -4
  79. package/wiki/references/base/datasources.md +6 -2
  80. package/wiki/references/base/dependency-injection.md +31 -0
  81. package/wiki/references/base/filter-system/fields-order-pagination.md +8 -1
  82. package/wiki/references/base/middlewares.md +2 -1
  83. package/wiki/references/base/models.md +144 -2
  84. package/wiki/references/base/repositories/advanced.md +2 -2
  85. package/wiki/references/base/repositories/index.md +24 -1
  86. package/wiki/references/base/repositories/soft-deletable.md +213 -0
  87. package/wiki/references/base/services.md +2 -1
  88. package/wiki/references/components/authentication/api.md +525 -205
  89. package/wiki/references/components/authentication/errors.md +502 -105
  90. package/wiki/references/components/authentication/index.md +388 -75
  91. package/wiki/references/components/authentication/usage.md +575 -247
  92. package/wiki/references/components/authorization/usage.md +62 -0
  93. package/wiki/references/components/health-check.md +2 -1
  94. package/wiki/references/components/socket-io/index.md +9 -4
  95. package/wiki/references/components/socket-io/usage.md +1 -1
  96. package/wiki/references/components/static-asset/index.md +3 -5
  97. package/wiki/references/components/swagger.md +2 -1
  98. package/wiki/references/configuration/environment-variables.md +2 -1
  99. package/wiki/references/configuration/index.md +40 -1
  100. package/wiki/references/helpers/error/index.md +1 -1
  101. package/wiki/references/helpers/inversion/index.md +1 -1
  102. package/wiki/references/helpers/redis/index.md +2 -9
  103. package/wiki/references/quick-reference.md +3 -5
  104. package/wiki/references/utilities/crypto.md +2 -2
  105. package/wiki/references/utilities/date.md +5 -5
  106. package/wiki/references/utilities/index.md +3 -11
  107. package/wiki/references/utilities/jsx.md +4 -2
  108. package/wiki/references/utilities/module.md +1 -1
  109. package/wiki/references/utilities/parse.md +24 -4
  110. package/wiki/references/utilities/performance.md +2 -2
  111. package/wiki/references/utilities/promise.md +4 -4
  112. package/wiki/references/utilities/request.md +2 -2
  113. package/wiki/references/utilities/schema.md +17 -8
@@ -58,7 +58,8 @@ For decorator-based routes, you do not need to explicitly annotate the return ty
58
58
  The generic `@api` decorator allows you to define a route with a full configuration object. The decorated method will automatically have its `context` parameter and return type inferred and type-checked against the provided route configuration. This ensures strong type safety throughout your API definitions.
59
59
 
60
60
  ```typescript
61
- import { api, BaseController, controller, HTTP, jsonContent, jsonResponse, z, TRouteContext } from '@venizia/ignis';
61
+ import { api, BaseController, controller, jsonContent, jsonResponse, z, TRouteContext } from '@venizia/ignis';
62
+ import { HTTP } from '@venizia/ignis-helpers';
62
63
 
63
64
  const MyRouteConfig = {
64
65
  method: 'get',
@@ -89,7 +90,8 @@ For convenience, `Ignis` provides decorator shortcuts for each HTTP method: Thes
89
90
  **Example using `@get` and `@post`:**
90
91
 
91
92
  ```typescript
92
- import { get, post, z, jsonContent, jsonResponse, Authentication, TRouteContext, HTTP } from '@venizia/ignis';
93
+ import { get, post, z, jsonContent, jsonResponse, Authentication, TRouteContext } from '@venizia/ignis';
94
+ import { HTTP } from '@venizia/ignis-helpers';
93
95
 
94
96
  // Define route configs as const
95
97
  const UserRoutes = {
@@ -153,7 +155,8 @@ const UserRoutes = {
153
155
  For better organization, you can define all your route configurations in a constant and reference them in your decorators. This approach also allows you to get a typed context for your handler.
154
156
 
155
157
  ```typescript
156
- import { api, BaseController, controller, TRouteContext, jsonContent, jsonResponse, HTTP } from '@venizia/ignis';
158
+ import { api, BaseController, controller, TRouteContext, jsonContent, jsonResponse } from '@venizia/ignis';
159
+ import { HTTP } from '@venizia/ignis-helpers';
157
160
  import { z } from 'hono/zod-openapi';
158
161
 
159
162
  const RouteConfigs = {
@@ -194,6 +197,29 @@ Manual route definition is useful for:
194
197
  - Complex routing logic that benefits from programmatic control
195
198
  :::
196
199
 
200
+ #### `defineJSXRoute`
201
+
202
+ Define a route that returns server-rendered JSX/HTML:
203
+
204
+ ```typescript
205
+ this.defineJSXRoute({
206
+ configs: {
207
+ path: '/dashboard',
208
+ method: 'get',
209
+ responses: htmlResponse({ description: 'Dashboard page' }),
210
+ },
211
+ handler: async (c) => {
212
+ const data = await this.dashboardService.getData();
213
+ return c.html(<DashboardPage data={data} />);
214
+ },
215
+ hook: (result, c) => {
216
+ // Optional hook for post-processing
217
+ },
218
+ });
219
+ ```
220
+
221
+ Works the same as `defineRoute()` but typed for JSX handler return values.
222
+
197
223
  #### `defineRoute`
198
224
 
199
225
  This method is for creating API endpoints. It now handles both public and authenticated routes by accepting an `authStrategies` array within the `configs`.
@@ -264,7 +290,8 @@ request: {
264
290
  The `defineRouteConfigs` function is a simple helper for creating a typed object containing multiple route configurations. This is particularly useful for organizing all of a controller's route definitions in a single, type-checked constant.
265
291
 
266
292
  ```typescript
267
- import { defineRouteConfigs, HTTP, jsonResponse, jsonContent, z } from '@venizia/ignis';
293
+ import { defineRouteConfigs, jsonResponse, jsonContent, z } from '@venizia/ignis';
294
+ import { HTTP } from '@venizia/ignis-helpers';
268
295
 
269
296
  const RouteConfigs = defineRouteConfigs({
270
297
  ROOT: {
@@ -18,7 +18,7 @@ Technical reference for DataSource classes - managing database connections in Ig
18
18
  | **AbstractDataSource** | Base implementation with logging | Extends `BaseHelper` |
19
19
  | **BaseDataSource** | Concrete class to extend | Auto-discovery, driver from decorator, transaction support |
20
20
  | **ITransaction** | Transaction object | `connector`, `isActive`, `commit()`, `rollback()` |
21
- | **IsolationLevels** | Isolation level constants | `READ_COMMITTED`, `REPEATABLE_READ`, `SERIALIZABLE` |
21
+ | **IsolationLevels** | Isolation level constants | `READ_UNCOMMITTED`, `READ_COMMITTED`, `REPEATABLE_READ`, `SERIALIZABLE` |
22
22
 
23
23
  ## `IDataSource` Interface
24
24
 
@@ -61,6 +61,9 @@ This class extends `AbstractDataSource` and provides a constructor with **auto-d
61
61
  | **Schema Auto-Discovery** | Schema is automatically built from registered `@repository` decorators |
62
62
  | **Manual Override** | You can still manually provide schema in constructor for full control |
63
63
 
64
+ > [!TIP]
65
+ > Set `metadata.autoDiscovery` to `false` in the `@datasource` decorator to disable automatic schema discovery. This is useful when you want to manually provide the schema.
66
+
64
67
  ### Constructor Options
65
68
 
66
69
  ```typescript
@@ -288,7 +291,7 @@ DataSources provide built-in transaction management through the `beginTransactio
288
291
  |------|-------------|
289
292
  | `ITransaction<Schema>` | Transaction object with `commit()`, `rollback()`, and `connector` |
290
293
  | `ITransactionOptions` | Options for starting a transaction (e.g., `isolationLevel`) |
291
- | `TIsolationLevel` | Union type: `'READ COMMITTED'` \| `'REPEATABLE READ'` \| `'SERIALIZABLE'` |
294
+ | `TIsolationLevel` | Union type: `'READ UNCOMMITTED'` \| `'READ COMMITTED'` \| `'REPEATABLE READ'` \| `'SERIALIZABLE'` |
292
295
  | `IsolationLevels` | Static class with isolation level constants and validation |
293
296
 
294
297
  ### ITransaction Interface
@@ -312,6 +315,7 @@ Use the `IsolationLevels` static class for type-safe isolation level constants:
312
315
  import { IsolationLevels } from '@venizia/ignis';
313
316
 
314
317
  // Available levels
318
+ IsolationLevels.READ_UNCOMMITTED // Allows dirty reads (least strict)
315
319
  IsolationLevels.READ_COMMITTED // Default - prevents dirty reads
316
320
  IsolationLevels.REPEATABLE_READ // Consistent reads within transaction
317
321
  IsolationLevels.SERIALIZABLE // Strictest isolation
@@ -191,6 +191,37 @@ class UserController {
191
191
 
192
192
  > **Learn More:** See [Bootstrapping Concepts](/guides/core-concepts/application/bootstrapping)
193
193
 
194
+ ## Request Context Access
195
+
196
+ Access the current Hono request context from anywhere using `useRequestContext()`. This uses Hono's context storage middleware and requires `asyncContext.enable: true` in application config.
197
+
198
+ ```typescript
199
+ import { useRequestContext } from '@venizia/ignis';
200
+
201
+ class MyService extends BaseService {
202
+ async doSomething() {
203
+ const ctx = useRequestContext();
204
+ if (ctx) {
205
+ const userId = ctx.get('currentUser')?.id;
206
+ // Use context data without passing it through parameters
207
+ }
208
+ }
209
+ }
210
+ ```
211
+
212
+ > [!WARNING]
213
+ > `useRequestContext()` returns `undefined` outside of request handling. Always check for `undefined` before accessing context properties.
214
+
215
+ **Setup:** Enable async context in your application:
216
+ ```typescript
217
+ class MyApp extends BaseApplication {
218
+ configs = {
219
+ asyncContext: { enable: true },
220
+ // ...
221
+ };
222
+ }
223
+ ```
224
+
194
225
  ## See Also
195
226
 
196
227
  - **Related Concepts:**
@@ -94,7 +94,9 @@ await repo.find({
94
94
 
95
95
  ## Pagination
96
96
 
97
- ### Limit and Skip
97
+ ### Limit and Skip/Offset
98
+
99
+ Both `skip` and `offset` are supported as aliases — they both map to the SQL `OFFSET` clause. When both are provided, `skip` takes precedence.
98
100
 
99
101
  ```typescript
100
102
  // First 10 results
@@ -107,6 +109,11 @@ await repo.find({
107
109
  filter: { limit: 10, skip: 10 }
108
110
  });
109
111
 
112
+ // Using offset (equivalent to skip)
113
+ await repo.find({
114
+ filter: { limit: 10, offset: 10 }
115
+ });
116
+
110
117
  // Page N formula: skip = (page - 1) * limit
111
118
  const page = 3;
112
119
  const pageSize = 20;
@@ -294,7 +294,8 @@ app.use(requestSpy.value());
294
294
  #### Accessing Request ID
295
295
 
296
296
  ```typescript
297
- import { RequestSpyMiddleware, get, HTTP, jsonResponse, TRouteContext, z } from '@venizia/ignis';
297
+ import { RequestSpyMiddleware, get, jsonResponse, TRouteContext, z } from '@venizia/ignis';
298
+ import { HTTP } from '@venizia/ignis-helpers';
298
299
 
299
300
  const ExampleConfig = {
300
301
  method: HTTP.Methods.GET,
@@ -32,7 +32,7 @@ Fundamental building block wrapping a Drizzle ORM schema.
32
32
  | **Schema Encapsulation** | Holds Drizzle `pgTable` schema for consistent repository access |
33
33
  | **Metadata** | Works with `@model` decorator to mark database entities |
34
34
  | **Schema Generation** | Uses `drizzle-zod` to generate Zod schemas (`SELECT`, `CREATE`, `UPDATE`) |
35
- | **Static Properties** | Supports static `schema`, `relations`, and `TABLE_NAME` for cleaner syntax |
35
+ | **Static Properties** | Supports static `schema`, `relations`, `TABLE_NAME`, and `AUTHORIZATION_SUBJECT` |
36
36
  | **Convenience** | Includes `toObject()` and `toJSON()` methods |
37
37
 
38
38
  ### The `@model` Decorator
@@ -49,6 +49,10 @@ The `@model` decorator marks a class as a database entity and configures its beh
49
49
  settings?: {
50
50
  hiddenProperties?: string[], // Properties to exclude from query results
51
51
  defaultFilter?: TFilter, // Filter applied to all repository queries
52
+ authorize?: { // Authorization settings
53
+ principal: string, // Authorization subject name
54
+ [extra: string | symbol]: any, // Extensible metadata
55
+ },
52
56
  }
53
57
  })
54
58
  ```
@@ -60,6 +64,8 @@ The `@model` decorator marks a class as a database entity and configures its beh
60
64
  | `skipMigrate` | `boolean` | Skip this model during schema migrations |
61
65
  | `settings.hiddenProperties` | `string[]` | Array of property names to exclude from all repository query results |
62
66
  | `settings.defaultFilter` | `TFilter` | Filter automatically applied to all repository queries (see [Default Filter](/references/base/filter-system/default-filter)) |
67
+ | `settings.authorize` | `IModelAuthorizeSettings` | Authorization settings — declares the model's authorization principal (see [Authorization](/references/components/authorization/usage#model-based-resource-references)) |
68
+ | `settings.authorize.principal` | `string` | The authorization subject name for this model. Auto-populates `AUTHORIZATION_SUBJECT` static property |
63
69
 
64
70
  ### Hidden Properties
65
71
 
@@ -233,6 +239,7 @@ export class User extends BaseEntity<typeof userTable> {
233
239
  | `schema` | `TTableSchemaWithId` | Drizzle table schema defined with `pgTable()` |
234
240
  | `relations` | `TValueOrResolver<Array<TRelationConfig>>` | Relation definitions (can be a function for lazy loading) |
235
241
  | `TABLE_NAME` | `string \| undefined` | Optional table name (defaults to class name if not set) |
242
+ | `AUTHORIZATION_SUBJECT` | `string \| undefined` | Authorization principal name. Auto-populated from `@model` settings `authorize.principal` |
236
243
 
237
244
  ### IEntity Interface
238
245
 
@@ -269,6 +276,7 @@ export class BaseEntity<Schema extends TTableSchemaWithId = TTableSchemaWithId>
269
276
  static schema: TTableSchemaWithId;
270
277
  static relations?: TValueOrResolver<Array<TRelationConfig>>;
271
278
  static TABLE_NAME?: string; // Optional, defaults to class name
279
+ static AUTHORIZATION_SUBJECT?: string; // Auto-set by @model decorator from authorize.principal
272
280
 
273
281
  // Static singleton for schemaFactory - shared across all instances
274
282
  // Performance optimization: avoids creating new factory per entity
@@ -834,6 +842,139 @@ export const auditLogTable = pgTable('AuditLog', {
834
842
  ```
835
843
 
836
844
 
845
+ ### `generateDataTypeColumnDefs`
846
+
847
+ Adds polymorphic data storage columns for entities that need to store values of different types in a single table. This is useful for key-value stores, settings tables, or any schema where a row's value type is determined at runtime.
848
+
849
+ **File:** `packages/core/src/base/models/enrichers/data-type.enricher.ts`
850
+
851
+ #### Signature
852
+
853
+ ```typescript
854
+ generateDataTypeColumnDefs(opts?: TDataTypeEnricherOptions): {
855
+ dataType: PgTextBuilderInitial;
856
+ nValue: PgDoublePrecisionBuilderInitial;
857
+ tValue: PgTextBuilderInitial;
858
+ bValue: PgCustomColumnBuilder<Buffer>;
859
+ jValue: PgJsonbBuilderInitial<Record<string, any>>;
860
+ boValue: PgBooleanBuilderInitial;
861
+ }
862
+ ```
863
+
864
+ #### Options (`TDataTypeEnricherOptions`)
865
+
866
+ ```typescript
867
+ type TDataTypeEnricherOptions = {
868
+ defaultValue: Partial<{
869
+ dataType: string;
870
+ nValue: number;
871
+ tValue: string;
872
+ bValue: Buffer;
873
+ jValue: object;
874
+ boValue: boolean;
875
+ }>;
876
+ };
877
+ ```
878
+
879
+ #### Generated Columns
880
+
881
+ | Column | SQL Type | DB Column Name | TypeScript Type | Purpose |
882
+ |--------|----------|----------------|-----------------|---------|
883
+ | `dataType` | `text` | `data_type` | `string` | Type discriminator (e.g., `'number'`, `'text'`, `'json'`) |
884
+ | `nValue` | `double precision` | `n_value` | `number` | Numeric values |
885
+ | `tValue` | `text` | `t_value` | `string` | Text values |
886
+ | `bValue` | `bytea` | `b_value` | `Buffer` | Binary values |
887
+ | `jValue` | `jsonb` | `j_value` | `Record<string, any>` | JSON values |
888
+ | `boValue` | `boolean` | `bo_value` | `boolean` | Boolean values |
889
+
890
+ All columns are **nullable** by default (no `NOT NULL` constraint), since only one value column is typically populated per row depending on the `dataType` discriminator.
891
+
892
+ #### Usage Examples
893
+
894
+ **Basic usage:**
895
+
896
+ ```typescript
897
+ import { pgTable } from 'drizzle-orm/pg-core';
898
+ import { BaseEntity, model, generateIdColumnDefs, generateDataTypeColumnDefs } from '@venizia/ignis';
899
+
900
+ @model({ type: 'entity' })
901
+ export class Setting extends BaseEntity<typeof Setting.schema> {
902
+ static override schema = pgTable('Setting', {
903
+ ...generateIdColumnDefs({ id: { dataType: 'string' } }),
904
+ ...generateDataTypeColumnDefs(),
905
+ });
906
+ }
907
+ ```
908
+
909
+ **With default values:**
910
+
911
+ ```typescript
912
+ export const settingTable = pgTable('Setting', {
913
+ ...generateIdColumnDefs({ id: { dataType: 'string' } }),
914
+ ...generateDataTypeColumnDefs({
915
+ defaultValue: { dataType: 'text', tValue: '' },
916
+ }),
917
+ });
918
+
919
+ // Generates columns with SQL defaults:
920
+ // data_type text DEFAULT 'text'
921
+ // t_value text DEFAULT ''
922
+ // nValue, bValue, jValue, boValue — no defaults
923
+ ```
924
+
925
+ **Key-value store pattern:**
926
+
927
+ ```typescript
928
+ @model({ type: 'entity' })
929
+ export class AppConfig extends BaseEntity<typeof AppConfig.schema> {
930
+ static override schema = pgTable('AppConfig', {
931
+ ...generateIdColumnDefs({ id: { dataType: 'string' } }),
932
+ ...generateDataTypeColumnDefs(),
933
+ key: text('key').notNull().unique(),
934
+ description: text('description'),
935
+ });
936
+ }
937
+
938
+ // Usage:
939
+ // { key: 'max_retries', dataType: 'number', nValue: 3 }
940
+ // { key: 'welcome_message', dataType: 'text', tValue: 'Hello!' }
941
+ // { key: 'feature_flags', dataType: 'json', jValue: { darkMode: true } }
942
+ // { key: 'is_maintenance', dataType: 'boolean', boValue: false }
943
+ ```
944
+
945
+ ### `enrichDataTypes`
946
+
947
+ A convenience function that merges data type columns into an existing schema object, rather than spreading into `pgTable`.
948
+
949
+ #### Signature
950
+
951
+ ```typescript
952
+ enrichDataTypes(
953
+ baseSchema: TColumnDefinitions,
954
+ opts?: TDataTypeEnricherOptions,
955
+ ): TColumnDefinitions
956
+ ```
957
+
958
+ #### Usage
959
+
960
+ ```typescript
961
+ import { text } from 'drizzle-orm/pg-core';
962
+ import { enrichDataTypes, generateIdColumnDefs } from '@venizia/ignis';
963
+
964
+ const baseColumns = {
965
+ ...generateIdColumnDefs({ id: { dataType: 'string' } }),
966
+ key: text('key').notNull(),
967
+ };
968
+
969
+ // Merge data type columns into existing column definitions
970
+ const allColumns = enrichDataTypes(baseColumns);
971
+
972
+ export const configTable = pgTable('Config', allColumns);
973
+ ```
974
+
975
+ This is equivalent to spreading `generateDataTypeColumnDefs()` directly but useful when building column definitions programmatically.
976
+
977
+
837
978
  ## Schema Utilities
838
979
 
839
980
  ### `snakeToCamel`
@@ -902,7 +1043,8 @@ console.log(result);
902
1043
  **Use case:** API endpoint that accepts snake_case but works with camelCase internally
903
1044
 
904
1045
  ```typescript
905
- import { BaseController, controller, snakeToCamel, HTTP } from '@venizia/ignis';
1046
+ import { BaseController, controller, snakeToCamel } from '@venizia/ignis';
1047
+ import { HTTP } from '@venizia/ignis-helpers';
906
1048
  import { z } from '@hono/zod-openapi';
907
1049
 
908
1050
  const createUserSchema = snakeToCamel({
@@ -328,7 +328,7 @@ if (user) {
328
328
  - `find<R>()`, `findOne<R>()`, `findById<R>()`
329
329
  - `create<R>()`, `createAll<R>()`
330
330
  - `updateById<R>()`, `updateAll<R>()`
331
- - `deleteById<R>()`, `deleteAll<R>()`
331
+ - `deleteById<R>()`, `deleteAll<R>()`, `deleteBy<R>()`
332
332
 
333
333
 
334
334
  ## Debugging
@@ -355,7 +355,7 @@ await repo.updateById({
355
355
  });
356
356
  ```
357
357
 
358
- **Available on:** `create`, `createAll`, `updateById`, `updateAll`, `deleteById`, `deleteAll`
358
+ **Available on:** `create`, `createAll`, `updateById`, `updateAll`, `deleteById`, `deleteAll`, `deleteBy`
359
359
 
360
360
  ### Query Interface Validation
361
361
 
@@ -29,8 +29,9 @@ export class TodoRepository extends DefaultCRUDRepository<typeof Todo.schema> {
29
29
  | **ReadableRepository** | Read-only operations | Views, external tables |
30
30
  | **PersistableRepository** | Read + Write operations | Rarely used directly |
31
31
  | **DefaultCRUDRepository** | Full CRUD operations | Standard data tables |
32
+ | **SoftDeletableRepository** | CRUD + soft delete + restore | Tables with `deletedAt` column |
32
33
 
33
- **Most common:** Extend `DefaultCRUDRepository` for standard tables.
34
+ **Most common:** Extend `DefaultCRUDRepository` for standard tables, or `SoftDeletableRepository` for soft-delete patterns.
34
35
 
35
36
 
36
37
  ## Available Methods
@@ -54,6 +55,7 @@ export class TodoRepository extends DefaultCRUDRepository<typeof Todo.schema> {
54
55
  | `updateAll(opts)` | Update matching records | `repo.updateAll({ where: { status: 'draft' }, data: { status: 'published' } })` |
55
56
  | `deleteById(opts)` | Delete by primary key | `repo.deleteById({ id: '123' })` |
56
57
  | `deleteAll(opts)` | Delete matching records | `repo.deleteAll({ where: { status: 'archived' } })` |
58
+ | `deleteBy(opts)` | Delete by where condition | `repo.deleteBy({ where: { status: 'archived' } })` |
57
59
 
58
60
 
59
61
  ## Documentation Sections
@@ -94,6 +96,22 @@ await repo.find({
94
96
  });
95
97
  ```
96
98
 
99
+ ### [SoftDeletableRepository](./soft-deletable.md)
100
+ Soft-delete and restore operations using `deletedAt` timestamps instead of physical deletion.
101
+
102
+ ```typescript
103
+ // Preview
104
+ @repository({ model: Category, dataSource: PostgresDataSource })
105
+ export class CategoryRepository extends SoftDeletableRepository<typeof Category.schema> {}
106
+
107
+ // Soft delete (sets deletedAt)
108
+ await repo.deleteById({ id: '123' });
109
+ // Restore
110
+ await repo.restoreById({ id: '123' });
111
+ // Hard delete (physical removal)
112
+ await repo.deleteById({ id: '123', options: { shouldHardDelete: true } });
113
+ ```
114
+
97
115
  ### [Advanced Features](./advanced.md)
98
116
  Transactions, hidden properties, default filter bypass, performance optimization, and type inference.
99
117
 
@@ -193,6 +211,10 @@ await repo.deleteAll({ where: {}, options: { force: true } });
193
211
  | Create one | `repo.create({ data: { name: 'John' } })` |
194
212
  | Update by ID | `repo.updateById({ id: '123', data: { name: 'Jane' } })` |
195
213
  | Delete by ID | `repo.deleteById({ id: '123' })` |
214
+ | Delete by condition | `repo.deleteBy({ where: { status: 'archived' } })` |
215
+ | Soft delete | `repo.deleteById({ id: '123' })` (with `SoftDeletableRepository`) |
216
+ | Restore soft-deleted | `repo.restoreById({ id: '123' })` (with `SoftDeletableRepository`) |
217
+ | Hard delete (bypass soft) | `repo.deleteById({ id: '123', options: { shouldHardDelete: true } })` |
196
218
  | Count matching | `repo.count({ where: { status: 'active' } })` |
197
219
  | Check exists | `repo.existsWith({ where: { email: 'test@example.com' } })` |
198
220
 
@@ -201,6 +223,7 @@ await repo.deleteAll({ where: {}, options: { force: true } });
201
223
 
202
224
  - **New to filtering?** Start with [Filter System](/references/base/filter-system/)
203
225
  - **Need related data?** See [Relations & Includes](./relations.md)
226
+ - **Need soft delete?** See [SoftDeletableRepository](./soft-deletable.md)
204
227
  - **Need transactions?** Go to [Advanced Features](./advanced.md)
205
228
 
206
229
  ## See Also
@@ -0,0 +1,213 @@
1
+ ---
2
+ title: SoftDeletableRepository
3
+ description: Repository with soft-delete and restore operations using deletedAt timestamps
4
+ difficulty: intermediate
5
+ ---
6
+
7
+ # SoftDeletableRepository
8
+
9
+ A repository that overrides delete operations to set a `deletedAt` timestamp instead of physically removing records. Extends `DefaultCRUDRepository` with restore capabilities.
10
+
11
+ **File:** `packages/core/src/base/repositories/core/soft-deletable.ts`
12
+
13
+
14
+ ## Setup
15
+
16
+ ### 1. Define Model with Soft Delete
17
+
18
+ ```typescript
19
+ import { pgTable, text, timestamp } from 'drizzle-orm/pg-core';
20
+ import {
21
+ BaseEntity,
22
+ model,
23
+ generateIdColumnDefs,
24
+ generateTzColumnDefs,
25
+ } from '@venizia/ignis';
26
+
27
+ @model({
28
+ type: 'entity',
29
+ settings: {
30
+ hiddenProperties: ['deletedAt'],
31
+ defaultFilter: { where: { deletedAt: null } },
32
+ },
33
+ })
34
+ export class Category extends BaseEntity<typeof Category.schema> {
35
+ static override schema = pgTable('Category', {
36
+ ...generateIdColumnDefs({ id: { dataType: 'string' } }),
37
+ ...generateTzColumnDefs(),
38
+ name: text('name').notNull(),
39
+ deletedAt: timestamp('deleted_at', { mode: 'date', withTimezone: true }),
40
+ });
41
+ }
42
+ ```
43
+
44
+ > [!IMPORTANT]
45
+ > - The model **must** have a `deletedAt` column (`Date | null`).
46
+ > - Set `defaultFilter: { where: { deletedAt: null } }` so soft-deleted records are excluded by default.
47
+ > - Optionally add `deletedAt` to `hiddenProperties` to hide it from API responses.
48
+
49
+ ### 2. Create Repository
50
+
51
+ ```typescript
52
+ import { repository, SoftDeletableRepository } from '@venizia/ignis';
53
+ import { Category } from '@/models/category.model';
54
+ import { PostgresDataSource } from '@/datasources/postgres.datasource';
55
+
56
+ @repository({ model: Category, dataSource: PostgresDataSource })
57
+ export class CategoryRepository extends SoftDeletableRepository<typeof Category.schema> {}
58
+ ```
59
+
60
+
61
+ ## Delete Operations
62
+
63
+ All delete methods set `deletedAt = new Date()` instead of removing the row. They internally call the corresponding `update` method.
64
+
65
+ ### deleteById
66
+
67
+ ```typescript
68
+ // Soft delete — sets deletedAt timestamp
69
+ const result = await repo.deleteById({ id: '123' });
70
+ // { count: 1, data: { id: '123', name: 'Electronics', deletedAt: '2026-03-06T...' } }
71
+
72
+ // Hard delete — physically removes the row
73
+ const result = await repo.deleteById({
74
+ id: '123',
75
+ options: { shouldHardDelete: true },
76
+ });
77
+ ```
78
+
79
+ ### deleteAll
80
+
81
+ ```typescript
82
+ // Soft delete all matching records
83
+ const result = await repo.deleteAll({
84
+ where: { status: 'archived' },
85
+ options: { force: true },
86
+ });
87
+
88
+ // Hard delete all matching records
89
+ const result = await repo.deleteAll({
90
+ where: { status: 'archived' },
91
+ options: { shouldHardDelete: true, force: true },
92
+ });
93
+ ```
94
+
95
+ ### deleteBy
96
+
97
+ ```typescript
98
+ // Soft delete by where condition (requires non-empty where)
99
+ const result = await repo.deleteBy({
100
+ where: { name: 'Obsolete' },
101
+ });
102
+ ```
103
+
104
+
105
+ ## Restore Operations
106
+
107
+ Restore methods set `deletedAt = null` and automatically use `shouldSkipDefaultFilter: true` to find soft-deleted records.
108
+
109
+ ### restoreById
110
+
111
+ ```typescript
112
+ const result = await repo.restoreById({ id: '123' });
113
+ // { count: 1, data: { id: '123', name: 'Electronics', deletedAt: null } }
114
+
115
+ // Without returning data
116
+ const result = await repo.restoreById({
117
+ id: '123',
118
+ options: { shouldReturn: false },
119
+ });
120
+ ```
121
+
122
+ ### restoreAll
123
+
124
+ ```typescript
125
+ // Restore all soft-deleted records (requires force for empty where)
126
+ const result = await repo.restoreAll({
127
+ where: {},
128
+ options: { force: true },
129
+ });
130
+
131
+ // Restore matching records
132
+ const result = await repo.restoreAll({
133
+ where: { name: 'Electronics' },
134
+ });
135
+ ```
136
+
137
+ ### restoreBy
138
+
139
+ ```typescript
140
+ // Alias for restoreAll with required where
141
+ const result = await repo.restoreBy({
142
+ where: { status: 'archived' },
143
+ });
144
+ ```
145
+
146
+
147
+ ## Read Operations
148
+
149
+ ### findById with isStrict
150
+
151
+ `SoftDeletableRepository` overrides `findById` to support a `isStrict` option that throws a `404 Not Found` error when the record doesn't exist:
152
+
153
+ ```typescript
154
+ // Returns null if not found (default)
155
+ const category = await repo.findById({ id: '123' });
156
+
157
+ // Throws 404 if not found
158
+ const category = await repo.findById({
159
+ id: '123',
160
+ options: { isStrict: true },
161
+ });
162
+ ```
163
+
164
+
165
+ ## Options Reference
166
+
167
+ ### Delete Options
168
+
169
+ | Option | Type | Default | Description |
170
+ |--------|------|---------|-------------|
171
+ | `shouldHardDelete` | `boolean` | `false` | Bypass soft delete and physically remove the row |
172
+ | `shouldReturn` | `boolean` | `true` | Return the updated/deleted record |
173
+ | `force` | `boolean` | `false` | Allow empty `where` condition (deleteAll/deleteBy) |
174
+ | `transaction` | `ITransaction` | — | Transaction context |
175
+
176
+ ### Restore Options
177
+
178
+ | Option | Type | Default | Description |
179
+ |--------|------|---------|-------------|
180
+ | `shouldReturn` | `boolean` | `true` | Return the restored record |
181
+ | `force` | `boolean` | `false` | Allow empty `where` condition (restoreAll) |
182
+ | `transaction` | `ITransaction` | — | Transaction context |
183
+
184
+
185
+ ## How It Works
186
+
187
+ | Operation | Behavior |
188
+ |-----------|----------|
189
+ | `deleteById` | `UPDATE SET deletedAt = NOW() WHERE id = ?` |
190
+ | `deleteAll` | `UPDATE SET deletedAt = NOW() WHERE ...` |
191
+ | `restoreById` | `UPDATE SET deletedAt = NULL WHERE id = ?` (skips default filter) |
192
+ | `restoreAll` | `UPDATE SET deletedAt = NULL WHERE ...` (skips default filter) |
193
+ | `find` / `findOne` | Default filter automatically excludes `deletedAt IS NOT NULL` |
194
+
195
+ > [!TIP]
196
+ > Restore operations automatically set `shouldSkipDefaultFilter: true` so they can find soft-deleted records that would normally be hidden by the default filter.
197
+
198
+
199
+ ## With Transactions
200
+
201
+ ```typescript
202
+ const tx = await this.dataSource.beginTransaction();
203
+ try {
204
+ await this.categoryRepo.deleteById({ id: '123', options: { transaction: tx } });
205
+ await this.auditRepo.create({
206
+ data: { action: 'soft_delete', entityId: '123' },
207
+ options: { transaction: tx },
208
+ });
209
+ await tx.commit();
210
+ } catch {
211
+ await tx.rollback();
212
+ }
213
+ ```
@@ -64,7 +64,8 @@ Services are the core of your application's logic. They act as a bridge between
64
64
  ### Example
65
65
 
66
66
  ```typescript
67
- import { BaseService, inject, getError } from '@venizia/ignis';
67
+ import { BaseService, inject } from '@venizia/ignis';
68
+ import { getError } from '@venizia/ignis-helpers';
68
69
  import { UserRepository } from '../repositories/user.repository';
69
70
  import { TUser } from '../models/entities';
70
71