hekireki 0.8.1 → 0.8.3
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 +21 -0
- package/README.md +19 -0
- package/dist/bin/ajv.js +1 -1
- package/dist/bin/arktype.js +1 -1
- package/dist/bin/dbml.js +1 -1
- package/dist/bin/docs.js +1 -1
- package/dist/bin/drizzle.js +1 -1
- package/dist/bin/ecto.js +1 -1
- package/dist/bin/effect.js +1 -1
- package/dist/bin/gorm.js +1 -1
- package/dist/bin/mermaid-er.js +1 -1
- package/dist/bin/sea-orm.js +1 -1
- package/dist/bin/sqlalchemy.js +1 -1
- package/dist/bin/typebox.js +1 -1
- package/dist/bin/valibot.js +1 -1
- package/dist/bin/zod.js +1 -1
- package/dist/{bin-BqlsEIc6.js → bin-Brdok3Yj.js} +113 -20
- package/package.json +18 -18
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 - present, Hekireki Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -712,6 +712,25 @@ generator Hekireki-PNG {
|
|
|
712
712
|
}
|
|
713
713
|
```
|
|
714
714
|
|
|
715
|
+
### Logical Relations (without a Foreign Key)
|
|
716
|
+
|
|
717
|
+
To draw a relation that has **no physical foreign key**, add a `/// @relation <Parent>.<field> <Child>.<field> <cardinality>` doc-comment on the model:
|
|
718
|
+
|
|
719
|
+
```prisma
|
|
720
|
+
model User {
|
|
721
|
+
id String @id @default(uuid())
|
|
722
|
+
name String
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/// @relation User.id Post.userId one-to-many
|
|
726
|
+
model Post {
|
|
727
|
+
id String @id @default(uuid())
|
|
728
|
+
userId String
|
|
729
|
+
}
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
The relation is drawn in both the Mermaid and DBML output even though `Post.userId` has no `@relation(...)` foreign key. When a physical FK and an annotation describe the same pair, the annotation's cardinality wins in the Mermaid diagram.
|
|
733
|
+
|
|
715
734
|
### Docs
|
|
716
735
|
|
|
717
736
|
The `hekireki-docs` generator creates an HTML documentation page from your Prisma schema. Serve it locally with `hekireki docs serve`:
|
package/dist/bin/ajv.js
CHANGED
package/dist/bin/arktype.js
CHANGED
package/dist/bin/dbml.js
CHANGED
package/dist/bin/docs.js
CHANGED
package/dist/bin/drizzle.js
CHANGED
package/dist/bin/ecto.js
CHANGED
package/dist/bin/effect.js
CHANGED
package/dist/bin/gorm.js
CHANGED
package/dist/bin/mermaid-er.js
CHANGED
package/dist/bin/sea-orm.js
CHANGED
package/dist/bin/sqlalchemy.js
CHANGED
package/dist/bin/typebox.js
CHANGED
package/dist/bin/valibot.js
CHANGED
package/dist/bin/zod.js
CHANGED
|
@@ -124,6 +124,18 @@ async function emitMany(files, dir) {
|
|
|
124
124
|
}
|
|
125
125
|
//#endregion
|
|
126
126
|
//#region src/utils/index.ts
|
|
127
|
+
function parseRelation(line) {
|
|
128
|
+
const match = line.trim().match(/^@relation\s+(\w+)\.(\w+)\s+(\w+)\.(\w+)\s+(\w+-to-\w+)$/);
|
|
129
|
+
if (!match) return null;
|
|
130
|
+
const [, fromModel, fromField, toModel, toField, type] = match;
|
|
131
|
+
return {
|
|
132
|
+
fromModel,
|
|
133
|
+
fromField,
|
|
134
|
+
toModel,
|
|
135
|
+
toField,
|
|
136
|
+
type
|
|
137
|
+
};
|
|
138
|
+
}
|
|
127
139
|
function getString(v, fallback) {
|
|
128
140
|
return typeof v === "string" ? v : Array.isArray(v) ? v[0] ?? fallback : fallback;
|
|
129
141
|
}
|
|
@@ -436,6 +448,68 @@ async function arktype(options) {
|
|
|
436
448
|
return emit(arktypeCode(options.dmmf, getBool(options.generator.config?.type), getBool(options.generator.config?.comment), getBool(options.generator.config?.relation)), resolved.dir, resolved.file);
|
|
437
449
|
}
|
|
438
450
|
//#endregion
|
|
451
|
+
//#region src/helper/relation.ts
|
|
452
|
+
function isCardinality(value) {
|
|
453
|
+
return value === "zero-one" || value === "one" || value === "zero-many" || value === "many";
|
|
454
|
+
}
|
|
455
|
+
function erKey(relation) {
|
|
456
|
+
return `${relation.from.model}.${relation.from.field}->${relation.to.model}.${relation.to.field}`;
|
|
457
|
+
}
|
|
458
|
+
function inferredERRelations(models) {
|
|
459
|
+
return models.flatMap((model) => model.fields.filter((field) => field.kind === "object" && field.relationFromFields && field.relationFromFields.length > 0).map((field) => {
|
|
460
|
+
const toModel = model.name;
|
|
461
|
+
const fromModel = field.type;
|
|
462
|
+
const toField = field.relationFromFields?.[0] ?? "";
|
|
463
|
+
const fromField = field.relationToFields?.[0] ?? "id";
|
|
464
|
+
const toCardinality = (models.find((m) => m.name === fromModel)?.fields.find((f) => f.relationName === field.relationName && f.name !== field.name))?.isList ? field.isRequired ? "many" : "zero-many" : field.isRequired ? "one" : "zero-one";
|
|
465
|
+
return {
|
|
466
|
+
from: {
|
|
467
|
+
model: fromModel,
|
|
468
|
+
field: fromField,
|
|
469
|
+
cardinality: "one"
|
|
470
|
+
},
|
|
471
|
+
to: {
|
|
472
|
+
model: toModel,
|
|
473
|
+
field: toField,
|
|
474
|
+
cardinality: toCardinality
|
|
475
|
+
},
|
|
476
|
+
identifying: true,
|
|
477
|
+
origin: "inferred"
|
|
478
|
+
};
|
|
479
|
+
}));
|
|
480
|
+
}
|
|
481
|
+
function annotatedERRelations(models) {
|
|
482
|
+
return models.flatMap((model) => (model.documentation ?? "").split("\n").flatMap((line) => {
|
|
483
|
+
const relation = parseRelation(line);
|
|
484
|
+
if (relation === null) return [];
|
|
485
|
+
const [fromCard, toCard] = relation.type.split("-to-");
|
|
486
|
+
if (!isCardinality(fromCard) || !isCardinality(toCard)) return [];
|
|
487
|
+
return [{
|
|
488
|
+
from: {
|
|
489
|
+
model: relation.fromModel,
|
|
490
|
+
field: relation.fromField,
|
|
491
|
+
cardinality: fromCard
|
|
492
|
+
},
|
|
493
|
+
to: {
|
|
494
|
+
model: relation.toModel,
|
|
495
|
+
field: relation.toField,
|
|
496
|
+
cardinality: toCard
|
|
497
|
+
},
|
|
498
|
+
identifying: true,
|
|
499
|
+
origin: "annotated"
|
|
500
|
+
}];
|
|
501
|
+
}));
|
|
502
|
+
}
|
|
503
|
+
function mergeERRelations(models) {
|
|
504
|
+
const inferred = inferredERRelations(models);
|
|
505
|
+
const annotated = annotatedERRelations(models);
|
|
506
|
+
const inferredKeys = new Set(inferred.map(erKey));
|
|
507
|
+
return [...new Map([...inferred.map((r) => [erKey(r), r]), ...annotated.map((r) => [erKey(r), inferredKeys.has(erKey(r)) ? {
|
|
508
|
+
...r,
|
|
509
|
+
origin: "inferred"
|
|
510
|
+
} : r])]).values()];
|
|
511
|
+
}
|
|
512
|
+
//#endregion
|
|
439
513
|
//#region src/helper/dbml.ts
|
|
440
514
|
function escapeNote(str) {
|
|
441
515
|
return str.replace(/'/g, "\\'");
|
|
@@ -466,10 +540,18 @@ function makeIndex(index) {
|
|
|
466
540
|
index.name && `name: '${index.name}'`
|
|
467
541
|
].filter((c) => Boolean(c)))}`;
|
|
468
542
|
}
|
|
543
|
+
function toDbmlAction(action) {
|
|
544
|
+
if (action === "Cascade") return "cascade";
|
|
545
|
+
if (action === "SetNull") return "set null";
|
|
546
|
+
if (action === "SetDefault") return "set default";
|
|
547
|
+
if (action === "NoAction") return "no action";
|
|
548
|
+
if (action === "Restrict") return "restrict";
|
|
549
|
+
return action;
|
|
550
|
+
}
|
|
469
551
|
function makeRef(ref) {
|
|
470
552
|
const name = makeRefName(ref);
|
|
471
553
|
const operator = ref.type ?? ">";
|
|
472
|
-
const actions = [ref.onDelete && `delete: ${ref.onDelete}`, ref.onUpdate && `update: ${ref.onUpdate}`].filter((a) => Boolean(a));
|
|
554
|
+
const actions = [ref.onDelete && `delete: ${toDbmlAction(ref.onDelete)}`, ref.onUpdate && `update: ${toDbmlAction(ref.onUpdate)}`].filter((a) => Boolean(a));
|
|
473
555
|
const actionStr = actions.length > 0 ? ` [${actions.join(", ")}]` : "";
|
|
474
556
|
return `Ref ${name}: ${ref.fromTable}.${ref.fromColumn} ${operator} ${ref.toTable}.${ref.toColumn}${actionStr}`;
|
|
475
557
|
}
|
|
@@ -548,20 +630,41 @@ function makeRelations$1(models, mapToDbSchema = false) {
|
|
|
548
630
|
toTable: relationToName,
|
|
549
631
|
toColumn,
|
|
550
632
|
type: operator,
|
|
551
|
-
onDelete: field.relationOnDelete
|
|
633
|
+
onDelete: field.relationOnDelete,
|
|
634
|
+
onUpdate: field.relationOnUpdate
|
|
552
635
|
});
|
|
553
636
|
}));
|
|
554
637
|
}
|
|
638
|
+
function isMany(cardinality) {
|
|
639
|
+
return cardinality === "many" || cardinality === "zero-many";
|
|
640
|
+
}
|
|
641
|
+
function dbmlOperator(leftMany, rightMany) {
|
|
642
|
+
if (leftMany && !rightMany) return ">";
|
|
643
|
+
if (!leftMany && rightMany) return "<";
|
|
644
|
+
if (leftMany && rightMany) return "<>";
|
|
645
|
+
return "-";
|
|
646
|
+
}
|
|
647
|
+
function annotatedDbmlRefs(models) {
|
|
648
|
+
const inferredKeys = new Set(inferredERRelations(models).map(erKey));
|
|
649
|
+
return annotatedERRelations(models).filter((relation) => !inferredKeys.has(erKey(relation))).map((relation) => {
|
|
650
|
+
const left = `${relation.to.model}.${relation.to.field}`;
|
|
651
|
+
const right = `${relation.from.model}.${relation.from.field}`;
|
|
652
|
+
const operator = dbmlOperator(isMany(relation.to.cardinality), isMany(relation.from.cardinality));
|
|
653
|
+
return `Ref ${`${relation.to.model}_${relation.to.field}_${relation.from.model}_${relation.from.field}`}: ${left} ${operator} ${right}`;
|
|
654
|
+
});
|
|
655
|
+
}
|
|
555
656
|
//#endregion
|
|
556
657
|
//#region src/generator/dbml.ts
|
|
557
658
|
function dbmlContent(datamodel, mapToDbSchema = false) {
|
|
558
659
|
const tables = makeTables(datamodel.models, mapToDbSchema);
|
|
559
660
|
const enums = makeEnums(datamodel.enums);
|
|
560
661
|
const refs = makeRelations$1(datamodel.models, mapToDbSchema);
|
|
662
|
+
const logicalRefs = annotatedDbmlRefs(datamodel.models);
|
|
561
663
|
return [
|
|
562
664
|
...enums,
|
|
563
665
|
...tables,
|
|
564
|
-
...refs
|
|
666
|
+
...refs,
|
|
667
|
+
...logicalRefs
|
|
565
668
|
].join("\n\n");
|
|
566
669
|
}
|
|
567
670
|
function dbmlToPng(dbml) {
|
|
@@ -2235,7 +2338,7 @@ async function ecto(options) {
|
|
|
2235
2338
|
//#endregion
|
|
2236
2339
|
//#region src/helper/effect.ts
|
|
2237
2340
|
function makeEffectInfer(modelName) {
|
|
2238
|
-
return `export type ${modelName}
|
|
2341
|
+
return `export type ${modelName} = typeof ${modelName}Schema.Type`;
|
|
2239
2342
|
}
|
|
2240
2343
|
function makeEffectSchema(modelName, fields) {
|
|
2241
2344
|
return `export const ${modelName}Schema = Schema.Struct({\n${fields}\n})`;
|
|
@@ -2266,7 +2369,7 @@ function makeEffectRelations(model, relProps, options) {
|
|
|
2266
2369
|
if (relProps.length === 0) return null;
|
|
2267
2370
|
const base = `...${model.name}Schema.fields,`;
|
|
2268
2371
|
const rels = relProps.map((r) => `${r.key}:${r.isMany ? `Schema.Array(${r.targetModel}Schema)` : `${r.targetModel}Schema`},`).join("");
|
|
2269
|
-
const typeLine = options?.includeType ? `\n\nexport type ${model.name}
|
|
2372
|
+
const typeLine = options?.includeType ? `\n\nexport type ${model.name}Relations = typeof ${model.name}RelationsSchema.Type` : "";
|
|
2270
2373
|
return `export const ${model.name}RelationsSchema = Schema.Struct({${base}${rels}})${typeLine}`;
|
|
2271
2374
|
}
|
|
2272
2375
|
function effectSchemaCode(models, type, comment, enums) {
|
|
@@ -2623,15 +2726,15 @@ async function gorm(options) {
|
|
|
2623
2726
|
}
|
|
2624
2727
|
//#endregion
|
|
2625
2728
|
//#region src/helper/mermaid-er.ts
|
|
2626
|
-
function removeDuplicateRelations(relations) {
|
|
2627
|
-
return [...new Set(relations)];
|
|
2628
|
-
}
|
|
2629
2729
|
const RELATIONSHIPS = {
|
|
2630
2730
|
"zero-one": "|o",
|
|
2631
2731
|
one: "||",
|
|
2632
2732
|
"zero-many": "}o",
|
|
2633
2733
|
many: "}|"
|
|
2634
2734
|
};
|
|
2735
|
+
function erRelationLine(relation) {
|
|
2736
|
+
return ` ${relation.from.model} ${RELATIONSHIPS[relation.from.cardinality]}--${RELATIONSHIPS[relation.to.cardinality]} ${relation.to.model} : "(${relation.from.field}) - (${relation.to.field})"`;
|
|
2737
|
+
}
|
|
2635
2738
|
function modelFields(model) {
|
|
2636
2739
|
const fkFields = new Set(model.fields.filter((f) => f.relationFromFields && f.relationFromFields.length > 0).flatMap((f) => f.relationFromFields ?? []));
|
|
2637
2740
|
return model.fields.map((field) => {
|
|
@@ -2649,26 +2752,16 @@ function modelInfo(model) {
|
|
|
2649
2752
|
" }"
|
|
2650
2753
|
];
|
|
2651
2754
|
}
|
|
2652
|
-
function extractRelationsFromDmmf(models) {
|
|
2653
|
-
return models.flatMap((model) => model.fields.filter((field) => field.kind === "object" && field.relationFromFields && field.relationFromFields.length > 0).map((field) => {
|
|
2654
|
-
const toModel = model.name;
|
|
2655
|
-
const fromModel = field.type;
|
|
2656
|
-
const toField = field.relationFromFields?.[0];
|
|
2657
|
-
const fromField = field.relationToFields?.[0] ?? "id";
|
|
2658
|
-
const toCardinality = (models.find((m) => m.name === fromModel)?.fields.find((f) => f.relationName === field.relationName && f.name !== field.name))?.isList ? field.isRequired ? "many" : "zero-many" : field.isRequired ? "one" : "zero-one";
|
|
2659
|
-
return ` ${fromModel} ${RELATIONSHIPS.one}--${RELATIONSHIPS[toCardinality]} ${toModel} : "(${fromField}) - (${toField})"`;
|
|
2660
|
-
}));
|
|
2661
|
-
}
|
|
2662
2755
|
//#endregion
|
|
2663
2756
|
//#region src/generator/mermaid-er.ts
|
|
2664
2757
|
const ER_HEADER = ["```mermaid", "erDiagram"];
|
|
2665
2758
|
const ER_FOOTER = ["```"];
|
|
2666
2759
|
function erContent(models) {
|
|
2667
|
-
const
|
|
2760
|
+
const relations = mergeERRelations(models).map(erRelationLine);
|
|
2668
2761
|
const modelInfos = models.flatMap(modelInfo);
|
|
2669
2762
|
return [
|
|
2670
2763
|
...ER_HEADER,
|
|
2671
|
-
...
|
|
2764
|
+
...relations,
|
|
2672
2765
|
...modelInfos,
|
|
2673
2766
|
...ER_FOOTER
|
|
2674
2767
|
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hekireki",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
4
4
|
"description": "Hekireki is a tool that generates validation schemas for Zod, Valibot, ArkType, and Effect Schema, as well as ER diagrams and DBML, from Prisma schemas annotated with comments.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ajv",
|
|
@@ -54,38 +54,38 @@
|
|
|
54
54
|
"publishConfig": {
|
|
55
55
|
"access": "public"
|
|
56
56
|
},
|
|
57
|
-
"scripts": {
|
|
58
|
-
"generate": "prisma generate",
|
|
59
|
-
"deps": "rm -rf node_modules && pnpm install",
|
|
60
|
-
"build": "tsdown",
|
|
61
|
-
"release": "npm pkg fix && pnpm build && npm publish"
|
|
62
|
-
},
|
|
63
57
|
"dependencies": {
|
|
64
|
-
"@hono/node-server": "^2.0.
|
|
58
|
+
"@hono/node-server": "^2.0.4",
|
|
65
59
|
"@prisma/generator-helper": "^7.8.0",
|
|
66
60
|
"@resvg/resvg-js": "^2.6.2",
|
|
67
61
|
"@softwaretechnik/dbml-renderer": "^1.0.31",
|
|
68
|
-
"hono": "^4.12.
|
|
69
|
-
"oxfmt": "^0.
|
|
62
|
+
"hono": "^4.12.25",
|
|
63
|
+
"oxfmt": "^0.54.0"
|
|
70
64
|
},
|
|
71
65
|
"devDependencies": {
|
|
72
66
|
"@paralleldrive/cuid2": "^3.3.0",
|
|
73
67
|
"@prisma/client": "^7.8.0",
|
|
74
68
|
"@sinclair/typebox": "^0.34.49",
|
|
75
|
-
"@types/node": "^25.
|
|
76
|
-
"@typescript/native-preview": "7.0.0-dev.
|
|
69
|
+
"@types/node": "^25.9.3",
|
|
70
|
+
"@typescript/native-preview": "7.0.0-dev.20260611.2",
|
|
77
71
|
"ajv": "^8.20.0",
|
|
78
72
|
"arktype": "^2.2.0",
|
|
79
|
-
"better-auth": "^1.6.
|
|
73
|
+
"better-auth": "^1.6.17",
|
|
80
74
|
"drizzle-kit": "^0.31.10",
|
|
81
75
|
"drizzle-orm": "^0.45.2",
|
|
82
|
-
"effect": "^3.21.
|
|
76
|
+
"effect": "^3.21.3",
|
|
83
77
|
"json-schema-to-ts": "^3.1.1",
|
|
84
78
|
"prisma": "^7.8.0",
|
|
85
|
-
"tsdown": "^0.
|
|
86
|
-
"tsx": "^4.
|
|
79
|
+
"tsdown": "^0.22.2",
|
|
80
|
+
"tsx": "^4.22.4",
|
|
87
81
|
"typescript": "^6.0.3",
|
|
88
|
-
"valibot": "1.
|
|
82
|
+
"valibot": "1.4.1",
|
|
89
83
|
"zod": "^4.4.3"
|
|
84
|
+
},
|
|
85
|
+
"scripts": {
|
|
86
|
+
"generate": "prisma generate",
|
|
87
|
+
"deps": "rm -rf node_modules && pnpm install",
|
|
88
|
+
"build": "tsdown",
|
|
89
|
+
"release": "npm pkg fix && pnpm build && npm publish"
|
|
90
90
|
}
|
|
91
|
-
}
|
|
91
|
+
}
|