@tertium/prisma-codegen 0.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.
package/README.md ADDED
@@ -0,0 +1,236 @@
1
+ # @tertium/prisma-codegen
2
+
3
+ Universal code generation library for Prisma schemas. Reads your Prisma schema at runtime and generates:
4
+ - **REST API handlers** with CRUD operations
5
+ - **GraphQL resolvers** with filtering, search, pagination
6
+ - **TypeScript types** for all entities
7
+ - **Client-side generators** for frontend apps
8
+
9
+ Single source of truth: your Prisma schema. Everything else is auto-generated.
10
+
11
+ ## Why use this?
12
+
13
+ - ✅ **Zero manual CRUD code** — all handlers generated from schema
14
+ - ✅ **Single definition** — Prisma schema drives everything
15
+ - ✅ **Metadata-driven** — filtering, search, relations inferred automatically
16
+ - ✅ **Frontend/backend sync** — shared EntityMeta contract
17
+ - ✅ **Universal** — no project-specific names or patterns hardcoded
18
+
19
+ ## How it works
20
+
21
+ ```
22
+ Prisma schema
23
+
24
+
25
+ [generate-server.ts] ──uses──▶ @tertium/prisma-codegen/server
26
+
27
+ ├── writes: src/entities/*.types.auto.ts
28
+ ├── writes: src/entities/*.rest.auto.ts
29
+ ├── writes: src/core/rest.router.auto.ts
30
+ ├── writes: src/core/graphql.resolvers.auto.ts
31
+ └── exposes: GET /entities (EntityMeta JSON)
32
+
33
+
34
+ [generate-client.ts] ──uses──▶ @tertium/prisma-codegen/client
35
+
36
+ ├── writes: src/entities/*.types.auto.ts
37
+ ├── writes: src/entities/*.schema.auto.ts
38
+ └── writes: src/entities/*.client.auto.ts
39
+ ```
40
+
41
+ ## Installation
42
+
43
+ ```bash
44
+ npm install @tertium/prisma-codegen
45
+ ```
46
+
47
+ ## Quick start
48
+
49
+ ### 1. Copy script templates
50
+
51
+ Copy the two generation scripts into your project:
52
+
53
+ ```bash
54
+ cp node_modules/@tertium/prisma-codegen/scripts/generate-server.ts scripts/generate-server.ts
55
+ cp node_modules/@tertium/prisma-codegen/scripts/generate-client.ts scripts/generate-client.ts
56
+ ```
57
+
58
+ ### 2. Generate backend code
59
+
60
+ Edit `scripts/generate-server.ts` and set your paths:
61
+
62
+ ```ts
63
+ const PRISMA_CLIENT_IMPORT = './generated/prisma/client';
64
+ const PRISMA_SINGLETON_PATH = '../db/prisma';
65
+ const GRAPHQL_CONTEXT_PATH = './graphql.context';
66
+ const ENTITIES_DIR = 'src/entities';
67
+ const REST_ROUTER_OUT = 'src/core/rest.router.auto.ts';
68
+ const GRAPHQL_RESOLVERS_OUT = 'src/core/graphql.resolvers.auto.ts';
69
+
70
+ // Customize filtering/search behavior:
71
+ const SEARCHABLE_PATTERNS: RegExp[] = [/name/i, /title/i];
72
+ const ENUM_INT_PATTERNS: RegExp[] = [];
73
+ const SKIP_FILTERABLE: string[] = [];
74
+ ```
75
+
76
+ Then run:
77
+
78
+ ```bash
79
+ bun scripts/generate-server.ts
80
+ ```
81
+
82
+ ### 3. Expose `/entities` endpoint
83
+
84
+ Add this to your backend to serve entity metadata:
85
+
86
+ ```ts
87
+ import { PrismaClient } from './generated/prisma/client';
88
+ import { dmmfToEntityMeta } from '@tertium/prisma-codegen/dmmf/dmmf.utils';
89
+
90
+ const pc = new PrismaClient();
91
+ const runtime = (pc as any)._runtimeDataModel;
92
+
93
+ const dmmfModels = Object.entries(runtime.models).map(([name, m]: any) =>
94
+ ({ name, dbName: m.dbName, fields: m.fields }));
95
+ const dmmfEnums = Object.entries(runtime.enums).map(([name, e]: any) =>
96
+ ({ name, values: e.values }));
97
+
98
+ const { entities, enums } = dmmfToEntityMeta(dmmfModels, dmmfEnums);
99
+
100
+ // Return from GET /entities endpoint
101
+ ```
102
+
103
+ ### 4. Generate frontend code
104
+
105
+ Edit `scripts/generate-client.ts` and set your paths:
106
+
107
+ ```ts
108
+ const ENTITIES_DIR = 'src/entities';
109
+ const ENTITY_IMPORT_BASE = '../../entities';
110
+ const GRAPHQL_REQUEST_IMPORT = '../../core/graphql/graphql.client';
111
+ const API_TYPES_IMPORT = '../../core/graphql/graphql.types.auto';
112
+ const TABLE_SCHEMA_IMPORT = '../../core/rest/rest.types';
113
+ const OPTIONS_SERVICE_IMPORT = '../../core/graphql/graphql.service';
114
+ const SKIP_FIELDS = ['id', 'createdAt', 'updatedAt'];
115
+ ```
116
+
117
+ Then run:
118
+
119
+ ```bash
120
+ bun scripts/generate-client.ts --api http://localhost:8080
121
+ ```
122
+
123
+ ## Library structure
124
+
125
+ The library uses **direct imports only** — no central barrel files.
126
+
127
+ ```
128
+ @tertium/prisma-codegen/
129
+ ├── dmmf/
130
+ │ ├── dmmf.types.ts # DMMF + EntityMeta types
131
+ │ └── dmmf.utils.ts # Utilities + dmmfToEntityMeta()
132
+ ├── server/
133
+ │ ├── server.types.ts # Config types
134
+ │ ├── server.ts # Generators
135
+ │ └── server.test.ts # Tests (36 tests)
136
+ ├── client/
137
+ │ ├── client.types.ts # Config types
138
+ │ ├── client.ts # Generators
139
+ │ └── client.test.ts # Tests (12 tests)
140
+ └── scripts/
141
+ ├── generate-server.ts
142
+ └── generate-client.ts
143
+ ```
144
+
145
+ ## Imports
146
+
147
+ Import directly from the files you need:
148
+
149
+ ```ts
150
+ // Types
151
+ import type { DMMFModel, EntityMeta, FieldMeta, EnumMeta }
152
+ from '@tertium/prisma-codegen/dmmf/dmmf.types';
153
+
154
+ // Utilities
155
+ import { dmmfToEntityMeta, toCamelCase, toKebabCase }
156
+ from '@tertium/prisma-codegen/dmmf/dmmf.utils';
157
+
158
+ // Backend generators
159
+ import { parsePrismaModels, inferEntityMetadata, generateEntityTypesContent }
160
+ from '@tertium/prisma-codegen/server/server';
161
+
162
+ // Frontend generators
163
+ import { generateClientTypesContent, generateClientSchemaContent }
164
+ from '@tertium/prisma-codegen/client/client';
165
+ ```
166
+
167
+ ## API reference
168
+
169
+ ### `dmmf/dmmf.types.ts` — DMMF and EntityMeta types
170
+
171
+ | Export | Purpose |
172
+ |---|---|
173
+ | `DMMFModel`, `DMMFField`, `DMMFEnum` | Prisma runtime data model types |
174
+ | `FilterMode` | `'contains' \| 'equals'` for filtering |
175
+ | `EntityMeta`, `FieldMeta`, `EnumMeta` | Shared frontend/backend contract |
176
+
177
+ ### `dmmf/dmmf.utils.ts` — Utilities
178
+
179
+ | Export | Purpose |
180
+ |---|---|
181
+ | `dmmfToEntityMeta(models, enums)` | Convert DMMF to EntityMeta (for `/entities` endpoint) |
182
+ | `toCamelCase(str)` | `PascalCase` → `camelCase` |
183
+ | `toKebabCase(str)` | `PascalCase` → `kebab-case` |
184
+ | `toDisplayName(str)` | `PascalCase` → `Pascal Case` |
185
+
186
+ ### `server/server.ts` — Backend generators
187
+
188
+ | Export | Purpose |
189
+ |---|---|
190
+ | `parsePrismaModels(dmmfModels)` | Parse DMMF models into internal representation |
191
+ | `inferEntityMetadata(dmmfModels, options)` | Infer filtering/search/relation metadata |
192
+ | `generateEntityTypesContent(model)` | Generate `*.types.auto.ts` |
193
+ | `generateRestHandlerContent(name, meta, config)` | Generate `*.rest.auto.ts` (5 CRUD functions) |
194
+ | `generateRestRouterContent(models, config)` | Generate REST router dispatching all entities |
195
+ | `generateGraphQLResolversContent(meta, dmmfModels, config)` | Generate GraphQL resolvers |
196
+
197
+ ### `client/client.ts` — Frontend generators
198
+
199
+ | Export | Purpose |
200
+ |---|---|
201
+ | `generateClientTypesContent(entity, allEntities, enums, config)` | Generate `*.types.auto.ts` |
202
+ | `generateClientSchemaContent(entity, config)` | Generate `*.schema.auto.ts` (TableSchema) |
203
+ | `generateGraphQLClientContent(entity, config)` | Generate `*.client.auto.ts` (GraphQL CRUD) |
204
+ | `generateClientBarrelContent(entities, config)` | Generate client barrel (re-exports all) |
205
+ | `generateTypesBarrelContent(entities, enums, config)` | Generate types barrel |
206
+ | `generateSchemasBarrelContent(entities, config)` | Generate schemas barrel |
207
+ | `generateEnumsContent(enums)` | Generate enum declarations |
208
+
209
+ ## Testing
210
+
211
+ Run all 48 tests:
212
+
213
+ ```bash
214
+ bun test
215
+ ```
216
+
217
+ - **server.test.ts** — 36 tests for backend generators
218
+ - **client.test.ts** — 12 tests for frontend generators
219
+
220
+ All tests use generic fixtures (no project-specific names).
221
+
222
+ ## Release
223
+
224
+ Release scripts use semantic versioning:
225
+
226
+ ```bash
227
+ npm run release:patch # 0.1.0 → 0.1.1
228
+ npm run release:minor # 0.1.0 → 0.2.0
229
+ npm run release:major # 0.1.0 → 1.0.0
230
+ ```
231
+
232
+ Each bumps version and publishes to npm.
233
+
234
+ ## License
235
+
236
+ ISC
@@ -0,0 +1,275 @@
1
+ import { describe, it, expect } from 'bun:test';
2
+ import {
3
+ generateClientTypesContent,
4
+ generateClientSchemaContent,
5
+ generateGraphQLClientContent,
6
+ generateClientBarrelContent,
7
+ generateTypesBarrelContent,
8
+ generateSchemasBarrelContent,
9
+ generateEnumsContent,
10
+ } from './client';
11
+ import type { EntityMeta, EnumMeta } from '../dmmf/dmmf.types';
12
+
13
+ // ── Shared fixtures ───────────────────────────────────────────────────────────
14
+
15
+ const userEntity: EntityMeta = {
16
+ name: 'User',
17
+ camel: 'user',
18
+ kebab: 'user',
19
+ displayName: 'User',
20
+ fields: [
21
+ {
22
+ name: 'id',
23
+ prismaType: 'String',
24
+ tsType: 'string',
25
+ formType: 'text',
26
+ required: true,
27
+ isPrimary: true,
28
+ isRelation: false,
29
+ isArray: false,
30
+ relationModel: null,
31
+ },
32
+ {
33
+ name: 'email',
34
+ prismaType: 'String',
35
+ tsType: 'string',
36
+ formType: 'text',
37
+ required: true,
38
+ isPrimary: false,
39
+ isRelation: false,
40
+ isArray: false,
41
+ relationModel: null,
42
+ },
43
+ {
44
+ name: 'name',
45
+ prismaType: 'String',
46
+ tsType: 'string | null',
47
+ formType: 'text',
48
+ required: false,
49
+ isPrimary: false,
50
+ isRelation: false,
51
+ isArray: false,
52
+ relationModel: null,
53
+ },
54
+ ],
55
+ };
56
+
57
+ const postEntity: EntityMeta = {
58
+ name: 'Post',
59
+ camel: 'post',
60
+ kebab: 'post',
61
+ displayName: 'Post',
62
+ fields: [
63
+ {
64
+ name: 'id',
65
+ prismaType: 'String',
66
+ tsType: 'string',
67
+ formType: 'text',
68
+ required: true,
69
+ isPrimary: true,
70
+ isRelation: false,
71
+ isArray: false,
72
+ relationModel: null,
73
+ },
74
+ {
75
+ name: 'title',
76
+ prismaType: 'String',
77
+ tsType: 'string',
78
+ formType: 'text',
79
+ required: true,
80
+ isPrimary: false,
81
+ isRelation: false,
82
+ isArray: false,
83
+ relationModel: null,
84
+ },
85
+ {
86
+ name: 'userId',
87
+ prismaType: 'String',
88
+ tsType: 'string',
89
+ formType: 'relation',
90
+ required: true,
91
+ isPrimary: false,
92
+ isRelation: false,
93
+ isArray: false,
94
+ relationModel: 'User',
95
+ },
96
+ ],
97
+ };
98
+
99
+ const statusEnum: EnumMeta = {
100
+ name: 'Status',
101
+ values: ['DRAFT', 'PUBLISHED', 'ARCHIVED'],
102
+ };
103
+
104
+ // ── generateClientTypesContent ───────────────────────────────────────────────
105
+
106
+ describe('generateClientTypesContent', () => {
107
+ it('generates TypeScript interface for entity', () => {
108
+ const output = generateClientTypesContent(userEntity, [userEntity], [], {
109
+ entityImportBase: '../../entities',
110
+ enumsImport: '../../enums',
111
+ });
112
+
113
+ expect(output).toContain('export interface User');
114
+ expect(output).toContain("id: string;");
115
+ expect(output).toContain("email: string;");
116
+ expect(output).toContain("name?: string | null;");
117
+ });
118
+
119
+ it('imports related entities when fields reference them', () => {
120
+ const output = generateClientTypesContent(postEntity, [userEntity, postEntity], [], {
121
+ entityImportBase: '../../entities',
122
+ enumsImport: '../../enums',
123
+ });
124
+
125
+ expect(output).toContain('export interface Post');
126
+ expect(output).toContain('userId');
127
+ });
128
+
129
+ it('imports enums when entity uses them', () => {
130
+ const entityWithEnum: EntityMeta = {
131
+ ...postEntity,
132
+ fields: [
133
+ ...postEntity.fields,
134
+ {
135
+ name: 'status',
136
+ prismaType: 'Status',
137
+ tsType: 'Status',
138
+ formType: 'text',
139
+ required: true,
140
+ isPrimary: false,
141
+ isRelation: false,
142
+ isArray: false,
143
+ relationModel: null,
144
+ },
145
+ ],
146
+ };
147
+
148
+ const output = generateClientTypesContent(entityWithEnum, [userEntity, postEntity], [statusEnum], {
149
+ entityImportBase: '../../entities',
150
+ enumsImport: '../../enums',
151
+ });
152
+
153
+ expect(output).toContain('status: Status;');
154
+ });
155
+ });
156
+
157
+ // ── generateClientSchemaContent ──────────────────────────────────────────────
158
+
159
+ describe('generateClientSchemaContent', () => {
160
+ it('generates TableSchema with fields', () => {
161
+ const output = generateClientSchemaContent(userEntity, {
162
+ tableSchemaImport: '../../types',
163
+ skipFields: ['id'],
164
+ });
165
+
166
+ expect(output).toContain("const userSchema");
167
+ expect(output).toContain("email");
168
+ expect(output).toContain("name");
169
+ });
170
+
171
+ it('generates schema with primaryKey and sortField', () => {
172
+ const output = generateClientSchemaContent(userEntity, {
173
+ tableSchemaImport: '../../types',
174
+ skipFields: [],
175
+ });
176
+
177
+ expect(output).toContain("primaryKey: 'id'");
178
+ expect(output).toContain("sortField:");
179
+ });
180
+ });
181
+
182
+ // ── generateGraphQLClientContent ─────────────────────────────────────────────
183
+
184
+ describe('generateGraphQLClientContent', () => {
185
+ it('generates GraphQL CRUD functions with fetch prefix', () => {
186
+ const output = generateGraphQLClientContent(userEntity, {
187
+ graphqlRequestImport: '../../graphql',
188
+ apiTypesImport: '../../types',
189
+ });
190
+
191
+ expect(output).toContain('export async function fetchUser');
192
+ expect(output).toContain('export async function fetchUserList');
193
+ expect(output).toContain('export async function createUser');
194
+ expect(output).toContain('export async function updateUser');
195
+ expect(output).toContain('export async function deleteUser');
196
+ });
197
+
198
+ it('generates GraphQL queries and mutations', () => {
199
+ const output = generateGraphQLClientContent(userEntity, {
200
+ graphqlRequestImport: '../../graphql',
201
+ apiTypesImport: '../../types',
202
+ });
203
+
204
+ expect(output).toContain('query GetUser');
205
+ expect(output).toContain('mutation CreateUser');
206
+ expect(output).toContain('mutation UpdateUser');
207
+ expect(output).toContain('mutation DeleteUser');
208
+ });
209
+ });
210
+
211
+ // ── generateClientBarrelContent ──────────────────────────────────────────────
212
+
213
+ describe('generateClientBarrelContent', () => {
214
+ it('re-exports all client functions', () => {
215
+ const output = generateClientBarrelContent([userEntity, postEntity], {
216
+ entityImportBase: '../../entities',
217
+ });
218
+
219
+ expect(output).toContain("from '../../entities/user/user.client.auto'");
220
+ expect(output).toContain("from '../../entities/post/post.client.auto'");
221
+ });
222
+ });
223
+
224
+ // ── generateTypesBarrelContent ───────────────────────────────────────────────
225
+
226
+ describe('generateTypesBarrelContent', () => {
227
+ it('re-exports all entity types', () => {
228
+ const output = generateTypesBarrelContent([userEntity, postEntity], [statusEnum], {
229
+ entityImportBase: '../../entities',
230
+ enumsImport: '../../enums',
231
+ });
232
+
233
+ expect(output).toContain("from '../../entities/user/user.types.auto'");
234
+ expect(output).toContain("from '../../entities/post/post.types.auto'");
235
+ expect(output).toContain("from '../../enums'");
236
+ });
237
+ });
238
+
239
+ // ── generateSchemasBarrelContent ─────────────────────────────────────────────
240
+
241
+ describe('generateSchemasBarrelContent', () => {
242
+ it('re-exports all schemas', () => {
243
+ const output = generateSchemasBarrelContent([userEntity, postEntity], {
244
+ entityImportBase: '../../entities',
245
+ });
246
+
247
+ expect(output).toContain("from '../../entities/user/user.schema.auto'");
248
+ expect(output).toContain("from '../../entities/post/post.schema.auto'");
249
+ });
250
+ });
251
+
252
+ // ── generateEnumsContent ─────────────────────────────────────────────────────
253
+
254
+ describe('generateEnumsContent', () => {
255
+ it('generates enum declarations', () => {
256
+ const output = generateEnumsContent([statusEnum]);
257
+
258
+ expect(output).toContain('export enum Status');
259
+ expect(output).toContain('DRAFT');
260
+ expect(output).toContain('PUBLISHED');
261
+ expect(output).toContain('ARCHIVED');
262
+ });
263
+
264
+ it('handles multiple enums', () => {
265
+ const roleEnum: EnumMeta = {
266
+ name: 'Role',
267
+ values: ['ADMIN', 'USER'],
268
+ };
269
+
270
+ const output = generateEnumsContent([statusEnum, roleEnum]);
271
+
272
+ expect(output).toContain('export enum Status');
273
+ expect(output).toContain('export enum Role');
274
+ });
275
+ });