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 +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-B_9WDdSx.js} +102 -18
- package/package.json +1 -1
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, "\\'");
|
|
@@ -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}
|
|
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}
|
|
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
|
|
2751
|
+
const relations = mergeERRelations(models).map(erRelationLine);
|
|
2668
2752
|
const modelInfos = models.flatMap(modelInfo);
|
|
2669
2753
|
return [
|
|
2670
2754
|
...ER_HEADER,
|
|
2671
|
-
...
|
|
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.
|
|
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",
|