@venizia/ignis-docs 0.0.8 → 0.2.0
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/README.md +7 -7
- package/content/best-practices/api-usage-examples.md +15 -12
- package/content/best-practices/architectural-patterns.md +70 -78
- package/content/best-practices/architecture-decisions.md +91 -60
- package/content/best-practices/code-style-standards/advanced-patterns.md +56 -44
- package/content/best-practices/code-style-standards/constants-configuration.md +11 -11
- package/content/best-practices/code-style-standards/control-flow.md +5 -2
- package/content/best-practices/code-style-standards/documentation.md +13 -13
- package/content/best-practices/code-style-standards/function-patterns.md +9 -10
- package/content/best-practices/code-style-standards/index.md +1 -1
- package/content/best-practices/code-style-standards/naming-conventions.md +10 -8
- package/content/best-practices/code-style-standards/route-definitions.md +30 -12
- package/content/best-practices/code-style-standards/tooling.md +8 -5
- package/content/best-practices/code-style-standards/type-safety.md +13 -12
- package/content/best-practices/common-pitfalls.md +56 -37
- package/content/best-practices/contribution-workflow.md +13 -14
- package/content/best-practices/data-modeling.md +46 -22
- package/content/best-practices/deployment-strategies.md +28 -27
- package/content/best-practices/error-handling.md +48 -24
- package/content/best-practices/index.md +5 -5
- package/content/best-practices/performance-optimization.md +40 -31
- package/content/best-practices/security-guidelines.md +52 -23
- package/content/best-practices/testing-strategies.md +65 -51
- package/content/best-practices/troubleshooting-tips.md +24 -24
- package/content/extensions/components/{swagger.md → api-reference.md} +40 -31
- package/content/extensions/components/authentication/api.md +19 -19
- package/content/extensions/components/authentication/errors.md +7 -7
- package/content/extensions/components/authentication/index.md +10 -8
- package/content/extensions/components/authentication/usage.md +101 -6
- package/content/extensions/components/authorization/api.md +45 -25
- package/content/extensions/components/authorization/errors.md +6 -6
- package/content/extensions/components/authorization/index.md +11 -10
- package/content/extensions/components/authorization/usage.md +21 -21
- package/content/extensions/components/health-check.md +1 -1
- package/content/extensions/components/index.md +5 -5
- package/content/extensions/components/mail/errors.md +15 -15
- package/content/extensions/components/mail/index.md +1 -2
- package/content/extensions/components/mail/usage.md +1 -1
- package/content/extensions/components/request-tracker.md +1 -1
- package/content/extensions/components/socket-io/api.md +9 -9
- package/content/extensions/components/socket-io/errors.md +5 -5
- package/content/extensions/components/socket-io/index.md +8 -8
- package/content/extensions/components/socket-io/usage.md +1 -1
- package/content/extensions/components/static-asset/api.md +17 -4
- package/content/extensions/components/static-asset/errors.md +4 -4
- package/content/extensions/components/static-asset/index.md +26 -28
- package/content/extensions/components/static-asset/usage.md +13 -12
- package/content/extensions/components/template/index.md +2 -2
- package/content/extensions/components/template/setup-page.md +1 -1
- package/content/extensions/components/websocket/api.md +3 -3
- package/content/extensions/components/websocket/errors.md +5 -5
- package/content/extensions/components/websocket/index.md +5 -5
- package/content/extensions/components/websocket/usage.md +3 -3
- package/content/extensions/helpers/cron/index.md +2 -2
- package/content/extensions/helpers/crypto/index.md +1 -1
- package/content/extensions/helpers/env/index.md +27 -12
- package/content/extensions/helpers/error/index.md +81 -25
- package/content/extensions/helpers/index.md +2 -3
- package/content/extensions/helpers/inversion/index.md +15 -7
- package/content/extensions/helpers/kafka/compile-binary.md +92 -0
- package/content/extensions/helpers/kafka/examples.md +1 -1
- package/content/extensions/helpers/kafka/index.md +3 -0
- package/content/extensions/helpers/logger/index.md +32 -2
- package/content/extensions/helpers/network/index.md +6 -0
- package/content/extensions/helpers/queue/index.md +14 -17
- package/content/extensions/helpers/redis/index.md +548 -323
- package/content/extensions/helpers/socket-io/index.md +14 -10
- package/content/extensions/helpers/storage/api.md +44 -8
- package/content/extensions/helpers/storage/index.md +43 -7
- package/content/extensions/helpers/template/index.md +6 -3
- package/content/extensions/helpers/types/index.md +11 -8
- package/content/extensions/helpers/websocket/api.md +9 -9
- package/content/extensions/helpers/websocket/index.md +7 -7
- package/content/extensions/helpers/worker-thread/index.md +2 -2
- package/content/extensions/index.md +3 -4
- package/content/extensions/src-details/mcp-server.md +18 -24
- package/content/guides/core-concepts/application/bootstrapping.md +11 -14
- package/content/guides/core-concepts/application/index.md +3 -3
- package/content/guides/core-concepts/components.md +19 -10
- package/content/guides/core-concepts/dependency-injection.md +6 -3
- package/content/guides/core-concepts/grpc-controllers.md +6 -5
- package/content/guides/core-concepts/persistent/datasources.md +42 -43
- package/content/guides/core-concepts/persistent/index.md +16 -7
- package/content/guides/core-concepts/persistent/models.md +24 -20
- package/content/guides/core-concepts/persistent/postgres-drivers.md +201 -0
- package/content/guides/core-concepts/persistent/repositories.md +40 -23
- package/content/guides/core-concepts/persistent/search-meilisearch.md +185 -0
- package/content/guides/core-concepts/persistent/search-typesense.md +431 -0
- package/content/guides/core-concepts/persistent/transactions.md +61 -25
- package/content/guides/core-concepts/rest-controllers.md +12 -9
- package/content/guides/core-concepts/services.md +330 -60
- package/content/guides/get-started/5-minute-quickstart.md +15 -15
- package/content/guides/get-started/philosophy.md +36 -36
- package/content/guides/get-started/setup.md +3 -3
- package/content/guides/index.md +3 -3
- package/content/guides/migrations/redis-helpers-migration.md +177 -0
- package/content/guides/migrations/scoped-rbac-migration.md +17 -17
- package/content/guides/migrations/unified-connectors-migration.md +113 -0
- package/content/guides/reference/glossary.md +19 -12
- package/content/guides/reference/mcp-docs-server.md +22 -18
- package/content/guides/tutorials/building-a-crud-api.md +37 -44
- package/content/guides/tutorials/complete-installation.md +17 -17
- package/content/guides/tutorials/ecommerce-api.md +163 -124
- package/content/guides/tutorials/realtime-chat.md +181 -135
- package/content/guides/tutorials/testing.md +65 -523
- package/content/index.md +2 -180
- package/content/public/apple-touch-icon.png +0 -0
- package/content/public/og-image.png +0 -0
- package/content/public/site.webmanifest +11 -0
- package/content/references/base/application.md +4 -5
- package/content/references/base/bootstrapping.md +18 -5
- package/content/references/base/components.md +149 -120
- package/content/references/base/connectors.md +178 -0
- package/content/references/base/controllers.md +41 -30
- package/content/references/base/datasources.md +163 -92
- package/content/references/base/dependency-injection.md +34 -22
- package/content/references/base/filter-system/application-usage.md +17 -14
- package/content/references/base/filter-system/array-operators.md +7 -2
- package/content/references/base/filter-system/comparison-operators.md +3 -0
- package/content/references/base/filter-system/default-filter.md +89 -71
- package/content/references/base/filter-system/fields-order-pagination.md +22 -22
- package/content/references/base/filter-system/index.md +6 -3
- package/content/references/base/filter-system/json-filtering.md +20 -1
- package/content/references/base/filter-system/list-operators.md +1 -1
- package/content/references/base/filter-system/logical-operators.md +33 -1
- package/content/references/base/filter-system/null-operators.md +30 -1
- package/content/references/base/filter-system/quick-reference.md +23 -4
- package/content/references/base/filter-system/tips.md +5 -5
- package/content/references/base/filter-system/use-cases.md +12 -12
- package/content/references/base/grpc-controllers.md +13 -13
- package/content/references/base/index.md +24 -12
- package/content/references/base/middlewares.md +265 -327
- package/content/references/base/models.md +63 -49
- package/content/references/base/providers.md +136 -130
- package/content/references/base/repositories/advanced.md +59 -58
- package/content/references/base/repositories/index.md +115 -91
- package/content/references/base/repositories/mixins.md +55 -291
- package/content/references/base/repositories/relations.md +54 -64
- package/content/references/base/repositories/soft-deletable.md +31 -30
- package/content/references/base/services.md +296 -93
- package/content/references/configuration/environment-variables.md +49 -31
- package/content/references/configuration/index.md +6 -6
- package/content/references/index.md +17 -12
- package/content/references/quick-reference.md +65 -106
- package/content/references/utilities/crypto.md +65 -23
- package/content/references/utilities/index.md +3 -3
- package/content/references/utilities/jsx.md +6 -4
- package/content/references/utilities/module.md +68 -20
- package/content/references/utilities/parse.md +4 -14
- package/content/references/utilities/promise.md +9 -7
- package/content/references/utilities/schema.md +5 -3
- package/dist/mcp-server/common/guards.d.ts +8 -0
- package/dist/mcp-server/common/guards.d.ts.map +1 -0
- package/dist/mcp-server/common/guards.js +14 -0
- package/dist/mcp-server/common/guards.js.map +1 -0
- package/dist/mcp-server/common/index.d.ts +1 -0
- package/dist/mcp-server/common/index.d.ts.map +1 -1
- package/dist/mcp-server/common/index.js +1 -0
- package/dist/mcp-server/common/index.js.map +1 -1
- package/dist/mcp-server/helpers/docs.helper.d.ts.map +1 -1
- package/dist/mcp-server/helpers/docs.helper.js +4 -2
- package/dist/mcp-server/helpers/docs.helper.js.map +1 -1
- package/dist/mcp-server/helpers/github.helper.js +1 -1
- package/dist/mcp-server/index.js +7 -2
- package/dist/mcp-server/index.js.map +1 -1
- package/dist/mcp-server/tools/base.tool.d.ts +6 -2
- package/dist/mcp-server/tools/base.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/base.tool.js.map +1 -1
- package/dist/mcp-server/tools/docs/search-documents.tool.d.ts +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
- package/dist/mcp-server/tools/github/search-code.tool.d.ts +1 -1
- package/dist/mcp-server/tools/github/search-code.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/github/search-code.tool.js +4 -1
- package/dist/mcp-server/tools/github/search-code.tool.js.map +1 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.js +3 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.js.map +1 -1
- package/package.json +9 -9
- package/content/extensions/helpers/testing/index.md +0 -510
- package/content/references/base/middleware.md +0 -347
|
@@ -6,11 +6,11 @@ Models define your data structure using Drizzle ORM schemas. A model is a single
|
|
|
6
6
|
|
|
7
7
|
```typescript
|
|
8
8
|
// src/models/entities/user.model.ts
|
|
9
|
-
import {
|
|
9
|
+
import { BasePostgresEntity, generateIdColumnDefs, generateTzColumnDefs, model } from '@venizia/ignis';
|
|
10
10
|
import { pgTable, text } from 'drizzle-orm/pg-core';
|
|
11
11
|
|
|
12
12
|
@model({ type: 'entity' })
|
|
13
|
-
export class User extends
|
|
13
|
+
export class User extends BasePostgresEntity<typeof User.schema> {
|
|
14
14
|
// Define schema as static property
|
|
15
15
|
static override schema = pgTable('User', {
|
|
16
16
|
...generateIdColumnDefs({ id: { dataType: 'string' } }),
|
|
@@ -28,7 +28,7 @@ export class User extends BaseEntity<typeof User.schema> {
|
|
|
28
28
|
|
|
29
29
|
- Schema is defined inline as `static override schema`
|
|
30
30
|
- Relations are defined as `static override relations`
|
|
31
|
-
- No constructor needed -
|
|
31
|
+
- No constructor needed - BasePostgresEntity auto-discovers from static properties
|
|
32
32
|
- Type parameter uses `typeof User.schema` (self-referencing)
|
|
33
33
|
|
|
34
34
|
## Creating a Model with Relations
|
|
@@ -36,7 +36,7 @@ export class User extends BaseEntity<typeof User.schema> {
|
|
|
36
36
|
```typescript
|
|
37
37
|
// src/models/entities/configuration.model.ts
|
|
38
38
|
import {
|
|
39
|
-
|
|
39
|
+
BasePostgresEntity,
|
|
40
40
|
generateDataTypeColumnDefs,
|
|
41
41
|
generateIdColumnDefs,
|
|
42
42
|
generateTzColumnDefs,
|
|
@@ -49,7 +49,7 @@ import { foreignKey, index, pgTable, text, unique } from 'drizzle-orm/pg-core';
|
|
|
49
49
|
import { User } from './user.model';
|
|
50
50
|
|
|
51
51
|
@model({ type: 'entity' })
|
|
52
|
-
export class Configuration extends
|
|
52
|
+
export class Configuration extends BasePostgresEntity<typeof Configuration.schema> {
|
|
53
53
|
static override schema = pgTable(
|
|
54
54
|
'Configuration',
|
|
55
55
|
{
|
|
@@ -114,11 +114,10 @@ Enrichers are helper functions that generate common database columns automatical
|
|
|
114
114
|
```typescript
|
|
115
115
|
static override schema = pgTable('User', {
|
|
116
116
|
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
|
|
117
|
-
status: text('status').notNull().default('ACTIVE'),
|
|
118
|
-
createdBy: text('created_by'),
|
|
119
|
-
modifiedBy: text('modified_by'),
|
|
120
117
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
121
118
|
modifiedAt: timestamp('modified_at', { withTimezone: true }).notNull().defaultNow(),
|
|
119
|
+
createdBy: text('created_by'),
|
|
120
|
+
modifiedBy: text('modified_by'),
|
|
122
121
|
// ... your fields
|
|
123
122
|
});
|
|
124
123
|
```
|
|
@@ -127,9 +126,12 @@ static override schema = pgTable('User', {
|
|
|
127
126
|
|
|
128
127
|
```typescript
|
|
129
128
|
static override schema = pgTable('User', {
|
|
130
|
-
...generateIdColumnDefs({ id: { dataType: 'string' } }),
|
|
129
|
+
...generateIdColumnDefs({ id: { dataType: 'string' } }), // id (text with UUID default)
|
|
131
130
|
...generateTzColumnDefs(), // createdAt, modifiedAt
|
|
132
|
-
...generateUserAuditColumnDefs(
|
|
131
|
+
...generateUserAuditColumnDefs({
|
|
132
|
+
created: { dataType: 'string', columnName: 'created_by' },
|
|
133
|
+
modified: { dataType: 'string', columnName: 'modified_by' },
|
|
134
|
+
}), // createdBy, modifiedBy
|
|
133
135
|
// ... your fields
|
|
134
136
|
});
|
|
135
137
|
```
|
|
@@ -144,7 +146,7 @@ static override schema = pgTable('User', {
|
|
|
144
146
|
| `generateDataTypeColumnDefs()` | `dataType`, `tValue`, `nValue`, etc. | Configuration tables |
|
|
145
147
|
|
|
146
148
|
:::note User Audit Options
|
|
147
|
-
The `generateUserAuditColumnDefs` enricher supports an `allowAnonymous` option (default: `true`). Set to `false` to require authenticated user context and throw errors for anonymous operations:
|
|
149
|
+
The `generateUserAuditColumnDefs` enricher defaults both columns to `dataType: 'number'` (integer user ids) - pass `dataType: 'string'` for text ids. It also supports an `allowAnonymous` option (default: `true`). Set to `false` to require authenticated user context and throw errors for anonymous operations:
|
|
148
150
|
```typescript
|
|
149
151
|
...generateUserAuditColumnDefs({
|
|
150
152
|
created: { dataType: 'string', columnName: 'created_by', allowAnonymous: false },
|
|
@@ -162,7 +164,7 @@ For a complete list of enrichers and options, see the [Schema Enrichers Referenc
|
|
|
162
164
|
Protect sensitive data by configuring properties that are **never returned** through repository queries. Hidden properties are excluded at the SQL level for maximum security and performance.
|
|
163
165
|
|
|
164
166
|
```typescript
|
|
165
|
-
import {
|
|
167
|
+
import { BasePostgresEntity, generateIdColumnDefs, model } from '@venizia/ignis';
|
|
166
168
|
import { pgTable, text } from 'drizzle-orm/pg-core';
|
|
167
169
|
|
|
168
170
|
@model({
|
|
@@ -171,7 +173,7 @@ import { pgTable, text } from 'drizzle-orm/pg-core';
|
|
|
171
173
|
hiddenProperties: ['password', 'secret'], // Never returned via repository
|
|
172
174
|
},
|
|
173
175
|
})
|
|
174
|
-
export class User extends
|
|
176
|
+
export class User extends BasePostgresEntity<typeof User.schema> {
|
|
175
177
|
static override schema = pgTable('User', {
|
|
176
178
|
...generateIdColumnDefs({ id: { dataType: 'string' } }),
|
|
177
179
|
email: text('email').notNull(),
|
|
@@ -222,7 +224,7 @@ Apply automatic filters to all repository queries. This is commonly used for sof
|
|
|
222
224
|
hiddenProperties: ['deletedAt'],
|
|
223
225
|
},
|
|
224
226
|
})
|
|
225
|
-
export class Article extends
|
|
227
|
+
export class Article extends BasePostgresEntity<typeof Article.schema> {
|
|
226
228
|
// ...
|
|
227
229
|
}
|
|
228
230
|
```
|
|
@@ -231,10 +233,11 @@ The default filter is applied automatically to all read operations. Bypass it wi
|
|
|
231
233
|
|
|
232
234
|
```typescript
|
|
233
235
|
// Normal query - auto-filters out soft-deleted records
|
|
234
|
-
const articles = await articleRepo.find({});
|
|
236
|
+
const articles = await articleRepo.find({ filter: {} });
|
|
235
237
|
|
|
236
238
|
// Include deleted records
|
|
237
239
|
const allArticles = await articleRepo.find({
|
|
240
|
+
filter: {},
|
|
238
241
|
options: { shouldSkipDefaultFilter: true },
|
|
239
242
|
});
|
|
240
243
|
```
|
|
@@ -244,7 +247,7 @@ const allArticles = await articleRepo.find({
|
|
|
244
247
|
Declare your model's authorization principal directly in `@model` settings. The decorator auto-populates `AUTHORIZATION_SUBJECT` for type-safe references in route configs:
|
|
245
248
|
|
|
246
249
|
```typescript
|
|
247
|
-
import {
|
|
250
|
+
import { BasePostgresEntity, generateIdColumnDefs, model, AuthorizationActions } from '@venizia/ignis';
|
|
248
251
|
import { pgTable, text } from 'drizzle-orm/pg-core';
|
|
249
252
|
|
|
250
253
|
@model({
|
|
@@ -253,14 +256,14 @@ import { pgTable, text } from 'drizzle-orm/pg-core';
|
|
|
253
256
|
authorize: { principal: 'article' },
|
|
254
257
|
},
|
|
255
258
|
})
|
|
256
|
-
export class Article extends
|
|
259
|
+
export class Article extends BasePostgresEntity<typeof Article.schema> {
|
|
257
260
|
static override schema = pgTable('Article', {
|
|
258
261
|
...generateIdColumnDefs({ id: { dataType: 'string' } }),
|
|
259
262
|
title: text('title').notNull(),
|
|
260
263
|
});
|
|
261
264
|
}
|
|
262
265
|
|
|
263
|
-
// Use in route configs
|
|
266
|
+
// Use in route configs - no hardcoded strings
|
|
264
267
|
authorize: {
|
|
265
268
|
action: AuthorizationActions.READ,
|
|
266
269
|
resource: Article.AUTHORIZATION_SUBJECT, // 'article'
|
|
@@ -282,16 +285,17 @@ The `@model` decorator accepts the following metadata:
|
|
|
282
285
|
| `skipMigrate` | `boolean` | Skip this model during migrations |
|
|
283
286
|
| `settings.hiddenProperties` | `string[]` | Properties excluded from all query results |
|
|
284
287
|
| `settings.defaultFilter` | `TFilter` | Default filter auto-applied to all queries |
|
|
288
|
+
| `settings.defaultLimit` | `number` | Default row limit when a query omits `limit` (must be a positive integer; falls back to `10`) |
|
|
285
289
|
| `settings.authorize.principal` | `string` | Authorization subject name for this model |
|
|
286
290
|
|
|
287
291
|
## Model Template
|
|
288
292
|
|
|
289
293
|
```typescript
|
|
290
|
-
import {
|
|
294
|
+
import { BasePostgresEntity, generateIdColumnDefs, model, TRelationConfig } from '@venizia/ignis';
|
|
291
295
|
import { pgTable, text } from 'drizzle-orm/pg-core';
|
|
292
296
|
|
|
293
297
|
@model({ type: 'entity' })
|
|
294
|
-
export class MyModel extends
|
|
298
|
+
export class MyModel extends BasePostgresEntity<typeof MyModel.schema> {
|
|
295
299
|
static override schema = pgTable('MyModel', {
|
|
296
300
|
...generateIdColumnDefs({ id: { dataType: 'string' } }),
|
|
297
301
|
name: text('name').notNull(),
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# Postgres Drivers & Supabase
|
|
2
|
+
|
|
3
|
+
IGNIS talks to PostgreSQL through a **driver seam**: `IRelationalDriver` owns connection acquisition and the raw transaction control statements, and everything above it - repositories, transactions, the Casbin adapters - is driver-agnostic. Two drivers ship today:
|
|
4
|
+
|
|
5
|
+
- **node-postgres** (`pg`) - the default IGNIS has always used
|
|
6
|
+
- **postgres-js** (`postgres`) - required for Supabase's transaction pooler, and a faster option anywhere else
|
|
7
|
+
|
|
8
|
+
Supabase is unmodified PostgreSQL, so it is not a separate connector: it varies the **driver**, not the SQL dialect. The `@venizia/ignis/postgres/supabase` submodule adds the two things Supabase deployments actually need - pooler presets and an RLS auth-context helper.
|
|
9
|
+
|
|
10
|
+
> [!IMPORTANT] Every database client is optional
|
|
11
|
+
> `pg` and `postgres` are both **optional peer dependencies**. The `@venizia/ignis/postgres` module pulls in neither - only the driver class you import and name in `@datasource({ driver })` reaches your bundle. Install the one your app uses:
|
|
12
|
+
>
|
|
13
|
+
> ```bash
|
|
14
|
+
> bun add pg # node-postgres
|
|
15
|
+
> bun add postgres # postgres-js (>= 3.4.0)
|
|
16
|
+
> ```
|
|
17
|
+
|
|
18
|
+
## Import Paths
|
|
19
|
+
|
|
20
|
+
| Import | Contents | Loads |
|
|
21
|
+
| :--- | :--- | :--- |
|
|
22
|
+
| `@venizia/ignis/postgres` | `BasePostgresDataSource`, `IRelationalDriver`, repository hierarchy | no client library |
|
|
23
|
+
| `@venizia/ignis/postgres/node-postgres` | `NodePostgresDriver` | `pg` |
|
|
24
|
+
| `@venizia/ignis/postgres/postgres-js` | `PostgresJsDriver` | `postgres` |
|
|
25
|
+
| `@venizia/ignis/postgres/supabase` | `PoolerModes`, `buildPostgresJsOptions`, `withAuthContext`, Supabase role re-exports | `drizzle-orm/supabase` |
|
|
26
|
+
|
|
27
|
+
## Naming the Driver Class
|
|
28
|
+
|
|
29
|
+
`@datasource({ driver })` takes the driver **class**, not a driver-name string:
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import { datasource, ValueOrPromise } from '@venizia/ignis';
|
|
33
|
+
import { BasePostgresDataSource } from '@venizia/ignis/postgres';
|
|
34
|
+
import { NodePostgresDriver } from '@venizia/ignis/postgres/node-postgres';
|
|
35
|
+
import { Pool } from 'pg';
|
|
36
|
+
|
|
37
|
+
@datasource({ driver: NodePostgresDriver })
|
|
38
|
+
export class PostgresDataSource extends BasePostgresDataSource<IDataSourceConfigs> {
|
|
39
|
+
override configure(): ValueOrPromise<void> {
|
|
40
|
+
this.client = new Pool({ connectionString: this.getConnectionString() }); // that is all
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`configure()` only builds `this.client` - the raw `pg.Pool` (or postgres-js `Sql`) your app's connection settings produce. The base class wires the driver **and** the connector lazily, on first call to `getConnector()` or `beginTransaction()`: it reads the class named in `@datasource({ driver })`, instantiates it over `this.client`, and builds the pooled Drizzle connector from that. `getClient()` hands `this.client` back as the raw-client escape hatch. A datasource that sets neither `this.client` nor a driver (via `useDriver()`, below) throws `No driver and no client` on first use.
|
|
46
|
+
|
|
47
|
+
> [!IMPORTANT] Why a class, not a name
|
|
48
|
+
> A driver-name string cannot carry `pg` or `postgres` into your bundle - it is just text. A dynamic `import('./node-postgres.js')` keyed off that string would defer *execution*, not *packaging*: every bundler statically resolves a literal specifier and packages whatever it points to, so a build that only used node-postgres would still fail with `Could not resolve: "postgres"` the moment postgres-js's import appeared anywhere in the module graph reachable at build time. Naming the class instead makes the driver module a real value reference - the one thing a bundler is forced to keep - which is what lets `pg` and `postgres` stay genuinely optional peers. A bare side-effect import (`import '@venizia/ignis/postgres/node-postgres'`) would not work either: `@venizia/ignis` declares `sideEffects: false`, so a bundler is free to drop an import whose exports go unused.
|
|
49
|
+
>
|
|
50
|
+
> Two tests pin this from different angles: `packages/core/src/__tests__/connectors/postgres/no-eager-driver-import.test.ts` proves no barrel **loads** a driver package in a fresh process (the runtime module graph), and `packages/core/src/__tests__/connectors/postgres/bundle/optional-peers.test.ts` proves no barrel gets a driver package **packaged** by a real bundler.
|
|
51
|
+
|
|
52
|
+
## Using postgres-js
|
|
53
|
+
|
|
54
|
+
Same shape, different class:
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { datasource, ValueOrPromise } from '@venizia/ignis';
|
|
58
|
+
import { BasePostgresDataSource } from '@venizia/ignis/postgres';
|
|
59
|
+
import { PostgresJsDriver } from '@venizia/ignis/postgres/postgres-js';
|
|
60
|
+
import postgres from 'postgres';
|
|
61
|
+
import type { Sql } from 'postgres';
|
|
62
|
+
|
|
63
|
+
@datasource({ driver: PostgresJsDriver })
|
|
64
|
+
export class PostgresDataSource extends BasePostgresDataSource<
|
|
65
|
+
IDataSourceConfigs,
|
|
66
|
+
typeof schema,
|
|
67
|
+
{},
|
|
68
|
+
Sql // getClient() is now honestly typed as postgres-js's Sql, not pg.Pool
|
|
69
|
+
> {
|
|
70
|
+
override configure(): ValueOrPromise<void> {
|
|
71
|
+
this.client = postgres(this.getConnectionString());
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The fourth type parameter (`Client`) defaults to `pg.Pool`; declare it when the raw client escape hatch (`getClient()`) should carry the real type.
|
|
77
|
+
|
|
78
|
+
## Driver Constructors Validate Their Client
|
|
79
|
+
|
|
80
|
+
Both shipped drivers throw immediately if constructed with the wrong shape of client, instead of failing later inside a query:
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
new NodePostgresDriver({ client: pool }); // client must expose connect() AND totalCount (pool accounting)
|
|
84
|
+
new PostgresJsDriver({ client: sql }); // client must expose reserve() AND unsafe()
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`NodePostgresDriver` rejects a bare `pg.Client` - it exposes `connect()` too, but has no pool accounting and cannot hand out a dedicated connection per transaction. `PostgresJsDriver` rejects a `pg.Pool` the same way. You will not normally construct these yourself: `wireDriverFromMetadata()` does it for you from `this.client`, so this validation fires the first time a datasource wired the wrong client behind the wrong `@datasource({ driver })` class.
|
|
88
|
+
|
|
89
|
+
## Custom or Third-Party Drivers: `useDriver()`
|
|
90
|
+
|
|
91
|
+
For a driver IGNIS does not ship, wire it explicitly with `useDriver()` - it assigns the driver **and** builds the pooled connector in one step, so the half-wired state (driver set, connector forgotten) cannot exist:
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
export class PostgresDataSource extends BasePostgresDataSource<IDataSourceConfigs> {
|
|
95
|
+
override configure(): ValueOrPromise<void> {
|
|
96
|
+
this.useDriver({
|
|
97
|
+
driver: new MyCustomDriver({ client: myClient }),
|
|
98
|
+
schema: this.getSchema(),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`useDriver()` bypasses `@datasource({ driver })` entirely - you never need to name a class in the decorator when you wire the driver yourself in `configure()`.
|
|
105
|
+
|
|
106
|
+
> [!WARNING] postgres-js cannot destroy a poisoned connection
|
|
107
|
+
> After a failed `COMMIT` or `ROLLBACK`, node-postgres **destroys** the connection instead of pooling it - the session may still hold an open transaction that the next borrower would inherit. postgres-js has no destroy semantics (`ReservedSql.release()` takes no argument), so the connection is returned to the pool anyway. This asymmetry is real and IGNIS does not paper over it; it is pinned by the driver's own tests.
|
|
108
|
+
|
|
109
|
+
## The Driver Contract
|
|
110
|
+
|
|
111
|
+
Every driver satisfies the same neutral interface, proven by a shared conformance suite - a seam only one driver can satisfy is not a seam:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
interface IRelationalDriver<Schema, Client> {
|
|
115
|
+
createConnector(opts: { schema: Schema }): TRelationalConnector<Schema>; // pooled Drizzle
|
|
116
|
+
acquire(opts: { schema: Schema }): Promise<IRelationalConnection<Schema>>; // one dedicated connection
|
|
117
|
+
getClient(): Client; // raw client escape: pg.Pool or Sql
|
|
118
|
+
end(): Promise<void>;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface IRelationalConnection<Schema> {
|
|
122
|
+
connector: TRelationalConnector<Schema>; // Drizzle bound to THIS connection, not the pool
|
|
123
|
+
execute(opts: { statement: string }): Promise<IStatementResult>; // { count } - control statements
|
|
124
|
+
release(opts?: { destroy?: boolean }): void;
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`acquire()` matters for transactions: `BEGIN` and `COMMIT` must land on the same backend, so each explicit transaction gets a dedicated connection (`pool.connect()` for pg, `sql.reserve()` for postgres-js - the reason for the `>= 3.4.0` floor).
|
|
129
|
+
|
|
130
|
+
The connection is checked out of the pool before Drizzle is constructed on top of it. If that constructor throws - a malformed discovered schema, a drizzle mismatch - both drivers catch the error, release the connection back to the pool first, and rethrow. Without this, every failed `acquire()` would strand a connection, and the pool would exhaust after enough of them.
|
|
131
|
+
|
|
132
|
+
`execute()` resolves to the neutral `IStatementResult` (`{ count }` - the same `count` the repository verbs speak). Each driver maps its native result shape at its own boundary; nothing above the seam ever inspects a driver-specific type.
|
|
133
|
+
|
|
134
|
+
## Supabase
|
|
135
|
+
|
|
136
|
+
### Choosing a connection mode
|
|
137
|
+
|
|
138
|
+
Supabase exposes three ways in, and one of them silently breaks prepared statements:
|
|
139
|
+
|
|
140
|
+
| Mode | Port | Prepared statements | When |
|
|
141
|
+
| :--- | :--- | :--- | :--- |
|
|
142
|
+
| `PoolerModes.DIRECT` | 5432 | yes | long-lived servers connecting straight to the database |
|
|
143
|
+
| `PoolerModes.SESSION` | 5432 (pooler) | yes | pooled, one backend per client session |
|
|
144
|
+
| `PoolerModes.TRANSACTION` | 6543 | **no** | serverless / many short-lived connections |
|
|
145
|
+
|
|
146
|
+
The transaction pooler (Supavisor) rebinds the backend per transaction, so a server-side prepared statement created on one backend simply is not there next time. `buildPostgresJsOptions` encodes this so you cannot forget it:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
import { datasource } from '@venizia/ignis';
|
|
150
|
+
import { BasePostgresDataSource } from '@venizia/ignis/postgres';
|
|
151
|
+
import { PostgresJsDriver } from '@venizia/ignis/postgres/postgres-js';
|
|
152
|
+
import { buildPostgresJsOptions, PoolerModes } from '@venizia/ignis/postgres/supabase';
|
|
153
|
+
import postgres from 'postgres';
|
|
154
|
+
|
|
155
|
+
@datasource({ driver: PostgresJsDriver })
|
|
156
|
+
export class SupabaseDataSource extends BasePostgresDataSource<IDataSourceConfigs> {
|
|
157
|
+
override configure() {
|
|
158
|
+
this.client = postgres(connectionString, {
|
|
159
|
+
...buildPostgresJsOptions({ mode: PoolerModes.TRANSACTION, max: 10 }),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`prepare: false` is emitted only for `TRANSACTION` mode; `max` is forwarded only when you pass it, so postgres-js's own default survives. Naming `PostgresJsDriver` in `@datasource` is what wires it - `configure()` only needs to build the client, same as node-postgres.
|
|
166
|
+
|
|
167
|
+
### Row Level Security
|
|
168
|
+
|
|
169
|
+
`withAuthContext` sets the Supabase auth context for the remainder of **one transaction**, so `auth.uid()` resolves inside RLS policies:
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
import { withAuthContext } from '@venizia/ignis/postgres/supabase';
|
|
173
|
+
|
|
174
|
+
const transaction = await this.dataSource.beginTransaction();
|
|
175
|
+
try {
|
|
176
|
+
// role defaults to claims.role (PostgREST semantics); pass it explicitly to override
|
|
177
|
+
await withAuthContext({ transaction, claims: jwtPayload });
|
|
178
|
+
|
|
179
|
+
await this.orderRepository.create({ data, options: { transaction } });
|
|
180
|
+
await transaction.commit();
|
|
181
|
+
} catch (error) {
|
|
182
|
+
try {
|
|
183
|
+
await transaction.rollback();
|
|
184
|
+
} catch (rollbackError) {
|
|
185
|
+
logger.error('Rollback failed | %s', rollbackError);
|
|
186
|
+
}
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Three properties make this safe under a transaction-mode pooler:
|
|
192
|
+
|
|
193
|
+
- `claims` is **bound as a query parameter**, never interpolated into SQL text.
|
|
194
|
+
- `role` must be a bare identifier (`/^[a-z_][a-z0-9_]*$/`) because `set local role $1` is not valid SQL - a role taken from a JWT and interpolated unvalidated would be a privilege-escalation vector. Validation runs **before** any statement, so a rejected call leaves the session untouched.
|
|
195
|
+
- Everything is `SET LOCAL` / `set_config(..., true)` - transaction-scoped. A plain `SET` would leak the caller's identity to the next borrower of the pooled connection, and is deliberately not offered.
|
|
196
|
+
|
|
197
|
+
The submodule also re-exports Drizzle's Supabase helpers (`anonRole`, `authenticatedRole`, `serviceRole`, `authUid`, `authUsers`, ...) so RLS-aware schema files need one import.
|
|
198
|
+
|
|
199
|
+
## Adding a Driver
|
|
200
|
+
|
|
201
|
+
One file under `src/connectors/postgres/drivers/`, implementing the four verbs above, plus a fake client and a test that runs the shared conformance suite (`run({ driver, buildDriverProbe })` in `src/__tests__/connectors/postgres/drivers/conformance/`). Register a sub-path export and an optional peer dependency; never re-export the driver from the drivers barrel - that is what would make its package load eagerly for everyone.
|
|
@@ -48,15 +48,15 @@ export class UserRepository extends ReadableRepository<typeof User.schema> {
|
|
|
48
48
|
dataSource: PostgresDataSource, // Must be concrete type, not 'any'
|
|
49
49
|
|
|
50
50
|
// After first arg, you can inject any additional dependencies
|
|
51
|
-
@inject({ key: '
|
|
52
|
-
private
|
|
51
|
+
@inject({ key: 'services.CacheService' })
|
|
52
|
+
private cacheService: CacheService,
|
|
53
53
|
) {
|
|
54
54
|
super(dataSource);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
async findByRealm(opts: { realm: string }) {
|
|
58
58
|
// Use injected dependencies
|
|
59
|
-
const cached = await this.
|
|
59
|
+
const cached = await this.cacheService.get(`user:realm:${opts.realm}`);
|
|
60
60
|
if (cached) {
|
|
61
61
|
return cached;
|
|
62
62
|
}
|
|
@@ -74,13 +74,17 @@ export class UserRepository extends ReadableRepository<typeof User.schema> {
|
|
|
74
74
|
## Repository Hierarchy
|
|
75
75
|
|
|
76
76
|
```
|
|
77
|
-
AbstractRepository (base
|
|
77
|
+
AbstractRepository (engine-neutral base in src/base - lazy dataSource/entity resolution,
|
|
78
|
+
@model settings getters: hiddenProperties, defaultFilter, defaultLimit)
|
|
79
|
+
↓
|
|
80
|
+
PostgresBaseRepository (postgres connector - filter building, hidden-column exclusion,
|
|
81
|
+
default-filter merging, transaction-aware connector resolution)
|
|
78
82
|
↓
|
|
79
83
|
ReadableRepository (read-only: find, findOne, findById, count, existsWith)
|
|
80
84
|
↓
|
|
81
|
-
PersistableRepository (+ create, updateById, updateAll)
|
|
85
|
+
PersistableRepository (+ create, createAll, updateById, updateAll, deleteById, deleteAll)
|
|
82
86
|
↓
|
|
83
|
-
DefaultCRUDRepository (
|
|
87
|
+
DefaultCRUDRepository (no additional methods - recommended default)
|
|
84
88
|
↓
|
|
85
89
|
SoftDeletableRepository (overrides delete to set deletedAt timestamp)
|
|
86
90
|
```
|
|
@@ -88,14 +92,16 @@ SoftDeletableRepository (overrides delete to set deletedAt timestamp)
|
|
|
88
92
|
| Type | Description |
|
|
89
93
|
|------|-------------|
|
|
90
94
|
| `ReadableRepository` | Read-only operations. Write operations throw errors. |
|
|
91
|
-
| `PersistableRepository` | Read + write operations (create, update). Extends ReadableRepository. |
|
|
92
|
-
| `DefaultCRUDRepository` |
|
|
95
|
+
| `PersistableRepository` | Read + write operations (create, update, delete). Extends ReadableRepository. |
|
|
96
|
+
| `DefaultCRUDRepository` | Extends PersistableRepository with no additional logic. **Recommended for most use cases.** |
|
|
93
97
|
| `SoftDeletableRepository` | Extends DefaultCRUDRepository. Overrides delete to set `deletedAt` timestamp instead of physically removing records. |
|
|
94
98
|
|
|
95
99
|
## Querying Data
|
|
96
100
|
|
|
97
101
|
For advanced filtering with operators like `gt`, `lt`, `like`, `in`, `between`, and more, see [Filter System](../../../references/base/filter-system/).
|
|
98
102
|
|
|
103
|
+
Return shapes: read methods return values directly (`find` returns an array, `findOne`/`findById` return a record or `null`, `count` returns `{ count }`), while write methods (`create`, `updateById`, `deleteById`, ...) return a `{ count, data }` envelope.
|
|
104
|
+
|
|
99
105
|
```typescript
|
|
100
106
|
const repo = this.get<ConfigurationRepository>({ key: 'repositories.ConfigurationRepository' });
|
|
101
107
|
|
|
@@ -128,8 +134,8 @@ const sorted = await repo.find({
|
|
|
128
134
|
}
|
|
129
135
|
});
|
|
130
136
|
|
|
131
|
-
// Create a record
|
|
132
|
-
const newConfig = await repo.create({
|
|
137
|
+
// Create a record - write operations return a { count, data } envelope
|
|
138
|
+
const { data: newConfig } = await repo.create({
|
|
133
139
|
data: {
|
|
134
140
|
code: 'NEW_SETTING',
|
|
135
141
|
group: 'SYSTEM',
|
|
@@ -137,13 +143,13 @@ const newConfig = await repo.create({
|
|
|
137
143
|
}
|
|
138
144
|
});
|
|
139
145
|
|
|
140
|
-
// Update by ID
|
|
141
|
-
await repo.updateById({
|
|
146
|
+
// Update by ID - also returns { count, data }
|
|
147
|
+
const { data: updatedConfig } = await repo.updateById({
|
|
142
148
|
id: 'uuid-here',
|
|
143
149
|
data: { description: 'Updated description' }
|
|
144
150
|
});
|
|
145
151
|
|
|
146
|
-
// Delete by ID
|
|
152
|
+
// Delete by ID - also returns { count, data }
|
|
147
153
|
await repo.deleteById({ id: 'uuid-here' });
|
|
148
154
|
```
|
|
149
155
|
|
|
@@ -153,10 +159,13 @@ All repository operations accept an `options` parameter with these fields:
|
|
|
153
159
|
|
|
154
160
|
| Option | Type | Description |
|
|
155
161
|
| :--- | :--- | :--- |
|
|
156
|
-
| `transaction` | `
|
|
157
|
-
| `shouldReturn` | `boolean` |
|
|
158
|
-
| `shouldQueryRange` | `boolean` |
|
|
162
|
+
| `transaction` | `IDatabaseTransaction` | Transaction context for atomic operations |
|
|
163
|
+
| `shouldReturn` | `boolean` | Write methods only - whether to return created/updated data (default: `true`) |
|
|
164
|
+
| `shouldQueryRange` | `boolean` | `find` only - return `{ data, range: { start, end, total } }` for pagination |
|
|
159
165
|
| `shouldSkipDefaultFilter` | `boolean` | Bypass the model's default filter (e.g., soft delete) |
|
|
166
|
+
| `force` | `boolean` | `updateAll`/`deleteAll` only - allow an empty `where` (table-wide operation) |
|
|
167
|
+
| `lock` | `TLockOptions` | Row-level locking for reads (`{ strength: 'update' }`, ...). Requires a transaction; incompatible with `include`/`fields` |
|
|
168
|
+
| `log` | `TRepositoryLogOptions` | Per-operation logging (`{ use: true, level?: 'info' }`) |
|
|
160
169
|
|
|
161
170
|
```typescript
|
|
162
171
|
// Create without returning data (faster)
|
|
@@ -178,7 +187,7 @@ const result = await repo.find({
|
|
|
178
187
|
filter: { limit: 20, skip: 0 },
|
|
179
188
|
options: { shouldQueryRange: true }
|
|
180
189
|
});
|
|
181
|
-
// result = { data: [...], range: {
|
|
190
|
+
// result = { data: [...], range: { start: 0, end: 19, total: 150 } }
|
|
182
191
|
```
|
|
183
192
|
|
|
184
193
|
## Querying with Relations
|
|
@@ -193,7 +202,7 @@ const configWithCreator = await repo.findOne({
|
|
|
193
202
|
},
|
|
194
203
|
});
|
|
195
204
|
|
|
196
|
-
console.log('Created by:', configWithCreator
|
|
205
|
+
console.log('Created by:', configWithCreator?.creator.name);
|
|
197
206
|
```
|
|
198
207
|
|
|
199
208
|
## Registering Repositories
|
|
@@ -214,8 +223,15 @@ export class Application extends BaseApplication {
|
|
|
214
223
|
For soft-delete patterns, use `SoftDeletableRepository` which overrides delete operations to set a `deletedAt` timestamp instead of physically removing records:
|
|
215
224
|
|
|
216
225
|
```typescript
|
|
217
|
-
import {
|
|
218
|
-
|
|
226
|
+
import {
|
|
227
|
+
BasePostgresEntity,
|
|
228
|
+
generateIdColumnDefs,
|
|
229
|
+
generateTzColumnDefs,
|
|
230
|
+
model,
|
|
231
|
+
repository,
|
|
232
|
+
SoftDeletableRepository,
|
|
233
|
+
} from '@venizia/ignis';
|
|
234
|
+
import { pgTable, text, timestamp } from 'drizzle-orm/pg-core';
|
|
219
235
|
|
|
220
236
|
@model({
|
|
221
237
|
type: 'entity',
|
|
@@ -224,7 +240,7 @@ import { pgTable, timestamp } from 'drizzle-orm/pg-core';
|
|
|
224
240
|
defaultFilter: { where: { deletedAt: null } },
|
|
225
241
|
},
|
|
226
242
|
})
|
|
227
|
-
export class Category extends
|
|
243
|
+
export class Category extends BasePostgresEntity<typeof Category.schema> {
|
|
228
244
|
static override schema = pgTable('Category', {
|
|
229
245
|
...generateIdColumnDefs({ id: { dataType: 'string' } }),
|
|
230
246
|
...generateTzColumnDefs(),
|
|
@@ -237,6 +253,8 @@ export class Category extends BaseEntity<typeof Category.schema> {
|
|
|
237
253
|
export class CategoryRepository extends SoftDeletableRepository<typeof Category.schema> {}
|
|
238
254
|
```
|
|
239
255
|
|
|
256
|
+
Delete operations accept `shouldHardDelete: true` in options to physically remove a row, and soft-deleted records can be restored via `restoreById`/`restoreAll`.
|
|
257
|
+
|
|
240
258
|
## Repository Template
|
|
241
259
|
|
|
242
260
|
```typescript
|
|
@@ -252,7 +270,7 @@ export class MyModelRepository extends DefaultCRUDRepository<typeof MyModel.sche
|
|
|
252
270
|
|
|
253
271
|
### Performance: Core API Optimization
|
|
254
272
|
|
|
255
|
-
|
|
273
|
+
IGNIS automatically optimizes "flat" queries (no relations, no field selection) by using Drizzle's Core API. This provides **~15-20% faster** queries for simple reads. The `canUseCoreAPI()` method on `ReadableRepository` determines when this optimization applies.
|
|
256
274
|
|
|
257
275
|
### Modular Persistence with Components
|
|
258
276
|
|
|
@@ -283,7 +301,6 @@ export class UserManagementComponent extends BaseComponent {
|
|
|
283
301
|
- [Filter System](/references/base/filter-system/) - Query operators and filtering
|
|
284
302
|
- [Relations & Includes](/references/base/repositories/relations) - Loading related data
|
|
285
303
|
- [Advanced Features](/references/base/repositories/advanced) - JSON queries, performance tuning
|
|
286
|
-
- [Repository Mixins](/references/base/repositories/mixins) - Soft delete and auditing
|
|
287
304
|
|
|
288
305
|
- **Best Practices:**
|
|
289
306
|
- [Data Modeling](/best-practices/data-modeling) - Repository design patterns
|