hekireki 0.7.1 โ 0.7.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 +136 -3
- package/dist/generator/gorm/index.d.ts +6 -0
- package/dist/generator/gorm/index.js +370 -0
- package/dist/generator/sea-orm/index.d.ts +6 -0
- package/dist/generator/sea-orm/index.js +444 -0
- package/dist/generator/sqlalchemy/index.d.ts +6 -0
- package/dist/generator/sqlalchemy/index.js +458 -0
- package/package.json +10 -7
package/README.md
CHANGED
|
@@ -2,20 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
# Hekireki
|
|
4
4
|
|
|
5
|
-
**[Hekireki](https://www.npmjs.com/package/hekireki)** is a tool that generates validation schemas
|
|
5
|
+
**[Hekireki](https://www.npmjs.com/package/hekireki)** is a tool that generates validation schemas, ORM models, and ER diagrams from [Prisma](https://www.prisma.io/) schemas โ supporting TypeScript, Python, Go, Rust, and Elixir.
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
|
+
### TypeScript Validation Libraries
|
|
10
|
+
|
|
9
11
|
- ๐ Automatically generates [Zod](https://zod.dev/) schemas from your Prisma schema
|
|
10
12
|
- ๐ค Automatically generates [Valibot](https://valibot.dev/) schemas from your Prisma schema
|
|
11
13
|
- ๐น Automatically generates [ArkType](https://arktype.io/) schemas from your Prisma schema
|
|
12
14
|
- โก Automatically generates [Effect Schema](https://effect.website/docs/schema/introduction/) from your Prisma schema
|
|
13
15
|
- ๐ฆ Automatically generates [TypeBox](https://github.com/sinclairzx81/typebox) schemas from your Prisma schema
|
|
14
16
|
- ๐ Automatically generates [AJV](https://ajv.js.org/)-compatible JSON Schema objects from your Prisma schema
|
|
17
|
+
|
|
18
|
+
### ORM / Schema Generation (Multi-Language)
|
|
19
|
+
|
|
15
20
|
- ๐๏ธ Automatically generates [Drizzle ORM](https://orm.drizzle.team/) table schemas and relations from your Prisma schema
|
|
21
|
+
- ๐ Automatically generates [SQLAlchemy](https://www.sqlalchemy.org/) models (Python) โ with `Mapped[T]` type hints, relationships, enums, composite keys, and index support
|
|
22
|
+
- ๐น Automatically generates [GORM](https://gorm.io/) models (Go) โ with struct tags, JSON tags, relationships, enums, composite keys, and index support
|
|
23
|
+
- ๐ฆ Automatically generates [Sea-ORM](https://www.sea-ql.org/SeaORM/) entities (Rust) โ with `DeriveEntityModel`, relations, enums, serde support, and `rename_all`
|
|
24
|
+
- ๐งช Generates [Ecto](https://hexdocs.pm/ecto/Ecto.Schema.html) schemas (Elixir) โ with associations (`belongs_to`, `has_many`, `has_one`), composite primary keys, `@type t` typespecs, array fields, `@@map`/`@map` support, and `@moduledoc`
|
|
25
|
+
|
|
26
|
+
### Diagrams & Documentation
|
|
27
|
+
|
|
16
28
|
- ๐ Creates [Mermaid](https://mermaid.js.org/) ER diagrams with PK/FK markers
|
|
17
29
|
- ๐ Generates [DBML](https://dbml.dbdiagram.io/) (Database Markup Language) files and **PNG** ER diagrams via [dbml-renderer](https://github.com/softwaretechnik-berlin/dbml-renderer) โ output format is determined by the file extension (`.dbml` or `.png`)
|
|
18
|
-
- ๐งช Generates [Ecto](https://hexdocs.pm/ecto/Ecto.Schema.html) schemas for Elixir projects โ with associations (`belongs_to`, `has_many`, `has_one`), composite primary keys, `@type t` typespecs, array fields, `@@map`/`@map` support, and `@moduledoc`
|
|
19
30
|
|
|
20
31
|
## Installation
|
|
21
32
|
|
|
@@ -82,6 +93,23 @@ generator Hekireki-Drizzle {
|
|
|
82
93
|
provider = "hekireki-drizzle"
|
|
83
94
|
}
|
|
84
95
|
|
|
96
|
+
generator Hekireki-SQLAlchemy {
|
|
97
|
+
provider = "hekireki-sqlalchemy"
|
|
98
|
+
output = "./sqlalchemy"
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
generator Hekireki-GORM {
|
|
102
|
+
provider = "hekireki-gorm"
|
|
103
|
+
output = "./gorm"
|
|
104
|
+
package = "model"
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
generator Hekireki-SeaORM {
|
|
108
|
+
provider = "hekireki-sea-orm"
|
|
109
|
+
output = "./sea_orm"
|
|
110
|
+
renameAll = "camelCase"
|
|
111
|
+
}
|
|
112
|
+
|
|
85
113
|
generator Hekireki-Ecto {
|
|
86
114
|
provider = "hekireki-ecto"
|
|
87
115
|
output = "./ecto"
|
|
@@ -520,6 +548,91 @@ defmodule DBSchema.Post do
|
|
|
520
548
|
end
|
|
521
549
|
```
|
|
522
550
|
|
|
551
|
+
### SQLAlchemy
|
|
552
|
+
|
|
553
|
+
```python
|
|
554
|
+
from sqlalchemy import ForeignKey
|
|
555
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
class Base(DeclarativeBase):
|
|
559
|
+
pass
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
class User(Base):
|
|
563
|
+
__tablename__ = "user"
|
|
564
|
+
|
|
565
|
+
id: Mapped[str] = mapped_column(primary_key=True)
|
|
566
|
+
name: Mapped[str]
|
|
567
|
+
|
|
568
|
+
posts: Mapped[list["Post"]] = relationship(back_populates="user")
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
class Post(Base):
|
|
572
|
+
__tablename__ = "post"
|
|
573
|
+
|
|
574
|
+
id: Mapped[str] = mapped_column(primary_key=True)
|
|
575
|
+
title: Mapped[str]
|
|
576
|
+
content: Mapped[str]
|
|
577
|
+
user_id: Mapped[str] = mapped_column(ForeignKey("user.id"))
|
|
578
|
+
|
|
579
|
+
user: Mapped["User"] = relationship(back_populates="posts")
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
### GORM
|
|
583
|
+
|
|
584
|
+
```go
|
|
585
|
+
package model
|
|
586
|
+
|
|
587
|
+
type User struct {
|
|
588
|
+
ID string `gorm:"column:id;primaryKey;type:char(36)" json:"id"`
|
|
589
|
+
Name string `gorm:"column:name;not null" json:"name"`
|
|
590
|
+
Posts []Post `gorm:"foreignKey:UserID"`
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
type Post struct {
|
|
594
|
+
ID string `gorm:"column:id;primaryKey;type:char(36)" json:"id"`
|
|
595
|
+
Title string `gorm:"column:title;not null" json:"title"`
|
|
596
|
+
Content string `gorm:"column:content;not null" json:"content"`
|
|
597
|
+
UserID string `gorm:"column:user_id;not null" json:"user_id"`
|
|
598
|
+
User User
|
|
599
|
+
}
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
### Sea-ORM
|
|
603
|
+
|
|
604
|
+
Each model is output as a separate `.rs` file with `mod.rs` and `prelude.rs`, following Sea-ORM conventions.
|
|
605
|
+
|
|
606
|
+
**user.rs:**
|
|
607
|
+
|
|
608
|
+
```rust
|
|
609
|
+
use sea_orm::entity::prelude::*;
|
|
610
|
+
use serde::{Deserialize, Serialize};
|
|
611
|
+
|
|
612
|
+
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
|
613
|
+
#[serde(rename_all = "camelCase")]
|
|
614
|
+
#[sea_orm(table_name = "user")]
|
|
615
|
+
pub struct Model {
|
|
616
|
+
#[sea_orm(primary_key, auto_increment = false)]
|
|
617
|
+
pub id: String,
|
|
618
|
+
pub name: String,
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
|
622
|
+
pub enum Relation {
|
|
623
|
+
#[sea_orm(has_many = "super::post::Entity")]
|
|
624
|
+
Posts,
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
impl Related<super::post::Entity> for Entity {
|
|
628
|
+
fn to() -> RelationDef {
|
|
629
|
+
Relation::Posts.def()
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
impl ActiveModelBehavior for ActiveModel {}
|
|
634
|
+
```
|
|
635
|
+
|
|
523
636
|
### DBML
|
|
524
637
|
|
|
525
638
|
```dbml
|
|
@@ -634,7 +747,27 @@ generator Hekireki-ER {
|
|
|
634
747
|
output = "./mermaid-er" // Output path (default: ./mermaid-er/ER.md)
|
|
635
748
|
}
|
|
636
749
|
|
|
637
|
-
//
|
|
750
|
+
// SQLAlchemy Generator (Python)
|
|
751
|
+
generator Hekireki-SQLAlchemy {
|
|
752
|
+
provider = "hekireki-sqlalchemy"
|
|
753
|
+
output = "./sqlalchemy" // Output path (default: ./sqlalchemy/models.py)
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// GORM Generator (Go)
|
|
757
|
+
generator Hekireki-GORM {
|
|
758
|
+
provider = "hekireki-gorm"
|
|
759
|
+
output = "./gorm" // Output path (default: ./gorm/models.go)
|
|
760
|
+
package = "model" // Go package name (default: model)
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Sea-ORM Generator (Rust)
|
|
764
|
+
generator Hekireki-SeaORM {
|
|
765
|
+
provider = "hekireki-sea-orm"
|
|
766
|
+
output = "./sea_orm" // Output directory for .rs files
|
|
767
|
+
renameAll = "camelCase" // #[serde(rename_all = "...")] attribute (optional)
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Ecto Generator (Elixir)
|
|
638
771
|
generator Hekireki-Ecto {
|
|
639
772
|
provider = "hekireki-ecto"
|
|
640
773
|
output = "./ecto" // Output directory (default: ./ecto/)
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { d as mkdir, f as writeFile, n as getString, o as makeSnakeCase } from "../../utils-COHZyQue.js";
|
|
3
|
+
import path, { dirname } from "node:path";
|
|
4
|
+
import pkg from "@prisma/generator-helper";
|
|
5
|
+
|
|
6
|
+
//#region src/helper/gorm.ts
|
|
7
|
+
const PRISMA_TO_GO = {
|
|
8
|
+
String: "string",
|
|
9
|
+
Int: "int",
|
|
10
|
+
BigInt: "int64",
|
|
11
|
+
Float: "float64",
|
|
12
|
+
Decimal: "float64",
|
|
13
|
+
Boolean: "bool",
|
|
14
|
+
DateTime: "time.Time",
|
|
15
|
+
Json: "datatypes.JSON",
|
|
16
|
+
Bytes: "[]byte"
|
|
17
|
+
};
|
|
18
|
+
function prismaTypeToGoType(type, isRequired) {
|
|
19
|
+
const base = PRISMA_TO_GO[type] ?? "string";
|
|
20
|
+
if (!isRequired && base !== "[]byte" && base !== "datatypes.JSON") return `*${base}`;
|
|
21
|
+
return base;
|
|
22
|
+
}
|
|
23
|
+
function resolveNativeType(field) {
|
|
24
|
+
if (!field.nativeType) return null;
|
|
25
|
+
const [nativeName, nativeArgs] = field.nativeType;
|
|
26
|
+
const args = nativeArgs ?? [];
|
|
27
|
+
switch (nativeName) {
|
|
28
|
+
case "VarChar":
|
|
29
|
+
case "Char": return args.length > 0 ? `varchar(${args[0]})` : null;
|
|
30
|
+
case "Text":
|
|
31
|
+
case "MediumText":
|
|
32
|
+
case "LongText":
|
|
33
|
+
case "TinyText": return "text";
|
|
34
|
+
case "SmallInt":
|
|
35
|
+
case "TinyInt": return "smallint";
|
|
36
|
+
case "MediumInt": return "mediumint";
|
|
37
|
+
case "DoublePrecision":
|
|
38
|
+
case "Double":
|
|
39
|
+
case "Real": return "double precision";
|
|
40
|
+
case "Decimal":
|
|
41
|
+
case "Money": return args.length >= 2 ? `decimal(${args[0]},${args[1]})` : "decimal";
|
|
42
|
+
case "Uuid": return "char(36)";
|
|
43
|
+
case "Timestamp":
|
|
44
|
+
case "Timestamptz": return "timestamp";
|
|
45
|
+
case "Date": return "date";
|
|
46
|
+
case "Time":
|
|
47
|
+
case "Timetz": return "time";
|
|
48
|
+
case "JsonB": return "jsonb";
|
|
49
|
+
case "Xml": return "xml";
|
|
50
|
+
default: return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function getAssociations(model, allModels) {
|
|
54
|
+
const belongsTo = [];
|
|
55
|
+
const hasMany = [];
|
|
56
|
+
const hasOne = [];
|
|
57
|
+
const manyToMany = [];
|
|
58
|
+
for (const field of model.fields) {
|
|
59
|
+
if (field.kind !== "object") continue;
|
|
60
|
+
if (field.relationFromFields && field.relationFromFields.length > 0) belongsTo.push({
|
|
61
|
+
name: field.name,
|
|
62
|
+
targetModel: field.type,
|
|
63
|
+
foreignKey: field.relationFromFields[0],
|
|
64
|
+
references: field.relationToFields?.[0] ?? "id"
|
|
65
|
+
});
|
|
66
|
+
else if (field.isList) {
|
|
67
|
+
const targetModel = allModels.find((m) => m.name === field.type);
|
|
68
|
+
if (!targetModel) continue;
|
|
69
|
+
if (targetModel.fields.find((f) => f.relationName === field.relationName && f.kind === "object")?.isList) manyToMany.push({
|
|
70
|
+
name: field.name,
|
|
71
|
+
targetModel: field.type,
|
|
72
|
+
relationName: field.relationName ?? `${model.name}To${field.type}`
|
|
73
|
+
});
|
|
74
|
+
else {
|
|
75
|
+
const fkField = targetModel.fields.find((f) => f.relationName === field.relationName && f.relationFromFields && f.relationFromFields.length > 0);
|
|
76
|
+
const foreignKey = fkField?.relationFromFields?.[0];
|
|
77
|
+
if (!foreignKey) continue;
|
|
78
|
+
const references = fkField?.relationToFields?.[0] ?? "id";
|
|
79
|
+
hasMany.push({
|
|
80
|
+
name: field.name,
|
|
81
|
+
targetModel: field.type,
|
|
82
|
+
foreignKey,
|
|
83
|
+
references,
|
|
84
|
+
isList: true
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
const targetModel = allModels.find((m) => m.name === field.type);
|
|
89
|
+
if (!targetModel) continue;
|
|
90
|
+
const fkField = targetModel.fields.find((f) => f.relationName === field.relationName && f.relationFromFields && f.relationFromFields.length > 0);
|
|
91
|
+
const foreignKey = fkField?.relationFromFields?.[0];
|
|
92
|
+
if (!foreignKey) continue;
|
|
93
|
+
const references = fkField?.relationToFields?.[0] ?? "id";
|
|
94
|
+
hasOne.push({
|
|
95
|
+
name: field.name,
|
|
96
|
+
targetModel: field.type,
|
|
97
|
+
foreignKey,
|
|
98
|
+
references,
|
|
99
|
+
isList: false
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
belongsTo,
|
|
105
|
+
hasMany,
|
|
106
|
+
hasOne,
|
|
107
|
+
manyToMany
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function isFunctionDefault(def) {
|
|
111
|
+
return def !== null && typeof def === "object" && "name" in def;
|
|
112
|
+
}
|
|
113
|
+
function isAutoincrement(field) {
|
|
114
|
+
return isFunctionDefault(field.default) && field.default.name === "autoincrement";
|
|
115
|
+
}
|
|
116
|
+
function formatGoDefault(def) {
|
|
117
|
+
if (def === void 0 || def === null) return null;
|
|
118
|
+
if (typeof def === "boolean") return def ? "true" : "false";
|
|
119
|
+
if (typeof def === "number") return String(def);
|
|
120
|
+
if (typeof def === "string") return def;
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
function buildGormTags(field, isPk, isCompositePk, compositeIndexTags) {
|
|
124
|
+
const columnName = field.dbName ?? makeSnakeCase(field.name);
|
|
125
|
+
const parts = [`column:${columnName}`];
|
|
126
|
+
if (isPk) {
|
|
127
|
+
parts.push("primaryKey");
|
|
128
|
+
if (isAutoincrement(field)) parts.push("autoIncrement");
|
|
129
|
+
if (isFunctionDefault(field.default) && field.default.name === "uuid") parts.push("type:char(36)");
|
|
130
|
+
}
|
|
131
|
+
if (field.isUnique) parts.push("uniqueIndex");
|
|
132
|
+
for (const tag of compositeIndexTags) parts.push(tag);
|
|
133
|
+
const nativeType = resolveNativeType(field);
|
|
134
|
+
if (nativeType && !isPk) parts.push(`type:${nativeType}`);
|
|
135
|
+
else if (nativeType && isPk && !isFunctionDefault(field.default)) parts.push(`type:${nativeType}`);
|
|
136
|
+
else if (nativeType && isPk && isFunctionDefault(field.default) && field.default.name !== "uuid") parts.push(`type:${nativeType}`);
|
|
137
|
+
if (!isPk || isCompositePk) if (field.type === "DateTime" && isFunctionDefault(field.default) && field.default.name === "now") parts.push("autoCreateTime");
|
|
138
|
+
else if (field.isUpdatedAt) {} else {
|
|
139
|
+
const defaultVal = formatGoDefault(field.default);
|
|
140
|
+
if (defaultVal !== null) parts.push(`default:${defaultVal}`);
|
|
141
|
+
}
|
|
142
|
+
else if (isPk && !isCompositePk) {
|
|
143
|
+
if (field.type === "DateTime" && isFunctionDefault(field.default) && field.default.name === "now") parts.push("autoCreateTime");
|
|
144
|
+
}
|
|
145
|
+
if (field.isUpdatedAt) parts.push("autoUpdateTime");
|
|
146
|
+
if (field.isRequired && !isPk) parts.push("not null");
|
|
147
|
+
return `\`gorm:"${parts.join(";")}" json:"${columnName}"\``;
|
|
148
|
+
}
|
|
149
|
+
function collectCompositeIndexTags(model, indexes) {
|
|
150
|
+
const tagMap = /* @__PURE__ */ new Map();
|
|
151
|
+
const addTag = (fieldName, tag) => {
|
|
152
|
+
const existing = tagMap.get(fieldName) ?? [];
|
|
153
|
+
existing.push(tag);
|
|
154
|
+
tagMap.set(fieldName, existing);
|
|
155
|
+
};
|
|
156
|
+
for (const fields of model.uniqueFields) {
|
|
157
|
+
if (fields.length <= 1) continue;
|
|
158
|
+
const idxName = `idx_${fields.map((f) => {
|
|
159
|
+
return model.fields.find((mf) => mf.name === f)?.dbName ?? makeSnakeCase(f);
|
|
160
|
+
}).join("_")}_unique`;
|
|
161
|
+
for (const f of fields) addTag(f, `uniqueIndex:${idxName}`);
|
|
162
|
+
}
|
|
163
|
+
for (const idx of indexes) {
|
|
164
|
+
if (idx.model !== model.name) continue;
|
|
165
|
+
if (idx.type !== "normal" && idx.type !== "fulltext") continue;
|
|
166
|
+
const idxName = idx.dbName ?? idx.name ?? `idx_${idx.fields.map((f) => makeSnakeCase(f.name)).join("_")}`;
|
|
167
|
+
for (const f of idx.fields) addTag(f.name, `index:${idxName}`);
|
|
168
|
+
}
|
|
169
|
+
return tagMap;
|
|
170
|
+
}
|
|
171
|
+
const GO_INITIALISMS = new Set([
|
|
172
|
+
"acl",
|
|
173
|
+
"api",
|
|
174
|
+
"ascii",
|
|
175
|
+
"cpu",
|
|
176
|
+
"css",
|
|
177
|
+
"dns",
|
|
178
|
+
"eof",
|
|
179
|
+
"guid",
|
|
180
|
+
"html",
|
|
181
|
+
"http",
|
|
182
|
+
"https",
|
|
183
|
+
"id",
|
|
184
|
+
"ip",
|
|
185
|
+
"json",
|
|
186
|
+
"lhs",
|
|
187
|
+
"qps",
|
|
188
|
+
"ram",
|
|
189
|
+
"rhs",
|
|
190
|
+
"rpc",
|
|
191
|
+
"sla",
|
|
192
|
+
"smtp",
|
|
193
|
+
"sql",
|
|
194
|
+
"ssh",
|
|
195
|
+
"tcp",
|
|
196
|
+
"tls",
|
|
197
|
+
"ttl",
|
|
198
|
+
"udp",
|
|
199
|
+
"ui",
|
|
200
|
+
"uid",
|
|
201
|
+
"uri",
|
|
202
|
+
"url",
|
|
203
|
+
"utf8",
|
|
204
|
+
"uuid",
|
|
205
|
+
"vm",
|
|
206
|
+
"xml",
|
|
207
|
+
"xmpp",
|
|
208
|
+
"xsrf",
|
|
209
|
+
"xss"
|
|
210
|
+
]);
|
|
211
|
+
/**
|
|
212
|
+
* Split a camelCase/PascalCase name into words, applying Go initialism rules.
|
|
213
|
+
* e.g. "userId" -> ["User", "ID"], "avatarUrl" -> ["Avatar", "URL"],
|
|
214
|
+
* "ipAddress" -> ["IP", "Address"], "createdAt" -> ["Created", "At"]
|
|
215
|
+
*/
|
|
216
|
+
function splitGoWords(name) {
|
|
217
|
+
const parts = name.replace(/([a-z0-9])([A-Z])/g, "$1\0$2").split("\0");
|
|
218
|
+
const result = [];
|
|
219
|
+
let i = 0;
|
|
220
|
+
while (i < parts.length) {
|
|
221
|
+
const lower = parts[i].toLowerCase();
|
|
222
|
+
if (GO_INITIALISMS.has(lower)) {
|
|
223
|
+
result.push(lower.toUpperCase());
|
|
224
|
+
i++;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
result.push(parts[i].charAt(0).toUpperCase() + parts[i].slice(1));
|
|
228
|
+
i++;
|
|
229
|
+
}
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
function goFieldName(name) {
|
|
233
|
+
return splitGoWords(name).join("");
|
|
234
|
+
}
|
|
235
|
+
function generateStructField(field, isPk, isCompositePk, compositeIndexTags, _enumNames) {
|
|
236
|
+
const fieldName = goFieldName(field.name);
|
|
237
|
+
let goType;
|
|
238
|
+
if (field.kind === "enum") goType = field.isRequired ? "string" : "*string";
|
|
239
|
+
else goType = prismaTypeToGoType(field.type, field.isRequired);
|
|
240
|
+
const tag = buildGormTags(field, isPk, isCompositePk, compositeIndexTags);
|
|
241
|
+
const tagStr = tag ? ` ${tag}` : "";
|
|
242
|
+
return `\t${fieldName} ${goType}${tagStr}`;
|
|
243
|
+
}
|
|
244
|
+
function needsReferencesTag(references) {
|
|
245
|
+
return references !== "id";
|
|
246
|
+
}
|
|
247
|
+
function buildRelationTag(parts) {
|
|
248
|
+
return `\`gorm:"${parts.join(";")}"\``;
|
|
249
|
+
}
|
|
250
|
+
function generateRelationFields(model, associations) {
|
|
251
|
+
const lines = [];
|
|
252
|
+
for (const assoc of associations.belongsTo) {
|
|
253
|
+
const fieldName = goFieldName(assoc.name);
|
|
254
|
+
const fkFieldName = goFieldName(assoc.foreignKey);
|
|
255
|
+
const refsFieldName = goFieldName(assoc.references);
|
|
256
|
+
const isAmbiguous = fieldName !== assoc.targetModel || associations.belongsTo.filter((a) => a.targetModel === assoc.targetModel).length > 1;
|
|
257
|
+
const tagParts = [];
|
|
258
|
+
if (isAmbiguous) tagParts.push(`foreignKey:${fkFieldName}`);
|
|
259
|
+
if (needsReferencesTag(assoc.references)) tagParts.push(`references:${refsFieldName}`);
|
|
260
|
+
if (tagParts.length > 0) lines.push(`\t${fieldName} ${assoc.targetModel} ${buildRelationTag(tagParts)}`);
|
|
261
|
+
else lines.push(`\t${fieldName} ${assoc.targetModel}`);
|
|
262
|
+
}
|
|
263
|
+
for (const assoc of associations.hasMany) {
|
|
264
|
+
const tagParts = [`foreignKey:${goFieldName(assoc.foreignKey)}`];
|
|
265
|
+
if (needsReferencesTag(assoc.references)) tagParts.push(`references:${goFieldName(assoc.references)}`);
|
|
266
|
+
lines.push(`\t${goFieldName(assoc.name)} []${assoc.targetModel} ${buildRelationTag(tagParts)}`);
|
|
267
|
+
}
|
|
268
|
+
for (const assoc of associations.hasOne) {
|
|
269
|
+
const tagParts = [`foreignKey:${goFieldName(assoc.foreignKey)}`];
|
|
270
|
+
if (needsReferencesTag(assoc.references)) tagParts.push(`references:${goFieldName(assoc.references)}`);
|
|
271
|
+
lines.push(`\t${goFieldName(assoc.name)} ${assoc.targetModel} ${buildRelationTag(tagParts)}`);
|
|
272
|
+
}
|
|
273
|
+
for (const assoc of associations.manyToMany) {
|
|
274
|
+
const [leftName, rightName] = model.name < assoc.targetModel ? [model.name, assoc.targetModel] : [assoc.targetModel, model.name];
|
|
275
|
+
const joinTable = `_${leftName}To${rightName}`;
|
|
276
|
+
lines.push(`\t${goFieldName(assoc.name)} []${assoc.targetModel} \`gorm:"many2many:${joinTable};"\``);
|
|
277
|
+
}
|
|
278
|
+
return lines;
|
|
279
|
+
}
|
|
280
|
+
function generateModelStruct(model, allModels, enums, indexes) {
|
|
281
|
+
const idField = model.fields.find((f) => f.isId);
|
|
282
|
+
const compositePkFieldNames = new Set(model.primaryKey?.fields ?? []);
|
|
283
|
+
const isCompositePk = !idField && compositePkFieldNames.size > 0;
|
|
284
|
+
if (!(idField || isCompositePk)) return null;
|
|
285
|
+
const associations = getAssociations(model, allModels);
|
|
286
|
+
const enumNames = new Set((enums ?? []).map((e) => e.name));
|
|
287
|
+
const compositeTagMap = collectCompositeIndexTags(model, indexes);
|
|
288
|
+
const tableName = model.dbName ?? makeSnakeCase(model.name);
|
|
289
|
+
const fieldLines = model.fields.filter((f) => f.kind !== "object").map((field) => {
|
|
290
|
+
return generateStructField(field, field.isId || compositePkFieldNames.has(field.name), isCompositePk, compositeTagMap.get(field.name) ?? [], enumNames);
|
|
291
|
+
});
|
|
292
|
+
const relationLines = generateRelationFields(model, associations);
|
|
293
|
+
const lines = [
|
|
294
|
+
`type ${model.name} struct {`,
|
|
295
|
+
...fieldLines,
|
|
296
|
+
...relationLines.length > 0 ? relationLines : [],
|
|
297
|
+
"}"
|
|
298
|
+
];
|
|
299
|
+
if (tableName !== makeSnakeCase(model.name)) {
|
|
300
|
+
lines.push("");
|
|
301
|
+
lines.push(`func (${model.name}) TableName() string {`);
|
|
302
|
+
lines.push(`\treturn "${tableName}"`);
|
|
303
|
+
lines.push("}");
|
|
304
|
+
}
|
|
305
|
+
return lines.join("\n");
|
|
306
|
+
}
|
|
307
|
+
function collectImports(models) {
|
|
308
|
+
const imports = [];
|
|
309
|
+
const needsTime = models.some((m) => m.fields.some((f) => f.kind !== "object" && f.type === "DateTime"));
|
|
310
|
+
const needsDatatypes = models.some((m) => m.fields.some((f) => f.kind !== "object" && f.type === "Json"));
|
|
311
|
+
if (needsTime) imports.push("\"time\"");
|
|
312
|
+
if (needsDatatypes) imports.push("\"gorm.io/datatypes\"");
|
|
313
|
+
return imports;
|
|
314
|
+
}
|
|
315
|
+
function generateGormModels(models, enums, indexes, packageName = "model") {
|
|
316
|
+
const idx = indexes ?? [];
|
|
317
|
+
const modelBodies = models.map((model) => generateModelStruct(model, models, enums, idx)).filter((body) => body !== null);
|
|
318
|
+
const imports = collectImports(models);
|
|
319
|
+
const lines = [`package ${packageName}`];
|
|
320
|
+
if (imports.length > 0) {
|
|
321
|
+
lines.push("");
|
|
322
|
+
if (imports.length === 1) lines.push(`import ${imports[0]}`);
|
|
323
|
+
else {
|
|
324
|
+
lines.push("import (");
|
|
325
|
+
for (const imp of imports) lines.push(`\t${imp}`);
|
|
326
|
+
lines.push(")");
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
lines.push("");
|
|
330
|
+
lines.push(modelBodies.join("\n\n"));
|
|
331
|
+
lines.push("");
|
|
332
|
+
return lines.join("\n");
|
|
333
|
+
}
|
|
334
|
+
async function writeGormFile(models, outPath, enums, indexes, packageName = "model") {
|
|
335
|
+
const mkdirResult = await mkdir(dirname(outPath));
|
|
336
|
+
if (!mkdirResult.ok) return mkdirResult;
|
|
337
|
+
const writeResult = await writeFile(outPath, generateGormModels(models, enums, indexes, packageName));
|
|
338
|
+
if (!writeResult.ok) return writeResult;
|
|
339
|
+
console.log(`wrote ${outPath}`);
|
|
340
|
+
return {
|
|
341
|
+
ok: true,
|
|
342
|
+
value: void 0
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
//#endregion
|
|
347
|
+
//#region src/generator/gorm/index.ts
|
|
348
|
+
const { generatorHandler } = pkg;
|
|
349
|
+
async function main(options) {
|
|
350
|
+
if (!(options.generator.isCustomOutput && options.generator.output?.value)) throw new Error("output is required for Hekireki-GORM. Please specify output in your generator config.");
|
|
351
|
+
const output = options.generator.output.value;
|
|
352
|
+
const resolved = path.extname(output) ? output : path.join(output, "models.go");
|
|
353
|
+
const packageName = getString(options.generator.config.package, "model");
|
|
354
|
+
const enums = options.dmmf.datamodel.enums;
|
|
355
|
+
const indexes = options.dmmf.datamodel.indexes;
|
|
356
|
+
const result = await writeGormFile(options.dmmf.datamodel.models, resolved, enums, indexes, packageName);
|
|
357
|
+
if (!result.ok) throw new Error(`Failed to write GORM models: ${result.error}`);
|
|
358
|
+
}
|
|
359
|
+
generatorHandler({
|
|
360
|
+
onManifest() {
|
|
361
|
+
return {
|
|
362
|
+
defaultOutput: ".",
|
|
363
|
+
prettyName: "Hekireki-GORM"
|
|
364
|
+
};
|
|
365
|
+
},
|
|
366
|
+
onGenerate: main
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
//#endregion
|
|
370
|
+
export { main };
|