hekireki 0.3.0 → 0.4.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 +28 -0
- package/dist/generator/mermaid-er/index.js +6 -2
- package/dist/generator/valibot/generator/schema.d.ts +9 -0
- package/dist/generator/valibot/generator/schema.js +42 -0
- package/dist/generator/valibot/index.d.ts +1 -1
- package/dist/generator/valibot/index.js +39 -9
- package/dist/generator/valibot/utils/index.d.ts +2 -0
- package/dist/generator/valibot/utils/index.js +14 -0
- package/dist/generator/zod/generator/schema.d.ts +9 -0
- package/dist/generator/zod/generator/schema.js +28 -0
- package/dist/generator/zod/index.d.ts +1 -1
- package/dist/generator/zod/index.js +50 -10
- package/dist/generator/zod/utils/index.d.ts +4 -0
- package/dist/generator/zod/utils/index.js +14 -0
- package/dist/shared/generator/index.d.ts +11 -0
- package/dist/shared/generator/index.js +23 -0
- package/dist/shared/helper/relations.d.ts +7 -0
- package/dist/shared/helper/relations.js +5 -0
- package/dist/shared/utils/index.d.ts +9 -0
- package/dist/shared/utils/index.js +21 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Hekireki
|
|
2
2
|
|
|
3
|
+

|
|
4
|
+
|
|
3
5
|
**[Hekireki](https://www.npmjs.com/package/hekireki)** is a tool that generates validation schemas for Zod and Valibot, as well as ER diagrams, from [Prisma](https://www.prisma.io/) schemas annotated with comments.
|
|
4
6
|
|
|
5
7
|
## Features
|
|
@@ -38,12 +40,14 @@ generator Hekireki-Zod {
|
|
|
38
40
|
provider = "hekireki-zod"
|
|
39
41
|
type = true
|
|
40
42
|
comment = true
|
|
43
|
+
relation = true
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
generator Hekireki-Valibot {
|
|
44
47
|
provider = "hekireki-valibot"
|
|
45
48
|
type = true
|
|
46
49
|
comment = true
|
|
50
|
+
relation = true
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
generator Hekireki-Ecto {
|
|
@@ -128,6 +132,20 @@ export const PostSchema = z.object({
|
|
|
128
132
|
})
|
|
129
133
|
|
|
130
134
|
export type Post = z.infer<typeof PostSchema>
|
|
135
|
+
|
|
136
|
+
export const UserRelationsSchema = z.object({
|
|
137
|
+
...UserSchema.shape,
|
|
138
|
+
posts: z.array(PostSchema),
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
export type UserRelations = z.infer<typeof UserRelationsSchema>
|
|
142
|
+
|
|
143
|
+
export const PostRelationsSchema = z.object({
|
|
144
|
+
...PostSchema.shape,
|
|
145
|
+
user: UserSchema,
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
export type PostRelations = z.infer<typeof PostRelationsSchema>
|
|
131
149
|
```
|
|
132
150
|
|
|
133
151
|
## Valibot
|
|
@@ -167,6 +185,14 @@ export const PostSchema = v.object({
|
|
|
167
185
|
})
|
|
168
186
|
|
|
169
187
|
export type Post = v.InferInput<typeof PostSchema>
|
|
188
|
+
|
|
189
|
+
export const UserRelationsSchema = v.object({ ...UserSchema.entries, posts: v.array(PostSchema) })
|
|
190
|
+
|
|
191
|
+
export type UserRelations = v.InferInput<typeof UserRelationsSchema>
|
|
192
|
+
|
|
193
|
+
export const PostRelationsSchema = v.object({ ...PostSchema.entries, user: UserSchema })
|
|
194
|
+
|
|
195
|
+
export type PostRelations = v.InferInput<typeof PostRelationsSchema>
|
|
170
196
|
```
|
|
171
197
|
|
|
172
198
|
## Mermaid
|
|
@@ -237,6 +263,7 @@ end
|
|
|
237
263
|
| `type` | `boolean` | `false` | Generate TypeScript types |
|
|
238
264
|
| `comment` | `boolean` | `false` | Include schema documentation |
|
|
239
265
|
| `zod` | `string` | `'v4'` | Zod import version (`'mini'`, `'@hono/zod-openapi'`, or default `'v4'`) |
|
|
266
|
+
| `relation` | `boolean` | `false` | Generate relation schemas |
|
|
240
267
|
|
|
241
268
|
### Valibot Generator Options
|
|
242
269
|
|
|
@@ -246,6 +273,7 @@ end
|
|
|
246
273
|
| `file` | `string` | `index.ts` | File Name |
|
|
247
274
|
| `type` | `boolean` | `false` | Generate TypeScript types |
|
|
248
275
|
| `comment` | `boolean` | `false` | Include schema documentation |
|
|
276
|
+
| `relation` | `boolean` | `false` | Generate relation schemas |
|
|
249
277
|
|
|
250
278
|
### Mermaid ER Generator Options
|
|
251
279
|
|
|
@@ -7,8 +7,12 @@ export async function main(options) {
|
|
|
7
7
|
const content = erContent(options.dmmf.datamodel.models);
|
|
8
8
|
const output = options.generator.output?.value ?? './mermaid-er';
|
|
9
9
|
const file = options.generator.config?.file ?? 'ER.md';
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// Handle case where output is a file path (contains extension)
|
|
11
|
+
const isOutputFile = output.includes('.');
|
|
12
|
+
const outputDir = isOutputFile ? '.' : output;
|
|
13
|
+
const outputFile = isOutputFile ? output : `${output}/${file}`;
|
|
14
|
+
await fsp.mkdir(outputDir, { recursive: true });
|
|
15
|
+
await fsp.writeFile(outputFile, content.join('\n'), { encoding: 'utf-8' });
|
|
12
16
|
}
|
|
13
17
|
// prisma generator handler
|
|
14
18
|
generatorHandler({
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import type { DMMF } from '@prisma/generator-helper';
|
|
2
|
+
export declare function buildValibotModel(model: DMMF.Model): string;
|
|
3
|
+
export declare function buildValibotRelations(model: DMMF.Model, relProps: readonly {
|
|
4
|
+
key: string;
|
|
5
|
+
targetModel: string;
|
|
6
|
+
isMany: boolean;
|
|
7
|
+
}[], options?: Readonly<{
|
|
8
|
+
includeType?: boolean;
|
|
9
|
+
}>): string | null;
|
|
1
10
|
/**
|
|
2
11
|
* Generate Valibot schema
|
|
3
12
|
* @param modelName - The name of the model
|
|
@@ -1,3 +1,45 @@
|
|
|
1
|
+
import { extractAnno, jsdoc } from '../utils/index.js';
|
|
2
|
+
const vPrim = (f) => {
|
|
3
|
+
const anno = extractAnno(f.documentation ?? '', '@v.');
|
|
4
|
+
return wrapV(`v.${anno}`, f);
|
|
5
|
+
};
|
|
6
|
+
const wrapV = (expr, f) => {
|
|
7
|
+
const card = f.isList ? `v.array(${expr})` : expr;
|
|
8
|
+
return f.isRequired ? card : `v.optional(${card})`;
|
|
9
|
+
};
|
|
10
|
+
// jsdoc moved to utils
|
|
11
|
+
export function buildValibotModel(model) {
|
|
12
|
+
const fields = model.fields
|
|
13
|
+
.filter((f) => f.kind !== 'object')
|
|
14
|
+
.map((f) => `${jsdoc(f.documentation)} ${f.name}: ${vPrim(f)},`)
|
|
15
|
+
.join('\n');
|
|
16
|
+
const modelAnno = extractAnno(model.documentation ?? '', '@v.');
|
|
17
|
+
const objectDef = modelAnno === 'strictObject'
|
|
18
|
+
? `v.strictObject({\n${fields}\n})`
|
|
19
|
+
: modelAnno === 'looseObject'
|
|
20
|
+
? `v.looseObject({\n${fields}\n})`
|
|
21
|
+
: `v.object({\n${fields}\n})`;
|
|
22
|
+
return `export const ${model.name}Schema = ${objectDef}\n\nexport type ${model.name} = v.InferInput<typeof ${model.name}Schema>`;
|
|
23
|
+
}
|
|
24
|
+
export function buildValibotRelations(model, relProps, options) {
|
|
25
|
+
if (relProps.length === 0)
|
|
26
|
+
return null;
|
|
27
|
+
const base = `...${model.name}Schema.entries`;
|
|
28
|
+
const rels = relProps
|
|
29
|
+
.map((r) => `${r.key}: ${r.isMany ? `v.array(${r.targetModel}Schema)` : `${r.targetModel}Schema`}`)
|
|
30
|
+
.join(', ');
|
|
31
|
+
const modelAnno = extractAnno(model.documentation ?? '', '@v.');
|
|
32
|
+
const objectDef = modelAnno === 'strictObject'
|
|
33
|
+
? `v.strictObject({ ${base}, ${rels} })`
|
|
34
|
+
: modelAnno === 'looseObject'
|
|
35
|
+
? `v.looseObject({ ${base}, ${rels} })`
|
|
36
|
+
: `v.object({ ${base}, ${rels} })`;
|
|
37
|
+
const typeLine = options?.includeType
|
|
38
|
+
? `\n\nexport type ${model.name}Relations = v.InferInput<typeof ${model.name}RelationsSchema>`
|
|
39
|
+
: '';
|
|
40
|
+
return `export const ${model.name}RelationsSchema = ${objectDef}${typeLine}`;
|
|
41
|
+
}
|
|
42
|
+
// extractAnno provided by utils
|
|
1
43
|
/**
|
|
2
44
|
* Generate Valibot schema
|
|
3
45
|
* @param modelName - The name of the model
|
|
@@ -1,17 +1,47 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fsp from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
3
4
|
import pkg from '@prisma/generator-helper';
|
|
4
5
|
import { fmt } from '../../shared/format/index.js';
|
|
6
|
+
import { collectRelationProps } from '../../shared/helper/relations.js';
|
|
7
|
+
import { buildValibotRelations } from './generator/schema.js';
|
|
5
8
|
import { valibot } from './generator/valibot.js';
|
|
6
9
|
const { generatorHandler } = pkg;
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
const buildRelationsOnly = (dmmf, includeType) => {
|
|
11
|
+
const models = dmmf.datamodel.models;
|
|
12
|
+
const relIndex = collectRelationProps(models);
|
|
13
|
+
const relByModel = relIndex.reduce((acc, r) => {
|
|
14
|
+
if (!acc[r.model])
|
|
15
|
+
acc[r.model] = [];
|
|
16
|
+
acc[r.model].push(r);
|
|
17
|
+
return acc;
|
|
18
|
+
}, {});
|
|
19
|
+
return models
|
|
20
|
+
.map((model) => buildValibotRelations(model, (relByModel[model.name] ?? []).map(({ key, targetModel, isMany }) => ({
|
|
21
|
+
key,
|
|
22
|
+
targetModel,
|
|
23
|
+
isMany,
|
|
24
|
+
})), { includeType }))
|
|
25
|
+
.filter((code) => Boolean(code))
|
|
26
|
+
.join('\n\n');
|
|
27
|
+
};
|
|
28
|
+
const getString = (v, fallback) => typeof v === 'string' ? v : Array.isArray(v) ? (v[0] ?? fallback) : fallback;
|
|
29
|
+
const getBool = (v, fallback = false) => v === true || v === 'true' || (Array.isArray(v) && v[0] === 'true') ? true : fallback;
|
|
30
|
+
const emit = async (options, enableRelation) => {
|
|
31
|
+
const outDir = options.generator.output?.value ?? './valibot';
|
|
32
|
+
const file = getString(options.generator.config?.file, 'index.ts') ?? 'index.ts';
|
|
33
|
+
const base = valibot(options.dmmf.datamodel.models, getBool(options.generator.config?.type), getBool(options.generator.config?.comment));
|
|
34
|
+
const relations = enableRelation
|
|
35
|
+
? buildRelationsOnly(options.dmmf, getBool(options.generator.config?.type))
|
|
36
|
+
: '';
|
|
37
|
+
const full = [base, relations].filter(Boolean).join('\n\n');
|
|
38
|
+
const code = await fmt(full);
|
|
39
|
+
await fsp.mkdir(outDir, { recursive: true });
|
|
40
|
+
await fsp.writeFile(path.join(outDir, file), code, 'utf8');
|
|
41
|
+
};
|
|
42
|
+
export const onGenerate = (options) => emit(options, options.generator.config?.relation === 'true' ||
|
|
43
|
+
(Array.isArray(options.generator.config?.relation) &&
|
|
44
|
+
options.generator.config?.relation[0] === 'true'));
|
|
15
45
|
generatorHandler({
|
|
16
46
|
onManifest() {
|
|
17
47
|
return {
|
|
@@ -19,5 +49,5 @@ generatorHandler({
|
|
|
19
49
|
prettyName: 'Hekireki-Valibot',
|
|
20
50
|
};
|
|
21
51
|
},
|
|
22
|
-
onGenerate
|
|
52
|
+
onGenerate,
|
|
23
53
|
});
|
|
@@ -40,3 +40,5 @@ export declare function isValibotDocument(documentation?: string): string[];
|
|
|
40
40
|
* @returns The Valibot expression without "@v." prefix, or null if not found.
|
|
41
41
|
*/
|
|
42
42
|
export declare function isValibot(documentation?: string): string | null;
|
|
43
|
+
export declare const extractAnno: (doc: string, tag: "@z." | "@v.") => string | null;
|
|
44
|
+
export declare const jsdoc: (doc?: string) => string;
|
|
@@ -59,3 +59,17 @@ export function isValibot(documentation) {
|
|
|
59
59
|
const match = documentation.match(/@v\.(.+?)(?:\n|$)/);
|
|
60
60
|
return match ? match[1].trim() : null;
|
|
61
61
|
}
|
|
62
|
+
export const extractAnno = (doc, tag) => {
|
|
63
|
+
const line = doc
|
|
64
|
+
.split('\n')
|
|
65
|
+
.map((s) => s.trim())
|
|
66
|
+
.find((l) => l.startsWith(tag));
|
|
67
|
+
return line ? line.slice(tag.length) : null;
|
|
68
|
+
};
|
|
69
|
+
export const jsdoc = (doc) => {
|
|
70
|
+
const lines = (doc ?? '')
|
|
71
|
+
.split('\n')
|
|
72
|
+
.map((s) => s.trim())
|
|
73
|
+
.filter((l) => l && !l.startsWith('@z.') && !l.startsWith('@v.'));
|
|
74
|
+
return lines.length ? `/**\n * ${lines.join('\n * ')}\n */\n` : '';
|
|
75
|
+
};
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import type { DMMF } from '@prisma/generator-helper';
|
|
2
|
+
export declare function buildZodModel(model: DMMF.Model): Readonly<string>;
|
|
3
|
+
export declare function buildZodRelations(model: DMMF.Model, relProps: readonly {
|
|
4
|
+
key: string;
|
|
5
|
+
targetModel: string;
|
|
6
|
+
isMany: boolean;
|
|
7
|
+
}[], options?: Readonly<{
|
|
8
|
+
includeType?: boolean;
|
|
9
|
+
}>): string | null;
|
|
1
10
|
/**
|
|
2
11
|
* Generate Zod schema
|
|
3
12
|
* @param modelName - The name of the model
|
|
@@ -1,3 +1,31 @@
|
|
|
1
|
+
import { buildZodObject, extractAnno, jsdoc, wrapCardinality } from '../utils/index.js';
|
|
2
|
+
const zPrim = (f) => {
|
|
3
|
+
const anno = extractAnno(f.documentation ?? '', '@z.');
|
|
4
|
+
return wrapCardinality(`z.${anno}`, f);
|
|
5
|
+
};
|
|
6
|
+
// moved to utils
|
|
7
|
+
export function buildZodModel(model) {
|
|
8
|
+
const fields = model.fields
|
|
9
|
+
.filter((f) => f.kind !== 'object')
|
|
10
|
+
.map((f) => `${jsdoc(f.documentation)} ${f.name}: ${zPrim(f)},`)
|
|
11
|
+
.join('\n');
|
|
12
|
+
const objectDef = buildZodObject(fields, model.documentation);
|
|
13
|
+
return `export const ${model.name}Schema = ${objectDef}\n\nexport type ${model.name} = z.infer<typeof ${model.name}Schema>`;
|
|
14
|
+
}
|
|
15
|
+
export function buildZodRelations(model, relProps, options) {
|
|
16
|
+
if (relProps.length === 0)
|
|
17
|
+
return null;
|
|
18
|
+
const base = `...${model.name}Schema.shape`;
|
|
19
|
+
const rels = relProps
|
|
20
|
+
.map((r) => `${r.key}: ${r.isMany ? `z.array(${r.targetModel}Schema)` : `${r.targetModel}Schema`}`)
|
|
21
|
+
.join(', ');
|
|
22
|
+
const objectDef = buildZodObject(`${base}, ${rels}`, model.documentation);
|
|
23
|
+
const typeLine = options?.includeType
|
|
24
|
+
? `\n\nexport type ${model.name}Relations = z.infer<typeof ${model.name}RelationsSchema>`
|
|
25
|
+
: '';
|
|
26
|
+
return `export const ${model.name}RelationsSchema = ${objectDef}${typeLine}`;
|
|
27
|
+
}
|
|
28
|
+
// moved to utils
|
|
1
29
|
/**
|
|
2
30
|
* Generate Zod schema
|
|
3
31
|
* @param modelName - The name of the model
|
|
@@ -1,18 +1,58 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fsp from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
3
4
|
import pkg from '@prisma/generator-helper';
|
|
4
5
|
import { fmt } from '../../shared/format/index.js';
|
|
6
|
+
import { collectRelationProps } from '../../shared/helper/relations.js';
|
|
7
|
+
import { buildZodRelations } from './generator/schema.js';
|
|
5
8
|
import { zod } from './generator/zod.js';
|
|
6
9
|
const { generatorHandler } = pkg;
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
10
|
+
const buildRelationsOnly = (dmmf, includeType) => {
|
|
11
|
+
const models = dmmf.datamodel.models;
|
|
12
|
+
const relIndex = collectRelationProps(models);
|
|
13
|
+
const relByModel = relIndex.reduce((acc, r) => {
|
|
14
|
+
if (!acc[r.model])
|
|
15
|
+
acc[r.model] = [];
|
|
16
|
+
acc[r.model].push(r);
|
|
17
|
+
return acc;
|
|
18
|
+
}, {});
|
|
19
|
+
const blocks = models
|
|
20
|
+
.map((model) => {
|
|
21
|
+
const relProps = (relByModel[model.name] ?? []).map(({ key, targetModel, isMany }) => ({
|
|
22
|
+
key,
|
|
23
|
+
targetModel,
|
|
24
|
+
isMany,
|
|
25
|
+
}));
|
|
26
|
+
if (relProps.length === 0)
|
|
27
|
+
return '';
|
|
28
|
+
const schema = buildZodRelations(model, relProps);
|
|
29
|
+
const typeLine = includeType
|
|
30
|
+
? `\n\nexport type ${model.name}Relations = z.infer<typeof ${model.name}RelationsSchema>`
|
|
31
|
+
: '';
|
|
32
|
+
return `${schema}${typeLine}`;
|
|
33
|
+
})
|
|
34
|
+
.filter(Boolean);
|
|
35
|
+
return blocks.join('\n\n');
|
|
36
|
+
};
|
|
37
|
+
const getString = (v, fallback) => typeof v === 'string' ? v : Array.isArray(v) ? (v[0] ?? fallback) : fallback;
|
|
38
|
+
const getBool = (v, fallback = false) => v === true || v === 'true' || (Array.isArray(v) && v[0] === 'true') ? true : fallback;
|
|
39
|
+
const emit = async (options, enableRelation) => {
|
|
40
|
+
const outDir = options.generator.output?.value ?? './zod';
|
|
41
|
+
const file = getString(options.generator.config?.file, 'index.ts') ?? 'index.ts';
|
|
42
|
+
const zodVersion = getString(options.generator.config?.zod, 'v4');
|
|
43
|
+
const base = zod(options.dmmf.datamodel.models, getBool(options.generator.config?.type), getBool(options.generator.config?.comment), zodVersion);
|
|
44
|
+
// Respect the `type` flag when generating relation schemas
|
|
45
|
+
const relations = enableRelation
|
|
46
|
+
? buildRelationsOnly(options.dmmf, getBool(options.generator.config?.type))
|
|
47
|
+
: '';
|
|
48
|
+
const full = [base, relations].filter(Boolean).join('\n\n');
|
|
49
|
+
const code = await fmt(full);
|
|
50
|
+
await fsp.mkdir(outDir, { recursive: true });
|
|
51
|
+
await fsp.writeFile(path.join(outDir, file), code, 'utf8');
|
|
52
|
+
};
|
|
53
|
+
export const onGenerate = (options) => emit(options, options.generator.config?.relation === 'true' ||
|
|
54
|
+
(Array.isArray(options.generator.config?.relation) &&
|
|
55
|
+
options.generator.config?.relation[0] === 'true'));
|
|
16
56
|
generatorHandler({
|
|
17
57
|
onManifest() {
|
|
18
58
|
return {
|
|
@@ -20,5 +60,5 @@ generatorHandler({
|
|
|
20
60
|
prettyName: 'Hekireki-Zod',
|
|
21
61
|
};
|
|
22
62
|
},
|
|
23
|
-
onGenerate
|
|
63
|
+
onGenerate,
|
|
24
64
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DMMF } from '@prisma/generator-helper';
|
|
1
2
|
/**
|
|
2
3
|
* Generates a `z.infer` type for the specified model.
|
|
3
4
|
*
|
|
@@ -40,3 +41,6 @@ export declare function isZodDocument(documentation?: string): string[];
|
|
|
40
41
|
* @returns The Zod validation string without the "@z." prefix, or null if not found.
|
|
41
42
|
*/
|
|
42
43
|
export declare function isZod(documentation?: string): string | null;
|
|
44
|
+
export { extractAnno, jsdoc } from '../../../shared/utils/index.js';
|
|
45
|
+
export declare const wrapCardinality: (expr: string, field: DMMF.Field) => string;
|
|
46
|
+
export declare const buildZodObject: (inner: string, documentation?: string) => string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { extractAnno } from '../../../shared/utils/index.js';
|
|
1
2
|
/**
|
|
2
3
|
* Generates a `z.infer` type for the specified model.
|
|
3
4
|
*
|
|
@@ -59,3 +60,16 @@ export function isZod(documentation) {
|
|
|
59
60
|
const match = documentation.match(/@z\.(.+?)(?:\n|$)/);
|
|
60
61
|
return match ? match[1].trim() : null;
|
|
61
62
|
}
|
|
63
|
+
export { extractAnno, jsdoc } from '../../../shared/utils/index.js';
|
|
64
|
+
export const wrapCardinality = (expr, field) => {
|
|
65
|
+
const withList = field.isList ? `z.array(${expr})` : expr;
|
|
66
|
+
return field.isRequired ? withList : `${withList}.optional()`;
|
|
67
|
+
};
|
|
68
|
+
export const buildZodObject = (inner, documentation) => {
|
|
69
|
+
const anno = extractAnno(documentation ?? '', '@z.');
|
|
70
|
+
return anno === 'strictObject'
|
|
71
|
+
? `z.strictObject({\n${inner}\n})`
|
|
72
|
+
: anno === 'looseObject'
|
|
73
|
+
? `z.looseObject({\n${inner}\n})`
|
|
74
|
+
: `z.object({\n${inner}\n})`;
|
|
75
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DMMF, GeneratorOptions } from '@prisma/generator-helper';
|
|
2
|
+
export declare function schemaGenerator(outDir: Readonly<string>, dmmf: Readonly<GeneratorOptions['dmmf']>, importCode: string, buildSchema: (model: DMMF.Model) => Readonly<string>, buildRelations: (model: DMMF.Model, relProps: readonly {
|
|
3
|
+
key: string;
|
|
4
|
+
targetModel: string;
|
|
5
|
+
isMany: boolean;
|
|
6
|
+
}[]) => Readonly<string>, collectRelationProps: (models: readonly DMMF.Model[]) => Readonly<{
|
|
7
|
+
model: string;
|
|
8
|
+
key: string;
|
|
9
|
+
targetModel: string;
|
|
10
|
+
isMany: boolean;
|
|
11
|
+
}[]>): Promise<void>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import fsp from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export async function schemaGenerator(outDir, dmmf, importCode, buildSchema, buildRelations, collectRelationProps) {
|
|
4
|
+
const models = dmmf.datamodel.models;
|
|
5
|
+
const relIndex = collectRelationProps(models);
|
|
6
|
+
const relByModel = Object.groupBy(relIndex, (r) => r.model);
|
|
7
|
+
const baseSchemas = models.map((m) => buildSchema(m)).join('\n\n');
|
|
8
|
+
const relationSchemas = models
|
|
9
|
+
.map((m) => {
|
|
10
|
+
const relProps = (relByModel[m.name] ?? []).map(({ key, targetModel, isMany }) => ({
|
|
11
|
+
key,
|
|
12
|
+
targetModel,
|
|
13
|
+
isMany,
|
|
14
|
+
}));
|
|
15
|
+
return buildRelations(m, relProps);
|
|
16
|
+
})
|
|
17
|
+
.filter(Boolean)
|
|
18
|
+
.join('\n\n');
|
|
19
|
+
const body = relationSchemas ? `${baseSchemas}\n\n${relationSchemas}` : baseSchemas;
|
|
20
|
+
const code = `${importCode}\n${body}\n`;
|
|
21
|
+
await fsp.mkdir(outDir, { recursive: true });
|
|
22
|
+
await fsp.writeFile(path.join(outDir, 'index.ts'), code, 'utf8');
|
|
23
|
+
}
|
|
@@ -50,3 +50,12 @@ export declare function isFields(modelFields: {
|
|
|
50
50
|
comment: string[];
|
|
51
51
|
validation: string | null;
|
|
52
52
|
}>[];
|
|
53
|
+
/**
|
|
54
|
+
* Extracts annotation content from documentation lines.
|
|
55
|
+
* Returns the substring after the tag (e.g. '@z.' or '@v.').
|
|
56
|
+
*/
|
|
57
|
+
export declare const extractAnno: (doc: string, tag: "@z." | "@v.") => string | null;
|
|
58
|
+
/**
|
|
59
|
+
* Builds JSDoc from documentation, excluding annotation lines like '@z.' and '@v.'.
|
|
60
|
+
*/
|
|
61
|
+
export declare const jsdoc: (doc?: string) => string;
|
|
@@ -40,3 +40,24 @@ export function groupByModel(validFields) {
|
|
|
40
40
|
export function isFields(modelFields) {
|
|
41
41
|
return modelFields.flat().filter((field) => field.validation !== null);
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Extracts annotation content from documentation lines.
|
|
45
|
+
* Returns the substring after the tag (e.g. '@z.' or '@v.').
|
|
46
|
+
*/
|
|
47
|
+
export const extractAnno = (doc, tag) => {
|
|
48
|
+
const line = doc
|
|
49
|
+
.split('\n')
|
|
50
|
+
.map((s) => s.trim())
|
|
51
|
+
.find((l) => l.startsWith(tag));
|
|
52
|
+
return line ? line.slice(tag.length) : null;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Builds JSDoc from documentation, excluding annotation lines like '@z.' and '@v.'.
|
|
56
|
+
*/
|
|
57
|
+
export const jsdoc = (doc) => {
|
|
58
|
+
const lines = (doc ?? '')
|
|
59
|
+
.split('\n')
|
|
60
|
+
.map((s) => s.trim())
|
|
61
|
+
.filter((l) => l && !l.startsWith('@z.') && !l.startsWith('@v.'));
|
|
62
|
+
return lines.length ? `/**\n * ${lines.join('\n * ')}\n */\n` : '';
|
|
63
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hekireki",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Hekireki is a tool that generates validation schemas for Zod and Valibot, as well as ER diagrams, from Prisma schemas annotated with comments.",
|
|
7
7
|
"keywords": [
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@prisma/client": "^6.10.1",
|
|
51
|
-
"@types/node": "^22.
|
|
51
|
+
"@types/node": "^22.18.0",
|
|
52
52
|
"@vitest/coverage-v8": "^3.2.4",
|
|
53
53
|
"prisma": "^6.10.1",
|
|
54
54
|
"tsx": "^4.20.3",
|