@venizia/ignis-docs 0.2.0 → 0.2.1-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 (142) hide show
  1. package/README.md +14 -14
  2. package/content/best-practices/api-usage-examples.md +39 -19
  3. package/content/best-practices/architectural-patterns.md +22 -11
  4. package/content/best-practices/architecture-decisions.md +29 -16
  5. package/content/best-practices/code-style-standards/advanced-patterns.md +43 -31
  6. package/content/best-practices/code-style-standards/constants-configuration.md +19 -4
  7. package/content/best-practices/code-style-standards/control-flow.md +33 -1
  8. package/content/best-practices/code-style-standards/documentation.md +28 -8
  9. package/content/best-practices/code-style-standards/function-patterns.md +15 -4
  10. package/content/best-practices/code-style-standards/index.md +7 -2
  11. package/content/best-practices/code-style-standards/naming-conventions.md +26 -2
  12. package/content/best-practices/code-style-standards/route-definitions.md +9 -5
  13. package/content/best-practices/code-style-standards/tooling.md +14 -1
  14. package/content/best-practices/code-style-standards/type-safety.md +39 -6
  15. package/content/best-practices/common-pitfalls.md +59 -35
  16. package/content/best-practices/contribution-workflow.md +6 -2
  17. package/content/best-practices/data-modeling.md +78 -62
  18. package/content/best-practices/deployment-strategies.md +56 -19
  19. package/content/best-practices/error-handling.md +182 -93
  20. package/content/best-practices/index.md +6 -0
  21. package/content/best-practices/performance-optimization.md +26 -13
  22. package/content/best-practices/security-guidelines.md +35 -9
  23. package/content/best-practices/testing-strategies.md +7 -2
  24. package/content/best-practices/troubleshooting-tips.md +27 -17
  25. package/content/extensions/components/api-reference.md +107 -322
  26. package/content/extensions/components/authentication/api.md +454 -603
  27. package/content/extensions/components/authentication/errors.md +121 -498
  28. package/content/extensions/components/authentication/index.md +88 -801
  29. package/content/extensions/components/authentication/usage.md +207 -956
  30. package/content/extensions/components/authorization/api.md +736 -656
  31. package/content/extensions/components/authorization/errors.md +168 -206
  32. package/content/extensions/components/authorization/index.md +82 -797
  33. package/content/extensions/components/authorization/usage.md +194 -527
  34. package/content/extensions/components/health-check.md +71 -243
  35. package/content/extensions/components/mail/api.md +504 -287
  36. package/content/extensions/components/mail/errors.md +73 -61
  37. package/content/extensions/components/mail/index.md +96 -467
  38. package/content/extensions/components/mail/usage.md +130 -172
  39. package/content/extensions/components/request-tracker.md +66 -173
  40. package/content/extensions/components/socket-io/api.md +195 -13
  41. package/content/extensions/components/socket-io/errors.md +3 -3
  42. package/content/extensions/components/socket-io/index.md +50 -337
  43. package/content/extensions/components/socket-io/usage.md +143 -26
  44. package/content/extensions/components/static-asset/api.md +410 -142
  45. package/content/extensions/components/static-asset/errors.md +110 -53
  46. package/content/extensions/components/static-asset/index.md +79 -608
  47. package/content/extensions/components/static-asset/usage.md +180 -300
  48. package/content/extensions/components/websocket/api.md +275 -399
  49. package/content/extensions/components/websocket/errors.md +47 -56
  50. package/content/extensions/components/websocket/index.md +74 -407
  51. package/content/extensions/components/websocket/usage.md +110 -341
  52. package/content/extensions/helpers/cron/index.md +51 -160
  53. package/content/extensions/helpers/crypto/index.md +62 -483
  54. package/content/extensions/helpers/crypto/reference.md +456 -0
  55. package/content/extensions/helpers/env/index.md +60 -178
  56. package/content/extensions/helpers/error/index.md +221 -207
  57. package/content/extensions/helpers/inversion/index.md +65 -556
  58. package/content/extensions/helpers/inversion/reference.md +522 -0
  59. package/content/extensions/helpers/kafka/admin.md +20 -1
  60. package/content/extensions/helpers/kafka/compile-binary.md +41 -35
  61. package/content/extensions/helpers/kafka/consumer.md +54 -20
  62. package/content/extensions/helpers/kafka/examples.md +21 -16
  63. package/content/extensions/helpers/kafka/index.md +80 -610
  64. package/content/extensions/helpers/kafka/producer.md +134 -5
  65. package/content/extensions/helpers/kafka/schema-registry.md +45 -70
  66. package/content/extensions/helpers/logger/hf-logger.md +193 -0
  67. package/content/extensions/helpers/logger/index.md +64 -563
  68. package/content/extensions/helpers/logger/pino.md +85 -0
  69. package/content/extensions/helpers/logger/reference.md +746 -0
  70. package/content/extensions/helpers/network/api.md +241 -195
  71. package/content/extensions/helpers/network/index.md +72 -530
  72. package/content/extensions/helpers/queue/index.md +72 -900
  73. package/content/extensions/helpers/queue/reference.md +467 -0
  74. package/content/extensions/helpers/redis/index.md +73 -645
  75. package/content/extensions/helpers/redis/reference.md +727 -0
  76. package/content/extensions/helpers/secrets/index.md +66 -0
  77. package/content/extensions/helpers/socket-io/api.md +305 -203
  78. package/content/extensions/helpers/socket-io/index.md +66 -432
  79. package/content/extensions/helpers/storage/api.md +564 -462
  80. package/content/extensions/helpers/storage/index.md +77 -573
  81. package/content/extensions/helpers/types/index.md +66 -499
  82. package/content/extensions/helpers/types/reference.md +650 -0
  83. package/content/extensions/helpers/uid/index.md +58 -227
  84. package/content/extensions/helpers/websocket/api.md +329 -216
  85. package/content/extensions/helpers/websocket/index.md +65 -503
  86. package/content/extensions/helpers/worker-thread/index.md +58 -396
  87. package/content/extensions/helpers/worker-thread/reference.md +428 -0
  88. package/content/guides/core-concepts/persistent/models.md +1 -1
  89. package/content/guides/core-concepts/persistent/search-typesense.md +3 -3
  90. package/content/guides/core-concepts/persistent/transactions.md +1 -1
  91. package/content/guides/core-concepts/secrets-vault.md +177 -0
  92. package/content/guides/core-concepts/services.md +1 -1
  93. package/content/guides/migrations/redis-helpers-migration.md +1 -1
  94. package/content/guides/migrations/unified-connectors-migration.md +2 -2
  95. package/content/guides/tutorials/ecommerce-api.md +3 -8
  96. package/content/references/base/application.md +1 -1
  97. package/content/references/base/connectors.md +79 -136
  98. package/content/references/base/datasources-reference.md +599 -0
  99. package/content/references/base/datasources.md +84 -444
  100. package/content/references/base/dependency-injection.md +17 -39
  101. package/content/references/base/filter-system/application-usage.md +69 -121
  102. package/content/references/base/filter-system/array-operators.md +12 -0
  103. package/content/references/base/filter-system/comparison-operators.md +12 -0
  104. package/content/references/base/filter-system/default-filter.md +136 -348
  105. package/content/references/base/filter-system/fields-order-pagination.md +38 -16
  106. package/content/references/base/filter-system/index.md +106 -257
  107. package/content/references/base/filter-system/json-filtering.md +12 -2
  108. package/content/references/base/filter-system/list-operators.md +16 -2
  109. package/content/references/base/filter-system/logical-operators.md +13 -0
  110. package/content/references/base/filter-system/null-operators.md +13 -0
  111. package/content/references/base/filter-system/pattern-matching.md +12 -0
  112. package/content/references/base/filter-system/quick-reference.md +11 -2
  113. package/content/references/base/filter-system/range-operators.md +12 -0
  114. package/content/references/base/filter-system/tips.md +70 -133
  115. package/content/references/base/filter-system/use-cases.md +156 -233
  116. package/content/references/base/middlewares.md +35 -21
  117. package/content/references/base/models-reference.md +886 -0
  118. package/content/references/base/models.md +80 -1452
  119. package/content/references/base/repositories/advanced.md +156 -192
  120. package/content/references/base/repositories/index.md +77 -650
  121. package/content/references/base/repositories/mixins.md +22 -18
  122. package/content/references/base/repositories/relations.md +123 -171
  123. package/content/references/base/repositories/soft-deletable.md +58 -56
  124. package/content/references/base/secrets.md +263 -0
  125. package/content/references/base/services.md +2 -2
  126. package/content/references/configuration/environment-variables.md +48 -4
  127. package/content/references/configuration/index.md +49 -31
  128. package/content/references/quick-reference.md +3 -16
  129. package/content/references/utilities/crypto.md +35 -76
  130. package/content/references/utilities/date.md +33 -73
  131. package/content/references/utilities/index.md +1 -1
  132. package/content/references/utilities/jsx-reference.md +298 -0
  133. package/content/references/utilities/jsx.md +82 -525
  134. package/content/references/utilities/module.md +29 -62
  135. package/content/references/utilities/parse.md +34 -64
  136. package/content/references/utilities/performance.md +33 -58
  137. package/content/references/utilities/promise.md +28 -62
  138. package/content/references/utilities/request.md +57 -218
  139. package/content/references/utilities/schema.md +43 -137
  140. package/content/references/utilities/statuses-reference.md +361 -0
  141. package/content/references/utilities/statuses.md +63 -667
  142. package/package.json +8 -8
@@ -1,1525 +1,153 @@
1
1
  ---
2
- title: Models & Enrichers Reference
3
- description: Technical reference for model architecture and schema enrichers
2
+ title: Models
3
+ description: Declare a database table's schema and behavior with a model class
4
4
  difficulty: intermediate
5
5
  ---
6
6
 
7
- # Deep Dive: Models and Enrichers
7
+ # Models
8
8
 
9
- Technical reference for model architecture and schema enrichers in IGNIS.
9
+ A model is a class that declares a database table's schema and behavior in one place.
10
10
 
11
- > [!IMPORTANT] Base vs. Connectors
12
- > The engine-neutral root `AbstractEntity` (`packages/core/src/base/models/base.ts`) has no Drizzle, no `pgTable`, and no `drizzle-zod` - just a `name`, an abstract `getSchema()`, a `getIdType(): TIdSchemaType` method (default `'string'`), and `toObject()`/`toJSON()`. Everything described below - the Drizzle-backed `BaseEntity`, `drizzle-zod` schema generation, and all schema enrichers - belongs to the **PostgreSQL connector**'s `BasePostgresEntity`, not the neutral base. See [Connectors](./connectors) for the full base-vs-connectors architecture.
11
+ ## In one example
13
12
 
14
- **Files:**
15
- - `packages/core/src/base/models/base.ts` (neutral `AbstractEntity`)
16
- - `packages/core/src/connectors/postgres/models/base.ts` (PostgreSQL `BasePostgresEntity`)
17
- - `packages/core/src/connectors/postgres/models/enrichers/*.ts`
18
-
19
- ## Quick Reference
20
-
21
- | Component | Purpose | Key Features |
22
- |-----------|---------|--------------|
23
- | **BasePostgresEntity** (alias: `BaseEntity`) | Wraps Drizzle schema | Schema encapsulation, Zod generation, `toObject()`/`toJSON()` |
24
- | **Schema Enrichers** | Add common columns to tables | `generateIdColumnDefs()`, `generateTzColumnDefs()`, etc. |
25
-
26
- ## `BasePostgresEntity` Class (alias: `BaseEntity`)
27
-
28
- PostgreSQL connector's entity class, wrapping a Drizzle ORM schema. Extends the neutral `AbstractEntity`.
29
-
30
- **File:** `packages/core/src/connectors/postgres/models/base.ts`
31
-
32
- > [!TIP] Naming
33
- > `BasePostgresEntity` is the canonical, engine-carrying name. `BaseEntity` is a compatibility alias re-exporting the same class from `connectors/postgres/models/index.ts` (`export { BasePostgresEntity as BaseEntity } from './base'`) - both resolve to identical runtime behavior. Code samples on this page use `BaseEntity` since it remains the most common import today.
34
-
35
- ### Purpose
36
-
37
- | Feature | Description |
38
- |---------|-------------|
39
- | **Schema Encapsulation** | Holds Drizzle `pgTable` schema for consistent repository access |
40
- | **Metadata** | Works with `@model` decorator to mark database entities |
41
- | **Schema Generation** | Uses `drizzle-zod` to generate Zod schemas (`select`, `create`, `update`) |
42
- | **Static Properties** | Supports static `schema`, `relations`, `TABLE_NAME`, and `AUTHORIZATION_SUBJECT` |
43
- | **Convenience** | Includes `toObject()` and `toJSON()` methods |
44
-
45
- ### The `@model` Decorator
46
-
47
- The `@model` decorator marks a class as a database entity and configures its behavior.
48
-
49
- **File:** `packages/core/src/base/metadata/persistents.ts`
50
-
51
- #### Decorator Options
52
-
53
- ```typescript
54
- @model({
55
- type: 'entity' | 'view',
56
- tableName?: string,
57
- skipMigrate?: boolean,
58
- settings?: {
59
- hiddenProperties?: string[], // Properties to exclude from query results
60
- defaultFilter?: TFilter, // Filter applied to all repository queries
61
- defaultLimit?: number, // Default row limit when a query omits `limit`
62
- authorize?: { // Authorization settings
63
- principal: string, // Authorization subject name
64
- [extra: string | symbol]: any, // Extensible metadata
65
- },
66
- }
67
- })
68
- ```
69
-
70
- | Option | Type | Description |
71
- |--------|------|-------------|
72
- | `type` | `'entity' \| 'view'` | Entity type - `'entity'` for tables, `'view'` for database views |
73
- | `tableName` | `string` | Optional custom table name. Resolution order: `tableName` > static `TABLE_NAME` > class name |
74
- | `skipMigrate` | `boolean` | Skip this model during schema migrations |
75
- | `settings.hiddenProperties` | `string[]` | Array of property names to exclude from all repository query results |
76
- | `settings.defaultFilter` | `TFilter` | Filter automatically applied to all repository queries (see [Default Filter](/references/base/filter-system/default-filter)) |
77
- | `settings.defaultLimit` | `number` | Default row limit applied when a query omits `limit`. Must be a positive integer (validated at decoration time). Falls back to the global `DEFAULT_LIMIT` (10). See [Pagination](/references/base/filter-system/fields-order-pagination#default-limit) |
78
- | `settings.authorize` | `IModelAuthorizeSettings` | Authorization settings - declares the model's authorization principal (see [Authorization](/extensions/components/authorization/usage#model-based-resource-references)) |
79
- | `settings.authorize.principal` | `string` | The authorization subject name for this model. Auto-populates `AUTHORIZATION_SUBJECT` static property |
80
-
81
- #### `@model` Behavior
82
-
83
- When the `@model` decorator is applied:
84
- 1. If `settings.defaultLimit` is provided, it is validated to be a positive integer - otherwise the decorator throws at decoration (boot) time
85
- 2. If `settings.authorize.principal` is provided and `AUTHORIZATION_SUBJECT` is not already defined on the class, it auto-populates `AUTHORIZATION_SUBJECT` with the principal value
86
- 3. The model is registered in the `MetadataRegistry` model registry, keyed by table name (resolved as: `metadata.tableName` > `static TABLE_NAME` > class name)
87
- 4. The static `relations` property is stored as a resolver (not immediately resolved) to avoid circular dependency issues between models
88
-
89
- ### Hidden Properties
90
-
91
- Hidden properties are **excluded at the SQL level** - they are never fetched from the database when querying through repositories. This provides:
92
-
93
- - **Security**: Sensitive data like passwords are never accidentally exposed
94
- - **Performance**: Less data transferred from database
95
- - **Consistency**: Hidden properties are excluded from ALL repository operations
13
+ The smallest real model: a Drizzle table wrapped in a class, registered with `@model`.
96
14
 
97
15
  ```typescript
98
16
  import { pgTable, text } from 'drizzle-orm/pg-core';
99
17
  import { BaseEntity, model, generateIdColumnDefs } from '@venizia/ignis';
100
18
 
101
- @model({
102
- type: 'entity',
103
- settings: {
104
- hiddenProperties: ['password', 'secret'], // Never returned via repository
105
- },
106
- })
19
+ @model({ type: 'entity' })
107
20
  export class User extends BaseEntity<typeof User.schema> {
108
21
  static override schema = pgTable('User', {
109
22
  ...generateIdColumnDefs({ id: { dataType: 'string' } }),
110
23
  email: text('email').notNull(),
111
- password: text('password'), // Hidden - never in query results
112
- secret: text('secret'), // Hidden - never in query results
113
24
  });
114
25
  }
115
26
  ```
116
27
 
117
- #### Behavior
118
-
119
- | Operation | Hidden Properties |
120
- |-----------|-------------------|
121
- | `find()`, `findOne()`, `findById()` | Excluded from SELECT |
122
- | `create()`, `createAll()` | Excluded from RETURNING |
123
- | `updateById()`, `updateAll()` | Excluded from RETURNING |
124
- | `deleteById()`, `deleteAll()` | Excluded from RETURNING |
125
- | `count()`, `existsWith()` | Can filter by hidden fields |
126
- | Direct connector query | **Included** (bypasses repository) |
127
-
128
- #### Important Notes
129
-
130
- - Hidden properties can still be used in `where` clauses for filtering
131
- - Data is still **stored** in the database - only excluded from query results
132
- - Use direct connector queries when you need to access hidden data:
133
-
134
- ```typescript
135
- // Repository query - password/secret NOT included
136
- const user = await userRepository.findById({ id: '123' });
137
- // user = { id: '123', email: 'john@example.com' }
138
-
139
- // Direct connector query - ALL fields included
140
- const connector = userRepository.getConnector();
141
- const [fullUser] = await connector
142
- .select()
143
- .from(User.schema)
144
- .where(eq(User.schema.id, '123'));
145
- // fullUser = { id: '123', email: 'john@example.com', password: 'hashed...', secret: '...' }
146
- ```
147
-
148
- ### Default Filter
149
-
150
- Default filters are **automatically applied** to all repository queries for a model. This is useful for:
151
-
152
- - **Soft Delete**: Automatically exclude deleted records
153
- - **Multi-Tenancy**: Isolate data by tenant
154
- - **Active Records**: Filter to active/non-expired records
155
- - **Query Limits**: Prevent unbounded queries
156
-
157
- ```typescript
158
- @model({
159
- type: 'entity',
160
- settings: {
161
- defaultFilter: {
162
- where: { isDeleted: false }, // Applied to all queries
163
- limit: 100, // Prevents unbounded queries
164
- },
165
- },
166
- })
167
- export class Post extends BaseEntity<typeof Post.schema> {
168
- static override schema = postTable;
169
- }
170
- ```
171
-
172
- #### Behavior
173
-
174
- | Operation | Default Filter |
175
- |-----------|----------------|
176
- | `find()`, `findOne()`, `findById()` | Applied to WHERE clause |
177
- | `count()`, `existsWith()` | Applied to WHERE clause |
178
- | `updateById()`, `updateAll()` | Applied to WHERE clause |
179
- | `deleteById()`, `deleteAll()` | Applied to WHERE clause |
180
- | `create()`, `createAll()` | **Not applied** |
181
-
182
- #### Bypassing
183
-
184
- Use `shouldSkipDefaultFilter: true` to bypass:
185
-
186
- ```typescript
187
- // Normal query - includes default filter
188
- await postRepository.find({ filter: {} });
189
- // WHERE isDeleted = false LIMIT 100
190
-
191
- // Admin query - bypass default filter
192
- await postRepository.find({
193
- filter: {},
194
- options: { shouldSkipDefaultFilter: true }
195
- });
196
- // No WHERE clause (includes deleted)
197
- ```
198
-
199
- > [!TIP]
200
- > See [Default Filter](/references/base/filter-system/default-filter) for full documentation including merge strategies and common patterns.
201
-
202
- ### Definition Patterns
203
-
204
- `BaseEntity` supports two patterns for defining models:
205
-
206
- #### Pattern 1: Static Properties (Recommended)
207
-
208
- Define schema and relations as static properties:
209
-
210
- ```typescript
211
- import { pgTable, text } from 'drizzle-orm/pg-core';
212
- import { BaseEntity, model, generateIdColumnDefs, createRelations } from '@venizia/ignis';
213
-
214
- // Define table schema
215
- export const userTable = pgTable('User', {
216
- ...generateIdColumnDefs({ id: { dataType: 'string' } }),
217
- name: text('name').notNull(),
218
- email: text('email').notNull(),
219
- });
220
-
221
- // Define relations
222
- export const userRelations = createRelations({
223
- source: userTable,
224
- relations: [],
225
- });
226
-
227
- // Entity class with static properties
228
- @model({ type: 'entity' })
229
- export class User extends BaseEntity<typeof User.schema> {
230
- static override schema = userTable;
231
- static override relations = () => userRelations.definitions;
232
- static override TABLE_NAME = 'User';
233
- }
234
- ```
235
-
236
- **Benefits:**
237
- - Schema and relations are auto-resolved by repositories
238
- - No need to pass `relations` in repository constructor
239
- - Cleaner, more declarative syntax
240
-
241
- #### Pattern 2: Constructor-Based (Legacy)
242
-
243
- Pass schema in constructor:
244
-
245
- ```typescript
246
- @model({ type: 'entity' })
247
- export class User extends BaseEntity<typeof userTable> {
248
- constructor() {
249
- super({ name: 'User', schema: userTable });
250
- }
251
- }
252
- ```
253
-
254
- ### Static Properties
255
-
256
- | Property | Type | Description |
257
- |----------|------|-------------|
258
- | `schema` | `TTableSchemaWithId` | Drizzle table schema defined with `pgTable()` |
259
- | `relations` | `TValueOrResolver<Array<TRelationConfig>>` | Relation definitions (can be a function for lazy loading to avoid circular deps) |
260
- | `TABLE_NAME` | `string \| undefined` | Optional table name (defaults to class name if not set) |
261
- | `AUTHORIZATION_SUBJECT` | `string \| undefined` | Authorization principal name. Auto-populated from `@model` settings `authorize.principal` |
262
-
263
- ### IEntity Interface
264
-
265
- Models implementing static properties conform to the `IEntity` interface:
266
-
267
- ```typescript
268
- interface IEntity<Schema extends TTableSchemaWithId = TTableSchemaWithId> {
269
- TABLE_NAME?: string;
270
- schema: Schema;
271
- relations?: TValueOrResolver<Array<TRelationConfig>>;
272
- }
273
- ```
274
-
275
- ### Instance Methods
276
-
277
- | Method | Description |
278
- |--------|-------------|
279
- | `getSchema({ type })` | Get Zod schema for validation (`'select'`, `'create'`, `'update'`) |
280
- | `toObject()` | Convert to plain object (shallow spread of `this`) |
281
- | `toJSON()` | Delegates to `toObject()` - returns a plain object (used by `JSON.stringify`) |
282
-
283
- ### `getSchema` Method
284
-
285
- Generates a Zod validation schema from the Drizzle table schema using `drizzle-zod`.
286
-
287
- ```typescript
288
- getSchema(opts: { type: TSchemaType }): ZodSchema
289
- ```
290
-
291
- The `type` parameter accepts lowercase string values defined in the `SchemaTypes` class:
292
-
293
- | Type | Value | Zod Schema Generated | Description |
294
- |------|-------|---------------------|-------------|
295
- | `SchemaTypes.SELECT` | `'select'` | `createSelectSchema(schema)` | Schema for query results |
296
- | `SchemaTypes.CREATE` | `'create'` | `createInsertSchema(schema)` | Schema for insert operations |
297
- | `SchemaTypes.UPDATE` | `'update'` | `createUpdateSchema(schema)` | Schema for update operations |
298
-
299
- ```typescript
300
- const user = new User();
301
-
302
- // Get Zod schema for validating insert data
303
- const createSchema = user.getSchema({ type: 'create' });
304
-
305
- // Get Zod schema for validating query results
306
- const selectSchema = user.getSchema({ type: 'select' });
307
-
308
- // Get Zod schema for validating update data
309
- const updateSchema = user.getSchema({ type: 'update' });
310
- ```
311
-
312
- The `schemaFactory` is a static lazy singleton created via `drizzle-zod`'s `createSchemaFactory()`, shared across all `BaseEntity` instances to avoid per-entity overhead.
313
-
314
- ### Class Definition
315
-
316
- ```typescript
317
- export class BasePostgresEntity<Schema extends TTableSchemaWithId = TTableSchemaWithId>
318
- extends AbstractEntity
319
- implements IEntity<Schema>
320
- {
321
- // Instance property (name, toObject(), toJSON() are inherited from AbstractEntity)
322
- schema: Schema;
323
-
324
- // Static properties - override in subclass
325
- static schema: TTableSchemaWithId;
326
- static relations?: TValueOrResolver<Array<TRelationConfig>>;
327
- static TABLE_NAME?: string; // Optional, defaults to class name
328
- static AUTHORIZATION_SUBJECT?: string; // Auto-set by @model decorator from authorize.principal
329
-
330
- // Static singleton for schemaFactory - shared across all instances
331
- // Performance optimization: avoids creating new factory per entity
332
- private static _schemaFactory?: ReturnType<typeof createSchemaFactory>;
333
- protected static get schemaFactory(): ReturnType<typeof createSchemaFactory> {
334
- return (BasePostgresEntity._schemaFactory ??= createSchemaFactory());
335
- }
336
-
337
- // Constructor supports both patterns
338
- constructor(opts?: { name?: string; schema?: Schema }) {
339
- const ctor = new.target as typeof BasePostgresEntity;
340
- // Resolution order: opts.name > static TABLE_NAME > class name
341
- const name = opts?.name ?? ctor.TABLE_NAME ?? ctor.name;
342
-
343
- super({ name });
344
-
345
- this.schema = opts?.schema || (ctor.schema as Schema);
346
- }
347
-
348
- // Maps the pgTable id column's Drizzle dataType to 'number' or 'string'
349
- override getIdType(): TIdSchemaType {
350
- return getIdType({ entity: this.schema }) === 'number' ? 'number' : 'string';
351
- }
352
-
353
- getSchema(opts: { type: TSchemaType }) {
354
- const factory = BasePostgresEntity.schemaFactory; // Uses static singleton
355
- switch (opts.type) {
356
- case SchemaTypes.CREATE:
357
- return factory.createInsertSchema(this.schema);
358
- case SchemaTypes.UPDATE:
359
- return factory.createUpdateSchema(this.schema);
360
- case SchemaTypes.SELECT:
361
- return factory.createSelectSchema(this.schema);
362
- default:
363
- throw getError({
364
- message: `[getSchema] Invalid schema type | type: ${opts.type}`,
365
- });
366
- }
367
- }
368
- }
369
- ```
370
-
371
- ## Key Types
372
-
373
- ### `TTableSchemaWithId`
374
-
375
- Ensures a Drizzle `PgTable` has an `id` column:
376
-
377
- ```typescript
378
- type TTableSchemaWithId<TC extends TableConfig = TableConfig> = PgTable<TC> & {
379
- id: TIdColumn;
380
- };
381
- ```
382
-
383
- ### `TTableObject`
384
-
385
- Infers the select (output) type from a table schema:
386
-
387
- ```typescript
388
- type TTableObject<T extends TTableSchemaWithId> = T['$inferSelect'];
389
- ```
390
-
391
- ### `TTableInsert`
392
-
393
- Infers the insert (input) type from a table schema:
394
-
395
- ```typescript
396
- type TTableInsert<T extends TTableSchemaWithId> = T['$inferInsert'];
397
- ```
398
-
399
- ### `TGetIdType`
400
-
401
- Extracts the `id` field type from a table schema:
402
-
403
- ```typescript
404
- type TGetIdType<T extends TTableSchemaWithId> = TTableObject<T>['id'];
405
- ```
406
-
407
- ### `IdType`
408
-
409
- Union of supported ID types:
410
-
411
- ```typescript
412
- type NumberIdType = number;
413
- type StringIdType = string;
414
- type BigIntIdType = bigint;
415
- type IdType = NumberIdType | StringIdType | BigIntIdType;
416
- ```
417
-
418
- ### `TRelationConfig`
419
-
420
- Configuration for entity relationships:
421
-
422
- ```typescript
423
- type TRelationConfig = {
424
- name: string;
425
- } & (
426
- | { type: 'one'; schema: TTableSchemaWithId; metadata: /* Drizzle one() params */ }
427
- | { type: 'many'; schema: TTableSchemaWithId; metadata: /* Drizzle many() params */ }
428
- );
429
- ```
430
-
431
- Relation types are defined in the `RelationTypes` class:
432
-
433
- | Type | Value | Description |
434
- |------|-------|-------------|
435
- | `RelationTypes.ONE` | `'one'` | One-to-one or many-to-one relationship |
436
- | `RelationTypes.MANY` | `'many'` | One-to-many relationship |
437
-
438
- ### `TValueOrResolver`
439
-
440
- From `@venizia/ignis-helpers`, enables lazy resolution to avoid circular dependencies:
441
-
442
- ```typescript
443
- type TValueOrResolver<T> = T | TResolver<T>; // T or () => T
444
- ```
445
-
446
- Used for `relations` on `BaseEntity` - store a function that returns the relations array, resolved lazily when `DataSource.buildSchema()` is called.
447
-
448
- ## Schema Enrichers
449
-
450
- Enrichers are helper functions located in `packages/core/src/connectors/postgres/models/enrichers/` that return an object of Drizzle ORM column definitions. They are designed to be spread into a `pgTable` definition to quickly add common, standardized fields to your models.
451
-
452
- ### Available Enrichers
453
-
454
- | Enricher Function | Convenience Wrapper | Purpose |
455
- | :--- | :--- | :--- |
456
- | **`generateIdColumnDefs`** | `enrichId` | Adds a primary key `id` column (string UUID, numeric integer, or big integer). |
457
- | **`generateTzColumnDefs`** | `enrichTz` | Adds `createdAt`, `modifiedAt`, and `deletedAt` timestamp columns with timezone support. |
458
- | **`generateUserAuditColumnDefs`** | `enrichUserAudit` | Adds `createdBy` and `modifiedBy` columns to track user audit information. |
459
- | **`generatePrincipalColumnDefs`** | `enrichPrincipal` | Adds polymorphic principal columns (`{discriminator}Id` and `{discriminator}Type`). |
460
- | **`generateDataTypeColumnDefs`** | `enrichDataTypes` | Adds generic data type columns (`dataType`, `nValue`, `tValue`, `bValue`, `jValue`, `boValue`) for flexible data storage. |
461
- | **`extraUserColumns`** | - | Adds common user fields (`realm`, `status`, `type`, `activatedAt`, `lastLoginAt`, `parentId`). Imported from `@venizia/ignis` (part of auth component). |
462
-
463
- Each `generate*` function returns column definition objects for spreading into `pgTable`. The `enrich*` convenience wrappers accept an existing `TColumnDefinitions` object as the first argument and merge the generated columns into it.
464
-
465
- ### Example Usage
466
-
467
- ```typescript
468
- import { pgTable, text } from 'drizzle-orm/pg-core';
469
- import {
470
- generateIdColumnDefs,
471
- generateTzColumnDefs,
472
- generateUserAuditColumnDefs,
473
- } from '@venizia/ignis';
474
-
475
- export const myTable = pgTable('MyTable', {
476
- ...generateIdColumnDefs({ id: { dataType: 'string' } }),
477
- ...generateTzColumnDefs(),
478
- ...generateUserAuditColumnDefs({
479
- created: { dataType: 'string', columnName: 'created_by' },
480
- modified: { dataType: 'string', columnName: 'modified_by' },
481
- }),
482
- name: text('name').notNull(),
483
- });
484
- ```
485
-
486
-
487
- ## Detailed Enricher Reference
488
-
489
- ### `generateIdColumnDefs`
490
-
491
- Adds a primary key `id` column with support for string UUID, integer, or big integer types with full TypeScript type inference.
492
-
493
- **File:** `packages/core/src/connectors/postgres/models/enrichers/id.enricher.ts`
494
-
495
- #### Signature
496
-
497
- ```typescript
498
- generateIdColumnDefs<Opts extends TIdEnricherOptions | undefined>(
499
- opts?: Opts,
500
- ): TIdColumnDef<Opts>
501
- ```
502
-
503
- #### Options (`TIdEnricherOptions`)
504
-
505
- ```typescript
506
- type TIdEnricherOptions = {
507
- id?: { columnName?: string } & (
508
- | { dataType: 'string'; generator?: () => string } // Optional custom ID generator
509
- | {
510
- dataType: 'number';
511
- sequenceOptions?: PgSequenceOptions;
512
- }
513
- | {
514
- dataType: 'big-number';
515
- numberMode: 'number' | 'bigint'; // Required for big-number
516
- sequenceOptions?: PgSequenceOptions;
517
- }
518
- );
519
- };
520
- ```
521
-
522
- **Default values:**
523
- - `dataType`: `'number'` (auto-incrementing integer)
524
- - `columnName`: `'id'`
525
-
526
- #### Generated Columns
527
-
528
- | Data Type | Column Type | Constraints | Description |
529
- |-----------|------------|-------------|-------------|
530
- | `'string'` | `text` | Primary Key, Default: `crypto.randomUUID()` | Text column with customizable ID generator (default: UUID) |
531
- | `'number'` | `integer` | Primary Key, `GENERATED ALWAYS AS IDENTITY` | Auto-incrementing integer |
532
- | `'big-number'` | `bigint` | Primary Key, `GENERATED ALWAYS AS IDENTITY` | Auto-incrementing big integer (mode: 'number' or 'bigint') |
533
-
534
- #### Type Inference
535
-
536
- The function provides **full TypeScript type inference** based on the configuration options:
537
-
538
- ```typescript
539
- // Type aliases for readability
540
- type TStringIdCol = HasRuntimeDefault<
541
- HasDefault<IsPrimaryKey<NotNull<PgTextBuilderInitial<'id', [string, ...string[]]>>>>
542
- >;
543
- type TNumberIdCol = IsIdentity<IsPrimaryKey<NotNull<PgIntegerBuilderInitial<'id'>>>, 'always'>;
544
- type TBigInt53IdCol = IsIdentity<IsPrimaryKey<NotNull<PgBigInt53BuilderInitial<'id'>>>, 'always'>;
545
- type TBigInt64IdCol = IsIdentity<IsPrimaryKey<NotNull<PgBigInt64BuilderInitial<'id'>>>, 'always'>;
546
-
547
- type TIdColumnDef<Opts extends TIdEnricherOptions | undefined> = Opts extends {
548
- id: infer IdOpts;
549
- }
550
- ? IdOpts extends { dataType: 'string' }
551
- ? { id: TStringIdCol }
552
- : IdOpts extends { dataType: 'number' }
553
- ? { id: TNumberIdCol }
554
- : IdOpts extends { dataType: 'big-number' }
555
- ? IdOpts extends { numberMode: 'number' }
556
- ? { id: TBigInt53IdCol }
557
- : { id: TBigInt64IdCol }
558
- : { id: TNumberIdCol }
559
- : { id: TNumberIdCol };
560
- ```
561
-
562
- This ensures that TypeScript correctly infers the exact column type based on your configuration.
563
-
564
- #### Usage Examples
565
-
566
- **Default (auto-incrementing integer):**
567
-
568
- ```typescript
569
- import { pgTable, text } from 'drizzle-orm/pg-core';
570
- import { generateIdColumnDefs } from '@venizia/ignis';
571
-
572
- export const myTable = pgTable('MyTable', {
573
- ...generateIdColumnDefs(),
574
- name: text('name').notNull(),
575
- });
576
-
577
- // Generates: id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY
578
- ```
579
-
580
- **Text-based string ID (UUID by default):**
581
-
582
- ```typescript
583
- export const myTable = pgTable('MyTable', {
584
- ...generateIdColumnDefs({ id: { dataType: 'string' } }),
585
- name: text('name').notNull(),
586
- });
587
-
588
- // Generates: id text PRIMARY KEY with $defaultFn(() => crypto.randomUUID())
589
- // Uses text column for maximum database compatibility
590
- ```
591
-
592
- **Custom ID generator (e.g., nanoid, cuid):**
593
-
594
- ```typescript
595
- import { nanoid } from 'nanoid';
596
-
597
- export const myTable = pgTable('MyTable', {
598
- ...generateIdColumnDefs({
599
- id: {
600
- dataType: 'string',
601
- generator: () => nanoid(), // Custom generator function
602
- },
603
- }),
604
- name: text('name').notNull(),
605
- });
606
-
607
- // Generates: id text PRIMARY KEY with $defaultFn(() => nanoid())
608
- ```
609
-
610
- **Auto-incrementing integer with sequence options:**
611
-
612
- ```typescript
613
- export const myTable = pgTable('MyTable', {
614
- ...generateIdColumnDefs({
615
- id: {
616
- dataType: 'number',
617
- sequenceOptions: { startWith: 1000, increment: 1 },
618
- },
619
- }),
620
- name: text('name').notNull(),
621
- });
622
-
623
- // Generates: id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (START WITH 1000 INCREMENT BY 1)
624
- ```
625
-
626
- **Big number with JavaScript number mode (up to 2^53-1):**
627
-
628
- ```typescript
629
- export const myTable = pgTable('MyTable', {
630
- ...generateIdColumnDefs({
631
- id: {
632
- dataType: 'big-number',
633
- numberMode: 'number', // Required field
634
- sequenceOptions: { startWith: 1, increment: 1 },
635
- },
636
- }),
637
- name: text('name').notNull(),
638
- });
639
-
640
- // Generates: id bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY
641
- // Type-safe: Returns PgBigInt53BuilderInitial (safe for JavaScript numbers)
642
- ```
643
-
644
- **Big number with BigInt mode (for values > 2^53-1):**
645
-
646
- ```typescript
647
- export const myTable = pgTable('MyTable', {
648
- ...generateIdColumnDefs({
649
- id: {
650
- dataType: 'big-number',
651
- numberMode: 'bigint', // Required field
652
- sequenceOptions: { startWith: 1, increment: 1 },
653
- },
654
- }),
655
- name: text('name').notNull(),
656
- });
657
-
658
- // Generates: id bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY
659
- // Type-safe: Returns PgBigInt64BuilderInitial (requires BigInt in JavaScript)
660
- ```
661
-
662
- #### Important Notes
663
-
664
- - **Text Column:** When using `dataType: 'string'`, a `text` column is used for maximum database compatibility. This allows you to use any ID format (UUID, nanoid, cuid, etc.) without database-specific constraints.
665
- - **Custom Generator:** You can provide a custom `generator` function to generate IDs. Default is `crypto.randomUUID()`.
666
- - **Type Safety:** The return type is fully inferred based on your options, providing better autocomplete and type checking
667
- - **Big Number Mode:** For `dataType: 'big-number'`, the `numberMode` field is required to specify whether to use JavaScript `number` (up to 2^53-1) or `bigint` (for larger values)
668
- - **Sequence Options:** Available for `number` and `big-number` types to customize identity generation behavior
669
-
670
- #### Convenience Wrapper: `enrichId`
671
-
672
- ```typescript
673
- enrichId(baseColumns: TColumnDefinitions, opts?: TIdEnricherOptions): TColumnDefinitions
674
- ```
675
-
676
- Merges the generated ID column into an existing column definitions object:
677
-
678
- ```typescript
679
- import { text } from 'drizzle-orm/pg-core';
680
- import { enrichId } from '@venizia/ignis';
681
-
682
- const columns = enrichId(
683
- { name: text('name').notNull() },
684
- { id: { dataType: 'string' } },
685
- );
686
- ```
687
-
688
-
689
- ### `generateTzColumnDefs`
690
-
691
- Adds timestamp columns for tracking entity creation, modification, and soft deletion.
692
-
693
- **File:** `packages/core/src/connectors/postgres/models/enrichers/tz.enricher.ts`
694
-
695
- #### Signature
696
-
697
- ```typescript
698
- generateTzColumnDefs<Opts extends TTzEnricherOptions | undefined>(
699
- opts?: Opts,
700
- ): TTzEnricherResult<Opts>
701
- ```
702
-
703
- #### Options (`TTzEnricherOptions`)
704
-
705
- ```typescript
706
- type TTzEnricherOptions = {
707
- created?: { columnName: string; withTimezone: boolean };
708
- modified?: { enable: false } | { enable?: true; columnName: string; withTimezone: boolean };
709
- deleted?: { enable: false } | { enable?: true; columnName: string; withTimezone: boolean };
710
- };
711
- ```
712
-
713
- The `modified` and `deleted` options use a discriminated union pattern:
714
- - When `enable: false`, no other properties are needed
715
- - When `enable: true` (or omitted), `columnName` and `withTimezone` are required
716
-
717
- **Default values:**
718
- - `created`: `{ columnName: 'created_at', withTimezone: true }`
719
- - `modified`: `{ enable: true, columnName: 'modified_at', withTimezone: true }`
720
- - `deleted`: `{ enable: false }` (disabled by default)
721
-
722
- #### Generated Columns
723
-
724
- | Column | Type | Constraints | Default | Description |
725
- |--------|------|-------------|---------|-------------|
726
- | `createdAt` | `timestamp` | `NOT NULL` | `now()` | When the record was created (always included) |
727
- | `modifiedAt` | `timestamp` | `NOT NULL` | `now()`, auto-updates via `$onUpdate(() => new Date().toISOString())` | When the record was last modified (optional, enabled by default) |
728
- | `deletedAt` | `timestamp` | nullable | `null` | When the record was soft-deleted (optional, **disabled by default**) |
729
-
730
- #### Usage Examples
731
-
732
- **Basic usage (default columns):**
733
-
734
- ```typescript
735
- import { pgTable, text } from 'drizzle-orm/pg-core';
736
- import { generateTzColumnDefs } from '@venizia/ignis';
737
-
738
- export const myTable = pgTable('MyTable', {
739
- ...generateTzColumnDefs(),
740
- name: text('name').notNull(),
741
- });
742
-
743
- // Generates: createdAt, modifiedAt (deletedAt is disabled by default)
744
- ```
745
-
746
- **Enable soft delete:**
747
-
748
- ```typescript
749
- export const myTable = pgTable('MyTable', {
750
- ...generateTzColumnDefs({
751
- deleted: { enable: true, columnName: 'deleted_at', withTimezone: true },
752
- }),
753
- name: text('name').notNull(),
754
- });
755
-
756
- // Generates: createdAt, modifiedAt, deletedAt
757
- ```
758
-
759
- **Custom column names:**
760
-
761
- ```typescript
762
- export const myTable = pgTable('MyTable', {
763
- ...generateTzColumnDefs({
764
- created: { columnName: 'created_date', withTimezone: true },
765
- modified: { enable: true, columnName: 'updated_date', withTimezone: true },
766
- deleted: { enable: true, columnName: 'removed_date', withTimezone: true },
767
- }),
768
- name: text('name').notNull(),
769
- });
770
- ```
771
-
772
- **Without timezone:**
773
-
774
- ```typescript
775
- export const myTable = pgTable('MyTable', {
776
- ...generateTzColumnDefs({
777
- created: { columnName: 'created_at', withTimezone: false },
778
- modified: { enable: true, columnName: 'modified_at', withTimezone: false },
779
- deleted: { enable: true, columnName: 'deleted_at', withTimezone: false },
780
- }),
781
- name: text('name').notNull(),
782
- });
783
- ```
784
-
785
-
786
-
787
- **Minimal setup (only createdAt):**
788
-
789
- ```typescript
790
- export const myTable = pgTable('MyTable', {
791
- ...generateTzColumnDefs({
792
- modified: { enable: false },
793
- deleted: { enable: false },
794
- }),
795
- name: text('name').notNull(),
796
- });
797
-
798
- // Generates: createdAt only
799
- ```
800
-
801
- #### Soft Delete Pattern
802
-
803
- The `deletedAt` column enables the soft delete pattern, where records are marked as deleted rather than physically removed from the database.
804
-
805
- **Example soft delete query:**
806
-
807
- ```typescript
808
- import { eq, isNull } from 'drizzle-orm';
809
-
810
- // Soft delete: set deletedAt timestamp
811
- await db.update(myTable)
812
- .set({ deletedAt: new Date() })
813
- .where(eq(myTable.id, id));
814
-
815
- // Query only active (non-deleted) records
816
- const activeRecords = await db.select()
817
- .from(myTable)
818
- .where(isNull(myTable.deletedAt));
819
-
820
- // Query deleted records
821
- const deletedRecords = await db.select()
822
- .from(myTable)
823
- .where(isNotNull(myTable.deletedAt));
824
-
825
- // Restore a soft-deleted record
826
- await db.update(myTable)
827
- .set({ deletedAt: null })
828
- .where(eq(myTable.id, id));
829
- ```
830
-
831
- #### Type Inference
832
-
833
- The enricher provides **conditional TypeScript type inference** based on the options:
834
-
835
- ```typescript
836
- type TIsoTimestampColumn = ReturnType<typeof isoTimestamp>; // custom ISO 8601 timestamp column
837
-
838
- type TTzEnricherResult<Opts extends TTzEnricherOptions | undefined = undefined> = {
839
- createdAt: NotNull<HasDefault<TIsoTimestampColumn>>;
840
- } & (/* modifiedAt included unless opts.modified.enable === false */)
841
- & (/* deletedAt included only when opts.deleted.enable === true */);
842
- ```
843
-
844
- - `createdAt` is always present
845
- - `modifiedAt` is present by default; excluded only when `modified: { enable: false }`
846
- - `deletedAt` is absent by default; included only when `deleted: { enable: true, ... }`
847
-
848
- #### Convenience Wrapper: `enrichTz`
849
-
850
- ```typescript
851
- enrichTz(baseSchema: TColumnDefinitions, opts?: TTzEnricherOptions): TColumnDefinitions
852
- ```
853
-
854
- Merges timestamp columns into an existing column definitions object.
855
-
856
-
857
- ### `generateUserAuditColumnDefs`
858
-
859
- Adds `createdBy` and `modifiedBy` columns to track which user created or modified a record.
860
-
861
- **File:** `packages/core/src/connectors/postgres/models/enrichers/user-audit.enricher.ts`
862
-
863
- #### Signature
864
-
865
- ```typescript
866
- generateUserAuditColumnDefs(opts?: TUserAuditEnricherOptions): {
867
- createdBy: PgIntegerBuilderInitial | PgTextBuilderInitial;
868
- modifiedBy: PgIntegerBuilderInitial | PgTextBuilderInitial;
869
- }
870
- ```
871
-
872
- #### Options (`TUserAuditEnricherOptions`)
873
-
874
- ```typescript
875
- type TUserAuditColumnOpts = {
876
- dataType: 'string' | 'number'; // Required - type of user ID
877
- columnName: string; // Column name in database
878
- allowAnonymous?: boolean; // Allow null user ID (default: true)
879
- };
880
-
881
- type TUserAuditEnricherOptions = {
882
- created?: TUserAuditColumnOpts;
883
- modified?: TUserAuditColumnOpts;
884
- };
885
- ```
886
-
887
- **Default values:**
888
- - `created`: `{ dataType: 'number', columnName: 'created_by', allowAnonymous: true }`
889
- - `modified`: `{ dataType: 'number', columnName: 'modified_by', allowAnonymous: true }`
890
-
891
- #### How It Works
892
-
893
- The enricher uses Hono's `contextStorage` (via `tryGetContext()`) to automatically retrieve the current user ID from the request context at insert/update time:
894
-
895
- - **`createdBy`**: Set via `$default()` - only populated on record creation
896
- - **`modifiedBy`**: Set via both `$default()` and `$onUpdate()` - populated on creation and updated on every modification
897
-
898
- The user ID is read from the `Authentication.AUDIT_USER_ID` key in the Hono context.
899
-
900
- #### `allowAnonymous` Behavior
901
-
902
- The `allowAnonymous` option controls whether the enricher requires an authenticated user context:
903
-
904
- | `allowAnonymous` | No Context | No User ID | Has User ID |
905
- |------------------|------------|------------|-------------|
906
- | `true` (default) | Returns `null` | Returns `null` | Returns user ID |
907
- | `false` | Throws error | Throws error | Returns user ID |
28
+ A `@repository` binds this model to a datasource, and the datasource auto-discovers the schema from that binding - no manual schema registration.
908
29
 
909
- **When to use `allowAnonymous: false`:**
910
- - Sensitive audit trails that must track the responsible user
911
- - Tables where anonymous operations should be forbidden
912
- - Compliance requirements that mandate user attribution
30
+ ## How it works
913
31
 
914
- **When to use `allowAnonymous: true` (default):**
915
- - Background jobs, migrations, or seed scripts without user context
916
- - System-generated records
917
- - Tables that allow both authenticated and anonymous operations
32
+ - **Registration.** The `@model` decorator registers the class in the framework's metadata registry, keyed by table name (resolved as `tableName` > static `TABLE_NAME` > class name).
33
+ - **Validation at decoration time.** It validates `settings.defaultLimit` and, when you declare an authorization principal, copies it onto the static `AUTHORIZATION_SUBJECT` property.
34
+ - **Plain Drizzle schema.** The static `schema` is a plain Drizzle `pgTable`. Enrichers such as `generateIdColumnDefs` return column definitions you spread into that table, so common columns (id, timestamps, audit, principal) stay standardized across models.
35
+ - **Zod on demand.** `BaseEntity` generates Zod schemas from the Drizzle schema via `getSchema({ type })` - `'select'`, `'create'`, and `'update'` variants for validating query results, inserts, and updates. The generator is a shared lazy singleton, so there is no per-entity cost.
918
36
 
919
- > [!WARNING]
920
- > Fire-and-forget promises may run outside the async context, losing access to `AUDIT_USER_ID`. Ensure audit-critical operations complete within the request lifecycle.
37
+ **Two layers**
921
38
 
922
- #### Generated Columns
39
+ | Layer | Class | Carries |
40
+ |-------|-------|---------|
41
+ | Engine-neutral root | `AbstractEntity` | A name, `getSchema()`, `getIdType()`, and `toObject()`/`toJSON()` |
42
+ | PostgreSQL connector | `BaseEntity` (canonical class `BaseRelationalEntity`) | Adds the Drizzle-backed schema and Zod generation |
923
43
 
924
- | Column | Data Type | Column Name | Description |
925
- |--------|-----------|-------------|-------------|
926
- | `createdBy` | `integer` or `text` | `created_by` | User ID who created the record |
927
- | `modifiedBy` | `integer` or `text` | `modified_by` | User ID who last modified the record |
44
+ Everything on this page is the PostgreSQL connector - see the [Full reference](/references/base/models-reference) and [Connectors](/references/base/connectors) for the base-vs-connector split.
928
45
 
929
- #### Validation
46
+ ## Common tasks
930
47
 
931
- The enricher validates the `dataType` option and throws an error for invalid values:
48
+ ### Add id and timestamp columns
932
49
 
933
- ```typescript
934
- // Valid
935
- generateUserAuditColumnDefs({ created: { dataType: 'number', columnName: 'created_by' } });
936
- generateUserAuditColumnDefs({ created: { dataType: 'string', columnName: 'created_by' } });
937
-
938
- // Invalid - throws error
939
- generateUserAuditColumnDefs({ created: { dataType: 'uuid', columnName: 'created_by' } });
940
- // Error: [enrichUserAudit] Invalid dataType for 'createdBy' | value: uuid | valid: ['number', 'string']
941
- ```
942
-
943
- #### Usage Examples
944
-
945
- **Default (integer user IDs):**
50
+ Enrichers return column definitions - spread them into the `pgTable`. `generateIdColumnDefs` adds the primary key; `generateTzColumnDefs` adds `createdAt` and `modifiedAt`.
946
51
 
947
52
  ```typescript
948
53
  import { pgTable, text } from 'drizzle-orm/pg-core';
949
- import { generateIdColumnDefs, generateUserAuditColumnDefs } from '@venizia/ignis';
950
-
951
- export const myTable = pgTable('MyTable', {
952
- ...generateIdColumnDefs(),
953
- ...generateUserAuditColumnDefs(),
954
- name: text('name').notNull(),
955
- });
956
-
957
- // Generates:
958
- // createdBy: integer('created_by')
959
- // modifiedBy: integer('modified_by')
960
- ```
961
-
962
- **String user IDs (UUID):**
963
-
964
- ```typescript
965
- export const myTable = pgTable('MyTable', {
966
- ...generateIdColumnDefs({ id: { dataType: 'string' } }),
967
- ...generateUserAuditColumnDefs({
968
- created: { dataType: 'string', columnName: 'created_by' },
969
- modified: { dataType: 'string', columnName: 'modified_by' },
970
- }),
971
- name: text('name').notNull(),
972
- });
973
-
974
- // Generates:
975
- // createdBy: text('created_by')
976
- // modifiedBy: text('modified_by')
977
- ```
978
-
979
- **Custom column names:**
980
-
981
- ```typescript
982
- export const myTable = pgTable('MyTable', {
983
- ...generateIdColumnDefs(),
984
- ...generateUserAuditColumnDefs({
985
- created: { dataType: 'number', columnName: 'author_id' },
986
- modified: { dataType: 'number', columnName: 'editor_id' },
987
- }),
988
- name: text('name').notNull(),
989
- });
990
-
991
- // Generates:
992
- // createdBy: integer('author_id')
993
- // modifiedBy: integer('editor_id')
994
- ```
995
-
996
- **Requiring authenticated user (allowAnonymous: false):**
997
-
998
- ```typescript
999
- // For sensitive tables that must track the responsible user
1000
- export const auditLogTable = pgTable('AuditLog', {
1001
- ...generateIdColumnDefs(),
1002
- ...generateUserAuditColumnDefs({
1003
- created: { dataType: 'number', columnName: 'created_by', allowAnonymous: false },
1004
- modified: { dataType: 'number', columnName: 'modified_by', allowAnonymous: false },
1005
- }),
1006
- action: text('action').notNull(),
1007
- details: text('details'),
1008
- });
1009
-
1010
- // If no authenticated user context is available, throws:
1011
- // Error: [getCurrentUserId] Invalid request context to identify user | columnName: createdBy | allowAnonymous: false
1012
- ```
1013
-
1014
- #### Convenience Wrapper: `enrichUserAudit`
1015
-
1016
- ```typescript
1017
- enrichUserAudit<ColumnDefinitions extends TColumnDefinitions>(
1018
- baseSchema: ColumnDefinitions,
1019
- opts?: TUserAuditEnricherOptions,
1020
- ): TUserAuditEnricherResult<ColumnDefinitions>
1021
- ```
1022
-
1023
- Merges user audit columns into an existing column definitions object with proper type inference.
1024
-
1025
-
1026
- ### `generatePrincipalColumnDefs`
1027
-
1028
- Adds polymorphic principal columns for associating a record with different entity types. This is the polymorphic association pattern where a row can belong to different parent types (e.g., a comment can belong to a Post, User, or Product).
1029
-
1030
- **File:** `packages/core/src/connectors/postgres/models/enrichers/principal.enricher.ts`
1031
-
1032
- #### Signature
1033
-
1034
- ```typescript
1035
- generatePrincipalColumnDefs<
1036
- Discriminator extends string = 'principal',
1037
- IdType extends 'number' | 'string' = 'number',
1038
- >(
1039
- opts: TPrincipalEnricherOptions<Discriminator, IdType>,
1040
- ): TPrincipalColumnDef<Discriminator, IdType>
1041
- ```
1042
-
1043
- #### Options (`TPrincipalEnricherOptions`)
1044
-
1045
- ```typescript
1046
- type TPrincipalEnricherOptions<
1047
- Discriminator extends string = string,
1048
- IdType extends 'number' | 'string' = 'number' | 'string',
1049
- > = {
1050
- discriminator?: Discriminator; // Field name prefix (default: 'principal')
1051
- defaultPolymorphic?: string; // Default value for the type column (default: '')
1052
- polymorphicIdType: IdType; // Required - type of the principal ID column
1053
- };
1054
- ```
1055
-
1056
- | Option | Type | Default | Description |
1057
- |--------|------|---------|-------------|
1058
- | `discriminator` | `string` | `'principal'` | Prefix for generated column names |
1059
- | `defaultPolymorphic` | `string` | `''` | Default value for the type discriminator column |
1060
- | `polymorphicIdType` | `'number' \| 'string'` | (required) | Data type of the ID column |
1061
-
1062
- #### Generated Columns
1063
-
1064
- Given `discriminator = 'principal'` (default):
1065
-
1066
- | Column | DB Column Name | Type | Constraints | Description |
1067
- |--------|---------------|------|-------------|-------------|
1068
- | `principalId` | `principal_id` | `integer` or `text` | `NOT NULL` | The ID of the associated entity |
1069
- | `principalType` | `principal_type` | `text` | `DEFAULT ''` | The type discriminator (e.g., `'User'`, `'Post'`) |
1070
-
1071
- With a custom discriminator (e.g., `discriminator: 'owner'`):
1072
-
1073
- | Column | DB Column Name | Type |
1074
- |--------|---------------|------|
1075
- | `ownerId` | `owner_id` | `integer` or `text` |
1076
- | `ownerType` | `owner_type` | `text` |
1077
-
1078
- #### Usage Examples
1079
-
1080
- **Default (polymorphic principal with numeric ID):**
1081
-
1082
- ```typescript
1083
- import { pgTable, text } from 'drizzle-orm/pg-core';
1084
- import { generateIdColumnDefs, generatePrincipalColumnDefs } from '@venizia/ignis';
1085
-
1086
- export const commentTable = pgTable('Comment', {
1087
- ...generateIdColumnDefs(),
1088
- ...generatePrincipalColumnDefs({ polymorphicIdType: 'number' }),
1089
- content: text('content').notNull(),
1090
- });
1091
-
1092
- // Generates:
1093
- // principalId: integer('principal_id').notNull()
1094
- // principalType: text('principal_type').default('')
1095
- ```
1096
-
1097
- **Custom discriminator name:**
1098
-
1099
- ```typescript
1100
- export const attachmentTable = pgTable('Attachment', {
1101
- ...generateIdColumnDefs(),
1102
- ...generatePrincipalColumnDefs({
1103
- discriminator: 'owner',
1104
- polymorphicIdType: 'string',
1105
- defaultPolymorphic: 'User',
1106
- }),
1107
- filePath: text('file_path').notNull(),
1108
- });
1109
-
1110
- // Generates:
1111
- // ownerId: text('owner_id').notNull()
1112
- // ownerType: text('owner_type').default('User')
1113
- ```
1114
-
1115
- **Polymorphic association pattern:**
1116
-
1117
- ```typescript
1118
- // A notification can belong to different entity types
1119
- export const notificationTable = pgTable('Notification', {
1120
- ...generateIdColumnDefs({ id: { dataType: 'string' } }),
1121
- ...generatePrincipalColumnDefs({
1122
- discriminator: 'target',
1123
- polymorphicIdType: 'string',
1124
- }),
1125
- message: text('message').notNull(),
1126
- });
1127
-
1128
- // Usage:
1129
- // { targetId: 'user-123', targetType: 'User', message: 'Welcome!' }
1130
- // { targetId: 'order-456', targetType: 'Order', message: 'Order shipped' }
1131
- ```
1132
-
1133
- #### Convenience Wrapper: `enrichPrincipal`
1134
-
1135
- ```typescript
1136
- enrichPrincipal<ColumnDefinitions extends TColumnDefinitions>(
1137
- baseSchema: ColumnDefinitions,
1138
- opts: TPrincipalEnricherOptions,
1139
- ): ColumnDefinitions & TPrincipalColumnDef
1140
- ```
1141
-
1142
- Merges principal columns into an existing column definitions object.
1143
-
1144
-
1145
- ### `generateDataTypeColumnDefs`
1146
-
1147
- Adds polymorphic data storage columns for entities that need to store values of different types in a single table. This is useful for key-value stores, settings tables, or any schema where a row's value type is determined at runtime.
1148
-
1149
- **File:** `packages/core/src/connectors/postgres/models/enrichers/data-type.enricher.ts`
1150
-
1151
- #### Signature
1152
-
1153
- ```typescript
1154
- generateDataTypeColumnDefs(opts?: TDataTypeEnricherOptions): {
1155
- dataType: PgTextBuilderInitial;
1156
- nValue: PgDoublePrecisionBuilderInitial;
1157
- tValue: PgTextBuilderInitial;
1158
- bValue: PgCustomColumnBuilder<Buffer>;
1159
- jValue: PgJsonbBuilderInitial<Record<string, any>>;
1160
- boValue: PgBooleanBuilderInitial;
1161
- }
1162
- ```
1163
-
1164
- #### Options (`TDataTypeEnricherOptions`)
1165
-
1166
- ```typescript
1167
- type TDataTypeEnricherOptions = {
1168
- defaultValue: Partial<{
1169
- dataType: string;
1170
- nValue: number;
1171
- tValue: string;
1172
- bValue: Buffer;
1173
- jValue: object;
1174
- boValue: boolean;
1175
- }>;
1176
- };
1177
- ```
1178
-
1179
- #### Generated Columns
1180
-
1181
- | Column | SQL Type | DB Column Name | TypeScript Type | Purpose |
1182
- |--------|----------|----------------|-----------------|---------|
1183
- | `dataType` | `text` | `data_type` | `string` | Type discriminator (e.g., `'number'`, `'text'`, `'json'`) |
1184
- | `nValue` | `double precision` | `n_value` | `number` | Numeric values |
1185
- | `tValue` | `text` | `t_value` | `string` | Text values |
1186
- | `bValue` | `bytea` | `b_value` | `Buffer` | Binary values |
1187
- | `jValue` | `jsonb` | `j_value` | `Record<string, any>` | JSON values |
1188
- | `boValue` | `boolean` | `bo_value` | `boolean` | Boolean values |
1189
-
1190
- All columns are **nullable** by default (no `NOT NULL` constraint), since only one value column is typically populated per row depending on the `dataType` discriminator.
1191
-
1192
- #### Usage Examples
1193
-
1194
- **Basic usage:**
1195
-
1196
- ```typescript
1197
- import { pgTable } from 'drizzle-orm/pg-core';
1198
- import { BaseEntity, model, generateIdColumnDefs, generateDataTypeColumnDefs } from '@venizia/ignis';
54
+ import { BaseEntity, model, generateIdColumnDefs, generateTzColumnDefs } from '@venizia/ignis';
1199
55
 
1200
56
  @model({ type: 'entity' })
1201
- export class Setting extends BaseEntity<typeof Setting.schema> {
1202
- static override schema = pgTable('Setting', {
57
+ export class Article extends BaseEntity<typeof Article.schema> {
58
+ static override schema = pgTable('Article', {
1203
59
  ...generateIdColumnDefs({ id: { dataType: 'string' } }),
1204
- ...generateDataTypeColumnDefs(),
60
+ ...generateTzColumnDefs(),
61
+ title: text('title').notNull(),
1205
62
  });
1206
63
  }
1207
64
  ```
1208
65
 
1209
- **With default values:**
1210
-
1211
- ```typescript
1212
- export const settingTable = pgTable('Setting', {
1213
- ...generateIdColumnDefs({ id: { dataType: 'string' } }),
1214
- ...generateDataTypeColumnDefs({
1215
- defaultValue: { dataType: 'text', tValue: '' },
1216
- }),
1217
- });
1218
-
1219
- // Generates columns with SQL defaults:
1220
- // data_type text DEFAULT 'text'
1221
- // t_value text DEFAULT ''
1222
- // nValue, bValue, jValue, boValue - no defaults
1223
- ```
66
+ ### Hide a field
1224
67
 
1225
- **Key-value store pattern:**
68
+ List a column in `settings.hiddenProperties`. Hidden columns are excluded at the SQL level - never selected or returned through repositories.
1226
69
 
1227
70
  ```typescript
1228
- @model({ type: 'entity' })
1229
- export class AppConfig extends BaseEntity<typeof AppConfig.schema> {
1230
- static override schema = pgTable('AppConfig', {
71
+ @model({
72
+ type: 'entity',
73
+ settings: { hiddenProperties: ['password'] },
74
+ })
75
+ export class User extends BaseEntity<typeof User.schema> {
76
+ static override schema = pgTable('User', {
1231
77
  ...generateIdColumnDefs({ id: { dataType: 'string' } }),
1232
- ...generateDataTypeColumnDefs(),
1233
- key: text('key').notNull().unique(),
1234
- description: text('description'),
78
+ email: text('email').notNull(),
79
+ password: text('password'),
1235
80
  });
1236
81
  }
1237
-
1238
- // Usage:
1239
- // { key: 'max_retries', dataType: 'number', nValue: 3 }
1240
- // { key: 'welcome_message', dataType: 'text', tValue: 'Hello!' }
1241
- // { key: 'feature_flags', dataType: 'json', jValue: { darkMode: true } }
1242
- // { key: 'is_maintenance', dataType: 'boolean', boValue: false }
1243
82
  ```
1244
83
 
1245
- #### Convenience Wrapper: `enrichDataTypes`
84
+ ### Apply a default filter
1246
85
 
1247
- ```typescript
1248
- enrichDataTypes(
1249
- baseSchema: TColumnDefinitions,
1250
- opts?: TDataTypeEnricherOptions,
1251
- ): TColumnDefinitions
1252
- ```
1253
-
1254
- Merges data type columns into an existing column definitions object:
86
+ `settings.defaultFilter` is merged into every repository read, count, update, and delete for the model. A common use is soft delete.
1255
87
 
1256
88
  ```typescript
1257
- import { text } from 'drizzle-orm/pg-core';
1258
- import { enrichDataTypes, generateIdColumnDefs } from '@venizia/ignis';
1259
-
1260
- const baseColumns = {
1261
- ...generateIdColumnDefs({ id: { dataType: 'string' } }),
1262
- key: text('key').notNull(),
1263
- };
1264
-
1265
- // Merge data type columns into existing column definitions
1266
- const allColumns = enrichDataTypes(baseColumns);
1267
-
1268
- export const configTable = pgTable('Config', allColumns);
1269
- ```
1270
-
1271
-
1272
- ## Schema Utilities
1273
-
1274
- ### `idParamsSchema`
1275
-
1276
- Generates a Zod schema for path parameters containing an `id` field, suitable for OpenAPI route definitions.
1277
-
1278
- **File:** `packages/core/src/base/models/common/types.ts`
1279
-
1280
- #### Signature
1281
-
1282
- ```typescript
1283
- idParamsSchema(opts?: { idType: TIdSchemaType }): z.ZodObject<{ id: z.ZodNumber | z.ZodString }>
1284
- ```
1285
-
1286
- | `idType` | Default | Zod Type | Examples |
1287
- |----------|---------|----------|----------|
1288
- | `'number'` | Yes | `z.number()` | `[1, 2, 3]` |
1289
- | `'string'` | | `z.string()` | `['4651e634-...', 'some_unique_id']` |
89
+ import { model, BaseEntity } from '@venizia/ignis';
90
+ import { postTable } from '@/schemas';
1290
91
 
1291
- Throws an error for invalid `idType` values.
1292
-
1293
- ### `jsonContent`
1294
-
1295
- Creates an OpenAPI JSON content specification:
1296
-
1297
- ```typescript
1298
- jsonContent<T extends z.ZodType>(opts: {
1299
- schema: T;
1300
- description: string;
1301
- required?: boolean;
1302
- }): { description, content: { 'application/json': { schema } }, required? }
1303
- ```
1304
-
1305
- ### `jsonResponse`
1306
-
1307
- Creates a complete OpenAPI response specification with success and error responses:
1308
-
1309
- ```typescript
1310
- jsonResponse<ContentSchema, HeaderSchema>(opts: {
1311
- schema: ContentSchema;
1312
- description?: string; // Default: 'Success Response'
1313
- required?: boolean;
1314
- headers?: HeaderSchema;
1315
- }): {
1316
- 200: { description, content, headers? },
1317
- '4xx | 5xx': { description: 'Error Response', content: ErrorSchema }
92
+ @model({
93
+ type: 'entity',
94
+ settings: { defaultFilter: { where: { isDeleted: false } } },
95
+ })
96
+ export class Post extends BaseEntity<typeof Post.schema> {
97
+ static override schema = postTable;
1318
98
  }
1319
99
  ```
1320
100
 
1321
- ### `snakeToCamel`
1322
-
1323
- Converts a Zod schema from snake_case to camelCase, transforming both the schema shape and runtime data.
1324
-
1325
- **File:** `packages/core/src/base/models/common/types.ts`
1326
-
1327
- #### Signature
1328
-
1329
- ```typescript
1330
- snakeToCamel<T extends z.ZodRawShape>(shape: T): z.ZodEffects<...>
1331
- ```
1332
-
1333
- #### Purpose
1334
-
1335
- This utility is useful when working with databases that use snake_case column names but you want to work with camelCase in your TypeScript code. It creates a Zod schema that:
1336
-
1337
- 1. Accepts snake_case input (validates against original schema)
1338
- 2. Transforms the data to camelCase at runtime
1339
- 3. Validates the transformed data against a camelCase schema
1340
-
1341
- #### Usage Example
1342
-
1343
- ```typescript
1344
- import { z } from 'zod';
1345
- import { snakeToCamel } from '@venizia/ignis';
1346
-
1347
- // Define schema with snake_case fields
1348
- const userSnakeSchema = {
1349
- user_id: z.number(),
1350
- first_name: z.string(),
1351
- last_name: z.string(),
1352
- created_at: z.date(),
1353
- is_active: z.boolean(),
1354
- };
1355
-
1356
- // Convert to camelCase schema
1357
- const userCamelSchema = snakeToCamel(userSnakeSchema);
1358
-
1359
- // Input data from database (snake_case)
1360
- const dbData = {
1361
- user_id: 123,
1362
- first_name: 'John',
1363
- last_name: 'Doe',
1364
- created_at: new Date(),
1365
- is_active: true,
1366
- };
1367
-
1368
- // Parse and transform to camelCase
1369
- const result = userCamelSchema.parse(dbData);
1370
-
1371
- // Result is automatically camelCase:
1372
- console.log(result);
1373
- // {
1374
- // userId: 123,
1375
- // firstName: 'John',
1376
- // lastName: 'Doe',
1377
- // createdAt: Date,
1378
- // isActive: true
1379
- // }
1380
- ```
101
+ Pass `options: { shouldSkipDefaultFilter: true }` on a query to bypass it. See [Default Filter](/references/base/filter-system/default-filter).
1381
102
 
1382
- #### Real-world Example
103
+ ### Set a default limit
1383
104
 
1384
- **Use case:** API endpoint that accepts snake_case but works with camelCase internally
105
+ `settings.defaultLimit` caps queries that omit `limit`. It must be a positive integer (validated at decoration time) and falls back to the global `DEFAULT_LIMIT` (10).
1385
106
 
1386
107
  ```typescript
1387
- import { BaseRestController, controller, snakeToCamel } from '@venizia/ignis';
1388
- import { HTTP } from '@venizia/ignis-helpers';
1389
- import { z } from '@hono/zod-openapi';
1390
-
1391
- const createUserSchema = snakeToCamel({
1392
- first_name: z.string().min(1),
1393
- last_name: z.string().min(1),
1394
- email_address: z.string().email(),
1395
- phone_number: z.string().optional(),
1396
- });
1397
-
1398
- @controller({ path: '/users' })
1399
- export class UserController extends BaseRestController {
1400
- override binding() {
1401
- this.bindRoute({
1402
- configs: {
1403
- path: '/',
1404
- method: 'post',
1405
- request: {
1406
- body: {
1407
- content: {
1408
- 'application/json': { schema: createUserSchema },
1409
- },
1410
- },
1411
- },
1412
- },
1413
- }).to({
1414
- handler: async (ctx) => {
1415
- // Request body is automatically camelCase
1416
- const data = ctx.req.valid('json');
1417
-
1418
- // data = {
1419
- // firstName: string,
1420
- // lastName: string,
1421
- // emailAddress: string,
1422
- // phoneNumber?: string
1423
- // }
108
+ import { model, BaseEntity } from '@venizia/ignis';
109
+ import { eventTable } from '@/schemas';
1424
110
 
1425
- // Work with camelCase data
1426
- console.log(data.firstName); // TypeScript knows this exists
1427
- console.log(data.first_name); // TypeScript error
1428
-
1429
- return ctx.json({ success: true }, HTTP.ResultCodes.RS_2.Ok);
1430
- },
1431
- });
1432
- }
111
+ @model({
112
+ type: 'entity',
113
+ settings: { defaultLimit: 50 },
114
+ })
115
+ export class Event extends BaseEntity<typeof Event.schema> {
116
+ static override schema = eventTable;
1433
117
  }
1434
118
  ```
1435
119
 
1436
- #### Type Transformation
120
+ See [Pagination](/references/base/filter-system/fields-order-pagination#default-limit).
1437
121
 
1438
- The utility includes sophisticated TypeScript type transformation:
122
+ ### Declare an authorization principal
1439
123
 
1440
- ```typescript
1441
- type TSnakeToCamelCase<S extends string> =
1442
- S extends `${infer T}_${infer U}`
1443
- ? `${T}${Capitalize<TSnakeToCamelCase<U>>}`
1444
- : S;
1445
-
1446
- type TCamelCaseKeys<T extends z.ZodRawShape> = {
1447
- [K in keyof T as K extends string ? TSnakeToCamelCase<K> : K]:
1448
- T[K] extends z.ZodType<infer U> ? z.ZodType<U> : T[K];
1449
- };
1450
- ```
1451
-
1452
- This ensures full type safety: TypeScript will know that `first_name` becomes `firstName`, `created_at` becomes `createdAt`, etc.
1453
-
1454
- #### Validation
1455
-
1456
- The schema validates twice for safety:
1457
-
1458
- 1. **First validation:** Checks that input matches snake_case schema
1459
- 2. **Transformation:** Converts keys from snake_case to camelCase
1460
- 3. **Second validation:** Validates transformed data against camelCase schema
124
+ `settings.authorize.principal` names the model as an authorization subject. The decorator auto-populates the static `AUTHORIZATION_SUBJECT` from it.
1461
125
 
1462
126
  ```typescript
1463
- // If validation fails at any step, you get clear error messages
1464
- const invalidData = {
1465
- user_id: 'not-a-number', // Fails first validation
1466
- first_name: 'John',
1467
- last_name: 'Doe',
1468
- };
127
+ import { model, BaseEntity } from '@venizia/ignis';
128
+ import { userTable } from '@/schemas';
1469
129
 
1470
- try {
1471
- userCamelSchema.parse(invalidData);
1472
- } catch (error) {
1473
- // ZodError with clear message about user_id expecting number
130
+ @model({
131
+ type: 'entity',
132
+ settings: { authorize: { principal: 'User' } },
133
+ })
134
+ export class User extends BaseEntity<typeof User.schema> {
135
+ static override schema = userTable;
1474
136
  }
1475
137
  ```
1476
138
 
1477
- #### Notes
139
+ See [Authorization](/extensions/components/authorization/usage#model-based-resource-references).
1478
140
 
1479
- - Built on top of `keysToCamel()` and `toCamel()` utilities from `@venizia/ignis-helpers`
1480
- - Recursively handles nested objects
1481
- - Preserves array structures
1482
- - Works seamlessly with Zod's other features (refinements, transforms, etc.)
141
+ ## See also
1483
142
 
1484
- ### `getIdType`
1485
-
1486
- There are two distinct `getIdType`s in the framework - don't confuse them:
1487
-
1488
- | | Neutral instance method | PostgreSQL utility function |
1489
- |---|---|---|
1490
- | **Location** | `AbstractEntity.getIdType()` (`packages/core/src/base/models/base.ts`) | `getIdType()` (`packages/core/src/connectors/postgres/models/common/types.ts`) |
1491
- | **Signature** | `getIdType(): TIdSchemaType` | `getIdType<T extends TTableSchemaWithId>(opts: { entity: T }): string` |
1492
- | **Purpose** | Neutral capability every engine's entity implements - returns `'string'` \| `'number'` at the entity level. Used by `idParamsSchema` to build the right Zod schema for path parameters. | PostgreSQL-specific: inspects a Drizzle table schema's `id` column and returns its `dataType` (e.g., `'number'`, `'string'`), or `'unknown'` if not determinable |
1493
-
1494
- ```typescript
1495
- // Neutral - instance method every AbstractEntity subclass exposes (default 'string', BasePostgresEntity overrides based on the column)
1496
- const entity = new User();
1497
- entity.getIdType(); // 'string' | 'number'
143
+ - [Full reference](/references/base/models-reference) - every `@model` option, entity member, enricher, and edge case
144
+ - [Tutorial](/guides/core-concepts/persistent/models) - creating models step by step
145
+ - [Connectors](/references/base/connectors) - the base-vs-connector architecture
146
+ - [DataSources](/references/base/datasources) - binding a model's schema to a connection
147
+ - [Repositories](/references/base/repositories/) - the CRUD layer built on top of a model
148
+ - [Filter System](/references/base/filter-system/) - querying the model through a repository
1498
149
 
1499
- // PostgreSQL connector - standalone utility inspecting a raw Drizzle schema
1500
- import { getIdType } from '@venizia/ignis/postgres';
1501
- getIdType({ entity: User.schema }); // 'string' | 'number' | 'unknown'
1502
- ```
1503
-
1504
- ## See Also
1505
-
1506
- - **Related Concepts:**
1507
- - [Models Guide](/guides/core-concepts/persistent/models) - Creating models tutorial
1508
- - [Repositories](/guides/core-concepts/persistent/repositories) - Using models in repositories
1509
- - [DataSources](/guides/core-concepts/persistent/datasources) - Database connections
1510
-
1511
- - **References:**
1512
- - [Repositories API](/references/base/repositories/) - Data access layer
1513
- - [Relations](/references/base/repositories/relations) - Model relationships
1514
- - [Filter System](/references/base/filter-system/) - Querying models
1515
-
1516
- - **External Resources:**
1517
- - [Drizzle ORM Documentation](https://orm.drizzle.team/) - Schema definition guide
1518
- - [PostgreSQL Data Types](https://www.postgresql.org/docs/current/datatype.html) - Column types
1519
-
1520
- - **Best Practices:**
1521
- - [Data Modeling](/best-practices/data-modeling) - Schema design patterns
150
+ **Files:**
1522
151
 
1523
- - **Tutorials:**
1524
- - [Building a CRUD API](/guides/tutorials/building-a-crud-api) - Model examples
1525
- - [E-commerce API](/guides/tutorials/ecommerce-api) - Models with relations
152
+ - [`packages/core/src/base/models/base.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/base/models/base.ts) - neutral `AbstractEntity`
153
+ - [`packages/core/src/connectors/postgres/models/base.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/connectors/postgres/models/base.ts) - PostgreSQL `BaseEntity`