@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 +1 -1
- package/wiki/references/base/application.md +28 -0
- package/wiki/references/base/controllers.md +23 -0
- package/wiki/references/base/datasources.md +6 -2
- package/wiki/references/base/dependency-injection.md +31 -0
- package/wiki/references/base/filter-system/fields-order-pagination.md +8 -1
- package/wiki/references/base/models.md +133 -0
- package/wiki/references/base/repositories/advanced.md +2 -2
- package/wiki/references/base/repositories/index.md +24 -1
- package/wiki/references/base/repositories/soft-deletable.md +213 -0
- package/wiki/references/components/authentication/usage.md +166 -0
- package/wiki/references/configuration/index.md +38 -0
- package/wiki/references/helpers/kafka/admin.md +173 -0
- package/wiki/references/helpers/kafka/consumer.md +473 -0
- package/wiki/references/helpers/kafka/examples.md +234 -0
- package/wiki/references/helpers/kafka/index.md +397 -220
- package/wiki/references/helpers/kafka/producer.md +269 -0
- package/wiki/references/utilities/parse.md +20 -0
- package/wiki/references/utilities/schema.md +17 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@venizia/ignis-docs",
|
|
3
|
-
"version": "0.0.7-
|
|
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
|
+
```
|