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 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
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as registerGenerator } from "../bin-BqlsEIc6.js";
2
+ import { t as registerGenerator } from "../bin-Brdok3Yj.js";
3
3
  //#region src/bin/ajv.ts
4
4
  registerGenerator("ajv");
5
5
  //#endregion
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as registerGenerator } from "../bin-BqlsEIc6.js";
2
+ import { t as registerGenerator } from "../bin-Brdok3Yj.js";
3
3
  //#region src/bin/arktype.ts
4
4
  registerGenerator("arktype");
5
5
  //#endregion
package/dist/bin/dbml.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as registerGenerator } from "../bin-BqlsEIc6.js";
2
+ import { t as registerGenerator } from "../bin-Brdok3Yj.js";
3
3
  //#region src/bin/dbml.ts
4
4
  registerGenerator("dbml");
5
5
  //#endregion
package/dist/bin/docs.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as registerGenerator } from "../bin-BqlsEIc6.js";
2
+ import { t as registerGenerator } from "../bin-Brdok3Yj.js";
3
3
  //#region src/bin/docs.ts
4
4
  registerGenerator("docs");
5
5
  //#endregion
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as registerGenerator } from "../bin-BqlsEIc6.js";
2
+ import { t as registerGenerator } from "../bin-Brdok3Yj.js";
3
3
  //#region src/bin/drizzle.ts
4
4
  registerGenerator("drizzle");
5
5
  //#endregion
package/dist/bin/ecto.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as registerGenerator } from "../bin-BqlsEIc6.js";
2
+ import { t as registerGenerator } from "../bin-Brdok3Yj.js";
3
3
  //#region src/bin/ecto.ts
4
4
  registerGenerator("ecto");
5
5
  //#endregion
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as registerGenerator } from "../bin-BqlsEIc6.js";
2
+ import { t as registerGenerator } from "../bin-Brdok3Yj.js";
3
3
  //#region src/bin/effect.ts
4
4
  registerGenerator("effect");
5
5
  //#endregion
package/dist/bin/gorm.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as registerGenerator } from "../bin-BqlsEIc6.js";
2
+ import { t as registerGenerator } from "../bin-Brdok3Yj.js";
3
3
  //#region src/bin/gorm.ts
4
4
  registerGenerator("gorm");
5
5
  //#endregion
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as registerGenerator } from "../bin-BqlsEIc6.js";
2
+ import { t as registerGenerator } from "../bin-Brdok3Yj.js";
3
3
  //#region src/bin/mermaid-er.ts
4
4
  registerGenerator("mermaid-er");
5
5
  //#endregion
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as registerGenerator } from "../bin-BqlsEIc6.js";
2
+ import { t as registerGenerator } from "../bin-Brdok3Yj.js";
3
3
  //#region src/bin/sea-orm.ts
4
4
  registerGenerator("sea-orm");
5
5
  //#endregion
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as registerGenerator } from "../bin-BqlsEIc6.js";
2
+ import { t as registerGenerator } from "../bin-Brdok3Yj.js";
3
3
  //#region src/bin/sqlalchemy.ts
4
4
  registerGenerator("sqlalchemy");
5
5
  //#endregion
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as registerGenerator } from "../bin-BqlsEIc6.js";
2
+ import { t as registerGenerator } from "../bin-Brdok3Yj.js";
3
3
  //#region src/bin/typebox.ts
4
4
  registerGenerator("typebox");
5
5
  //#endregion
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as registerGenerator } from "../bin-BqlsEIc6.js";
2
+ import { t as registerGenerator } from "../bin-Brdok3Yj.js";
3
3
  //#region src/bin/valibot.ts
4
4
  registerGenerator("valibot");
5
5
  //#endregion
package/dist/bin/zod.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as registerGenerator } from "../bin-BqlsEIc6.js";
2
+ import { t as registerGenerator } from "../bin-Brdok3Yj.js";
3
3
  //#region src/bin/zod.ts
4
4
  registerGenerator("zod");
5
5
  //#endregion
@@ -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}Encoded = typeof ${modelName}Schema.Encoded`;
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}RelationsEncoded = typeof ${model.name}RelationsSchema.Encoded` : "";
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 uniqueRelations = removeDuplicateRelations(extractRelationsFromDmmf(models));
2760
+ const relations = mergeERRelations(models).map(erRelationLine);
2668
2761
  const modelInfos = models.flatMap(modelInfo);
2669
2762
  return [
2670
2763
  ...ER_HEADER,
2671
- ...uniqueRelations,
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.1",
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.1",
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.16",
69
- "oxfmt": "^0.47.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.6.0",
76
- "@typescript/native-preview": "7.0.0-dev.20260504.1",
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.9",
73
+ "better-auth": "^1.6.17",
80
74
  "drizzle-kit": "^0.31.10",
81
75
  "drizzle-orm": "^0.45.2",
82
- "effect": "^3.21.2",
76
+ "effect": "^3.21.3",
83
77
  "json-schema-to-ts": "^3.1.1",
84
78
  "prisma": "^7.8.0",
85
- "tsdown": "^0.21.10",
86
- "tsx": "^4.21.0",
79
+ "tsdown": "^0.22.2",
80
+ "tsx": "^4.22.4",
87
81
  "typescript": "^6.0.3",
88
- "valibot": "1.3.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
+ }