@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.
@@ -0,0 +1,328 @@
1
+ import type { EntityMeta, EnumMeta } from '../dmmf/dmmf.types';
2
+ import { toKebabCase as _toKebabCase } from '../dmmf/dmmf.utils';
3
+ import type {
4
+ ClientTypesConfig,
5
+ ClientSchemaConfig,
6
+ GraphQLClientConfig,
7
+ ClientBarrelConfig,
8
+ TypesBarrelConfig,
9
+ SchemasBarrelConfig,
10
+ } from './client.types';
11
+
12
+ // ── Types generator ───────────────────────────────────────────────────────────
13
+
14
+ export function generateClientTypesContent(
15
+ entity: EntityMeta,
16
+ allEntities: EntityMeta[],
17
+ enums: EnumMeta[],
18
+ config: ClientTypesConfig,
19
+ ): string {
20
+ const { entityImportBase, enumsImport } = config;
21
+
22
+ const entityNames = new Set(allEntities.map((e) => e.name));
23
+ const enumNames = new Set(enums.map((e) => e.name));
24
+
25
+ const entitiesToImport = new Set<string>();
26
+ const enumsToImport = new Set<string>();
27
+
28
+ for (const f of entity.fields) {
29
+ if (!f.isRelation || !f.tsType || f.tsType === entity.name) continue;
30
+ if (entityNames.has(f.tsType)) entitiesToImport.add(f.tsType);
31
+ else if (enumNames.has(f.tsType)) enumsToImport.add(f.tsType);
32
+ }
33
+
34
+ const entityImports = Array.from(entitiesToImport)
35
+ .sort()
36
+ .map((type) => {
37
+ const kebab = _toKebabCase(type);
38
+ return `import type { ${type} } from '${entityImportBase}/${kebab}/${kebab}.types.auto';`;
39
+ })
40
+ .join('\n');
41
+
42
+ const enumImport =
43
+ enumsToImport.size > 0
44
+ ? `import { ${Array.from(enumsToImport).sort().join(', ')} } from '${enumsImport}';`
45
+ : '';
46
+
47
+ const importSection = [entityImports, enumImport].filter(Boolean).join('\n');
48
+
49
+ const fields = entity.fields
50
+ .map((f) => {
51
+ const optional = f.required ? '' : '?';
52
+ if (f.isRelation && entityNames.has(f.tsType)) {
53
+ if (f.isArray) return ` ${f.name}${optional}: ${f.tsType}[];`;
54
+ return ` ${f.name}${optional}: ${f.tsType}${f.required ? '' : ' | null'};`;
55
+ }
56
+ let fieldType = f.tsType;
57
+ if (
58
+ f.isRelation &&
59
+ !entityNames.has(f.tsType) &&
60
+ !enumNames.has(f.tsType) &&
61
+ /^[A-Z]/.test(f.tsType) &&
62
+ !f.tsType.includes('|')
63
+ ) {
64
+ fieldType = 'string';
65
+ }
66
+ return ` ${f.name}${optional}: ${fieldType};`;
67
+ })
68
+ .join('\n');
69
+
70
+ return `/**
71
+ * ${entity.displayName} — auto-generated, do not edit
72
+ */
73
+
74
+ ${importSection ? importSection + '\n\n' : ''}export interface ${entity.name} {
75
+ ${fields}
76
+ }
77
+ `;
78
+ }
79
+
80
+ // ── Schema generator ──────────────────────────────────────────────────────────
81
+
82
+ function _prettifyLabel(name: string): string {
83
+ const words = name.replace(/([A-Z])/g, ' $1').trim().split(' ');
84
+ return words
85
+ .map((w, i) => {
86
+ const t = w.slice(0, 16);
87
+ return i === 0 ? t : t.charAt(0).toUpperCase() + t.slice(1);
88
+ })
89
+ .join('');
90
+ }
91
+
92
+ export function generateClientSchemaContent(entity: EntityMeta, config: ClientSchemaConfig): string {
93
+ const {
94
+ tableSchemaImport,
95
+ optionsServiceImport,
96
+ optionsServiceExport = 'fetchAllEntityOptions',
97
+ skipFields = [],
98
+ largeTextFields = [],
99
+ } = config;
100
+
101
+ const skipSet = new Set(skipFields);
102
+ const largeTextSet = new Set(largeTextFields);
103
+
104
+ const formFields = entity.fields.filter((f) => !f.isRelation && !skipSet.has(f.name));
105
+ const regularFields = formFields.filter((f) => !largeTextSet.has(f.name));
106
+ const textareaFields = formFields.filter((f) => largeTextSet.has(f.name));
107
+
108
+ const regularDefs = regularFields
109
+ .map((f) => {
110
+ const label = _prettifyLabel(f.name);
111
+ const required = f.required ? ', required: true' : '';
112
+
113
+ if (f.formType === 'relation' && f.relationModel) {
114
+ return ` {
115
+ name: '${f.name}',
116
+ label: '${label}',
117
+ type: 'relation' as const,
118
+ optionsLoader: async () => {
119
+ const { ${optionsServiceExport} } = await import('${optionsServiceImport}');
120
+ return ${optionsServiceExport}('${f.relationModel}');
121
+ },
122
+ }`;
123
+ }
124
+
125
+ return ` { name: '${f.name}', label: '${label}', type: '${f.formType}'${required} }`;
126
+ })
127
+ .join(',\n');
128
+
129
+ const textareaDefs = textareaFields
130
+ .map((f) => ` { name: '${f.name}', label: '${_prettifyLabel(f.name)}', type: 'textarea' }`)
131
+ .join(',\n');
132
+
133
+ const allDefs = [regularDefs, textareaDefs].filter(Boolean).join(',\n');
134
+
135
+ return `/**
136
+ * ${entity.displayName} Schema — auto-generated, do not edit
137
+ */
138
+
139
+ import type { TableSchema } from '${tableSchemaImport}';
140
+
141
+ export const ${entity.camel}Schema: TableSchema = {
142
+ name: '${entity.kebab}',
143
+ displayName: '${entity.displayName}',
144
+ primaryKey: 'id',
145
+ sortField: 'name',
146
+ fields: [
147
+ ${allDefs},
148
+ ],
149
+ };
150
+ `;
151
+ }
152
+
153
+ // ── GraphQL client generator ──────────────────────────────────────────────────
154
+
155
+ export function generateGraphQLClientContent(entity: EntityMeta, config: GraphQLClientConfig): string {
156
+ const { graphqlRequestImport, graphqlRequestExport = 'graphqlRequest', apiTypesImport } = config;
157
+
158
+ const allFields = entity.fields
159
+ .map((f) => {
160
+ if (f.isRelation) return ` ${f.name} {\n id\n title\n }`;
161
+ return ` ${f.name}`;
162
+ })
163
+ .join('\n');
164
+
165
+ return `/**
166
+ * ${entity.displayName} Client — auto-generated, do not edit
167
+ */
168
+
169
+ import { ${graphqlRequestExport} } from '${graphqlRequestImport}';
170
+ import type { ApiList, PaginationInput } from '${apiTypesImport}';
171
+ import type { ${entity.name} } from './${entity.kebab}.types.auto';
172
+
173
+ export async function fetch${entity.name}(id: string): Promise<${entity.name} | null> {
174
+ const data = await ${graphqlRequestExport}<{ ${entity.camel}: ${entity.name} | null }>(\`
175
+ query Get${entity.name}($id: String!) {
176
+ ${entity.camel}(id: $id) {
177
+ ${allFields}
178
+ }
179
+ }
180
+ \`, { id });
181
+ return data.${entity.camel};
182
+ }
183
+
184
+ export async function fetch${entity.name}List(filter?: any, pagination?: PaginationInput): Promise<ApiList<${entity.name}>> {
185
+ const data = await ${graphqlRequestExport}<{ ${entity.camel}List: ApiList<${entity.name}> }>(\`
186
+ query Get${entity.name}List($filter: JSON, $pagination: PaginationInput) {
187
+ ${entity.camel}List(filter: $filter, pagination: $pagination) {
188
+ data {
189
+ ${allFields}
190
+ }
191
+ total
192
+ }
193
+ }
194
+ \`, { filter, pagination });
195
+ return data.${entity.camel}List;
196
+ }
197
+
198
+ export async function create${entity.name}(input: Partial<${entity.name}>): Promise<${entity.name}> {
199
+ const data = await ${graphqlRequestExport}<{ create${entity.name}: ${entity.name} }>(\`
200
+ mutation Create${entity.name}($input: Create${entity.name}Input!) {
201
+ create${entity.name}(input: $input) { id }
202
+ }
203
+ \`, { input });
204
+ return data.create${entity.name};
205
+ }
206
+
207
+ export async function update${entity.name}(id: string, input: Partial<${entity.name}>): Promise<${entity.name}> {
208
+ const data = await ${graphqlRequestExport}<{ update${entity.name}: ${entity.name} }>(\`
209
+ mutation Update${entity.name}($id: String!, $input: Update${entity.name}Input!) {
210
+ update${entity.name}(id: $id, input: $input) { id }
211
+ }
212
+ \`, { id, input });
213
+ return data.update${entity.name};
214
+ }
215
+
216
+ export async function delete${entity.name}(id: string): Promise<boolean> {
217
+ const data = await ${graphqlRequestExport}<{ delete${entity.name}: boolean }>(\`
218
+ mutation Delete${entity.name}($id: String!) {
219
+ delete${entity.name}(id: $id)
220
+ }
221
+ \`, { id });
222
+ return data.delete${entity.name};
223
+ }
224
+ `;
225
+ }
226
+
227
+ // ── Barrel generators ─────────────────────────────────────────────────────────
228
+
229
+ export function generateClientBarrelContent(entities: EntityMeta[], config: ClientBarrelConfig): string {
230
+ const exports = entities
231
+ .map((e) => `export * from '${config.entityImportBase}/${e.kebab}/${e.kebab}.client.auto';`)
232
+ .join('\n');
233
+
234
+ return `/**
235
+ * GraphQL Client — auto-generated barrel, do not edit
236
+ */
237
+
238
+ ${exports}
239
+ `;
240
+ }
241
+
242
+ export function generateTypesBarrelContent(
243
+ entities: EntityMeta[],
244
+ enums: EnumMeta[],
245
+ config: TypesBarrelConfig,
246
+ ): string {
247
+ const { entityImportBase, enumsImport } = config;
248
+
249
+ const typeExports = entities
250
+ .map((e) => `export type { ${e.name} } from '${entityImportBase}/${e.kebab}/${e.kebab}.types.auto';`)
251
+ .join('\n');
252
+
253
+ const enumsLine = enumsImport && enums.length > 0 ? `\nexport * from '${enumsImport}';\n` : '';
254
+
255
+ return `/**
256
+ * API Types — auto-generated barrel, do not edit
257
+ */
258
+
259
+ export interface ApiList<T> {
260
+ data: T[];
261
+ total: number;
262
+ }
263
+
264
+ export interface PaginationInput {
265
+ limit?: number;
266
+ offset?: number;
267
+ }
268
+
269
+ export interface SortInput {
270
+ field: string;
271
+ direction: 'ASC' | 'DESC';
272
+ }
273
+
274
+ export interface EntityOption {
275
+ value: string;
276
+ label: string;
277
+ }
278
+
279
+ export interface EntityOptionsPage {
280
+ options: EntityOption[];
281
+ total: number;
282
+ hasMore: boolean;
283
+ }
284
+
285
+ export interface EntityItem {
286
+ id: string;
287
+ title?: string;
288
+ name?: string;
289
+ }
290
+
291
+ ${typeExports}${enumsLine}
292
+ `;
293
+ }
294
+
295
+ export function generateSchemasBarrelContent(entities: EntityMeta[], config: SchemasBarrelConfig): string {
296
+ const exports = entities
297
+ .map((e) => `export { ${e.camel}Schema } from '${config.entityImportBase}/${e.kebab}/${e.kebab}.schema.auto';`)
298
+ .join('\n');
299
+
300
+ return `/**
301
+ * Table Schemas — auto-generated barrel, do not edit
302
+ */
303
+
304
+ ${exports}
305
+ `;
306
+ }
307
+
308
+ export function generateEnumsContent(enums: EnumMeta[]): string {
309
+ if (enums.length === 0) {
310
+ return `/**
311
+ * API Enums — auto-generated, do not edit
312
+ */
313
+
314
+ // No enums defined
315
+ `;
316
+ }
317
+
318
+ const enumDefs = enums
319
+ .map((e) => `export enum ${e.name} {\n${e.values.map((v) => ` ${v} = '${v}',`).join('\n')}\n}`)
320
+ .join('\n\n');
321
+
322
+ return `/**
323
+ * API Enums — auto-generated from Prisma schema, do not edit
324
+ */
325
+
326
+ ${enumDefs}
327
+ `;
328
+ }
@@ -0,0 +1,33 @@
1
+ // ── Config types ──────────────────────────────────────────────────────────────
2
+
3
+ export interface ClientTypesConfig {
4
+ entityImportBase: string;
5
+ enumsImport: string;
6
+ }
7
+
8
+ export interface ClientSchemaConfig {
9
+ tableSchemaImport: string;
10
+ optionsServiceImport: string;
11
+ optionsServiceExport?: string;
12
+ skipFields?: string[];
13
+ largeTextFields?: string[];
14
+ }
15
+
16
+ export interface GraphQLClientConfig {
17
+ graphqlRequestImport: string;
18
+ graphqlRequestExport?: string;
19
+ apiTypesImport: string;
20
+ }
21
+
22
+ export interface ClientBarrelConfig {
23
+ entityImportBase: string;
24
+ }
25
+
26
+ export interface TypesBarrelConfig {
27
+ entityImportBase: string;
28
+ enumsImport?: string;
29
+ }
30
+
31
+ export interface SchemasBarrelConfig {
32
+ entityImportBase: string;
33
+ }
@@ -0,0 +1,53 @@
1
+ // ── DMMF input types (compatible with PrismaClient._runtimeDataModel) ────────
2
+
3
+ export type FilterMode = 'contains' | 'equals';
4
+
5
+ export type DMMFField = {
6
+ name: string;
7
+ kind: 'scalar' | 'object' | 'enum' | 'unsupported';
8
+ type: string;
9
+ isRequired: boolean;
10
+ isList: boolean;
11
+ isId: boolean;
12
+ relationName?: string;
13
+ relationFromFields?: readonly string[];
14
+ relationToFields?: readonly string[];
15
+ };
16
+
17
+ export type DMMFModel = {
18
+ name: string;
19
+ dbName?: string | null;
20
+ fields: readonly DMMFField[];
21
+ };
22
+
23
+ export type DMMFEnum = {
24
+ name: string;
25
+ values: readonly { name: string }[];
26
+ };
27
+
28
+ // ── EntityMeta types (served by /entities, consumed by frontend generator) ───
29
+
30
+ export interface FieldMeta {
31
+ name: string;
32
+ prismaType: string;
33
+ tsType: string;
34
+ formType: string;
35
+ required: boolean;
36
+ isPrimary: boolean;
37
+ isRelation: boolean;
38
+ isArray: boolean;
39
+ relationModel: string | null;
40
+ }
41
+
42
+ export interface EntityMeta {
43
+ name: string;
44
+ camel: string;
45
+ kebab: string;
46
+ displayName: string;
47
+ fields: FieldMeta[];
48
+ }
49
+
50
+ export interface EnumMeta {
51
+ name: string;
52
+ values: string[];
53
+ }
@@ -0,0 +1,125 @@
1
+ import type { DMMFField, DMMFModel, DMMFEnum, FieldMeta, EntityMeta, EnumMeta } from './dmmf.types';
2
+
3
+ // ── Type mappings ─────────────────────────────────────────────────────────────
4
+
5
+ const PRISMA_TO_TS: Record<string, string> = {
6
+ String: 'string',
7
+ Int: 'number',
8
+ Float: 'number',
9
+ Boolean: 'boolean',
10
+ DateTime: 'string',
11
+ BigInt: 'number',
12
+ Decimal: 'number',
13
+ Json: 'any',
14
+ Bytes: 'string',
15
+ };
16
+
17
+ const PRISMA_TO_FORM: Record<string, string> = {
18
+ Int: 'number',
19
+ BigInt: 'number',
20
+ Float: 'float',
21
+ Decimal: 'float',
22
+ Boolean: 'boolean',
23
+ DateTime: 'date',
24
+ Json: 'textarea',
25
+ };
26
+
27
+ // ── String utilities ───────────────────────────────────────────────────────────
28
+
29
+ export function toCamelCase(str: string): string {
30
+ return str.charAt(0).toLowerCase() + str.slice(1);
31
+ }
32
+
33
+ export function toKebabCase(str: string): string {
34
+ return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
35
+ }
36
+
37
+ export function toDisplayName(str: string): string {
38
+ return str.replace(/([A-Z])/g, ' $1').trim();
39
+ }
40
+
41
+ // ── Field type utilities ───────────────────────────────────────────────────────
42
+
43
+ export function scalarTsType(prismaType: string, required: boolean): string {
44
+ const base = PRISMA_TO_TS[prismaType] ?? 'string';
45
+ return required ? base : `${base} | null`;
46
+ }
47
+
48
+ export function scalarFormType(prismaType: string, fieldName: string): string {
49
+ if (fieldName.endsWith('Id')) return 'relation';
50
+ return PRISMA_TO_FORM[prismaType] ?? 'text';
51
+ }
52
+
53
+ // ── Field mapping ──────────────────────────────────────────────────────────────
54
+
55
+ export function buildFkRelationMap(model: DMMFModel): Map<string, string> {
56
+ const map = new Map<string, string>();
57
+ for (const field of model.fields) {
58
+ if (field.kind === 'object' && field.relationFromFields?.length) {
59
+ for (const fkName of field.relationFromFields) {
60
+ map.set(fkName, field.type);
61
+ }
62
+ }
63
+ }
64
+ return map;
65
+ }
66
+
67
+ export function mapField(f: DMMFField, fkRelationMap: Map<string, string>): FieldMeta {
68
+ if (f.kind === 'object') {
69
+ return {
70
+ name: f.name,
71
+ prismaType: f.type,
72
+ tsType: f.type,
73
+ formType: 'relation',
74
+ required: f.isRequired,
75
+ isPrimary: false,
76
+ isRelation: true,
77
+ isArray: f.isList,
78
+ relationModel: f.type,
79
+ };
80
+ }
81
+
82
+ const tsType =
83
+ f.kind === 'enum'
84
+ ? f.isRequired
85
+ ? f.type
86
+ : `${f.type} | null`
87
+ : scalarTsType(f.type, f.isRequired);
88
+
89
+ return {
90
+ name: f.name,
91
+ prismaType: f.type,
92
+ tsType,
93
+ formType: f.kind === 'enum' ? 'text' : scalarFormType(f.type, f.name),
94
+ required: f.isRequired,
95
+ isPrimary: f.isId,
96
+ isRelation: false,
97
+ isArray: f.isList,
98
+ relationModel: f.name.endsWith('Id') ? (fkRelationMap.get(f.name) ?? null) : null,
99
+ };
100
+ }
101
+
102
+ // ── DMMF to EntityMeta conversion ───────────────────────────────────────────────
103
+
104
+ export function dmmfToEntityMeta(
105
+ dmmfModels: readonly DMMFModel[],
106
+ dmmfEnums: readonly DMMFEnum[],
107
+ ): { entities: EntityMeta[]; enums: EnumMeta[] } {
108
+ const entities: EntityMeta[] = dmmfModels.map((model) => {
109
+ const fkRelationMap = buildFkRelationMap(model);
110
+ return {
111
+ name: model.name,
112
+ camel: toCamelCase(model.name),
113
+ kebab: toKebabCase(model.name),
114
+ displayName: toDisplayName(model.name),
115
+ fields: model.fields.map((f) => mapField(f, fkRelationMap)),
116
+ };
117
+ });
118
+
119
+ const enums: EnumMeta[] = dmmfEnums.map((e) => ({
120
+ name: e.name,
121
+ values: e.values.map((v) => v.name),
122
+ }));
123
+
124
+ return { entities, enums };
125
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@tertium/prisma-codegen",
3
+ "version": "0.1.0",
4
+ "description": "Universal Prisma schema code generation — REST handlers, GraphQL resolvers, TypeScript types.",
5
+ "type": "module",
6
+ "exports": {
7
+ "./server": "./server/server.ts",
8
+ "./client": "./client/client.ts",
9
+ "./dmmf": "./dmmf/dmmf.types.ts"
10
+ },
11
+ "files": [
12
+ "dmmf/",
13
+ "server/",
14
+ "client/",
15
+ "scripts/",
16
+ "README.md"
17
+ ],
18
+ "scripts": {
19
+ "test": "bun test",
20
+ "lint:ts:check": "bunx tsc --noEmit",
21
+ "release:patch": "node node_modules/@tertium/js/scripts/release.js patch",
22
+ "release:minor": "node node_modules/@tertium/js/scripts/release.js minor",
23
+ "release:major": "node node_modules/@tertium/js/scripts/release.js major"
24
+ },
25
+ "author": "Vitalii Balabanov",
26
+ "license": "ISC",
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "devDependencies": {
31
+ "@tertium/js": "^1.4.8",
32
+ "@types/bun": "^1.3.9",
33
+ "typescript": "^5.8.3"
34
+ }
35
+ }