@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.
Files changed (180) hide show
  1. package/README.md +7 -7
  2. package/content/best-practices/api-usage-examples.md +15 -12
  3. package/content/best-practices/architectural-patterns.md +70 -78
  4. package/content/best-practices/architecture-decisions.md +91 -60
  5. package/content/best-practices/code-style-standards/advanced-patterns.md +56 -44
  6. package/content/best-practices/code-style-standards/constants-configuration.md +11 -11
  7. package/content/best-practices/code-style-standards/control-flow.md +5 -2
  8. package/content/best-practices/code-style-standards/documentation.md +13 -13
  9. package/content/best-practices/code-style-standards/function-patterns.md +9 -10
  10. package/content/best-practices/code-style-standards/index.md +1 -1
  11. package/content/best-practices/code-style-standards/naming-conventions.md +10 -8
  12. package/content/best-practices/code-style-standards/route-definitions.md +30 -12
  13. package/content/best-practices/code-style-standards/tooling.md +8 -5
  14. package/content/best-practices/code-style-standards/type-safety.md +13 -12
  15. package/content/best-practices/common-pitfalls.md +56 -37
  16. package/content/best-practices/contribution-workflow.md +13 -14
  17. package/content/best-practices/data-modeling.md +46 -22
  18. package/content/best-practices/deployment-strategies.md +28 -27
  19. package/content/best-practices/error-handling.md +48 -24
  20. package/content/best-practices/index.md +5 -5
  21. package/content/best-practices/performance-optimization.md +40 -31
  22. package/content/best-practices/security-guidelines.md +52 -23
  23. package/content/best-practices/testing-strategies.md +65 -51
  24. package/content/best-practices/troubleshooting-tips.md +24 -24
  25. package/content/extensions/components/{swagger.md → api-reference.md} +40 -31
  26. package/content/extensions/components/authentication/api.md +19 -19
  27. package/content/extensions/components/authentication/errors.md +7 -7
  28. package/content/extensions/components/authentication/index.md +10 -8
  29. package/content/extensions/components/authentication/usage.md +101 -6
  30. package/content/extensions/components/authorization/api.md +45 -25
  31. package/content/extensions/components/authorization/errors.md +6 -6
  32. package/content/extensions/components/authorization/index.md +11 -10
  33. package/content/extensions/components/authorization/usage.md +21 -21
  34. package/content/extensions/components/health-check.md +1 -1
  35. package/content/extensions/components/index.md +5 -5
  36. package/content/extensions/components/mail/errors.md +15 -15
  37. package/content/extensions/components/mail/index.md +1 -2
  38. package/content/extensions/components/mail/usage.md +1 -1
  39. package/content/extensions/components/request-tracker.md +1 -1
  40. package/content/extensions/components/socket-io/api.md +9 -9
  41. package/content/extensions/components/socket-io/errors.md +5 -5
  42. package/content/extensions/components/socket-io/index.md +8 -8
  43. package/content/extensions/components/socket-io/usage.md +1 -1
  44. package/content/extensions/components/static-asset/api.md +17 -4
  45. package/content/extensions/components/static-asset/errors.md +4 -4
  46. package/content/extensions/components/static-asset/index.md +26 -28
  47. package/content/extensions/components/static-asset/usage.md +13 -12
  48. package/content/extensions/components/template/index.md +2 -2
  49. package/content/extensions/components/template/setup-page.md +1 -1
  50. package/content/extensions/components/websocket/api.md +3 -3
  51. package/content/extensions/components/websocket/errors.md +5 -5
  52. package/content/extensions/components/websocket/index.md +5 -5
  53. package/content/extensions/components/websocket/usage.md +3 -3
  54. package/content/extensions/helpers/cron/index.md +2 -2
  55. package/content/extensions/helpers/crypto/index.md +1 -1
  56. package/content/extensions/helpers/env/index.md +27 -12
  57. package/content/extensions/helpers/error/index.md +81 -25
  58. package/content/extensions/helpers/index.md +2 -3
  59. package/content/extensions/helpers/inversion/index.md +15 -7
  60. package/content/extensions/helpers/kafka/compile-binary.md +92 -0
  61. package/content/extensions/helpers/kafka/examples.md +1 -1
  62. package/content/extensions/helpers/kafka/index.md +3 -0
  63. package/content/extensions/helpers/logger/index.md +32 -2
  64. package/content/extensions/helpers/network/index.md +6 -0
  65. package/content/extensions/helpers/queue/index.md +14 -17
  66. package/content/extensions/helpers/redis/index.md +548 -323
  67. package/content/extensions/helpers/socket-io/index.md +14 -10
  68. package/content/extensions/helpers/storage/api.md +44 -8
  69. package/content/extensions/helpers/storage/index.md +43 -7
  70. package/content/extensions/helpers/template/index.md +6 -3
  71. package/content/extensions/helpers/types/index.md +11 -8
  72. package/content/extensions/helpers/websocket/api.md +9 -9
  73. package/content/extensions/helpers/websocket/index.md +7 -7
  74. package/content/extensions/helpers/worker-thread/index.md +2 -2
  75. package/content/extensions/index.md +3 -4
  76. package/content/extensions/src-details/mcp-server.md +18 -24
  77. package/content/guides/core-concepts/application/bootstrapping.md +11 -14
  78. package/content/guides/core-concepts/application/index.md +3 -3
  79. package/content/guides/core-concepts/components.md +19 -10
  80. package/content/guides/core-concepts/dependency-injection.md +6 -3
  81. package/content/guides/core-concepts/grpc-controllers.md +6 -5
  82. package/content/guides/core-concepts/persistent/datasources.md +42 -43
  83. package/content/guides/core-concepts/persistent/index.md +16 -7
  84. package/content/guides/core-concepts/persistent/models.md +24 -20
  85. package/content/guides/core-concepts/persistent/postgres-drivers.md +201 -0
  86. package/content/guides/core-concepts/persistent/repositories.md +40 -23
  87. package/content/guides/core-concepts/persistent/search-meilisearch.md +185 -0
  88. package/content/guides/core-concepts/persistent/search-typesense.md +431 -0
  89. package/content/guides/core-concepts/persistent/transactions.md +61 -25
  90. package/content/guides/core-concepts/rest-controllers.md +12 -9
  91. package/content/guides/core-concepts/services.md +330 -60
  92. package/content/guides/get-started/5-minute-quickstart.md +15 -15
  93. package/content/guides/get-started/philosophy.md +36 -36
  94. package/content/guides/get-started/setup.md +3 -3
  95. package/content/guides/index.md +3 -3
  96. package/content/guides/migrations/redis-helpers-migration.md +177 -0
  97. package/content/guides/migrations/scoped-rbac-migration.md +17 -17
  98. package/content/guides/migrations/unified-connectors-migration.md +113 -0
  99. package/content/guides/reference/glossary.md +19 -12
  100. package/content/guides/reference/mcp-docs-server.md +22 -18
  101. package/content/guides/tutorials/building-a-crud-api.md +37 -44
  102. package/content/guides/tutorials/complete-installation.md +17 -17
  103. package/content/guides/tutorials/ecommerce-api.md +163 -124
  104. package/content/guides/tutorials/realtime-chat.md +181 -135
  105. package/content/guides/tutorials/testing.md +65 -523
  106. package/content/index.md +2 -180
  107. package/content/public/apple-touch-icon.png +0 -0
  108. package/content/public/og-image.png +0 -0
  109. package/content/public/site.webmanifest +11 -0
  110. package/content/references/base/application.md +4 -5
  111. package/content/references/base/bootstrapping.md +18 -5
  112. package/content/references/base/components.md +149 -120
  113. package/content/references/base/connectors.md +178 -0
  114. package/content/references/base/controllers.md +41 -30
  115. package/content/references/base/datasources.md +163 -92
  116. package/content/references/base/dependency-injection.md +34 -22
  117. package/content/references/base/filter-system/application-usage.md +17 -14
  118. package/content/references/base/filter-system/array-operators.md +7 -2
  119. package/content/references/base/filter-system/comparison-operators.md +3 -0
  120. package/content/references/base/filter-system/default-filter.md +89 -71
  121. package/content/references/base/filter-system/fields-order-pagination.md +22 -22
  122. package/content/references/base/filter-system/index.md +6 -3
  123. package/content/references/base/filter-system/json-filtering.md +20 -1
  124. package/content/references/base/filter-system/list-operators.md +1 -1
  125. package/content/references/base/filter-system/logical-operators.md +33 -1
  126. package/content/references/base/filter-system/null-operators.md +30 -1
  127. package/content/references/base/filter-system/quick-reference.md +23 -4
  128. package/content/references/base/filter-system/tips.md +5 -5
  129. package/content/references/base/filter-system/use-cases.md +12 -12
  130. package/content/references/base/grpc-controllers.md +13 -13
  131. package/content/references/base/index.md +24 -12
  132. package/content/references/base/middlewares.md +265 -327
  133. package/content/references/base/models.md +63 -49
  134. package/content/references/base/providers.md +136 -130
  135. package/content/references/base/repositories/advanced.md +59 -58
  136. package/content/references/base/repositories/index.md +115 -91
  137. package/content/references/base/repositories/mixins.md +55 -291
  138. package/content/references/base/repositories/relations.md +54 -64
  139. package/content/references/base/repositories/soft-deletable.md +31 -30
  140. package/content/references/base/services.md +296 -93
  141. package/content/references/configuration/environment-variables.md +49 -31
  142. package/content/references/configuration/index.md +6 -6
  143. package/content/references/index.md +17 -12
  144. package/content/references/quick-reference.md +65 -106
  145. package/content/references/utilities/crypto.md +65 -23
  146. package/content/references/utilities/index.md +3 -3
  147. package/content/references/utilities/jsx.md +6 -4
  148. package/content/references/utilities/module.md +68 -20
  149. package/content/references/utilities/parse.md +4 -14
  150. package/content/references/utilities/promise.md +9 -7
  151. package/content/references/utilities/schema.md +5 -3
  152. package/dist/mcp-server/common/guards.d.ts +8 -0
  153. package/dist/mcp-server/common/guards.d.ts.map +1 -0
  154. package/dist/mcp-server/common/guards.js +14 -0
  155. package/dist/mcp-server/common/guards.js.map +1 -0
  156. package/dist/mcp-server/common/index.d.ts +1 -0
  157. package/dist/mcp-server/common/index.d.ts.map +1 -1
  158. package/dist/mcp-server/common/index.js +1 -0
  159. package/dist/mcp-server/common/index.js.map +1 -1
  160. package/dist/mcp-server/helpers/docs.helper.d.ts.map +1 -1
  161. package/dist/mcp-server/helpers/docs.helper.js +4 -2
  162. package/dist/mcp-server/helpers/docs.helper.js.map +1 -1
  163. package/dist/mcp-server/helpers/github.helper.js +1 -1
  164. package/dist/mcp-server/index.js +7 -2
  165. package/dist/mcp-server/index.js.map +1 -1
  166. package/dist/mcp-server/tools/base.tool.d.ts +6 -2
  167. package/dist/mcp-server/tools/base.tool.d.ts.map +1 -1
  168. package/dist/mcp-server/tools/base.tool.js.map +1 -1
  169. package/dist/mcp-server/tools/docs/search-documents.tool.d.ts +1 -1
  170. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
  171. package/dist/mcp-server/tools/github/search-code.tool.d.ts +1 -1
  172. package/dist/mcp-server/tools/github/search-code.tool.d.ts.map +1 -1
  173. package/dist/mcp-server/tools/github/search-code.tool.js +4 -1
  174. package/dist/mcp-server/tools/github/search-code.tool.js.map +1 -1
  175. package/dist/mcp-server/tools/github/verify-dependencies.tool.d.ts.map +1 -1
  176. package/dist/mcp-server/tools/github/verify-dependencies.tool.js +3 -1
  177. package/dist/mcp-server/tools/github/verify-dependencies.tool.js.map +1 -1
  178. package/package.json +9 -9
  179. package/content/extensions/helpers/testing/index.md +0 -510
  180. 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 { BaseEntity, generateIdColumnDefs, generateTzColumnDefs, model } from '@venizia/ignis';
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 BaseEntity<typeof User.schema> {
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 - BaseEntity auto-discovers from static properties
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
- BaseEntity,
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 BaseEntity<typeof Configuration.schema> {
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' } }), // id (text with UUID default)
129
+ ...generateIdColumnDefs({ id: { dataType: 'string' } }), // id (text with UUID default)
131
130
  ...generateTzColumnDefs(), // createdAt, modifiedAt
132
- ...generateUserAuditColumnDefs(), // createdBy, modifiedBy
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 { BaseEntity, generateIdColumnDefs, model } from '@venizia/ignis';
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 BaseEntity<typeof User.schema> {
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 BaseEntity<typeof Article.schema> {
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 { BaseEntity, generateIdColumnDefs, model, AuthorizationActions } from '@venizia/ignis';
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 BaseEntity<typeof Article.schema> {
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 no hardcoded strings
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 { BaseEntity, generateIdColumnDefs, model, TRelationConfig } from '@venizia/ignis';
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 BaseEntity<typeof MyModel.schema> {
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: 'some.cache' })
52
- private cache: SomeCache,
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.cache.get(`user:realm:${opts.realm}`);
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 + mixins: FieldsVisibilityMixin + DefaultFilterMixin)
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 (+ deleteById, deleteAll) recommended default
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` | Full CRUD including delete. Extends PersistableRepository. **Recommended for most use cases.** |
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` | `ITransaction` | Transaction context for atomic operations |
157
- | `shouldReturn` | `boolean` | Whether to return created/updated data (default: `true`) |
158
- | `shouldQueryRange` | `boolean` | Return `{ data, range: { total, skip, limit } }` for pagination |
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: { total: 150, skip: 0, limit: 20 } }
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.creator.name);
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 { SoftDeletableRepository, repository, model, BaseEntity } from '@venizia/ignis';
218
- import { pgTable, timestamp } from 'drizzle-orm/pg-core';
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 BaseEntity<typeof Category.schema> {
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
- 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.
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