@venizia/ignis-docs 0.0.7-0 → 0.0.7-2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@venizia/ignis-docs",
3
- "version": "0.0.7-0",
3
+ "version": "0.0.7-2",
4
4
  "description": "Interactive documentation site and MCP (Model Context Protocol) server for the Ignis Framework. Includes a VitePress-powered documentation site with guides, API references, and best practices. Ships an MCP server (CLI: ignis-docs-mcp) with 11 tools for AI assistants to search docs, browse source code, verify dependencies, and access real-time framework knowledge. Built with Mastra MCP SDK and Fuse.js fuzzy search.",
5
5
  "keywords": [
6
6
  "ai",
@@ -49,6 +49,34 @@ Extends `AbstractApplication` with concrete lifecycle implementations and resour
49
49
  | `repository(MyRepository, opts?)`| `repositories.MyRepository` (default) or custom key via `opts.binding` |
50
50
  | `dataSource(MyDataSource, opts?)`| `datasources.MyDataSource` (default) or custom key via `opts.binding` |
51
51
 
52
+ > [!TIP]
53
+ > All registration methods accept an optional `opts.binding` parameter to override the default namespace-based key:
54
+ > ```typescript
55
+ > this.controller(UserController, {
56
+ > binding: { namespace: 'controllers', key: 'CustomUserController' },
57
+ > });
58
+ > ```
59
+
60
+ ### registerDynamicBindings
61
+
62
+ Protected method for handling late-registration and circular dependency patterns. Iterates bindings in a namespace, configuring each instance and re-fetching to pick up dynamically added bindings.
63
+
64
+ ```typescript
65
+ protected async registerDynamicBindings<T extends IConfigurable>(opts: {
66
+ namespace: TBindingNamespace;
67
+ onBeforeConfigure?: (opts: { binding: Binding<T> }) => Promise<void>;
68
+ onAfterConfigure?: (opts: { binding: Binding<T>; instance: T }) => Promise<void>;
69
+ }): Promise<void>
70
+ ```
71
+
72
+ | Parameter | Type | Description |
73
+ |-----------|------|-------------|
74
+ | `namespace` | `TBindingNamespace` | Binding namespace to scan (e.g., `'components'`, `'controllers'`) |
75
+ | `onBeforeConfigure` | callback | Called before each binding's `configure()` — use for validation |
76
+ | `onAfterConfigure` | callback | Called after `configure()` — use for route mounting, post-setup |
77
+
78
+ The method tracks already-configured bindings to prevent duplicates and re-fetches after each configuration to handle bindings registered during the configure phase.
79
+
52
80
  ### `initialize()` Method Flow
53
81
 
54
82
  Startup sequence executed by the `initialize()` method:
@@ -197,6 +197,29 @@ Manual route definition is useful for:
197
197
  - Complex routing logic that benefits from programmatic control
198
198
  :::
199
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
+
200
223
  #### `defineRoute`
201
224
 
202
225
  This method is for creating API endpoints. It now handles both public and authenticated routes by accepting an `authStrategies` array within the `configs`.
@@ -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;
@@ -842,6 +842,139 @@ export const auditLogTable = pgTable('AuditLog', {
842
842
  ```
843
843
 
844
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
+
845
978
  ## Schema Utilities
846
979
 
847
980
  ### `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
+ ```