hekireki 0.7.1 → 0.7.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/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 for Zod, Valibot, ArkType, Effect Schema, TypeBox, and AJV (JSON Schema), as well as [Drizzle ORM](https://orm.drizzle.team/) schemas and ER diagrams, from [Prisma](https://www.prisma.io/) schemas annotated with comments.
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
- // Ecto Generator
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/)
@@ -7,14 +7,8 @@ async function fmt(input) {
7
7
  singleQuote: true,
8
8
  semi: false
9
9
  });
10
- if (errors.length > 0) return {
11
- ok: false,
12
- error: errors.map((e) => e.message).join("\n")
13
- };
14
- return {
15
- ok: true,
16
- value: code
17
- };
10
+ if (errors.length > 0) throw new Error(errors.map((e) => e.message).join("\n"));
11
+ return code;
18
12
  }
19
13
 
20
14
  //#endregion
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { t as fmt } from "../../format-CzXgkLDe.js";
3
- import { c as parseDocumentWithoutAnnotations, d as mkdir, f as writeFile, s as makeValidationExtractor, t as getBool } from "../../utils-COHZyQue.js";
4
- import { n as validationSchemas, t as makeRelationsOnly } from "../../prisma-ChsFqlYX.js";
2
+ import { t as fmt } from "../../format-DwiYldCm.js";
3
+ import { c as parseDocumentWithoutAnnotations, d as mkdir, f as writeFile, s as makeValidationExtractor, t as getBool } from "../../utils-0tE5jzYR.js";
4
+ import { n as validationSchemas, t as makeRelationsOnly } from "../../prisma-DHllEbMR.js";
5
5
  import path from "node:path";
6
6
  import pkg from "@prisma/generator-helper";
7
7
 
@@ -66,12 +66,9 @@ async function main(options) {
66
66
  file: path.join(output, "index.ts")
67
67
  };
68
68
  const enableRelation = getBool(options.generator.config?.relation);
69
- const fmtResult = await fmt([ajv(options.dmmf.datamodel.models, getBool(options.generator.config?.type), getBool(options.generator.config?.comment), options.dmmf.datamodel.enums), enableRelation ? makeRelationsOnly(options.dmmf, getBool(options.generator.config?.type), makeAjvRelations) : ""].filter(Boolean).join("\n\n"));
70
- if (!fmtResult.ok) throw new Error(`Format error: ${fmtResult.error}`);
71
- const mkdirResult = await mkdir(resolved.dir);
72
- if (!mkdirResult.ok) throw new Error(`Failed to create directory: ${mkdirResult.error}`);
73
- const writeResult = await writeFile(resolved.file, fmtResult.value);
74
- if (!writeResult.ok) throw new Error(`Failed to write file: ${writeResult.error}`);
69
+ const code = await fmt([ajv(options.dmmf.datamodel.models, getBool(options.generator.config?.type), getBool(options.generator.config?.comment), options.dmmf.datamodel.enums), enableRelation ? makeRelationsOnly(options.dmmf, getBool(options.generator.config?.type), makeAjvRelations) : ""].filter(Boolean).join("\n\n"));
70
+ await mkdir(resolved.dir);
71
+ await writeFile(resolved.file, code);
75
72
  }
76
73
  generatorHandler({
77
74
  onManifest() {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { t as fmt } from "../../format-CzXgkLDe.js";
3
- import { c as parseDocumentWithoutAnnotations, d as mkdir, f as writeFile, l as schemaFromFields, s as makeValidationExtractor, t as getBool } from "../../utils-COHZyQue.js";
4
- import { n as validationSchemas, t as makeRelationsOnly } from "../../prisma-ChsFqlYX.js";
2
+ import { t as fmt } from "../../format-DwiYldCm.js";
3
+ import { c as parseDocumentWithoutAnnotations, d as mkdir, f as writeFile, l as schemaFromFields, s as makeValidationExtractor, t as getBool } from "../../utils-0tE5jzYR.js";
4
+ import { n as validationSchemas, t as makeRelationsOnly } from "../../prisma-DHllEbMR.js";
5
5
  import path from "node:path";
6
6
  import pkg from "@prisma/generator-helper";
7
7
 
@@ -68,12 +68,9 @@ async function main(options) {
68
68
  file: path.join(output, "index.ts")
69
69
  };
70
70
  const enableRelation = getBool(options.generator.config?.relation);
71
- const fmtResult = await fmt([arktype(options.dmmf.datamodel.models, getBool(options.generator.config?.type), getBool(options.generator.config?.comment), options.dmmf.datamodel.enums), enableRelation ? makeRelationsOnly(options.dmmf, getBool(options.generator.config?.type), makeArktypeRelations) : ""].filter(Boolean).join("\n\n"));
72
- if (!fmtResult.ok) throw new Error(`Format error: ${fmtResult.error}`);
73
- const mkdirResult = await mkdir(resolved.dir);
74
- if (!mkdirResult.ok) throw new Error(`Failed to create directory: ${mkdirResult.error}`);
75
- const writeResult = await writeFile(resolved.file, fmtResult.value);
76
- if (!writeResult.ok) throw new Error(`Failed to write file: ${writeResult.error}`);
71
+ const code = await fmt([arktype(options.dmmf.datamodel.models, getBool(options.generator.config?.type), getBool(options.generator.config?.comment), options.dmmf.datamodel.enums), enableRelation ? makeRelationsOnly(options.dmmf, getBool(options.generator.config?.type), makeArktypeRelations) : ""].filter(Boolean).join("\n\n"));
72
+ await mkdir(resolved.dir);
73
+ await writeFile(resolved.file, code);
77
74
  }
78
75
  generatorHandler({
79
76
  onManifest() {
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { d as mkdir, f as writeFile, n as getString, p as writeFileBinary, u as stripAnnotations } from "../../utils-COHZyQue.js";
2
+ import { d as mkdir, f as writeFile, n as getString, p as writeFileBinary, u as stripAnnotations } from "../../utils-0tE5jzYR.js";
3
3
  import path from "node:path";
4
4
  import pkg from "@prisma/generator-helper";
5
5
  import { Resvg } from "@resvg/resvg-js";
@@ -129,23 +129,13 @@ function dbmlContent(datamodel, mapToDbSchema = false) {
129
129
  ].join("\n\n");
130
130
  }
131
131
  async function makeDbmlFile(outputDir, content, fileName) {
132
- const writeResult = await writeFile(`${outputDir}/${fileName}`, content);
133
- if (!writeResult.ok) return {
134
- ok: false,
135
- error: `Failed to write DBML: ${writeResult.error}`
136
- };
137
- return { ok: true };
132
+ await writeFile(`${outputDir}/${fileName}`, content);
138
133
  }
139
134
  async function makePng(outputDir, dbml, fileName) {
140
- return makePngFile(`${outputDir}/${fileName}`, dbml);
135
+ await makePngFile(`${outputDir}/${fileName}`, dbml);
141
136
  }
142
137
  async function makePngFile(outputPath, dbml) {
143
- const writeResult = await writeFileBinary(outputPath, new Resvg(run(dbml, "svg"), { font: { loadSystemFonts: true } }).render().asPng());
144
- if (!writeResult.ok) return {
145
- ok: false,
146
- error: `Failed to write PNG: ${writeResult.error}`
147
- };
148
- return { ok: true };
138
+ await writeFileBinary(outputPath, new Resvg(run(dbml, "svg"), { font: { loadSystemFonts: true } }).render().asPng());
149
139
  }
150
140
 
151
141
  //#endregion
@@ -158,15 +148,9 @@ async function main(options) {
158
148
  const mapToDbSchema = getString(config?.mapToDbSchema) !== "false";
159
149
  const content = dbmlContent(options.dmmf.datamodel, mapToDbSchema);
160
150
  if (output.endsWith(".png") || output.endsWith(".dbml")) {
161
- const mkdirResult = await mkdir(path.dirname(output));
162
- if (!mkdirResult.ok) throw new Error(`Failed to create directory: ${mkdirResult.error}`);
163
- if (output.endsWith(".png")) {
164
- const pngResult = await makePngFile(output, content);
165
- if (!pngResult.ok) throw new Error(pngResult.error);
166
- } else {
167
- const dbmlResult = await writeFile(output, content);
168
- if (!dbmlResult.ok) throw new Error(`Failed to write DBML: ${dbmlResult.error}`);
169
- }
151
+ await mkdir(path.dirname(output));
152
+ if (output.endsWith(".png")) await makePngFile(output, content);
153
+ else await writeFile(output, content);
170
154
  } else {
171
155
  const resolved = path.extname(output) ? {
172
156
  dir: path.dirname(output),
@@ -175,16 +159,10 @@ async function main(options) {
175
159
  dir: output,
176
160
  file: path.join(output, "schema.dbml")
177
161
  };
178
- const mkdirResult = await mkdir(resolved.dir);
179
- if (!mkdirResult.ok) throw new Error(`Failed to create directory: ${mkdirResult.error}`);
162
+ await mkdir(resolved.dir);
180
163
  const fileName = path.basename(resolved.file);
181
- if (fileName.endsWith(".png")) {
182
- const pngResult = await makePng(resolved.dir, content, fileName);
183
- if (!pngResult.ok) throw new Error(pngResult.error);
184
- } else {
185
- const dbmlResult = await makeDbmlFile(resolved.dir, content, fileName);
186
- if (!dbmlResult.ok) throw new Error(dbmlResult.error);
187
- }
164
+ if (fileName.endsWith(".png")) await makePng(resolved.dir, content, fileName);
165
+ else await makeDbmlFile(resolved.dir, content, fileName);
188
166
  }
189
167
  }
190
168
  generatorHandler({
@@ -33,21 +33,6 @@ const transformDMMF = (dmmf) => {
33
33
  };
34
34
  };
35
35
 
36
- //#endregion
37
- //#region src/generator/docs/generator/helpers.ts
38
- const capitalize = (str) => str[0].toUpperCase() + str.slice(1);
39
- const lowerCase = (name) => name.substring(0, 1).toLowerCase() + name.substring(1);
40
- const primitiveTypes = [
41
- "String",
42
- "Boolean",
43
- "Int",
44
- "Float",
45
- "Json",
46
- "DateTime",
47
- "Null"
48
- ];
49
- const isScalarType = (type) => primitiveTypes.includes(type);
50
-
51
36
  //#endregion
52
37
  //#region src/generator/docs/styles.ts
53
38
  const globalCss = css`
@@ -76,16 +61,24 @@ const globalCss = css`
76
61
  --code-bg: #374151;
77
62
  --code-color: #e5e7eb;
78
63
  }
79
- * { box-sizing: border-box; }
64
+ * {
65
+ box-sizing: border-box;
66
+ }
80
67
  body {
81
68
  margin: 0;
82
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
69
+ font-family:
70
+ -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
83
71
  background-color: var(--bg-primary);
84
72
  color: var(--text-primary);
85
73
  min-height: 100vh;
86
74
  }
87
- a { color: var(--link-color); text-decoration: none; }
88
- a:hover { text-decoration: underline; }
75
+ a {
76
+ color: var(--link-color);
77
+ text-decoration: none;
78
+ }
79
+ a:hover {
80
+ text-decoration: underline;
81
+ }
89
82
  }
90
83
  `;
91
84
  const containerClass = css`
@@ -218,7 +211,8 @@ const codeBlockClass = css`
218
211
  padding: 1em;
219
212
  border-radius: 0.5em;
220
213
  overflow: auto;
221
- font-family: 'SFMono-Regular', Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
214
+ font-family:
215
+ 'SFMono-Regular', Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
222
216
  font-size: 14px;
223
217
  line-height: 1.5;
224
218
  margin: 0;
@@ -261,6 +255,21 @@ const ml4Class = css`
261
255
  margin-left: 1rem;
262
256
  `;
263
257
 
258
+ //#endregion
259
+ //#region src/generator/docs/generator/helpers.ts
260
+ const capitalize = (str) => str[0].toUpperCase() + str.slice(1);
261
+ const lowerCase = (name) => name.substring(0, 1).toLowerCase() + name.substring(1);
262
+ const primitiveTypes = [
263
+ "String",
264
+ "Boolean",
265
+ "Int",
266
+ "Float",
267
+ "Json",
268
+ "DateTime",
269
+ "Null"
270
+ ];
271
+ const isScalarType = (type) => primitiveTypes.includes(type);
272
+
264
273
  //#endregion
265
274
  //#region src/generator/docs/generator/apitypes.tsx
266
275
  const TypeRefLink = ({ typeRef, kind }) => {
@@ -940,14 +949,14 @@ const MainContent = ({ models, types }) => /* @__PURE__ */ jsxs("div", {
940
949
  class: mainContentClass,
941
950
  children: [models, types]
942
951
  });
943
- const generateHTML = async (data) => {
944
- return "<!DOCTYPE html>" + await (/* @__PURE__ */ jsx(Layout, { children: /* @__PURE__ */ jsxs("div", {
952
+ const generateHTML = (data) => {
953
+ return `<!DOCTYPE html>${(/* @__PURE__ */ jsx(Layout, { children: /* @__PURE__ */ jsxs("div", {
945
954
  class: containerClass,
946
955
  children: [/* @__PURE__ */ jsx(Sidebar, { toc: createTOC(data) }), /* @__PURE__ */ jsx(MainContent, {
947
956
  models: createModels(data),
948
957
  types: createTypes(data)
949
958
  })]
950
- }) })).toString();
959
+ }) })).toString()}`;
951
960
  };
952
961
 
953
962
  //#endregion
@@ -963,7 +972,7 @@ generatorHandler({
963
972
  async onGenerate(options) {
964
973
  if (!options.generator.isCustomOutput || !options.generator.output?.value) throw new Error("output is required for Hekireki-Docs. Please specify output in your generator config.");
965
974
  const output = options.generator.output.value;
966
- const html = await generateHTML(transformDMMF(options.dmmf));
975
+ const html = generateHTML(transformDMMF(options.dmmf));
967
976
  await fs.mkdir(output, { recursive: true });
968
977
  await fs.writeFile(path.join(output, "index.html"), html);
969
978
  }
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { t as fmt } from "../../format-CzXgkLDe.js";
3
- import { d as mkdir, f as writeFile, o as makeSnakeCase } from "../../utils-COHZyQue.js";
2
+ import { t as fmt } from "../../format-DwiYldCm.js";
3
+ import { d as mkdir, f as writeFile, o as makeSnakeCase } from "../../utils-0tE5jzYR.js";
4
4
  import path from "node:path";
5
5
  import pkg from "@prisma/generator-helper";
6
6
 
@@ -23,7 +23,7 @@ const MYSQL_SCALAR_MAP = {
23
23
  Float: "double()",
24
24
  Decimal: "decimal()",
25
25
  Boolean: "boolean()",
26
- DateTime: "datetime()",
26
+ DateTime: "datetime({ fsp: 3 })",
27
27
  Json: "json()",
28
28
  Bytes: "binary()"
29
29
  };
@@ -34,7 +34,7 @@ const SQLITE_SCALAR_MAP = {
34
34
  Float: "real()",
35
35
  Decimal: "numeric()",
36
36
  Boolean: "integer({ mode: 'boolean' })",
37
- DateTime: "integer({ mode: 'timestamp' })",
37
+ DateTime: "integer({ mode: 'timestamp_ms' })",
38
38
  Json: "text({ mode: 'json' })",
39
39
  Bytes: "blob()"
40
40
  };
@@ -88,8 +88,8 @@ function mysqlNativeType(name, args) {
88
88
  }
89
89
  case "Date": return "date()";
90
90
  case "Time": return args[0] ? `time({ fsp: ${args[0]} })` : "time()";
91
- case "DateTime": return args[0] ? `datetime({ fsp: ${args[0]} })` : "datetime()";
92
- case "Timestamp": return args[0] ? `timestamp({ fsp: ${args[0]} })` : "timestamp()";
91
+ case "DateTime": return `datetime({ fsp: ${args[0] ?? 3} })`;
92
+ case "Timestamp": return `timestamp({ fsp: ${args[0] ?? 3} })`;
93
93
  case "Json": return "json()";
94
94
  case "Binary": return args[0] ? `binary({ length: ${args[0]} })` : "binary()";
95
95
  case "VarBinary": return args[0] ? `varbinary({ length: ${args[0]} })` : "varbinary()";
@@ -169,36 +169,117 @@ function makeColumnExpr(field, provider, imports, enums) {
169
169
  const rest = baseExpr.slice(parenIdx + 1);
170
170
  return rest === ")" ? `${baseFnName}('${colName}')` : `${baseFnName}('${colName}', ${rest}`;
171
171
  }
172
- function makeDefaultChain(dflt, fieldType, provider, imports) {
173
- if (dflt === void 0 || dflt === null) return "";
172
+ function resolveDefaultValue(dflt, fieldType, provider) {
173
+ if (dflt === void 0 || dflt === null) return {
174
+ chain: "",
175
+ needsSql: false,
176
+ needsCuid: false
177
+ };
174
178
  if (isFieldDefault(dflt)) switch (dflt.name) {
175
- case "autoincrement": return "";
179
+ case "autoincrement": return {
180
+ chain: "",
181
+ needsSql: false,
182
+ needsCuid: false
183
+ };
176
184
  case "now":
177
- if (provider === "sqlite") {
178
- imports.addOrm("sql");
179
- return ".default(sql`(unixepoch())`)";
180
- }
181
- if (provider === "mysql") {
182
- imports.addOrm("sql");
183
- return ".default(sql`now()`)";
184
- }
185
- return ".defaultNow()";
186
- case "uuid": return ".$defaultFn(() => crypto.randomUUID())";
187
- case "cuid":
188
- imports.addExternal("@paralleldrive/cuid2", "createId");
189
- return ".$defaultFn(() => createId())";
185
+ if (provider === "sqlite") return {
186
+ chain: ".default(sql`(unixepoch() * 1000)`)",
187
+ needsSql: true,
188
+ needsCuid: false
189
+ };
190
+ if (provider === "mysql") return {
191
+ chain: ".default(sql`CURRENT_TIMESTAMP(3)`)",
192
+ needsSql: true,
193
+ needsCuid: false
194
+ };
195
+ return {
196
+ chain: ".defaultNow()",
197
+ needsSql: false,
198
+ needsCuid: false
199
+ };
200
+ case "uuid": return {
201
+ chain: ".$defaultFn(() => crypto.randomUUID())",
202
+ needsSql: false,
203
+ needsCuid: false
204
+ };
205
+ case "cuid": return {
206
+ chain: ".$defaultFn(() => createId())",
207
+ needsSql: false,
208
+ needsCuid: true
209
+ };
190
210
  case "dbgenerated":
191
- if (typeof dflt.args[0] === "string") {
192
- imports.addOrm("sql");
193
- return `.default(sql\`${dflt.args[0]}\`)`;
194
- }
195
- return "";
196
- default: return "";
211
+ if (typeof dflt.args[0] === "string") return {
212
+ chain: `.default(sql\`${dflt.args[0]}\`)`,
213
+ needsSql: true,
214
+ needsCuid: false
215
+ };
216
+ return {
217
+ chain: "",
218
+ needsSql: false,
219
+ needsCuid: false
220
+ };
221
+ default: return {
222
+ chain: "",
223
+ needsSql: false,
224
+ needsCuid: false
225
+ };
197
226
  }
198
- if (typeof dflt === "string") return `.default('${dflt}')`;
199
- if (typeof dflt === "number") return fieldType === "Decimal" ? `.default('${dflt}')` : `.default(${dflt})`;
200
- if (typeof dflt === "boolean") return `.default(${dflt})`;
201
- return "";
227
+ if (typeof dflt === "string") return {
228
+ chain: `.default('${dflt}')`,
229
+ needsSql: false,
230
+ needsCuid: false
231
+ };
232
+ if (typeof dflt === "number") return {
233
+ chain: fieldType === "Decimal" ? `.default('${dflt}')` : `.default(${dflt})`,
234
+ needsSql: false,
235
+ needsCuid: false
236
+ };
237
+ if (typeof dflt === "boolean") return {
238
+ chain: `.default(${dflt})`,
239
+ needsSql: false,
240
+ needsCuid: false
241
+ };
242
+ return {
243
+ chain: "",
244
+ needsSql: false,
245
+ needsCuid: false
246
+ };
247
+ }
248
+ function resolveUpdatedAtDefault(provider) {
249
+ if (provider === "sqlite") return {
250
+ chain: ".default(sql`(unixepoch() * 1000)`)",
251
+ needsSql: true
252
+ };
253
+ if (provider === "mysql") return {
254
+ chain: ".default(sql`CURRENT_TIMESTAMP(3)`)",
255
+ needsSql: true
256
+ };
257
+ return {
258
+ chain: ".defaultNow()",
259
+ needsSql: false
260
+ };
261
+ }
262
+ function makeDefaultChain(dflt, fieldType, provider, imports) {
263
+ const result = resolveDefaultValue(dflt, fieldType, provider);
264
+ if (result.needsSql) imports.addOrm("sql");
265
+ if (result.needsCuid) imports.addExternal("@paralleldrive/cuid2", "createId");
266
+ return result.chain;
267
+ }
268
+ const PRISMA_ACTION_MAP = {
269
+ Cascade: "cascade",
270
+ SetNull: "set null",
271
+ Restrict: "restrict",
272
+ NoAction: "no action",
273
+ SetDefault: "set default"
274
+ };
275
+ function makeFkReference(field, model) {
276
+ const relField = model.fields.find((f) => f.kind === "object" && f.relationFromFields && f.relationFromFields.includes(field.name));
277
+ if (!(relField?.relationFromFields && relField.relationToFields)) return "";
278
+ const targetVar = toCamelCase(relField.type);
279
+ const toCol = relField.relationToFields[0] ?? "id";
280
+ const onDelete = relField.relationOnDelete;
281
+ const drizzleAction = onDelete ? PRISMA_ACTION_MAP[onDelete] : void 0;
282
+ return `.references(() => ${targetVar}.${toCol}${drizzleAction ? `, { onDelete: '${drizzleAction}' }` : ""})`;
202
283
  }
203
284
  function makeColumn(field, model, provider, imports, enums) {
204
285
  if (field.kind === "object") return null;
@@ -210,24 +291,29 @@ function makeColumn(field, model, provider, imports, enums) {
210
291
  field.isId && !hasCompositePK ? isAutoincrement && provider === "sqlite" ? ".primaryKey({ autoIncrement: true })" : ".primaryKey()" : "",
211
292
  field.isRequired && !field.isId && !(isAutoincrement && provider === "postgresql") ? ".notNull()" : "",
212
293
  field.isUnique ? ".unique()" : "",
213
- isAutoincrement ? provider === "mysql" ? ".autoincrement()" : "" : makeDefaultChain(field.default, field.type, provider, imports),
294
+ makeFkReference(field, model),
295
+ isAutoincrement ? provider === "mysql" ? ".autoincrement()" : "" : field.isUpdatedAt && (field.default === void 0 || field.default === null) ? (() => {
296
+ const r = resolveUpdatedAtDefault(provider);
297
+ if (r.needsSql) imports.addOrm("sql");
298
+ return r.chain;
299
+ })() : makeDefaultChain(field.default, field.type, provider, imports),
214
300
  field.isUpdatedAt ? ".$onUpdate(() => new Date())" : "",
215
301
  field.isList && field.kind === "scalar" && provider === "postgresql" ? ".array()" : ""
216
302
  ].join("");
217
303
  return `${field.name}: ${colExpr}${chain}`;
218
304
  }
219
- function makeCompositeConstraints(model, imports, indexes) {
305
+ function makeCompositeConstraints(model, imports, indexes, tableName) {
220
306
  const pkLine = model.primaryKey ? (() => {
221
307
  imports.addCore("primaryKey");
222
308
  return `primaryKey({ columns: [${model.primaryKey.fields.map((f) => `table.${f}`).join(", ")}] })`;
223
309
  })() : null;
224
- const uniqueLines = model.uniqueFields.map((fields) => {
310
+ const uniqueLines = model.uniqueFields.filter((fields) => fields.length > 1).map((fields) => {
225
311
  imports.addCore("unique");
226
312
  return `unique().on(${fields.map((f) => `table.${f}`).join(", ")})`;
227
313
  });
228
314
  const indexLines = indexes.filter((idx) => idx.model === model.name && (idx.type === "normal" || idx.type === "fulltext")).map((idx) => {
229
315
  imports.addCore("index");
230
- return `index('${idx.dbName ?? idx.name ?? `idx_${idx.fields.map((f) => f.name).join("_")}`}').on(${idx.fields.map((f) => `table.${f.name}`).join(", ")})`;
316
+ return `index('${idx.dbName ?? idx.name ?? `idx_${tableName}_${idx.fields.map((f) => f.name).join("_")}`}').on(${idx.fields.map((f) => `table.${f.name}`).join(", ")})`;
231
317
  });
232
318
  const all = [
233
319
  pkLine,
@@ -242,7 +328,7 @@ function makeTable(model, provider, imports, enums, indexes) {
242
328
  const varName = toCamelCase(model.name);
243
329
  const tableName = model.dbName ?? makeSnakeCase(model.name);
244
330
  const columns = model.fields.map((field) => makeColumn(field, model, provider, imports, enums)).filter((c) => c !== null).join(", ");
245
- const constraints = makeCompositeConstraints(model, imports, indexes);
331
+ const constraints = makeCompositeConstraints(model, imports, indexes, tableName);
246
332
  return constraints ? `export const ${varName} = ${tableFunc}('${tableName}', { ${columns} }, (table) => [${constraints}])` : `export const ${varName} = ${tableFunc}('${tableName}', { ${columns} })`;
247
333
  }
248
334
  function makeRelationField(field, model, _models, relFields) {
@@ -312,12 +398,9 @@ async function main(options) {
312
398
  file: path.join(output, "schema.ts")
313
399
  };
314
400
  const provider = options.datasources[0]?.activeProvider ?? "postgresql";
315
- const fmtResult = await fmt(drizzleSchema(options.dmmf.datamodel, provider, options.dmmf.datamodel.indexes));
316
- if (!fmtResult.ok) throw new Error(`Format error: ${fmtResult.error}`);
317
- const mkdirResult = await mkdir(resolved.dir);
318
- if (!mkdirResult.ok) throw new Error(`Failed to create directory: ${mkdirResult.error}`);
319
- const writeResult = await writeFile(resolved.file, fmtResult.value);
320
- if (!writeResult.ok) throw new Error(`Failed to write file: ${writeResult.error}`);
401
+ const formatted = await fmt(drizzleSchema(options.dmmf.datamodel, provider, options.dmmf.datamodel.indexes));
402
+ await mkdir(resolved.dir);
403
+ await writeFile(resolved.file, formatted);
321
404
  }
322
405
  generatorHandler({
323
406
  onManifest() {