@venizia/ignis-docs 0.0.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.
- package/mcp-server/dist/common/config.d.ts +27 -0
- package/mcp-server/dist/common/config.d.ts.map +1 -0
- package/mcp-server/dist/common/config.js +27 -0
- package/mcp-server/dist/common/config.js.map +1 -0
- package/mcp-server/dist/common/index.d.ts +3 -0
- package/mcp-server/dist/common/index.d.ts.map +1 -0
- package/mcp-server/dist/common/index.js +19 -0
- package/mcp-server/dist/common/index.js.map +1 -0
- package/mcp-server/dist/common/paths.d.ts +13 -0
- package/mcp-server/dist/common/paths.d.ts.map +1 -0
- package/mcp-server/dist/common/paths.js +23 -0
- package/mcp-server/dist/common/paths.js.map +1 -0
- package/mcp-server/dist/helpers/docs.helper.d.ts +81 -0
- package/mcp-server/dist/helpers/docs.helper.d.ts.map +1 -0
- package/mcp-server/dist/helpers/docs.helper.js +171 -0
- package/mcp-server/dist/helpers/docs.helper.js.map +1 -0
- package/mcp-server/dist/helpers/index.d.ts +3 -0
- package/mcp-server/dist/helpers/index.d.ts.map +1 -0
- package/mcp-server/dist/helpers/index.js +19 -0
- package/mcp-server/dist/helpers/index.js.map +1 -0
- package/mcp-server/dist/helpers/logger.helper.d.ts +7 -0
- package/mcp-server/dist/helpers/logger.helper.d.ts.map +1 -0
- package/mcp-server/dist/helpers/logger.helper.js +22 -0
- package/mcp-server/dist/helpers/logger.helper.js.map +1 -0
- package/mcp-server/dist/index.d.ts +3 -0
- package/mcp-server/dist/index.d.ts.map +1 -0
- package/mcp-server/dist/index.js +62 -0
- package/mcp-server/dist/index.js.map +1 -0
- package/mcp-server/dist/tools/base.tool.d.ts +98 -0
- package/mcp-server/dist/tools/base.tool.d.ts.map +1 -0
- package/mcp-server/dist/tools/base.tool.js +47 -0
- package/mcp-server/dist/tools/base.tool.js.map +1 -0
- package/mcp-server/dist/tools/get-doc-content.tool.d.ts +30 -0
- package/mcp-server/dist/tools/get-doc-content.tool.d.ts.map +1 -0
- package/mcp-server/dist/tools/get-doc-content.tool.js +127 -0
- package/mcp-server/dist/tools/get-doc-content.tool.js.map +1 -0
- package/mcp-server/dist/tools/get-doc-metadata.tool.d.ts +40 -0
- package/mcp-server/dist/tools/get-doc-metadata.tool.d.ts.map +1 -0
- package/mcp-server/dist/tools/get-doc-metadata.tool.js +121 -0
- package/mcp-server/dist/tools/get-doc-metadata.tool.js.map +1 -0
- package/mcp-server/dist/tools/index.d.ts +8 -0
- package/mcp-server/dist/tools/index.d.ts.map +1 -0
- package/mcp-server/dist/tools/index.js +18 -0
- package/mcp-server/dist/tools/index.js.map +1 -0
- package/mcp-server/dist/tools/list-categories.tool.d.ts +20 -0
- package/mcp-server/dist/tools/list-categories.tool.d.ts.map +1 -0
- package/mcp-server/dist/tools/list-categories.tool.js +105 -0
- package/mcp-server/dist/tools/list-categories.tool.js.map +1 -0
- package/mcp-server/dist/tools/list-docs.tool.d.ts +32 -0
- package/mcp-server/dist/tools/list-docs.tool.d.ts.map +1 -0
- package/mcp-server/dist/tools/list-docs.tool.js +121 -0
- package/mcp-server/dist/tools/list-docs.tool.js.map +1 -0
- package/mcp-server/dist/tools/search-docs.tool.d.ts +32 -0
- package/mcp-server/dist/tools/search-docs.tool.d.ts.map +1 -0
- package/mcp-server/dist/tools/search-docs.tool.js +120 -0
- package/mcp-server/dist/tools/search-docs.tool.js.map +1 -0
- package/package.json +102 -0
- package/wiki/get-started/5-minute-quickstart.md +266 -0
- package/wiki/get-started/best-practices/api-usage-examples.md +222 -0
- package/wiki/get-started/best-practices/architectural-patterns.md +129 -0
- package/wiki/get-started/best-practices/code-style-standards.md +122 -0
- package/wiki/get-started/best-practices/common-pitfalls.md +136 -0
- package/wiki/get-started/best-practices/contribution-workflow.md +145 -0
- package/wiki/get-started/best-practices/deployment-strategies.md +121 -0
- package/wiki/get-started/best-practices/performance-optimization.md +88 -0
- package/wiki/get-started/best-practices/security-guidelines.md +97 -0
- package/wiki/get-started/best-practices/troubleshooting-tips.md +100 -0
- package/wiki/get-started/building-a-crud-api.md +717 -0
- package/wiki/get-started/core-concepts/application.md +168 -0
- package/wiki/get-started/core-concepts/components.md +96 -0
- package/wiki/get-started/core-concepts/controllers.md +441 -0
- package/wiki/get-started/core-concepts/dependency-injection.md +160 -0
- package/wiki/get-started/core-concepts/persistent.md +591 -0
- package/wiki/get-started/core-concepts/services.md +88 -0
- package/wiki/get-started/index.md +65 -0
- package/wiki/get-started/mcp-docs-server.md +840 -0
- package/wiki/get-started/philosophy.md +123 -0
- package/wiki/get-started/prerequisites.md +113 -0
- package/wiki/get-started/quickstart.md +382 -0
- package/wiki/index.md +48 -0
- package/wiki/references/base/application.md +67 -0
- package/wiki/references/base/components.md +80 -0
- package/wiki/references/base/controllers.md +361 -0
- package/wiki/references/base/datasources.md +105 -0
- package/wiki/references/base/dependency-injection.md +83 -0
- package/wiki/references/base/models.md +104 -0
- package/wiki/references/base/repositories.md +118 -0
- package/wiki/references/base/services.md +97 -0
- package/wiki/references/components/authentication.md +224 -0
- package/wiki/references/components/health-check.md +190 -0
- package/wiki/references/components/index.md +61 -0
- package/wiki/references/components/request-tracker.md +35 -0
- package/wiki/references/components/socket-io.md +127 -0
- package/wiki/references/components/swagger.md +175 -0
- package/wiki/references/helpers/cron.md +94 -0
- package/wiki/references/helpers/crypto.md +117 -0
- package/wiki/references/helpers/env.md +67 -0
- package/wiki/references/helpers/error.md +80 -0
- package/wiki/references/helpers/index.md +21 -0
- package/wiki/references/helpers/inversion.md +141 -0
- package/wiki/references/helpers/logger.md +98 -0
- package/wiki/references/helpers/network.md +143 -0
- package/wiki/references/helpers/queue.md +131 -0
- package/wiki/references/helpers/redis.md +121 -0
- package/wiki/references/helpers/socket-io.md +103 -0
- package/wiki/references/helpers/storage.md +130 -0
- package/wiki/references/helpers/testing.md +115 -0
- package/wiki/references/helpers/worker-thread.md +162 -0
- package/wiki/references/src-details/core.md +249 -0
- package/wiki/references/src-details/dev-configs.md +302 -0
- package/wiki/references/src-details/docs.md +61 -0
- package/wiki/references/src-details/helpers.md +211 -0
- package/wiki/references/src-details/inversion.md +345 -0
- package/wiki/references/src-details/mcp-server.md +726 -0
- package/wiki/references/utilities/crypto.md +39 -0
- package/wiki/references/utilities/date.md +72 -0
- package/wiki/references/utilities/index.md +12 -0
- package/wiki/references/utilities/module.md +40 -0
- package/wiki/references/utilities/parse.md +68 -0
- package/wiki/references/utilities/performance.md +64 -0
- package/wiki/references/utilities/promise.md +83 -0
- package/wiki/references/utilities/request.md +66 -0
- package/wiki/references/utilities/schema.md +88 -0
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
# Persistent Layer: Models, DataSources, and Repositories
|
|
2
|
+
|
|
3
|
+
The persistent layer manages data using [Drizzle ORM](https://orm.drizzle.team/) for type-safe database access and the Repository pattern for data abstraction.
|
|
4
|
+
|
|
5
|
+
**Three main components:**
|
|
6
|
+
- **Models** - Define data structure (Drizzle schemas + Entity classes)
|
|
7
|
+
- **DataSources** - Manage database connections
|
|
8
|
+
- **Repositories** - Provide CRUD operations
|
|
9
|
+
|
|
10
|
+
## 1. Models: Defining Your Data Structure
|
|
11
|
+
|
|
12
|
+
A model in `Ignis` consists of two parts: a **Drizzle schema** that defines the database table and an **Entity class** that wraps it for use within the framework.
|
|
13
|
+
|
|
14
|
+
### Creating a Basic Model
|
|
15
|
+
|
|
16
|
+
Here's how to create a simple `User` model.
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
// src/models/entities/user.model.ts
|
|
20
|
+
import {
|
|
21
|
+
BaseEntity,
|
|
22
|
+
createRelations,
|
|
23
|
+
extraUserColumns,
|
|
24
|
+
generateIdColumnDefs,
|
|
25
|
+
model,
|
|
26
|
+
TTableObject,
|
|
27
|
+
} from '@venizia/ignis';
|
|
28
|
+
import { pgTable } from 'drizzle-orm/pg-core';
|
|
29
|
+
|
|
30
|
+
// 1. Define the Drizzle schema for the 'User' table
|
|
31
|
+
export const userTable = pgTable(User.name, {
|
|
32
|
+
...generateIdColumnDefs({ id: { dataType: 'string' } }),
|
|
33
|
+
...extraUserColumns({ idType: 'string' }),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// 2. Define relations (empty for now, but required)
|
|
37
|
+
export const userRelations = createRelations({
|
|
38
|
+
source: userTable,
|
|
39
|
+
relations: [],
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// 3. Define the TypeScript type for a User object
|
|
43
|
+
export type TUserSchema = typeof userTable;
|
|
44
|
+
export type TUser = TTableObject<TUserSchema>;
|
|
45
|
+
|
|
46
|
+
// 4. Create the Entity class, decorated with @model
|
|
47
|
+
@model({ type: 'entity' })
|
|
48
|
+
export class User extends BaseEntity<TUserSchema> {
|
|
49
|
+
static readonly TABLE_NAME = User.name;
|
|
50
|
+
|
|
51
|
+
constructor() {
|
|
52
|
+
super({ name: User.name, schema: userTable });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Understanding Enrichers: The Smart Column Generators
|
|
58
|
+
|
|
59
|
+
You might have noticed functions like `generateIdColumnDefs()` and `extraUserColumns()` in the model definition. These are **Enrichers**—powerful helper functions that generate common database columns automatically.
|
|
60
|
+
|
|
61
|
+
#### Why Enrichers Exist
|
|
62
|
+
|
|
63
|
+
**Without enrichers (the hard way):**
|
|
64
|
+
```typescript
|
|
65
|
+
export const userTable = pgTable('User', {
|
|
66
|
+
// Manually define every common column in every table
|
|
67
|
+
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
|
|
68
|
+
status: text('status').notNull().default('ACTIVE'),
|
|
69
|
+
type: text('type'),
|
|
70
|
+
createdBy: text('created_by'),
|
|
71
|
+
modifiedBy: text('modified_by'),
|
|
72
|
+
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
73
|
+
modifiedAt: timestamp('modified_at', { withTimezone: true }).notNull().defaultNow(),
|
|
74
|
+
// ... your actual user-specific fields
|
|
75
|
+
email: text('email').notNull(),
|
|
76
|
+
name: text('name'),
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**With enrichers (the smart way):**
|
|
81
|
+
```typescript
|
|
82
|
+
export const userTable = pgTable('User', {
|
|
83
|
+
...generateIdColumnDefs({ id: { dataType: 'string' } }), // Adds: id (UUID)
|
|
84
|
+
...extraUserColumns({ idType: 'string' }), // Adds: status, type, createdBy, modifiedBy, createdAt, modifiedAt
|
|
85
|
+
// Just your actual user-specific fields
|
|
86
|
+
email: text('email').notNull(),
|
|
87
|
+
name: text('name'),
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Result:** Same table structure, but with:
|
|
92
|
+
- 7 fewer lines of code
|
|
93
|
+
- Guaranteed consistency across all tables
|
|
94
|
+
- Less chance of typos or mistakes
|
|
95
|
+
- Easier to maintain
|
|
96
|
+
|
|
97
|
+
#### Common Enrichers
|
|
98
|
+
|
|
99
|
+
| Enricher | What It Adds | Use Case |
|
|
100
|
+
| :--- | :--- | :--- |
|
|
101
|
+
| `generateIdColumnDefs()` | Primary key `id` column (UUID or number) | Every table needs an ID |
|
|
102
|
+
| `generateTzColumnDefs()` | `createdAt` and `modifiedAt` timestamps | Track when records are created/updated |
|
|
103
|
+
| `generateUserAuditColumnDefs()` | `createdBy` and `modifiedBy` foreign keys | Track which user created/updated records |
|
|
104
|
+
| `generateDataTypeColumnDefs()` | `dataType` and type-specific value columns (`tValue`, `nValue`, etc.) | Configuration tables with mixed data types |
|
|
105
|
+
| `extraUserColumns()` | Combination of audit + status + type fields | Full-featured entity tables |
|
|
106
|
+
|
|
107
|
+
#### Practical Example: Building a Post Model
|
|
108
|
+
|
|
109
|
+
Let's create a blog post model using enrichers:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
// src/models/post.model.ts
|
|
113
|
+
import {
|
|
114
|
+
BaseEntity,
|
|
115
|
+
createRelations,
|
|
116
|
+
generateIdColumnDefs,
|
|
117
|
+
generateTzColumnDefs,
|
|
118
|
+
generateUserAuditColumnDefs,
|
|
119
|
+
model,
|
|
120
|
+
RelationTypes,
|
|
121
|
+
TTableObject,
|
|
122
|
+
} from '@venizia/ignis';
|
|
123
|
+
import { pgTable, text, boolean } from 'drizzle-orm/pg-core';
|
|
124
|
+
import { userTable } from './user.model';
|
|
125
|
+
|
|
126
|
+
export const postTable = pgTable('Post', {
|
|
127
|
+
// Use enrichers for common columns
|
|
128
|
+
...generateIdColumnDefs({ id: { dataType: 'string' } }), // id: UUID primary key
|
|
129
|
+
...generateTzColumnDefs(), // createdAt, modifiedAt
|
|
130
|
+
...generateUserAuditColumnDefs({ // createdBy, modifiedBy
|
|
131
|
+
created: { dataType: 'string', columnName: 'created_by' },
|
|
132
|
+
modified: { dataType: 'string', columnName: 'modified_by' },
|
|
133
|
+
}),
|
|
134
|
+
|
|
135
|
+
// Your post-specific fields
|
|
136
|
+
title: text('title').notNull(),
|
|
137
|
+
content: text('content').notNull(),
|
|
138
|
+
isPublished: boolean('is_published').default(false),
|
|
139
|
+
slug: text('slug').notNull().unique(),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
export const postRelations = createRelations({
|
|
143
|
+
source: postTable,
|
|
144
|
+
relations: [
|
|
145
|
+
{
|
|
146
|
+
name: 'author',
|
|
147
|
+
type: RelationTypes.ONE,
|
|
148
|
+
schema: userTable,
|
|
149
|
+
metadata: {
|
|
150
|
+
fields: [postTable.createdBy],
|
|
151
|
+
references: [userTable.id],
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
export type TPostSchema = typeof postTable;
|
|
158
|
+
export type TPost = TTableObject<TPostSchema>;
|
|
159
|
+
|
|
160
|
+
@model({ type: 'entity' })
|
|
161
|
+
export class Post extends BaseEntity<TPostSchema> {
|
|
162
|
+
static readonly TABLE_NAME = 'Post';
|
|
163
|
+
|
|
164
|
+
constructor() {
|
|
165
|
+
super({ name: Post.TABLE_NAME, schema: postTable });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**What this gives you:**
|
|
171
|
+
```typescript
|
|
172
|
+
interface Post {
|
|
173
|
+
id: string; // From generateIdColumnDefs
|
|
174
|
+
createdAt: Date; // From generateTzColumnDefs
|
|
175
|
+
modifiedAt: Date; // From generateTzColumnDefs
|
|
176
|
+
createdBy: string; // From generateUserAuditColumnDefs
|
|
177
|
+
modifiedBy: string; // From generateUserAuditColumnDefs
|
|
178
|
+
title: string; // Your field
|
|
179
|
+
content: string; // Your field
|
|
180
|
+
isPublished: boolean; // Your field
|
|
181
|
+
slug: string; // Your field
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
#### When NOT to Use Enrichers
|
|
186
|
+
|
|
187
|
+
You can always define columns manually if:
|
|
188
|
+
- You need a custom ID strategy (e.g., integer auto-increment)
|
|
189
|
+
- You don't need audit fields for a specific table
|
|
190
|
+
- You have very specific timestamp requirements
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
// Mixing enrichers with manual columns is perfectly fine
|
|
194
|
+
export const simpleTable = pgTable('Simple', {
|
|
195
|
+
...generateIdColumnDefs({ id: { dataType: 'number' } }), // Use enricher for ID
|
|
196
|
+
// But manually define everything else
|
|
197
|
+
name: text('name').notNull(),
|
|
198
|
+
value: integer('value'),
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
:::tip
|
|
203
|
+
For a complete list of available enrichers and their options, see the [**Schema Enrichers Reference**](../../references/base/models.md#schema-enrichers).
|
|
204
|
+
:::
|
|
205
|
+
|
|
206
|
+
**Key Concepts:**
|
|
207
|
+
- **`pgTable`**: The standard function from Drizzle ORM to define a table schema.
|
|
208
|
+
- **Enrichers**: Ignis provides helper functions like `generateIdColumnDefs()` and `extraUserColumns()` that add common, pre-configured columns (like `id`, `status`, `type`, etc.) to your schema, reducing boilerplate.
|
|
209
|
+
- **`createRelations`**: A helper for defining relationships between models. Even if there are no relations, you must call it.
|
|
210
|
+
- **`BaseEntity`**: The class your model extends. It wraps the Drizzle schema and provides utilities for the framework.
|
|
211
|
+
- **`@model`**: A decorator that registers the class with the framework as a database model.
|
|
212
|
+
|
|
213
|
+
### Creating a Model with Relations
|
|
214
|
+
|
|
215
|
+
Now, let's create a `Configuration` model that has a relationship with the `User` model.
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
// src/models/entities/configuration.model.ts
|
|
219
|
+
import {
|
|
220
|
+
BaseEntity,
|
|
221
|
+
createRelations, // Import createRelations
|
|
222
|
+
generateDataTypeColumnDefs,
|
|
223
|
+
generateIdColumnDefs,
|
|
224
|
+
generateTzColumnDefs,
|
|
225
|
+
generateUserAuditColumnDefs,
|
|
226
|
+
model,
|
|
227
|
+
RelationTypes,
|
|
228
|
+
TTableObject,
|
|
229
|
+
} from '@venizia/ignis';
|
|
230
|
+
import { foreignKey, index, pgTable, text, unique } from 'drizzle-orm/pg-core';
|
|
231
|
+
import { User, userTable } from './user.model';
|
|
232
|
+
|
|
233
|
+
// 1. Define the Drizzle schema for the 'Configuration' table
|
|
234
|
+
export const configurationTable = pgTable(
|
|
235
|
+
Configuration.name,
|
|
236
|
+
{
|
|
237
|
+
...generateIdColumnDefs({ id: { dataType: 'string' } }),
|
|
238
|
+
...generateTzColumnDefs(),
|
|
239
|
+
...generateDataTypeColumnDefs(),
|
|
240
|
+
...generateUserAuditColumnDefs({
|
|
241
|
+
created: { dataType: 'string', columnName: 'created_by' },
|
|
242
|
+
modified: { dataType: 'string', columnName: 'modified_by' },
|
|
243
|
+
}),
|
|
244
|
+
code: text('code').notNull(),
|
|
245
|
+
description: text('description'),
|
|
246
|
+
group: text('group').notNull(),
|
|
247
|
+
},
|
|
248
|
+
(def) => [
|
|
249
|
+
unique(`UQ_${Configuration.name}_code`).on(def.code),
|
|
250
|
+
index(`IDX_${Configuration.name}_group`).on(def.group),
|
|
251
|
+
foreignKey({
|
|
252
|
+
columns: [def.createdBy],
|
|
253
|
+
foreignColumns: [userTable.id],
|
|
254
|
+
name: `FK_${Configuration.name}_createdBy_${User.name}_id`,
|
|
255
|
+
}),
|
|
256
|
+
],
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
// 2. Define the relations using Ignis's `createRelations` helper
|
|
260
|
+
export const configurationRelations = createRelations({
|
|
261
|
+
source: configurationTable,
|
|
262
|
+
relations: [
|
|
263
|
+
{
|
|
264
|
+
name: 'creator',
|
|
265
|
+
type: RelationTypes.ONE,
|
|
266
|
+
schema: userTable,
|
|
267
|
+
metadata: {
|
|
268
|
+
fields: [configurationTable.createdBy],
|
|
269
|
+
references: [userTable.id],
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
name: 'modifier',
|
|
274
|
+
type: RelationTypes.ONE,
|
|
275
|
+
schema: userTable,
|
|
276
|
+
metadata: {
|
|
277
|
+
fields: [configurationTable.modifiedBy],
|
|
278
|
+
references: [userTable.id],
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// 3. Define types and the Entity class as before
|
|
285
|
+
export type TConfigurationSchema = typeof configurationTable;
|
|
286
|
+
export type TConfiguration = TTableObject<TConfigurationSchema>;
|
|
287
|
+
|
|
288
|
+
@model({ type: 'entity' })
|
|
289
|
+
export class Configuration extends BaseEntity<TConfigurationSchema> {
|
|
290
|
+
static readonly TABLE_NAME = Configuration.name;
|
|
291
|
+
|
|
292
|
+
constructor() {
|
|
293
|
+
super({ name: Configuration.TABLE_NAME, schema: configurationTable });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
**Key Concepts:**
|
|
298
|
+
- **`createRelations`**: This helper function from `Ignis` simplifies defining Drizzle ORM relations. It creates both a Drizzle `relations` object (for querying) and a `definitions` object (for repository configuration). Here, we define `creator` and `modifier` relations from `Configuration` to `User`. The names (`creator`, `modifier`) are important, as they will be used when querying.
|
|
299
|
+
|
|
300
|
+
> **Deep Dive:**
|
|
301
|
+
> - Explore the [**`BaseEntity`**](../../references/base/models.md#baseentity-class) class.
|
|
302
|
+
> - See all available [**Enrichers**](../../references/base/models.md#schema-enrichers) for schema generation.
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## 2. DataSources: Connecting to Your Database
|
|
307
|
+
|
|
308
|
+
A DataSource is a class responsible for managing the connection to your database and making the Drizzle ORM instance available to your application.
|
|
309
|
+
|
|
310
|
+
### Creating and Configuring a DataSource
|
|
311
|
+
|
|
312
|
+
A `DataSource` must be decorated with `@datasource`. **The most critical part** is correctly merging your table schemas and relations into a single object that Drizzle ORM can understand.
|
|
313
|
+
|
|
314
|
+
#### ⚠️ Understanding Schema Merging (CRITICAL CONCEPT)
|
|
315
|
+
|
|
316
|
+
This is one of the most important concepts in Ignis. If you don't get this right, your relations won't work.
|
|
317
|
+
|
|
318
|
+
**The Problem:**
|
|
319
|
+
Drizzle ORM needs to know about:
|
|
320
|
+
1. Your table structures (e.g., `userTable`, `configurationTable`)
|
|
321
|
+
2. The relationships between them (e.g., "Configuration belongs to User")
|
|
322
|
+
|
|
323
|
+
**The Solution:**
|
|
324
|
+
You must merge both into a single `schema` object in your DataSource constructor.
|
|
325
|
+
|
|
326
|
+
#### Step-by-Step Example
|
|
327
|
+
|
|
328
|
+
Let's say you have two models: `User` and `Configuration`. Here's how to set up the DataSource:
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
// src/datasources/postgres.datasource.ts
|
|
332
|
+
import {
|
|
333
|
+
Configuration,
|
|
334
|
+
configurationTable, // The table structure
|
|
335
|
+
configurationRelations, // The relationships
|
|
336
|
+
User,
|
|
337
|
+
userTable, // The table structure
|
|
338
|
+
userRelations, // The relationships
|
|
339
|
+
} from '@/models/entities';
|
|
340
|
+
import {
|
|
341
|
+
BaseDataSource,
|
|
342
|
+
datasource,
|
|
343
|
+
TNodePostgresConnector,
|
|
344
|
+
ValueOrPromise,
|
|
345
|
+
} from '@venizia/ignis';
|
|
346
|
+
import { drizzle } from 'drizzle-orm/node-postgres';
|
|
347
|
+
import { Pool } from 'pg';
|
|
348
|
+
|
|
349
|
+
// Configuration interface for database connection
|
|
350
|
+
interface IDSConfigs {
|
|
351
|
+
connection: {
|
|
352
|
+
host?: string;
|
|
353
|
+
port?: number;
|
|
354
|
+
user?: string;
|
|
355
|
+
password?: string;
|
|
356
|
+
database?: string;
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
@datasource()
|
|
361
|
+
export class PostgresDataSource extends BaseDataSource<TNodePostgresConnector, IDSConfigs> {
|
|
362
|
+
constructor() {
|
|
363
|
+
super({
|
|
364
|
+
name: PostgresDataSource.name,
|
|
365
|
+
driver: 'node-postgres',
|
|
366
|
+
config: {
|
|
367
|
+
connection: {
|
|
368
|
+
host: process.env.APP_ENV_POSTGRES_HOST,
|
|
369
|
+
port: +(process.env.APP_ENV_POSTGRES_PORT ?? 5432),
|
|
370
|
+
user: process.env.APP_ENV_POSTGRES_USERNAME,
|
|
371
|
+
password: process.env.APP_ENV_POSTGRES_PASSWORD,
|
|
372
|
+
database: process.env.APP_ENV_POSTGRES_DATABASE,
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
// 🔥 CRITICAL: This is where you merge tables and relations
|
|
377
|
+
schema: Object.assign(
|
|
378
|
+
{},
|
|
379
|
+
// Step 1: Add your table schemas
|
|
380
|
+
{
|
|
381
|
+
[User.TABLE_NAME]: userTable,
|
|
382
|
+
[Configuration.TABLE_NAME]: configurationTable,
|
|
383
|
+
},
|
|
384
|
+
// Step 2: Spread the relations objects
|
|
385
|
+
userRelations.relations,
|
|
386
|
+
configurationRelations.relations,
|
|
387
|
+
),
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
override configure(): ValueOrPromise<void> {
|
|
392
|
+
// Pass the merged schema to Drizzle
|
|
393
|
+
this.connector = drizzle({
|
|
394
|
+
client: new Pool(this.settings.connection),
|
|
395
|
+
schema: this.schema, // This now contains both tables AND relations
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
override async connect(): Promise<TNodePostgresConnector | undefined> {
|
|
400
|
+
await (this.connector.client as Pool).connect();
|
|
401
|
+
return this.connector;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
override async disconnect(): Promise<void> {
|
|
405
|
+
await (this.connector.client as Pool).end();
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
#### Why This Pattern?
|
|
411
|
+
|
|
412
|
+
**Without the relations merged in:**
|
|
413
|
+
```typescript
|
|
414
|
+
// ❌ WRONG - Relations won't work!
|
|
415
|
+
schema: {
|
|
416
|
+
[User.TABLE_NAME]: userTable,
|
|
417
|
+
[Configuration.TABLE_NAME]: configurationTable,
|
|
418
|
+
}
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
**Result:** Your repository's `include` queries will fail. You won't be able to fetch related data.
|
|
422
|
+
|
|
423
|
+
**With tables and relations merged:**
|
|
424
|
+
```typescript
|
|
425
|
+
// ✅ CORRECT - Relations work perfectly!
|
|
426
|
+
schema: Object.assign(
|
|
427
|
+
{},
|
|
428
|
+
{
|
|
429
|
+
[User.TABLE_NAME]: userTable,
|
|
430
|
+
[Configuration.TABLE_NAME]: configurationTable,
|
|
431
|
+
},
|
|
432
|
+
userRelations.relations,
|
|
433
|
+
configurationRelations.relations,
|
|
434
|
+
)
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
**Result:** You can now do:
|
|
438
|
+
```typescript
|
|
439
|
+
const config = await configRepo.findOne({
|
|
440
|
+
filter: {
|
|
441
|
+
where: { id: '123' },
|
|
442
|
+
include: [{ relation: 'creator' }], // This works!
|
|
443
|
+
},
|
|
444
|
+
});
|
|
445
|
+
console.log(config.creator.name); // Access related User data
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
#### Adding New Models to Your DataSource
|
|
449
|
+
|
|
450
|
+
Every time you create a new model, you need to:
|
|
451
|
+
|
|
452
|
+
1. Import its table and relations
|
|
453
|
+
2. Add them to the schema object
|
|
454
|
+
|
|
455
|
+
**Example - Adding a `Post` model:**
|
|
456
|
+
```typescript
|
|
457
|
+
import {
|
|
458
|
+
Post,
|
|
459
|
+
postTable,
|
|
460
|
+
postRelations,
|
|
461
|
+
// ... other models
|
|
462
|
+
} from '@/models/entities';
|
|
463
|
+
|
|
464
|
+
// In your constructor:
|
|
465
|
+
schema: Object.assign(
|
|
466
|
+
{},
|
|
467
|
+
{
|
|
468
|
+
[User.TABLE_NAME]: userTable,
|
|
469
|
+
[Configuration.TABLE_NAME]: configurationTable,
|
|
470
|
+
[Post.TABLE_NAME]: postTable, // Add the table
|
|
471
|
+
},
|
|
472
|
+
userRelations.relations,
|
|
473
|
+
configurationRelations.relations,
|
|
474
|
+
postRelations.relations, // Add the relations
|
|
475
|
+
),
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
**Pro tip:** As your app grows, consider using a helper function to reduce boilerplate:
|
|
479
|
+
|
|
480
|
+
```typescript
|
|
481
|
+
function buildSchema(models: Array<{ table: any; relations: any; name: string }>) {
|
|
482
|
+
const tables = models.reduce((acc, m) => ({ ...acc, [m.name]: m.table }), {});
|
|
483
|
+
const relations = models.map(m => m.relations.relations);
|
|
484
|
+
return Object.assign({}, tables, ...relations);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Usage:
|
|
488
|
+
schema: buildSchema([
|
|
489
|
+
{ name: User.TABLE_NAME, table: userTable, relations: userRelations },
|
|
490
|
+
{ name: Configuration.TABLE_NAME, table: configurationTable, relations: configurationRelations },
|
|
491
|
+
{ name: Post.TABLE_NAME, table: postTable, relations: postRelations },
|
|
492
|
+
])
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### Registering a DataSource
|
|
496
|
+
|
|
497
|
+
Finally, register your `DataSource` with the application in `src/application.ts`.
|
|
498
|
+
|
|
499
|
+
```typescript
|
|
500
|
+
// src/application.ts
|
|
501
|
+
import { PostgresDataSource } from './datasources';
|
|
502
|
+
|
|
503
|
+
export class Application extends BaseApplication {
|
|
504
|
+
// ...
|
|
505
|
+
preConfigure(): ValueOrPromise<void> {
|
|
506
|
+
this.dataSource(PostgresDataSource);
|
|
507
|
+
// ...
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
> **Deep Dive:**
|
|
513
|
+
> - Explore the [**`BaseDataSource`**](../../references/base/datasources.md) class.
|
|
514
|
+
|
|
515
|
+
---
|
|
516
|
+
|
|
517
|
+
## 3. Repositories: The Data Access Layer
|
|
518
|
+
|
|
519
|
+
Repositories abstract the data access logic. They use the configured `DataSource` to perform type-safe queries against the database.
|
|
520
|
+
|
|
521
|
+
### Creating a Repository
|
|
522
|
+
|
|
523
|
+
A repository extends `DefaultCRUDRepository` (for full read/write operations), is decorated with `@repository`, and injects the `DataSource`.
|
|
524
|
+
|
|
525
|
+
```typescript
|
|
526
|
+
// src/repositories/configuration.repository.ts
|
|
527
|
+
import {
|
|
528
|
+
Configuration,
|
|
529
|
+
configurationRelations, // Import configurationRelations
|
|
530
|
+
TConfigurationSchema,
|
|
531
|
+
} from '@/models/entities';
|
|
532
|
+
import { IDataSource, inject, repository, DefaultCRUDRepository } from '@venizia/ignis';
|
|
533
|
+
|
|
534
|
+
// Decorator to mark this class as a repository for DI
|
|
535
|
+
@repository({})
|
|
536
|
+
export class ConfigurationRepository extends DefaultCRUDRepository<TConfigurationSchema> {
|
|
537
|
+
constructor(
|
|
538
|
+
// Inject the configured datasource
|
|
539
|
+
@inject({ key: 'datasources.PostgresDataSource' }) dataSource: IDataSource,
|
|
540
|
+
) {
|
|
541
|
+
// Pass the datasource, the model's Entity class, AND the relations definitions to the super constructor
|
|
542
|
+
super({ dataSource, entityClass: Configuration, relations: configurationRelations.definitions });
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
```
|
|
546
|
+
You would then register this repository in your `application.ts`: `this.repository(ConfigurationRepository);`
|
|
547
|
+
|
|
548
|
+
### Querying Data
|
|
549
|
+
|
|
550
|
+
Repositories provide a full suite of type-safe methods for CRUD operations using a standardized `filter` object.
|
|
551
|
+
|
|
552
|
+
```typescript
|
|
553
|
+
// Example usage in application.ts or a service
|
|
554
|
+
const repo = this.get<ConfigurationRepository>({ key: 'repositories.ConfigurationRepository' });
|
|
555
|
+
|
|
556
|
+
// Find multiple records
|
|
557
|
+
const someConfigs = await repo.find({
|
|
558
|
+
filter: {
|
|
559
|
+
where: { group: 'SYSTEM' },
|
|
560
|
+
limit: 10,
|
|
561
|
+
order: ['createdAt DESC'],
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// Create a new record
|
|
566
|
+
const newConfig = await repo.create({
|
|
567
|
+
data: {
|
|
568
|
+
code: 'NEW_CODE',
|
|
569
|
+
group: 'SYSTEM',
|
|
570
|
+
// ... other fields
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
### Querying with Relations
|
|
576
|
+
|
|
577
|
+
To query related data, use the `include` property in the filter object. The `relation` name must match one of the names you defined in `createRelations` (e.g., `creator`).
|
|
578
|
+
|
|
579
|
+
```typescript
|
|
580
|
+
const resultsWithCreator = await repo.find({
|
|
581
|
+
filter: {
|
|
582
|
+
where: { code: 'some_code' },
|
|
583
|
+
include: [{ relation: 'creator' }], // Fetch the related user
|
|
584
|
+
},
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
if (resultsWithCreator.length > 0) {
|
|
588
|
+
// `resultsWithCreator[0].creator` will contain the full User object
|
|
589
|
+
console.log('Configuration created by:', resultsWithCreator[0].creator.name);
|
|
590
|
+
}
|
|
591
|
+
```
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Services
|
|
2
|
+
|
|
3
|
+
Services contain your application's business logic, orchestrating data flow and executing use cases.
|
|
4
|
+
|
|
5
|
+
> **Deep Dive:** See [Services Reference](../../references/base/services.md) for advanced patterns.
|
|
6
|
+
|
|
7
|
+
## The Business Logic Layer
|
|
8
|
+
|
|
9
|
+
Services contain the core business logic of your application. They orchestrate the flow of data and execute the application's use cases. A service's primary responsibilities are:
|
|
10
|
+
|
|
11
|
+
- **Encapsulating Business Rules**: Centralizing logic such as calculations, data validation, and process workflows.
|
|
12
|
+
- **Coordinating Data Operations**: Using one or more repositories to fetch and persist data.
|
|
13
|
+
- **Isolating Controllers**: Keeping controllers thin by handling all the complex logic, so controllers are only responsible for handling the HTTP request and response.
|
|
14
|
+
|
|
15
|
+
### Creating a Service
|
|
16
|
+
|
|
17
|
+
To create a service, you extend the `BaseService` class and inject the repositories or other services it depends on.
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { BaseService, inject } from '@venizia/ignis';
|
|
21
|
+
import { ConfigurationRepository } from '../repositories';
|
|
22
|
+
import { UserRepository } from '../repositories';
|
|
23
|
+
import { TConfiguration } from '../models/entities';
|
|
24
|
+
|
|
25
|
+
export class ConfigurationService extends BaseService {
|
|
26
|
+
constructor(
|
|
27
|
+
@inject({ key: 'repositories.ConfigurationRepository' })
|
|
28
|
+
private configurationRepository: ConfigurationRepository,
|
|
29
|
+
@inject({ key: 'repositories.UserRepository' })
|
|
30
|
+
private userRepository: UserRepository,
|
|
31
|
+
) {
|
|
32
|
+
super({ scope: ConfigurationService.name });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async createConfigurationForUser(userId: string, data: Partial<TConfiguration>): Promise<TConfiguration> {
|
|
36
|
+
// Business logic: Check if the user exists
|
|
37
|
+
const user = await this.userRepository.findById({ id: userId });
|
|
38
|
+
if (!user) {
|
|
39
|
+
throw new Error(`User with ID ${userId} not found.`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Business logic: Check for duplicate configuration code
|
|
43
|
+
const existingConfig = await this.configurationRepository.findOne({ where: { code: data.code } });
|
|
44
|
+
if (existingConfig) {
|
|
45
|
+
throw new Error('Configuration with this code already exists.');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Use the repository to create the configuration, assigning the creator
|
|
49
|
+
const newConfigData = { ...data, createdBy: userId, modifiedBy: userId };
|
|
50
|
+
return this.configurationRepository.create(newConfigData);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## How Services Fit into the Architecture
|
|
56
|
+
|
|
57
|
+
Services act as the intermediary between controllers and repositories, ensuring a clean separation of concerns.
|
|
58
|
+
|
|
59
|
+
```mermaid
|
|
60
|
+
graph LR
|
|
61
|
+
A[Client Request] --> B(Controller);
|
|
62
|
+
B --> C{Service};
|
|
63
|
+
C --> D[Repository];
|
|
64
|
+
D --> E((Database));
|
|
65
|
+
E --> D;
|
|
66
|
+
D --> C;
|
|
67
|
+
C --> B;
|
|
68
|
+
B --> F[Client Response];
|
|
69
|
+
|
|
70
|
+
subgraph "Presentation Layer"
|
|
71
|
+
B
|
|
72
|
+
end
|
|
73
|
+
subgraph "Business Logic Layer"
|
|
74
|
+
C
|
|
75
|
+
end
|
|
76
|
+
subgraph "Data Access Layer"
|
|
77
|
+
D
|
|
78
|
+
end
|
|
79
|
+
subgraph "Data Store"
|
|
80
|
+
E
|
|
81
|
+
end
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
This layered architecture makes your application:
|
|
85
|
+
|
|
86
|
+
- **More Organized:** Each layer has a clear and distinct responsibility.
|
|
87
|
+
- **Easier to Test:** You can test your business logic in isolation by providing mock repositories to your services, without needing a live database.
|
|
88
|
+
- **More Flexible:** You can change your database or data access implementation (e.g., switch from PostgreSQL to MySQL) by only changing the repository and datasource layer, with no changes to your business logic.
|