@venizia/ignis-docs 0.0.1-1 → 0.0.1-10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +102 -0
- package/README.md +459 -0
- package/dist/mcp-server/common/config.d.ts +64 -0
- package/dist/mcp-server/common/config.d.ts.map +1 -0
- package/dist/mcp-server/common/config.js +82 -0
- package/dist/mcp-server/common/config.js.map +1 -0
- package/dist/mcp-server/common/index.d.ts +3 -0
- package/dist/mcp-server/common/index.d.ts.map +1 -0
- package/dist/mcp-server/common/index.js.map +1 -0
- package/dist/mcp-server/common/paths.d.ts.map +1 -0
- package/{mcp-server/dist → dist/mcp-server}/common/paths.js +13 -11
- package/dist/mcp-server/common/paths.js.map +1 -0
- package/{mcp-server/dist → dist/mcp-server}/helpers/docs.helper.d.ts +5 -5
- package/dist/mcp-server/helpers/docs.helper.d.ts.map +1 -0
- package/{mcp-server/dist → dist/mcp-server}/helpers/docs.helper.js +38 -34
- package/dist/mcp-server/helpers/docs.helper.js.map +1 -0
- package/dist/mcp-server/helpers/github.helper.d.ts +37 -0
- package/dist/mcp-server/helpers/github.helper.d.ts.map +1 -0
- package/dist/mcp-server/helpers/github.helper.js +100 -0
- package/dist/mcp-server/helpers/github.helper.js.map +1 -0
- package/dist/mcp-server/helpers/index.d.ts +4 -0
- package/dist/mcp-server/helpers/index.d.ts.map +1 -0
- package/{mcp-server/dist → dist/mcp-server}/helpers/index.js +1 -0
- package/dist/mcp-server/helpers/index.js.map +1 -0
- package/dist/mcp-server/helpers/logger.helper.d.ts.map +1 -0
- package/dist/mcp-server/helpers/logger.helper.js.map +1 -0
- package/{mcp-server/dist → dist/mcp-server}/index.d.ts.map +1 -1
- package/dist/mcp-server/index.js +90 -0
- package/dist/mcp-server/index.js.map +1 -0
- package/{mcp-server/dist → dist/mcp-server}/tools/base.tool.d.ts +8 -12
- package/dist/mcp-server/tools/base.tool.d.ts.map +1 -0
- package/{mcp-server/dist → dist/mcp-server}/tools/base.tool.js +3 -5
- package/dist/mcp-server/tools/base.tool.js.map +1 -0
- package/{mcp-server/dist/tools/get-doc-content.tool.d.ts → dist/mcp-server/tools/docs/get-document-content.tool.d.ts} +13 -17
- package/dist/mcp-server/tools/docs/get-document-content.tool.d.ts.map +1 -0
- package/{mcp-server/dist/tools/get-doc-content.tool.js → dist/mcp-server/tools/docs/get-document-content.tool.js} +20 -22
- package/dist/mcp-server/tools/docs/get-document-content.tool.js.map +1 -0
- package/{mcp-server/dist/tools/get-doc-metadata.tool.d.ts → dist/mcp-server/tools/docs/get-document-metadata.tool.d.ts} +21 -25
- package/dist/mcp-server/tools/docs/get-document-metadata.tool.d.ts.map +1 -0
- package/{mcp-server/dist/tools/get-doc-metadata.tool.js → dist/mcp-server/tools/docs/get-document-metadata.tool.js} +35 -27
- package/dist/mcp-server/tools/docs/get-document-metadata.tool.js.map +1 -0
- package/dist/mcp-server/tools/docs/get-package-overview.tool.d.ts +50 -0
- package/dist/mcp-server/tools/docs/get-package-overview.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/docs/get-package-overview.tool.js +221 -0
- package/dist/mcp-server/tools/docs/get-package-overview.tool.js.map +1 -0
- package/dist/mcp-server/tools/docs/index.d.ts +7 -0
- package/dist/mcp-server/tools/docs/index.d.ts.map +1 -0
- package/dist/mcp-server/tools/docs/index.js +23 -0
- package/dist/mcp-server/tools/docs/index.js.map +1 -0
- package/{mcp-server/dist/tools → dist/mcp-server/tools/docs}/list-categories.tool.d.ts +3 -3
- package/dist/mcp-server/tools/docs/list-categories.tool.d.ts.map +1 -0
- package/{mcp-server/dist/tools → dist/mcp-server/tools/docs}/list-categories.tool.js +10 -9
- package/dist/mcp-server/tools/docs/list-categories.tool.js.map +1 -0
- package/{mcp-server/dist/tools/list-docs.tool.d.ts → dist/mcp-server/tools/docs/list-documents.tool.d.ts} +5 -5
- package/dist/mcp-server/tools/docs/list-documents.tool.d.ts.map +1 -0
- package/{mcp-server/dist/tools/list-docs.tool.js → dist/mcp-server/tools/docs/list-documents.tool.js} +15 -14
- package/dist/mcp-server/tools/docs/list-documents.tool.js.map +1 -0
- package/{mcp-server/dist/tools/search-docs.tool.d.ts → dist/mcp-server/tools/docs/search-documents.tool.d.ts} +23 -19
- package/dist/mcp-server/tools/docs/search-documents.tool.d.ts.map +1 -0
- package/{mcp-server/dist/tools/search-docs.tool.js → dist/mcp-server/tools/docs/search-documents.tool.js} +29 -25
- package/dist/mcp-server/tools/docs/search-documents.tool.js.map +1 -0
- package/dist/mcp-server/tools/github/index.d.ts +5 -0
- package/dist/mcp-server/tools/github/index.d.ts.map +1 -0
- package/dist/mcp-server/tools/github/index.js +21 -0
- package/dist/mcp-server/tools/github/index.js.map +1 -0
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +28 -0
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/github/list-project-files.tool.js +98 -0
- package/dist/mcp-server/tools/github/list-project-files.tool.js.map +1 -0
- package/dist/mcp-server/tools/github/search-code.tool.d.ts +42 -0
- package/dist/mcp-server/tools/github/search-code.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/github/search-code.tool.js +194 -0
- package/dist/mcp-server/tools/github/search-code.tool.js.map +1 -0
- package/dist/mcp-server/tools/github/verify-dependencies.tool.d.ts +55 -0
- package/dist/mcp-server/tools/github/verify-dependencies.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/github/verify-dependencies.tool.js +167 -0
- package/dist/mcp-server/tools/github/verify-dependencies.tool.js.map +1 -0
- package/dist/mcp-server/tools/github/view-source-file.tool.d.ts +26 -0
- package/dist/mcp-server/tools/github/view-source-file.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/github/view-source-file.tool.js +91 -0
- package/dist/mcp-server/tools/github/view-source-file.tool.js.map +1 -0
- package/dist/mcp-server/tools/index.d.ts +4 -0
- package/dist/mcp-server/tools/index.d.ts.map +1 -0
- package/dist/mcp-server/tools/index.js +22 -0
- package/dist/mcp-server/tools/index.js.map +1 -0
- package/package.json +46 -23
- package/wiki/changelogs/2025-12-16-initial-architecture.md +145 -0
- package/wiki/changelogs/2025-12-16-model-repo-datasource-refactor.md +300 -0
- package/wiki/changelogs/2025-12-17-refactor.md +90 -0
- package/wiki/changelogs/2025-12-18-performance-optimizations.md +130 -0
- package/wiki/changelogs/2025-12-18-repository-validation-security.md +249 -0
- package/wiki/changelogs/index.md +33 -0
- package/wiki/changelogs/planned-transaction-support.md +216 -0
- package/wiki/changelogs/template.md +123 -0
- package/wiki/get-started/5-minute-quickstart.md +1 -1
- package/wiki/get-started/best-practices/api-usage-examples.md +54 -8
- package/wiki/get-started/best-practices/architectural-patterns.md +43 -2
- package/wiki/get-started/best-practices/code-style-standards.md +41 -0
- package/wiki/get-started/best-practices/common-pitfalls.md +5 -3
- package/wiki/get-started/best-practices/contribution-workflow.md +40 -6
- package/wiki/get-started/best-practices/data-modeling.md +177 -0
- package/wiki/get-started/best-practices/security-guidelines.md +3 -1
- package/wiki/get-started/building-a-crud-api.md +63 -78
- package/wiki/get-started/core-concepts/components.md +4 -2
- package/wiki/get-started/core-concepts/controllers.md +14 -14
- package/wiki/get-started/core-concepts/dependency-injection.md +13 -1
- package/wiki/get-started/core-concepts/persistent.md +383 -431
- package/wiki/get-started/core-concepts/services.md +21 -27
- package/wiki/get-started/mcp-docs-server.md +130 -32
- package/wiki/get-started/philosophy.md +198 -25
- package/wiki/get-started/quickstart.md +1 -1
- package/wiki/public/logo.svg +1 -0
- package/wiki/references/base/application.md +5 -5
- package/wiki/references/base/components.md +1 -1
- package/wiki/references/base/controllers.md +43 -17
- package/wiki/references/base/datasources.md +195 -33
- package/wiki/references/base/dependency-injection.md +8 -7
- package/wiki/references/base/models.md +713 -25
- package/wiki/references/base/repositories.md +475 -22
- package/wiki/references/base/services.md +2 -2
- package/wiki/references/components/authentication.md +228 -10
- package/wiki/references/components/health-check.md +1 -1
- package/wiki/references/components/index.md +4 -2
- package/wiki/references/components/static-asset.md +1289 -0
- package/wiki/references/components/swagger.md +1 -1
- package/wiki/references/helpers/error.md +2 -2
- package/wiki/references/helpers/inversion.md +29 -14
- package/wiki/references/helpers/storage.md +538 -11
- package/wiki/references/src-details/core.md +21 -6
- package/wiki/references/src-details/docs.md +19 -9
- package/wiki/references/src-details/inversion.md +4 -4
- package/wiki/references/src-details/mcp-server.md +185 -234
- package/wiki/references/utilities/index.md +1 -1
- package/wiki/references/utilities/request.md +162 -3
- package/mcp-server/dist/common/config.d.ts +0 -27
- package/mcp-server/dist/common/config.d.ts.map +0 -1
- package/mcp-server/dist/common/config.js +0 -27
- package/mcp-server/dist/common/config.js.map +0 -1
- package/mcp-server/dist/common/index.d.ts +0 -3
- package/mcp-server/dist/common/index.d.ts.map +0 -1
- package/mcp-server/dist/common/index.js.map +0 -1
- package/mcp-server/dist/common/paths.d.ts.map +0 -1
- package/mcp-server/dist/common/paths.js.map +0 -1
- package/mcp-server/dist/helpers/docs.helper.d.ts.map +0 -1
- package/mcp-server/dist/helpers/docs.helper.js.map +0 -1
- package/mcp-server/dist/helpers/index.d.ts +0 -3
- package/mcp-server/dist/helpers/index.d.ts.map +0 -1
- package/mcp-server/dist/helpers/index.js.map +0 -1
- package/mcp-server/dist/helpers/logger.helper.d.ts.map +0 -1
- package/mcp-server/dist/helpers/logger.helper.js.map +0 -1
- package/mcp-server/dist/index.js +0 -62
- package/mcp-server/dist/index.js.map +0 -1
- package/mcp-server/dist/tools/base.tool.d.ts.map +0 -1
- package/mcp-server/dist/tools/base.tool.js.map +0 -1
- package/mcp-server/dist/tools/get-doc-content.tool.d.ts.map +0 -1
- package/mcp-server/dist/tools/get-doc-content.tool.js.map +0 -1
- package/mcp-server/dist/tools/get-doc-metadata.tool.d.ts.map +0 -1
- package/mcp-server/dist/tools/get-doc-metadata.tool.js.map +0 -1
- package/mcp-server/dist/tools/index.d.ts +0 -8
- package/mcp-server/dist/tools/index.d.ts.map +0 -1
- package/mcp-server/dist/tools/index.js +0 -18
- package/mcp-server/dist/tools/index.js.map +0 -1
- package/mcp-server/dist/tools/list-categories.tool.d.ts.map +0 -1
- package/mcp-server/dist/tools/list-categories.tool.js.map +0 -1
- package/mcp-server/dist/tools/list-docs.tool.d.ts.map +0 -1
- package/mcp-server/dist/tools/list-docs.tool.js.map +0 -1
- package/mcp-server/dist/tools/search-docs.tool.d.ts.map +0 -1
- package/mcp-server/dist/tools/search-docs.tool.js.map +0 -1
- /package/{mcp-server/dist → dist/mcp-server}/common/index.js +0 -0
- /package/{mcp-server/dist → dist/mcp-server}/common/paths.d.ts +0 -0
- /package/{mcp-server/dist → dist/mcp-server}/helpers/logger.helper.d.ts +0 -0
- /package/{mcp-server/dist → dist/mcp-server}/helpers/logger.helper.js +0 -0
- /package/{mcp-server/dist → dist/mcp-server}/index.d.ts +0 -0
|
@@ -26,49 +26,151 @@ Fundamental building block wrapping a Drizzle ORM schema.
|
|
|
26
26
|
| **Schema Encapsulation** | Holds Drizzle `pgTable` schema for consistent repository access |
|
|
27
27
|
| **Metadata** | Works with `@model` decorator to mark database entities |
|
|
28
28
|
| **Schema Generation** | Uses `drizzle-zod` to generate Zod schemas (`SELECT`, `CREATE`, `UPDATE`) |
|
|
29
|
+
| **Static Properties** | Supports static `schema`, `relations`, and `TABLE_NAME` for cleaner syntax |
|
|
29
30
|
| **Convenience** | Includes `toObject()` and `toJSON()` methods |
|
|
30
31
|
|
|
31
|
-
###
|
|
32
|
+
### Definition Patterns
|
|
33
|
+
|
|
34
|
+
`BaseEntity` supports two patterns for defining models:
|
|
35
|
+
|
|
36
|
+
#### Pattern 1: Static Properties (Recommended)
|
|
37
|
+
|
|
38
|
+
Define schema and relations as static properties:
|
|
32
39
|
|
|
33
40
|
```typescript
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import { SchemaTypes, TSchemaType, TTableSchemaWithId } from './common';
|
|
41
|
+
import { pgTable, text } from 'drizzle-orm/pg-core';
|
|
42
|
+
import { BaseEntity, model, generateIdColumnDefs, createRelations } from '@venizia/ignis';
|
|
37
43
|
|
|
38
|
-
|
|
44
|
+
// Define table schema
|
|
45
|
+
export const userTable = pgTable('User', {
|
|
46
|
+
...generateIdColumnDefs({ id: { dataType: 'string' } }),
|
|
47
|
+
name: text('name').notNull(),
|
|
48
|
+
email: text('email').notNull(),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Define relations
|
|
52
|
+
export const userRelations = createRelations({
|
|
53
|
+
source: userTable,
|
|
54
|
+
relations: [],
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Entity class with static properties
|
|
58
|
+
@model({ type: 'entity' })
|
|
59
|
+
export class User extends BaseEntity<typeof User.schema> {
|
|
60
|
+
static override schema = userTable;
|
|
61
|
+
static override relations = () => userRelations.definitions;
|
|
62
|
+
static override TABLE_NAME = 'User';
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Benefits:**
|
|
67
|
+
- Schema and relations are auto-resolved by repositories
|
|
68
|
+
- No need to pass `relations` in repository constructor
|
|
69
|
+
- Cleaner, more declarative syntax
|
|
70
|
+
|
|
71
|
+
#### Pattern 2: Constructor-Based (Legacy)
|
|
72
|
+
|
|
73
|
+
Pass schema in constructor:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
@model({ type: 'entity' })
|
|
77
|
+
export class User extends BaseEntity<typeof userTable> {
|
|
78
|
+
constructor() {
|
|
79
|
+
super({ name: 'User', schema: userTable });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Static Properties
|
|
85
|
+
|
|
86
|
+
| Property | Type | Description |
|
|
87
|
+
|----------|------|-------------|
|
|
88
|
+
| `schema` | `TTableSchemaWithId` | Drizzle table schema defined with `pgTable()` |
|
|
89
|
+
| `relations` | `TValueOrResolver<Array<TRelationConfig>>` | Relation definitions (can be a function for lazy loading) |
|
|
90
|
+
| `TABLE_NAME` | `string \| undefined` | Optional table name (defaults to class name if not set) |
|
|
91
|
+
|
|
92
|
+
### IEntity Interface
|
|
93
|
+
|
|
94
|
+
Models implementing static properties conform to the `IEntity` interface:
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
interface IEntity<Schema extends TTableSchemaWithId = TTableSchemaWithId> {
|
|
98
|
+
TABLE_NAME?: string;
|
|
99
|
+
schema: Schema;
|
|
100
|
+
relations?: TValueOrResolver<Array<TRelationConfig>>;
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Instance Methods
|
|
105
|
+
|
|
106
|
+
| Method | Description |
|
|
107
|
+
|--------|-------------|
|
|
108
|
+
| `getSchema({ type })` | Get Zod schema for validation (`SELECT`, `CREATE`, `UPDATE`) |
|
|
109
|
+
| `toObject()` | Convert to plain object |
|
|
110
|
+
| `toJSON()` | Convert to JSON string |
|
|
111
|
+
|
|
112
|
+
### Class Definition
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
export class BaseEntity<Schema extends TTableSchemaWithId = TTableSchemaWithId>
|
|
116
|
+
extends BaseHelper
|
|
117
|
+
implements IEntity<Schema>
|
|
118
|
+
{
|
|
119
|
+
// Instance properties
|
|
39
120
|
name: string;
|
|
40
121
|
schema: Schema;
|
|
41
|
-
schemaFactory: ReturnType<typeof createSchemaFactory>;
|
|
42
122
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
123
|
+
// Static properties - override in subclass
|
|
124
|
+
static schema: TTableSchemaWithId;
|
|
125
|
+
static relations?: TValueOrResolver<Array<TRelationConfig>>;
|
|
126
|
+
static TABLE_NAME?: string; // Optional, defaults to class name
|
|
127
|
+
|
|
128
|
+
// Static singleton for schemaFactory - shared across all instances
|
|
129
|
+
// Performance optimization: avoids creating new factory per entity
|
|
130
|
+
private static _schemaFactory?: ReturnType<typeof createSchemaFactory>;
|
|
131
|
+
protected static get schemaFactory(): ReturnType<typeof createSchemaFactory> {
|
|
132
|
+
return (BaseEntity._schemaFactory ??= createSchemaFactory());
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Constructor supports both patterns
|
|
136
|
+
constructor(opts?: { name?: string; schema?: Schema }) {
|
|
137
|
+
const ctor = new.target as typeof BaseEntity;
|
|
138
|
+
// Use explicit TABLE_NAME if defined, otherwise fall back to class name
|
|
139
|
+
const name = opts?.name ?? ctor.TABLE_NAME ?? ctor.name;
|
|
140
|
+
|
|
141
|
+
super({ scope: name });
|
|
142
|
+
|
|
143
|
+
this.name = name;
|
|
144
|
+
this.schema = opts?.schema || (ctor.schema as Schema);
|
|
48
145
|
}
|
|
49
146
|
|
|
50
147
|
getSchema(opts: { type: TSchemaType }) {
|
|
148
|
+
const factory = BaseEntity.schemaFactory; // Uses static singleton
|
|
51
149
|
switch (opts.type) {
|
|
52
|
-
case SchemaTypes.CREATE:
|
|
53
|
-
return
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return this.schemaFactory.createSelectSchema(this.schema);
|
|
60
|
-
}
|
|
61
|
-
default: {
|
|
150
|
+
case SchemaTypes.CREATE:
|
|
151
|
+
return factory.createInsertSchema(this.schema);
|
|
152
|
+
case SchemaTypes.UPDATE:
|
|
153
|
+
return factory.createUpdateSchema(this.schema);
|
|
154
|
+
case SchemaTypes.SELECT:
|
|
155
|
+
return factory.createSelectSchema(this.schema);
|
|
156
|
+
default:
|
|
62
157
|
throw getError({
|
|
63
|
-
message: `[getSchema] Invalid schema type | type: ${opts.type}
|
|
158
|
+
message: `[getSchema] Invalid schema type | type: ${opts.type}`,
|
|
64
159
|
});
|
|
65
|
-
}
|
|
66
160
|
}
|
|
67
161
|
}
|
|
162
|
+
|
|
163
|
+
toObject() {
|
|
164
|
+
return { ...this };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
toJSON() {
|
|
168
|
+
return this.toObject();
|
|
169
|
+
}
|
|
68
170
|
}
|
|
69
171
|
```
|
|
70
172
|
|
|
71
|
-
|
|
173
|
+
**Performance Note:** The `schemaFactory` is implemented as a static lazy singleton, meaning it's created once and shared across all `BaseEntity` instances. This avoids the overhead of creating a new `drizzle-zod` schema factory for every entity instantiation.
|
|
72
174
|
|
|
73
175
|
## Schema Enrichers
|
|
74
176
|
|
|
@@ -79,7 +181,7 @@ Enrichers are helper functions located in `packages/core/src/base/models/enriche
|
|
|
79
181
|
| Enricher Function | Purpose |
|
|
80
182
|
| :--- | :--- |
|
|
81
183
|
| **`generateIdColumnDefs`** | Adds a primary key `id` column (string UUID or numeric serial). |
|
|
82
|
-
| **`generateTzColumnDefs`** | Adds `createdAt` and `
|
|
184
|
+
| **`generateTzColumnDefs`** | Adds `createdAt`, `modifiedAt`, and `deletedAt` timestamp columns with timezone support. |
|
|
83
185
|
| **`generateUserAuditColumnDefs`** | Adds `createdBy` and `modifiedBy` columns to track user audit information. |
|
|
84
186
|
| **`generateDataTypeColumnDefs`** | Adds generic data type columns (`dataType`, `nValue`, `tValue`, `bValue`, `jValue`, `boValue`) for flexible data storage. |
|
|
85
187
|
| **`generatePrincipalColumnDefs`** | Adds polymorphic fields for associating with different principal types. |
|
|
@@ -102,3 +204,589 @@ export const myTable = pgTable('MyTable', {
|
|
|
102
204
|
name: text('name').notNull(),
|
|
103
205
|
});
|
|
104
206
|
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Detailed Enricher Reference
|
|
211
|
+
|
|
212
|
+
### `generateIdColumnDefs`
|
|
213
|
+
|
|
214
|
+
Adds a primary key `id` column with support for string UUID, integer, or big integer types with full TypeScript type inference.
|
|
215
|
+
|
|
216
|
+
**File:** `packages/core/src/base/models/enrichers/id.enricher.ts`
|
|
217
|
+
|
|
218
|
+
#### Signature
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
generateIdColumnDefs<Opts extends TIdEnricherOptions | undefined>(
|
|
222
|
+
opts?: Opts,
|
|
223
|
+
): TIdColumnDef<Opts>
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
#### Options (`TIdEnricherOptions`)
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
type TIdEnricherOptions = {
|
|
230
|
+
id?: { columnName?: string } & (
|
|
231
|
+
| { dataType: 'string' }
|
|
232
|
+
| {
|
|
233
|
+
dataType: 'number';
|
|
234
|
+
sequenceOptions?: PgSequenceOptions;
|
|
235
|
+
}
|
|
236
|
+
| {
|
|
237
|
+
dataType: 'big-number';
|
|
238
|
+
numberMode: 'number' | 'bigint'; // Required for big-number
|
|
239
|
+
sequenceOptions?: PgSequenceOptions;
|
|
240
|
+
}
|
|
241
|
+
);
|
|
242
|
+
};
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**Default values:**
|
|
246
|
+
- `dataType`: `'number'` (auto-incrementing integer)
|
|
247
|
+
- `columnName`: `'id'`
|
|
248
|
+
|
|
249
|
+
#### Generated Columns
|
|
250
|
+
|
|
251
|
+
| Data Type | Column Type | Constraints | Description |
|
|
252
|
+
|-----------|------------|-------------|-------------|
|
|
253
|
+
| `'string'` | `uuid` | Primary Key, Default: `gen_random_uuid()` | Native PostgreSQL UUID (no extension required) |
|
|
254
|
+
| `'number'` | `integer` | Primary Key, `GENERATED ALWAYS AS IDENTITY` | Auto-incrementing integer |
|
|
255
|
+
| `'big-number'` | `bigint` | Primary Key, `GENERATED ALWAYS AS IDENTITY` | Auto-incrementing big integer (mode: 'number' or 'bigint') |
|
|
256
|
+
|
|
257
|
+
#### Type Inference
|
|
258
|
+
|
|
259
|
+
The function provides **full TypeScript type inference** based on the configuration options:
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
type TIdColumnDef<Opts extends TIdEnricherOptions | undefined> =
|
|
263
|
+
Opts extends { id: infer IdOpts }
|
|
264
|
+
? IdOpts extends { dataType: 'string' }
|
|
265
|
+
? { id: IsPrimaryKey<NotNull<HasDefault<PgUUIDBuilderInitial<'id'>>>> }
|
|
266
|
+
: IdOpts extends { dataType: 'number' }
|
|
267
|
+
? { id: IsIdentity<IsPrimaryKey<NotNull<PgIntegerBuilderInitial<'id'>>>, 'always'> }
|
|
268
|
+
: IdOpts extends { dataType: 'big-number' }
|
|
269
|
+
? IdOpts extends { numberMode: 'number' }
|
|
270
|
+
? { id: IsIdentity<IsPrimaryKey<NotNull<PgBigInt53BuilderInitial<'id'>>>, 'always'> }
|
|
271
|
+
: { id: IsIdentity<IsPrimaryKey<NotNull<PgBigInt64BuilderInitial<'id'>>>, 'always'> }
|
|
272
|
+
: { id: IsIdentity<IsPrimaryKey<NotNull<PgIntegerBuilderInitial<'id'>>>, 'always'> }
|
|
273
|
+
: { id: IsIdentity<IsPrimaryKey<NotNull<PgIntegerBuilderInitial<'id'>>>, 'always'> };
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
This ensures that TypeScript correctly infers the exact column type based on your configuration.
|
|
277
|
+
|
|
278
|
+
#### Usage Examples
|
|
279
|
+
|
|
280
|
+
**Default (auto-incrementing integer):**
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
import { pgTable, text } from 'drizzle-orm/pg-core';
|
|
284
|
+
import { generateIdColumnDefs } from '@venizia/ignis';
|
|
285
|
+
|
|
286
|
+
export const myTable = pgTable('MyTable', {
|
|
287
|
+
...generateIdColumnDefs(),
|
|
288
|
+
name: text('name').notNull(),
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Generates: id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
**UUID-based string ID:**
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
export const myTable = pgTable('MyTable', {
|
|
298
|
+
...generateIdColumnDefs({ id: { dataType: 'string' } }),
|
|
299
|
+
name: text('name').notNull(),
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Generates: id uuid PRIMARY KEY DEFAULT gen_random_uuid()
|
|
303
|
+
// No extension required - built into PostgreSQL 13+
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
**Auto-incrementing integer with sequence options:**
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
export const myTable = pgTable('MyTable', {
|
|
310
|
+
...generateIdColumnDefs({
|
|
311
|
+
id: {
|
|
312
|
+
dataType: 'number',
|
|
313
|
+
sequenceOptions: { startWith: 1000, increment: 1 },
|
|
314
|
+
},
|
|
315
|
+
}),
|
|
316
|
+
name: text('name').notNull(),
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// Generates: id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (START WITH 1000 INCREMENT BY 1)
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
**Big number with JavaScript number mode (up to 2^53-1):**
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
export const myTable = pgTable('MyTable', {
|
|
326
|
+
...generateIdColumnDefs({
|
|
327
|
+
id: {
|
|
328
|
+
dataType: 'big-number',
|
|
329
|
+
numberMode: 'number', // Required field
|
|
330
|
+
sequenceOptions: { startWith: 1, increment: 1 },
|
|
331
|
+
},
|
|
332
|
+
}),
|
|
333
|
+
name: text('name').notNull(),
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// Generates: id bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY
|
|
337
|
+
// Type-safe: Returns PgBigInt53BuilderInitial (safe for JavaScript numbers)
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
**Big number with BigInt mode (for values > 2^53-1):**
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
export const myTable = pgTable('MyTable', {
|
|
344
|
+
...generateIdColumnDefs({
|
|
345
|
+
id: {
|
|
346
|
+
dataType: 'big-number',
|
|
347
|
+
numberMode: 'bigint', // Required field
|
|
348
|
+
sequenceOptions: { startWith: 1, increment: 1 },
|
|
349
|
+
},
|
|
350
|
+
}),
|
|
351
|
+
name: text('name').notNull(),
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Generates: id bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY
|
|
355
|
+
// Type-safe: Returns PgBigInt64BuilderInitial (requires BigInt in JavaScript)
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
#### Important Notes
|
|
359
|
+
|
|
360
|
+
- **UUID Type:** When using `dataType: 'string'`, the native PostgreSQL `uuid` type is used with `gen_random_uuid()` - no extension required (built into PostgreSQL 13+). This is more efficient than `text` type (16 bytes vs 36 bytes) and provides better indexing performance.
|
|
361
|
+
- **Type Safety:** The return type is fully inferred based on your options, providing better autocomplete and type checking
|
|
362
|
+
- **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)
|
|
363
|
+
- **Sequence Options:** Available for `number` and `big-number` types to customize identity generation behavior
|
|
364
|
+
|
|
365
|
+
---
|
|
366
|
+
|
|
367
|
+
### `generateTzColumnDefs`
|
|
368
|
+
|
|
369
|
+
Adds timestamp columns for tracking entity creation, modification, and soft deletion.
|
|
370
|
+
|
|
371
|
+
**File:** `packages/core/src/base/models/enrichers/tz.enricher.ts`
|
|
372
|
+
|
|
373
|
+
#### Signature
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
generateTzColumnDefs(opts?: TTzEnricherOptions): TTzEnricherResult
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
#### Options (`TTzEnricherOptions`)
|
|
380
|
+
|
|
381
|
+
```typescript
|
|
382
|
+
type TTzEnricherOptions = {
|
|
383
|
+
created?: { columnName: string; withTimezone: boolean };
|
|
384
|
+
modified?: { enable: false } | { enable?: true; columnName: string; withTimezone: boolean };
|
|
385
|
+
deleted?: { enable: false } | { enable?: true; columnName: string; withTimezone: boolean };
|
|
386
|
+
};
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
The `modified` and `deleted` options use a discriminated union pattern:
|
|
390
|
+
- When `enable: false`, no other properties are needed
|
|
391
|
+
- When `enable: true` (or omitted), `columnName` and `withTimezone` are required
|
|
392
|
+
|
|
393
|
+
**Default values:**
|
|
394
|
+
- `created`: `{ columnName: 'created_at', withTimezone: true }`
|
|
395
|
+
- `modified`: `{ enable: true, columnName: 'modified_at', withTimezone: true }`
|
|
396
|
+
- `deleted`: `{ enable: false }` (disabled by default)
|
|
397
|
+
|
|
398
|
+
#### Generated Columns
|
|
399
|
+
|
|
400
|
+
| Column | Type | Constraints | Default | Description |
|
|
401
|
+
|--------|------|-------------|---------|-------------|
|
|
402
|
+
| `createdAt` | `timestamp` | `NOT NULL` | `now()` | When the record was created (always included) |
|
|
403
|
+
| `modifiedAt` | `timestamp` | `NOT NULL` | `now()` | When the record was last modified (optional, enabled by default) |
|
|
404
|
+
| `deletedAt` | `timestamp` | nullable | `null` | When the record was soft-deleted (optional, **disabled by default**) |
|
|
405
|
+
|
|
406
|
+
#### Usage Examples
|
|
407
|
+
|
|
408
|
+
**Basic usage (default columns):**
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
import { pgTable, text } from 'drizzle-orm/pg-core';
|
|
412
|
+
import { generateTzColumnDefs } from '@venizia/ignis';
|
|
413
|
+
|
|
414
|
+
export const myTable = pgTable('MyTable', {
|
|
415
|
+
...generateTzColumnDefs(),
|
|
416
|
+
name: text('name').notNull(),
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// Generates: createdAt, modifiedAt (deletedAt is disabled by default)
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
**Enable soft delete:**
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
export const myTable = pgTable('MyTable', {
|
|
426
|
+
...generateTzColumnDefs({
|
|
427
|
+
deleted: { enable: true, columnName: 'deleted_at', withTimezone: true },
|
|
428
|
+
}),
|
|
429
|
+
name: text('name').notNull(),
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// Generates: createdAt, modifiedAt, deletedAt
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
**Custom column names:**
|
|
436
|
+
|
|
437
|
+
```typescript
|
|
438
|
+
export const myTable = pgTable('MyTable', {
|
|
439
|
+
...generateTzColumnDefs({
|
|
440
|
+
created: { columnName: 'created_date', withTimezone: true },
|
|
441
|
+
modified: { enable: true, columnName: 'updated_date', withTimezone: true },
|
|
442
|
+
deleted: { enable: true, columnName: 'removed_date', withTimezone: true },
|
|
443
|
+
}),
|
|
444
|
+
name: text('name').notNull(),
|
|
445
|
+
});
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
**Without timezone:**
|
|
449
|
+
|
|
450
|
+
```typescript
|
|
451
|
+
export const myTable = pgTable('MyTable', {
|
|
452
|
+
...generateTzColumnDefs({
|
|
453
|
+
created: { columnName: 'created_at', withTimezone: false },
|
|
454
|
+
modified: { enable: true, columnName: 'modified_at', withTimezone: false },
|
|
455
|
+
deleted: { enable: true, columnName: 'deleted_at', withTimezone: false },
|
|
456
|
+
}),
|
|
457
|
+
name: text('name').notNull(),
|
|
458
|
+
});
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
**Minimal setup (only createdAt):**
|
|
464
|
+
|
|
465
|
+
```typescript
|
|
466
|
+
export const myTable = pgTable('MyTable', {
|
|
467
|
+
...generateTzColumnDefs({
|
|
468
|
+
modified: { enable: false },
|
|
469
|
+
deleted: { enable: false },
|
|
470
|
+
}),
|
|
471
|
+
name: text('name').notNull(),
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
// Generates: createdAt only
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
#### Soft Delete Pattern
|
|
478
|
+
|
|
479
|
+
The `deletedAt` column enables the soft delete pattern, where records are marked as deleted rather than physically removed from the database.
|
|
480
|
+
|
|
481
|
+
**Example soft delete query:**
|
|
482
|
+
|
|
483
|
+
```typescript
|
|
484
|
+
import { eq, isNull } from 'drizzle-orm';
|
|
485
|
+
|
|
486
|
+
// Soft delete: set deletedAt timestamp
|
|
487
|
+
await db.update(myTable)
|
|
488
|
+
.set({ deletedAt: new Date() })
|
|
489
|
+
.where(eq(myTable.id, id));
|
|
490
|
+
|
|
491
|
+
// Query only active (non-deleted) records
|
|
492
|
+
const activeRecords = await db.select()
|
|
493
|
+
.from(myTable)
|
|
494
|
+
.where(isNull(myTable.deletedAt));
|
|
495
|
+
|
|
496
|
+
// Query deleted records
|
|
497
|
+
const deletedRecords = await db.select()
|
|
498
|
+
.from(myTable)
|
|
499
|
+
.where(isNotNull(myTable.deletedAt));
|
|
500
|
+
|
|
501
|
+
// Restore a soft-deleted record
|
|
502
|
+
await db.update(myTable)
|
|
503
|
+
.set({ deletedAt: null })
|
|
504
|
+
.where(eq(myTable.id, id));
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
#### Type Inference
|
|
508
|
+
|
|
509
|
+
The enricher provides proper TypeScript type inference:
|
|
510
|
+
|
|
511
|
+
```typescript
|
|
512
|
+
type TTzEnricherResult<ColumnDefinitions extends TColumnDefinitions = TColumnDefinitions> = {
|
|
513
|
+
createdAt: PgTimestampBuilderInitial<string> & NotNull & HasDefault;
|
|
514
|
+
modifiedAt?: PgTimestampBuilderInitial<string> & NotNull & HasDefault;
|
|
515
|
+
deletedAt?: PgTimestampBuilderInitial<string>;
|
|
516
|
+
};
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
---
|
|
520
|
+
|
|
521
|
+
### `generateUserAuditColumnDefs`
|
|
522
|
+
|
|
523
|
+
Adds `createdBy` and `modifiedBy` columns to track which user created or modified a record.
|
|
524
|
+
|
|
525
|
+
**File:** `packages/core/src/base/models/enrichers/user-audit.enricher.ts`
|
|
526
|
+
|
|
527
|
+
#### Signature
|
|
528
|
+
|
|
529
|
+
```typescript
|
|
530
|
+
generateUserAuditColumnDefs(opts?: TUserAuditEnricherOptions): {
|
|
531
|
+
createdBy: PgIntegerBuilderInitial | PgTextBuilderInitial;
|
|
532
|
+
modifiedBy: PgIntegerBuilderInitial | PgTextBuilderInitial;
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
#### Options (`TUserAuditEnricherOptions`)
|
|
537
|
+
|
|
538
|
+
```typescript
|
|
539
|
+
type TUserAuditColumnOpts = {
|
|
540
|
+
dataType: 'string' | 'number'; // Required - type of user ID
|
|
541
|
+
columnName: string; // Column name in database
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
type TUserAuditEnricherOptions = {
|
|
545
|
+
created?: TUserAuditColumnOpts;
|
|
546
|
+
modified?: TUserAuditColumnOpts;
|
|
547
|
+
};
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
**Default values:**
|
|
551
|
+
- `created`: `{ dataType: 'number', columnName: 'created_by' }`
|
|
552
|
+
- `modified`: `{ dataType: 'number', columnName: 'modified_by' }`
|
|
553
|
+
|
|
554
|
+
#### Generated Columns
|
|
555
|
+
|
|
556
|
+
| Column | Data Type | Column Name | Description |
|
|
557
|
+
|--------|-----------|-------------|-------------|
|
|
558
|
+
| `createdBy` | `integer` or `text` | `created_by` | User ID who created the record |
|
|
559
|
+
| `modifiedBy` | `integer` or `text` | `modified_by` | User ID who last modified the record |
|
|
560
|
+
|
|
561
|
+
#### Validation
|
|
562
|
+
|
|
563
|
+
The enricher validates the `dataType` option and throws an error for invalid values:
|
|
564
|
+
|
|
565
|
+
```typescript
|
|
566
|
+
// ✅ Valid
|
|
567
|
+
generateUserAuditColumnDefs({ created: { dataType: 'number', columnName: 'created_by' } });
|
|
568
|
+
generateUserAuditColumnDefs({ created: { dataType: 'string', columnName: 'created_by' } });
|
|
569
|
+
|
|
570
|
+
// ❌ Invalid - throws error
|
|
571
|
+
generateUserAuditColumnDefs({ created: { dataType: 'uuid', columnName: 'created_by' } });
|
|
572
|
+
// Error: [enrichUserAudit] Invalid dataType for 'createdBy' | value: uuid | valid: ['number', 'string']
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
#### Usage Examples
|
|
576
|
+
|
|
577
|
+
**Default (integer user IDs):**
|
|
578
|
+
|
|
579
|
+
```typescript
|
|
580
|
+
import { pgTable, text } from 'drizzle-orm/pg-core';
|
|
581
|
+
import { generateIdColumnDefs, generateUserAuditColumnDefs } from '@venizia/ignis';
|
|
582
|
+
|
|
583
|
+
export const myTable = pgTable('MyTable', {
|
|
584
|
+
...generateIdColumnDefs(),
|
|
585
|
+
...generateUserAuditColumnDefs(),
|
|
586
|
+
name: text('name').notNull(),
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
// Generates:
|
|
590
|
+
// createdBy: integer('created_by')
|
|
591
|
+
// modifiedBy: integer('modified_by')
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
**String user IDs (UUID):**
|
|
595
|
+
|
|
596
|
+
```typescript
|
|
597
|
+
export const myTable = pgTable('MyTable', {
|
|
598
|
+
...generateIdColumnDefs({ id: { dataType: 'string' } }),
|
|
599
|
+
...generateUserAuditColumnDefs({
|
|
600
|
+
created: { dataType: 'string', columnName: 'created_by' },
|
|
601
|
+
modified: { dataType: 'string', columnName: 'modified_by' },
|
|
602
|
+
}),
|
|
603
|
+
name: text('name').notNull(),
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
// Generates:
|
|
607
|
+
// createdBy: text('created_by')
|
|
608
|
+
// modifiedBy: text('modified_by')
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
**Custom column names:**
|
|
612
|
+
|
|
613
|
+
```typescript
|
|
614
|
+
export const myTable = pgTable('MyTable', {
|
|
615
|
+
...generateIdColumnDefs(),
|
|
616
|
+
...generateUserAuditColumnDefs({
|
|
617
|
+
created: { dataType: 'number', columnName: 'author_id' },
|
|
618
|
+
modified: { dataType: 'number', columnName: 'editor_id' },
|
|
619
|
+
}),
|
|
620
|
+
name: text('name').notNull(),
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
// Generates:
|
|
624
|
+
// createdBy: integer('author_id')
|
|
625
|
+
// modifiedBy: integer('editor_id')
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
---
|
|
629
|
+
|
|
630
|
+
## Schema Utilities
|
|
631
|
+
|
|
632
|
+
### `snakeToCamel`
|
|
633
|
+
|
|
634
|
+
Converts a Zod schema from snake_case to camelCase, transforming both the schema shape and runtime data.
|
|
635
|
+
|
|
636
|
+
**File:** `packages/core/src/base/models/common/types.ts`
|
|
637
|
+
|
|
638
|
+
#### Signature
|
|
639
|
+
|
|
640
|
+
```typescript
|
|
641
|
+
snakeToCamel<T extends z.ZodRawShape>(shape: T): z.ZodEffects<...>
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
#### Purpose
|
|
645
|
+
|
|
646
|
+
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:
|
|
647
|
+
|
|
648
|
+
1. Accepts snake_case input (validates against original schema)
|
|
649
|
+
2. Transforms the data to camelCase at runtime
|
|
650
|
+
3. Validates the transformed data against a camelCase schema
|
|
651
|
+
|
|
652
|
+
#### Usage Example
|
|
653
|
+
|
|
654
|
+
```typescript
|
|
655
|
+
import { z } from 'zod';
|
|
656
|
+
import { snakeToCamel } from '@venizia/ignis';
|
|
657
|
+
|
|
658
|
+
// Define schema with snake_case fields
|
|
659
|
+
const userSnakeSchema = {
|
|
660
|
+
user_id: z.number(),
|
|
661
|
+
first_name: z.string(),
|
|
662
|
+
last_name: z.string(),
|
|
663
|
+
created_at: z.date(),
|
|
664
|
+
is_active: z.boolean(),
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
// Convert to camelCase schema
|
|
668
|
+
const userCamelSchema = snakeToCamel(userSnakeSchema);
|
|
669
|
+
|
|
670
|
+
// Input data from database (snake_case)
|
|
671
|
+
const dbData = {
|
|
672
|
+
user_id: 123,
|
|
673
|
+
first_name: 'John',
|
|
674
|
+
last_name: 'Doe',
|
|
675
|
+
created_at: new Date(),
|
|
676
|
+
is_active: true,
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
// Parse and transform to camelCase
|
|
680
|
+
const result = userCamelSchema.parse(dbData);
|
|
681
|
+
|
|
682
|
+
// Result is automatically camelCase:
|
|
683
|
+
console.log(result);
|
|
684
|
+
// {
|
|
685
|
+
// userId: 123,
|
|
686
|
+
// firstName: 'John',
|
|
687
|
+
// lastName: 'Doe',
|
|
688
|
+
// createdAt: Date,
|
|
689
|
+
// isActive: true
|
|
690
|
+
// }
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
#### Real-world Example
|
|
694
|
+
|
|
695
|
+
**Use case:** API endpoint that accepts snake_case but works with camelCase internally
|
|
696
|
+
|
|
697
|
+
```typescript
|
|
698
|
+
import { BaseController, controller, snakeToCamel, HTTP } from '@venizia/ignis';
|
|
699
|
+
import { z } from '@hono/zod-openapi';
|
|
700
|
+
|
|
701
|
+
const createUserSchema = snakeToCamel({
|
|
702
|
+
first_name: z.string().min(1),
|
|
703
|
+
last_name: z.string().min(1),
|
|
704
|
+
email_address: z.string().email(),
|
|
705
|
+
phone_number: z.string().optional(),
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
@controller({ path: '/users' })
|
|
709
|
+
export class UserController extends BaseController {
|
|
710
|
+
override binding() {
|
|
711
|
+
this.bindRoute({
|
|
712
|
+
configs: {
|
|
713
|
+
path: '/',
|
|
714
|
+
method: 'post',
|
|
715
|
+
request: {
|
|
716
|
+
body: {
|
|
717
|
+
content: {
|
|
718
|
+
'application/json': { schema: createUserSchema },
|
|
719
|
+
},
|
|
720
|
+
},
|
|
721
|
+
},
|
|
722
|
+
},
|
|
723
|
+
}).to({
|
|
724
|
+
handler: async (ctx) => {
|
|
725
|
+
// Request body is automatically camelCase
|
|
726
|
+
const data = ctx.req.valid('json');
|
|
727
|
+
|
|
728
|
+
// data = {
|
|
729
|
+
// firstName: string,
|
|
730
|
+
// lastName: string,
|
|
731
|
+
// emailAddress: string,
|
|
732
|
+
// phoneNumber?: string
|
|
733
|
+
// }
|
|
734
|
+
|
|
735
|
+
// Work with camelCase data
|
|
736
|
+
console.log(data.firstName); // ✅ TypeScript knows this exists
|
|
737
|
+
console.log(data.first_name); // ❌ TypeScript error
|
|
738
|
+
|
|
739
|
+
return ctx.json({ success: true }, HTTP.ResultCodes.RS_2.Ok);
|
|
740
|
+
},
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
#### Type Transformation
|
|
747
|
+
|
|
748
|
+
The utility includes sophisticated TypeScript type transformation:
|
|
749
|
+
|
|
750
|
+
```typescript
|
|
751
|
+
type TSnakeToCamelCase<S extends string> =
|
|
752
|
+
S extends `${infer T}_${infer U}`
|
|
753
|
+
? `${T}${Capitalize<TSnakeToCamelCase<U>>}`
|
|
754
|
+
: S;
|
|
755
|
+
|
|
756
|
+
type TCamelCaseKeys<T extends z.ZodRawShape> = {
|
|
757
|
+
[K in keyof T as K extends string ? TSnakeToCamelCase<K> : K]:
|
|
758
|
+
T[K] extends z.ZodType<infer U> ? z.ZodType<U> : T[K];
|
|
759
|
+
};
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
This ensures full type safety: TypeScript will know that `first_name` becomes `firstName`, `created_at` becomes `createdAt`, etc.
|
|
763
|
+
|
|
764
|
+
#### Validation
|
|
765
|
+
|
|
766
|
+
The schema validates twice for safety:
|
|
767
|
+
|
|
768
|
+
1. **First validation:** Checks that input matches snake_case schema
|
|
769
|
+
2. **Transformation:** Converts keys from snake_case to camelCase
|
|
770
|
+
3. **Second validation:** Validates transformed data against camelCase schema
|
|
771
|
+
|
|
772
|
+
```typescript
|
|
773
|
+
// If validation fails at any step, you get clear error messages
|
|
774
|
+
const invalidData = {
|
|
775
|
+
user_id: 'not-a-number', // ❌ Fails first validation
|
|
776
|
+
first_name: 'John',
|
|
777
|
+
last_name: 'Doe',
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
try {
|
|
781
|
+
userCamelSchema.parse(invalidData);
|
|
782
|
+
} catch (error) {
|
|
783
|
+
// ZodError with clear message about user_id expecting number
|
|
784
|
+
}
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
#### Notes
|
|
788
|
+
|
|
789
|
+
- Built on top of `keysToCamel()` and `toCamel()` utilities from `@venizia/ignis-helpers`
|
|
790
|
+
- Recursively handles nested objects
|
|
791
|
+
- Preserves array structures
|
|
792
|
+
- Works seamlessly with Zod's other features (refinements, transforms, etc.)
|