@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 +236 -0
- package/client/client.test.ts +275 -0
- package/client/client.ts +328 -0
- package/client/client.types.ts +33 -0
- package/dmmf/dmmf.types.ts +53 -0
- package/dmmf/dmmf.utils.ts +125 -0
- package/package.json +35 -0
- package/scripts/generate-client.ts +131 -0
- package/scripts/generate-server.ts +110 -0
- package/server/server.test.ts +352 -0
- package/server/server.ts +766 -0
- package/server/server.types.ts +72 -0
package/client/client.ts
ADDED
|
@@ -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
|
+
}
|