hekireki 0.8.1 → 0.8.2

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 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-B_9WDdSx.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-B_9WDdSx.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-B_9WDdSx.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-B_9WDdSx.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-B_9WDdSx.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-B_9WDdSx.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-B_9WDdSx.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-B_9WDdSx.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-B_9WDdSx.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-B_9WDdSx.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-B_9WDdSx.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-B_9WDdSx.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-B_9WDdSx.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-B_9WDdSx.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, "\\'");
@@ -552,16 +626,36 @@ function makeRelations$1(models, mapToDbSchema = false) {
552
626
  });
553
627
  }));
554
628
  }
629
+ function isMany(cardinality) {
630
+ return cardinality === "many" || cardinality === "zero-many";
631
+ }
632
+ function dbmlOperator(leftMany, rightMany) {
633
+ if (leftMany && !rightMany) return ">";
634
+ if (!leftMany && rightMany) return "<";
635
+ if (leftMany && rightMany) return "<>";
636
+ return "-";
637
+ }
638
+ function annotatedDbmlRefs(models) {
639
+ const inferredKeys = new Set(inferredERRelations(models).map(erKey));
640
+ return annotatedERRelations(models).filter((relation) => !inferredKeys.has(erKey(relation))).map((relation) => {
641
+ const left = `${relation.to.model}.${relation.to.field}`;
642
+ const right = `${relation.from.model}.${relation.from.field}`;
643
+ const operator = dbmlOperator(isMany(relation.to.cardinality), isMany(relation.from.cardinality));
644
+ return `Ref ${`${relation.to.model}_${relation.to.field}_${relation.from.model}_${relation.from.field}`}: ${left} ${operator} ${right}`;
645
+ });
646
+ }
555
647
  //#endregion
556
648
  //#region src/generator/dbml.ts
557
649
  function dbmlContent(datamodel, mapToDbSchema = false) {
558
650
  const tables = makeTables(datamodel.models, mapToDbSchema);
559
651
  const enums = makeEnums(datamodel.enums);
560
652
  const refs = makeRelations$1(datamodel.models, mapToDbSchema);
653
+ const logicalRefs = annotatedDbmlRefs(datamodel.models);
561
654
  return [
562
655
  ...enums,
563
656
  ...tables,
564
- ...refs
657
+ ...refs,
658
+ ...logicalRefs
565
659
  ].join("\n\n");
566
660
  }
567
661
  function dbmlToPng(dbml) {
@@ -2235,7 +2329,7 @@ async function ecto(options) {
2235
2329
  //#endregion
2236
2330
  //#region src/helper/effect.ts
2237
2331
  function makeEffectInfer(modelName) {
2238
- return `export type ${modelName}Encoded = typeof ${modelName}Schema.Encoded`;
2332
+ return `export type ${modelName} = typeof ${modelName}Schema.Type`;
2239
2333
  }
2240
2334
  function makeEffectSchema(modelName, fields) {
2241
2335
  return `export const ${modelName}Schema = Schema.Struct({\n${fields}\n})`;
@@ -2266,7 +2360,7 @@ function makeEffectRelations(model, relProps, options) {
2266
2360
  if (relProps.length === 0) return null;
2267
2361
  const base = `...${model.name}Schema.fields,`;
2268
2362
  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` : "";
2363
+ const typeLine = options?.includeType ? `\n\nexport type ${model.name}Relations = typeof ${model.name}RelationsSchema.Type` : "";
2270
2364
  return `export const ${model.name}RelationsSchema = Schema.Struct({${base}${rels}})${typeLine}`;
2271
2365
  }
2272
2366
  function effectSchemaCode(models, type, comment, enums) {
@@ -2623,15 +2717,15 @@ async function gorm(options) {
2623
2717
  }
2624
2718
  //#endregion
2625
2719
  //#region src/helper/mermaid-er.ts
2626
- function removeDuplicateRelations(relations) {
2627
- return [...new Set(relations)];
2628
- }
2629
2720
  const RELATIONSHIPS = {
2630
2721
  "zero-one": "|o",
2631
2722
  one: "||",
2632
2723
  "zero-many": "}o",
2633
2724
  many: "}|"
2634
2725
  };
2726
+ function erRelationLine(relation) {
2727
+ return ` ${relation.from.model} ${RELATIONSHIPS[relation.from.cardinality]}--${RELATIONSHIPS[relation.to.cardinality]} ${relation.to.model} : "(${relation.from.field}) - (${relation.to.field})"`;
2728
+ }
2635
2729
  function modelFields(model) {
2636
2730
  const fkFields = new Set(model.fields.filter((f) => f.relationFromFields && f.relationFromFields.length > 0).flatMap((f) => f.relationFromFields ?? []));
2637
2731
  return model.fields.map((field) => {
@@ -2649,26 +2743,16 @@ function modelInfo(model) {
2649
2743
  " }"
2650
2744
  ];
2651
2745
  }
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
2746
  //#endregion
2663
2747
  //#region src/generator/mermaid-er.ts
2664
2748
  const ER_HEADER = ["```mermaid", "erDiagram"];
2665
2749
  const ER_FOOTER = ["```"];
2666
2750
  function erContent(models) {
2667
- const uniqueRelations = removeDuplicateRelations(extractRelationsFromDmmf(models));
2751
+ const relations = mergeERRelations(models).map(erRelationLine);
2668
2752
  const modelInfos = models.flatMap(modelInfo);
2669
2753
  return [
2670
2754
  ...ER_HEADER,
2671
- ...uniqueRelations,
2755
+ ...relations,
2672
2756
  ...modelInfos,
2673
2757
  ...ER_FOOTER
2674
2758
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hekireki",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
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",