hekireki 0.7.0 → 0.7.1
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 +149 -14
- package/dist/generator/ajv/index.d.ts +6 -0
- package/dist/generator/ajv/index.js +87 -0
- package/dist/generator/arktype/index.js +17 -3
- package/dist/generator/dbml/index.js +43 -33
- package/dist/generator/drizzle/index.js +15 -24
- package/dist/generator/ecto/index.js +34 -9
- package/dist/generator/effect/index.js +17 -3
- package/dist/generator/mermaid-er/index.js +6 -12
- package/dist/generator/typebox/index.d.ts +6 -0
- package/dist/generator/typebox/index.js +93 -0
- package/dist/generator/valibot/index.js +15 -5
- package/dist/generator/zod/index.js +15 -5
- package/dist/{prisma-Cc0YxSiO.js → prisma-ChsFqlYX.js} +6 -6
- package/dist/utils-COHZyQue.js +116 -0
- package/package.json +10 -2
- package/dist/utils-DeZn2r_T.js +0 -287
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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, and
|
|
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.
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
- 🤖 Automatically generates [Valibot](https://valibot.dev/) schemas from your Prisma schema
|
|
11
11
|
- 🏹 Automatically generates [ArkType](https://arktype.io/) schemas from your Prisma schema
|
|
12
12
|
- ⚡ Automatically generates [Effect Schema](https://effect.website/docs/schema/introduction/) from your Prisma schema
|
|
13
|
+
- 📦 Automatically generates [TypeBox](https://github.com/sinclairzx81/typebox) schemas from your Prisma schema
|
|
14
|
+
- 📋 Automatically generates [AJV](https://ajv.js.org/)-compatible JSON Schema objects from your Prisma schema
|
|
13
15
|
- 🗄️ Automatically generates [Drizzle ORM](https://orm.drizzle.team/) table schemas and relations from your Prisma schema
|
|
14
16
|
- 📊 Creates [Mermaid](https://mermaid.js.org/) ER diagrams with PK/FK markers
|
|
15
17
|
- 📝 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`)
|
|
@@ -62,6 +64,20 @@ generator Hekireki-Effect {
|
|
|
62
64
|
relation = true
|
|
63
65
|
}
|
|
64
66
|
|
|
67
|
+
generator Hekireki-TypeBox {
|
|
68
|
+
provider = "hekireki-typebox"
|
|
69
|
+
type = true
|
|
70
|
+
comment = true
|
|
71
|
+
relation = true
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
generator Hekireki-AJV {
|
|
75
|
+
provider = "hekireki-ajv"
|
|
76
|
+
type = true
|
|
77
|
+
comment = true
|
|
78
|
+
relation = true
|
|
79
|
+
}
|
|
80
|
+
|
|
65
81
|
generator Hekireki-Drizzle {
|
|
66
82
|
provider = "hekireki-drizzle"
|
|
67
83
|
}
|
|
@@ -88,12 +104,16 @@ model User {
|
|
|
88
104
|
/// @v.pipe(v.string(), v.uuid())
|
|
89
105
|
/// @a."string.uuid"
|
|
90
106
|
/// @e.Schema.UUID
|
|
107
|
+
/// @t.Type.String({ format: 'uuid' })
|
|
108
|
+
/// @j.{ type: 'string' as const, format: 'uuid' as const }
|
|
91
109
|
id String @id @default(uuid())
|
|
92
110
|
/// Display name
|
|
93
111
|
/// @z.string().min(1).max(50)
|
|
94
112
|
/// @v.pipe(v.string(), v.minLength(1), v.maxLength(50))
|
|
95
113
|
/// @a."1 <= string <= 50"
|
|
96
114
|
/// @e.Schema.String.pipe(Schema.minLength(1), Schema.maxLength(50))
|
|
115
|
+
/// @t.Type.String({ minLength: 1, maxLength: 50 })
|
|
116
|
+
/// @j.{ type: 'string' as const, minLength: 1, maxLength: 50 }
|
|
97
117
|
name String
|
|
98
118
|
/// One-to-many relation to Post
|
|
99
119
|
posts Post[]
|
|
@@ -105,24 +125,32 @@ model Post {
|
|
|
105
125
|
/// @v.pipe(v.string(), v.uuid())
|
|
106
126
|
/// @a."string.uuid"
|
|
107
127
|
/// @e.Schema.UUID
|
|
128
|
+
/// @t.Type.String({ format: 'uuid' })
|
|
129
|
+
/// @j.{ type: 'string' as const, format: 'uuid' as const }
|
|
108
130
|
id String @id @default(uuid())
|
|
109
131
|
/// Article title
|
|
110
132
|
/// @z.string().min(1).max(100)
|
|
111
133
|
/// @v.pipe(v.string(), v.minLength(1), v.maxLength(100))
|
|
112
134
|
/// @a."1 <= string <= 100"
|
|
113
135
|
/// @e.Schema.String.pipe(Schema.minLength(1), Schema.maxLength(100))
|
|
136
|
+
/// @t.Type.String({ minLength: 1, maxLength: 100 })
|
|
137
|
+
/// @j.{ type: 'string' as const, minLength: 1, maxLength: 100 }
|
|
114
138
|
title String
|
|
115
139
|
/// Body content (no length limit)
|
|
116
140
|
/// @z.string()
|
|
117
141
|
/// @v.string()
|
|
118
142
|
/// @a."string"
|
|
119
143
|
/// @e.Schema.String
|
|
144
|
+
/// @t.Type.String()
|
|
145
|
+
/// @j.{ type: 'string' as const }
|
|
120
146
|
content String
|
|
121
147
|
/// Foreign key referencing User.id
|
|
122
148
|
/// @z.uuid()
|
|
123
149
|
/// @v.pipe(v.string(), v.uuid())
|
|
124
150
|
/// @a."string.uuid"
|
|
125
151
|
/// @e.Schema.UUID
|
|
152
|
+
/// @t.Type.String({ format: 'uuid' })
|
|
153
|
+
/// @j.{ type: 'string' as const, format: 'uuid' as const }
|
|
126
154
|
userId String
|
|
127
155
|
/// Prisma relation definition
|
|
128
156
|
user User @relation(fields: [userId], references: [id])
|
|
@@ -295,6 +323,108 @@ export const PostSchema = Schema.Struct({
|
|
|
295
323
|
export type Post = Schema.Schema.Type<typeof PostSchema>
|
|
296
324
|
```
|
|
297
325
|
|
|
326
|
+
### TypeBox
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
import { type Static, Type } from '@sinclair/typebox'
|
|
330
|
+
|
|
331
|
+
export const UserSchema = Type.Object({
|
|
332
|
+
/** Primary key */
|
|
333
|
+
id: Type.String({ format: 'uuid' }),
|
|
334
|
+
/** Display name */
|
|
335
|
+
name: Type.String({ minLength: 1, maxLength: 50 }),
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
export type User = Static<typeof UserSchema>
|
|
339
|
+
|
|
340
|
+
export const PostSchema = Type.Object({
|
|
341
|
+
/** Primary key */
|
|
342
|
+
id: Type.String({ format: 'uuid' }),
|
|
343
|
+
/** Article title */
|
|
344
|
+
title: Type.String({ minLength: 1, maxLength: 100 }),
|
|
345
|
+
/** Body content (no length limit) */
|
|
346
|
+
content: Type.String(),
|
|
347
|
+
/** Foreign key referencing User.id */
|
|
348
|
+
userId: Type.String({ format: 'uuid' }),
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
export type Post = Static<typeof PostSchema>
|
|
352
|
+
|
|
353
|
+
export const UserRelationsSchema = Type.Object({
|
|
354
|
+
...UserSchema.properties,
|
|
355
|
+
posts: Type.Array(PostSchema),
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
export type UserRelations = Static<typeof UserRelationsSchema>
|
|
359
|
+
|
|
360
|
+
export const PostRelationsSchema = Type.Object({
|
|
361
|
+
...PostSchema.properties,
|
|
362
|
+
user: UserSchema,
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
export type PostRelations = Static<typeof PostRelationsSchema>
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### AJV (JSON Schema)
|
|
369
|
+
|
|
370
|
+
```ts
|
|
371
|
+
import type { FromSchema } from 'json-schema-to-ts'
|
|
372
|
+
|
|
373
|
+
export const UserSchema = {
|
|
374
|
+
type: 'object' as const,
|
|
375
|
+
properties: {
|
|
376
|
+
/** Primary key */
|
|
377
|
+
id: { type: 'string' as const, format: 'uuid' as const },
|
|
378
|
+
/** Display name */
|
|
379
|
+
name: { type: 'string' as const, minLength: 1, maxLength: 50 },
|
|
380
|
+
},
|
|
381
|
+
required: ['id', 'name'] as const,
|
|
382
|
+
additionalProperties: false,
|
|
383
|
+
} as const
|
|
384
|
+
|
|
385
|
+
export type User = FromSchema<typeof UserSchema>
|
|
386
|
+
|
|
387
|
+
export const PostSchema = {
|
|
388
|
+
type: 'object' as const,
|
|
389
|
+
properties: {
|
|
390
|
+
/** Primary key */
|
|
391
|
+
id: { type: 'string' as const, format: 'uuid' as const },
|
|
392
|
+
/** Article title */
|
|
393
|
+
title: { type: 'string' as const, minLength: 1, maxLength: 100 },
|
|
394
|
+
/** Body content (no length limit) */
|
|
395
|
+
content: { type: 'string' as const },
|
|
396
|
+
/** Foreign key referencing User.id */
|
|
397
|
+
userId: { type: 'string' as const, format: 'uuid' as const },
|
|
398
|
+
},
|
|
399
|
+
required: ['id', 'title', 'content', 'userId'] as const,
|
|
400
|
+
additionalProperties: false,
|
|
401
|
+
} as const
|
|
402
|
+
|
|
403
|
+
export type Post = FromSchema<typeof PostSchema>
|
|
404
|
+
|
|
405
|
+
export const UserRelationsSchema = {
|
|
406
|
+
type: 'object' as const,
|
|
407
|
+
properties: {
|
|
408
|
+
...UserSchema.properties,
|
|
409
|
+
posts: { type: 'array' as const, items: PostSchema },
|
|
410
|
+
},
|
|
411
|
+
additionalProperties: false,
|
|
412
|
+
} as const
|
|
413
|
+
|
|
414
|
+
export type UserRelations = FromSchema<typeof UserRelationsSchema>
|
|
415
|
+
|
|
416
|
+
export const PostRelationsSchema = {
|
|
417
|
+
type: 'object' as const,
|
|
418
|
+
properties: {
|
|
419
|
+
...PostSchema.properties,
|
|
420
|
+
user: UserSchema,
|
|
421
|
+
},
|
|
422
|
+
additionalProperties: false,
|
|
423
|
+
} as const
|
|
424
|
+
|
|
425
|
+
export type PostRelations = FromSchema<typeof PostRelationsSchema>
|
|
426
|
+
```
|
|
427
|
+
|
|
298
428
|
### Drizzle
|
|
299
429
|
|
|
300
430
|
```ts
|
|
@@ -390,19 +520,6 @@ defmodule DBSchema.Post do
|
|
|
390
520
|
end
|
|
391
521
|
```
|
|
392
522
|
|
|
393
|
-
**Supported features:**
|
|
394
|
-
|
|
395
|
-
| Feature | Details |
|
|
396
|
-
|---|---|
|
|
397
|
-
| Primary keys | UUID (`@default(uuid())`), autoincrement (`@default(autoincrement())`), CUID, composite (`@@id`) |
|
|
398
|
-
| Associations | `belongs_to`, `has_many`, `has_one` with correct FK types |
|
|
399
|
-
| Timestamps | `timestamps()` with `inserted_at_source`/`updated_at_source` for `@map`/camelCase fields |
|
|
400
|
-
| Enums | `Ecto.Enum` with `values: [...]` |
|
|
401
|
-
| Array fields | `{:array, :type}` for Prisma list scalars (`String[]`, `Int[]`, etc.) |
|
|
402
|
-
| Name mapping | `@@map` → schema table name, `@map` → field `source:` option |
|
|
403
|
-
| Typespecs | `@type t :: %__MODULE__{...}` auto-generated for Dialyzer |
|
|
404
|
-
| `@moduledoc` | Uses `///` model documentation, or `false` when absent |
|
|
405
|
-
|
|
406
523
|
### DBML
|
|
407
524
|
|
|
408
525
|
```dbml
|
|
@@ -487,6 +604,24 @@ generator Hekireki-Effect {
|
|
|
487
604
|
relation = true // Generate relation schemas (default: false)
|
|
488
605
|
}
|
|
489
606
|
|
|
607
|
+
// TypeBox Generator
|
|
608
|
+
generator Hekireki-TypeBox {
|
|
609
|
+
provider = "hekireki-typebox"
|
|
610
|
+
output = "./typebox" // Output path (default: ./typebox/index.ts)
|
|
611
|
+
type = true // Generate TypeScript types (default: false)
|
|
612
|
+
comment = true // Include schema documentation (default: false)
|
|
613
|
+
relation = true // Generate relation schemas (default: false)
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// AJV (JSON Schema) Generator
|
|
617
|
+
generator Hekireki-AJV {
|
|
618
|
+
provider = "hekireki-ajv"
|
|
619
|
+
output = "./ajv" // Output path (default: ./ajv/index.ts)
|
|
620
|
+
type = true // Generate TypeScript types (default: false)
|
|
621
|
+
comment = true // Include schema documentation (default: false)
|
|
622
|
+
relation = true // Generate relation schemas (default: false)
|
|
623
|
+
}
|
|
624
|
+
|
|
490
625
|
// Drizzle ORM Schema Generator
|
|
491
626
|
generator Hekireki-Drizzle {
|
|
492
627
|
provider = "hekireki-drizzle"
|
|
@@ -0,0 +1,87 @@
|
|
|
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";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import pkg from "@prisma/generator-helper";
|
|
7
|
+
|
|
8
|
+
//#region src/helper/ajv.ts
|
|
9
|
+
function makeAjvInfer(modelName) {
|
|
10
|
+
return `export type ${modelName} = FromSchema<typeof ${modelName}Schema>`;
|
|
11
|
+
}
|
|
12
|
+
function makeAjvEnumExpression(values) {
|
|
13
|
+
return `{ enum: [${values.map((v) => `'${v}'`).join(", ")}] as const }`;
|
|
14
|
+
}
|
|
15
|
+
const PRISMA_TO_AJV = {
|
|
16
|
+
String: "{ type: 'string' as const }",
|
|
17
|
+
Int: "{ type: 'integer' as const }",
|
|
18
|
+
Float: "{ type: 'number' as const }",
|
|
19
|
+
Boolean: "{ type: 'boolean' as const }",
|
|
20
|
+
DateTime: "{ type: 'string' as const, format: 'date-time' as const }",
|
|
21
|
+
BigInt: "{ type: 'integer' as const }",
|
|
22
|
+
Decimal: "{ type: 'number' as const }",
|
|
23
|
+
Json: "{}",
|
|
24
|
+
Bytes: "{ type: 'string' as const }"
|
|
25
|
+
};
|
|
26
|
+
function makeAjvSchemas(modelFields, comment) {
|
|
27
|
+
const modelName = modelFields[0].modelName;
|
|
28
|
+
const properties = modelFields.map((field) => {
|
|
29
|
+
return `${comment && field.comment.length > 0 ? `${field.comment.map((c) => ` /** ${c} */`).join("\n")}\n` : ""} ${field.fieldName}: ${field.validation ?? "{ type: 'unknown' as const }"},`;
|
|
30
|
+
}).join("\n");
|
|
31
|
+
const requiredFields = modelFields.filter((f) => f.isRequired).map((f) => f.fieldName);
|
|
32
|
+
return `export const ${modelName}Schema = {\n type: 'object' as const,\n properties: {\n${properties}\n },${requiredFields.length > 0 ? `\n required: [${requiredFields.map((f) => `'${f}'`).join(", ")}] as const,` : ""}\n additionalProperties: false,\n} as const`;
|
|
33
|
+
}
|
|
34
|
+
function makeAjvRelations(model, relProps, options) {
|
|
35
|
+
if (relProps.length === 0) return null;
|
|
36
|
+
const base = ` ...${model.name}Schema.properties,`;
|
|
37
|
+
const rels = relProps.map((r) => ` ${r.key}: ${r.isMany ? `{ type: 'array' as const, items: ${r.targetModel}Schema }` : `${r.targetModel}Schema`},`).join("\n");
|
|
38
|
+
const typeLine = options?.includeType ? `\n\nexport type ${model.name}Relations = FromSchema<typeof ${model.name}RelationsSchema>` : "";
|
|
39
|
+
return `export const ${model.name}RelationsSchema = {\n type: 'object' as const,\n properties: {\n${base}\n${rels}\n },\n additionalProperties: false,\n} as const${typeLine}`;
|
|
40
|
+
}
|
|
41
|
+
function ajv(models, type, comment, enums) {
|
|
42
|
+
return validationSchemas(models, type, comment, {
|
|
43
|
+
importStatement: type ? `import type { FromSchema } from 'json-schema-to-ts'` : "",
|
|
44
|
+
annotationPrefix: "@j.",
|
|
45
|
+
parseDocument: parseDocumentWithoutAnnotations,
|
|
46
|
+
extractValidation: makeValidationExtractor("@j."),
|
|
47
|
+
inferType: makeAjvInfer,
|
|
48
|
+
schemas: makeAjvSchemas,
|
|
49
|
+
typeMapping: PRISMA_TO_AJV,
|
|
50
|
+
enums,
|
|
51
|
+
formatEnum: makeAjvEnumExpression
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region src/generator/ajv/index.ts
|
|
57
|
+
const { generatorHandler } = pkg;
|
|
58
|
+
async function main(options) {
|
|
59
|
+
if (!(options.generator.isCustomOutput && options.generator.output?.value)) throw new Error("output is required for Hekireki-AJV. Please specify output in your generator config.");
|
|
60
|
+
const output = options.generator.output.value;
|
|
61
|
+
const resolved = path.extname(output) ? {
|
|
62
|
+
dir: path.dirname(output),
|
|
63
|
+
file: output
|
|
64
|
+
} : {
|
|
65
|
+
dir: output,
|
|
66
|
+
file: path.join(output, "index.ts")
|
|
67
|
+
};
|
|
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}`);
|
|
75
|
+
}
|
|
76
|
+
generatorHandler({
|
|
77
|
+
onManifest() {
|
|
78
|
+
return {
|
|
79
|
+
defaultOutput: ".",
|
|
80
|
+
prettyName: "Hekireki-AJV"
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
onGenerate: main
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
//#endregion
|
|
87
|
+
export { main };
|
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { t as fmt } from "../../format-CzXgkLDe.js";
|
|
3
|
-
import {
|
|
4
|
-
import { n as validationSchemas, t as makeRelationsOnly } from "../../prisma-
|
|
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";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import pkg from "@prisma/generator-helper";
|
|
7
7
|
|
|
8
8
|
//#region src/helper/arktype.ts
|
|
9
|
+
function makeArktypeInfer(modelName) {
|
|
10
|
+
return `export type ${modelName} = typeof ${modelName}Schema.infer`;
|
|
11
|
+
}
|
|
12
|
+
function makeArktypeSchema(modelName, fields) {
|
|
13
|
+
return `export const ${modelName}Schema = type({\n${fields}\n})`;
|
|
14
|
+
}
|
|
15
|
+
function makeArktypeProperties(fields, comment) {
|
|
16
|
+
return fields.map((field) => {
|
|
17
|
+
return `${comment && field.comment.length > 0 ? `${field.comment.map((c) => ` /** ${c} */`).join("\n")}\n` : ""} ${field.fieldName}: ${field.validation ?? "\"unknown\""},`;
|
|
18
|
+
}).join("\n");
|
|
19
|
+
}
|
|
20
|
+
function makeArktypeEnumExpression(values) {
|
|
21
|
+
return `"${values.map((v) => `'${v}'`).join(" | ")}"`;
|
|
22
|
+
}
|
|
9
23
|
const PRISMA_TO_ARKTYPE = {
|
|
10
24
|
String: "\"string\"",
|
|
11
25
|
Int: "\"number\"",
|
|
@@ -53,7 +67,7 @@ async function main(options) {
|
|
|
53
67
|
dir: output,
|
|
54
68
|
file: path.join(output, "index.ts")
|
|
55
69
|
};
|
|
56
|
-
const enableRelation =
|
|
70
|
+
const enableRelation = getBool(options.generator.config?.relation);
|
|
57
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"));
|
|
58
72
|
if (!fmtResult.ok) throw new Error(`Format error: ${fmtResult.error}`);
|
|
59
73
|
const mkdirResult = await mkdir(resolved.dir);
|
|
@@ -1,11 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { d as mkdir, f as writeFile, n as getString, p as writeFileBinary, u as stripAnnotations } from "../../utils-COHZyQue.js";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import pkg from "@prisma/generator-helper";
|
|
5
5
|
import { Resvg } from "@resvg/resvg-js";
|
|
6
6
|
import { run } from "@softwaretechnik/dbml-renderer";
|
|
7
7
|
|
|
8
8
|
//#region src/helper/dbml.ts
|
|
9
|
+
function escapeNote(str) {
|
|
10
|
+
return str.replace(/'/g, "\\'");
|
|
11
|
+
}
|
|
12
|
+
function formatConstraints(constraints) {
|
|
13
|
+
return constraints.length > 0 ? ` [${constraints.join(", ")}]` : "";
|
|
14
|
+
}
|
|
15
|
+
function makeEnum(enumDef) {
|
|
16
|
+
return [
|
|
17
|
+
`Enum ${enumDef.name} {`,
|
|
18
|
+
...enumDef.values.map((v) => ` ${v}`),
|
|
19
|
+
"}"
|
|
20
|
+
].join("\n");
|
|
21
|
+
}
|
|
22
|
+
function makeRefName(ref) {
|
|
23
|
+
return ref.name ?? `${ref.fromTable}_${ref.fromColumn}_${ref.toTable}_${ref.toColumn}_fk`;
|
|
24
|
+
}
|
|
25
|
+
function combineKeys(keys) {
|
|
26
|
+
return keys.length > 1 ? `(${keys.join(", ")})` : keys[0];
|
|
27
|
+
}
|
|
9
28
|
function quote(value) {
|
|
10
29
|
return `'${escapeNote(value)}'`;
|
|
11
30
|
}
|
|
@@ -34,43 +53,37 @@ function makePrismaColumn(column) {
|
|
|
34
53
|
].filter((c) => Boolean(c));
|
|
35
54
|
return ` ${column.name} ${column.type}${formatConstraints(constraints)}`;
|
|
36
55
|
}
|
|
37
|
-
function resolveFieldType(field, models, mapToDbSchema) {
|
|
38
|
-
const baseType = mapToDbSchema ? models.find((m) => m.name === field.type)?.dbName ?? field.type : field.type;
|
|
39
|
-
return field.isList && !field.relationName ? `${baseType}[]` : baseType;
|
|
40
|
-
}
|
|
41
|
-
function resolveDefaultValue(field) {
|
|
42
|
-
const defaultDef = field.default;
|
|
43
|
-
if (defaultDef?.name === "autoincrement") return void 0;
|
|
44
|
-
if (defaultDef?.name === "now") return "`now()`";
|
|
45
|
-
if (field.hasDefaultValue && typeof field.default !== "object") return field.type === "String" || field.type === "Json" || field.kind === "enum" ? `'${field.default}'` : String(field.default);
|
|
46
|
-
}
|
|
47
56
|
function toDBMLColumn(field, models, mapToDbSchema) {
|
|
48
57
|
const defaultDef = field.default;
|
|
58
|
+
const baseType = mapToDbSchema ? models.find((m) => m.name === field.type)?.dbName ?? field.type : field.type;
|
|
59
|
+
const type = field.isList && !field.relationName ? `${baseType}[]` : baseType;
|
|
60
|
+
const defaultValue = (() => {
|
|
61
|
+
if (defaultDef?.name === "autoincrement") return void 0;
|
|
62
|
+
if (defaultDef?.name === "now") return "`now()`";
|
|
63
|
+
if (field.hasDefaultValue && typeof field.default !== "object") return field.type === "String" || field.type === "Json" || field.kind === "enum" ? `'${field.default}'` : String(field.default);
|
|
64
|
+
})();
|
|
49
65
|
return {
|
|
50
66
|
name: field.name,
|
|
51
|
-
type
|
|
67
|
+
type,
|
|
52
68
|
isPrimaryKey: field.isId,
|
|
53
69
|
isIncrement: defaultDef?.name === "autoincrement",
|
|
54
70
|
isUnique: field.isUnique,
|
|
55
71
|
isNotNull: field.isRequired && !field.isId,
|
|
56
|
-
defaultValue
|
|
72
|
+
defaultValue,
|
|
57
73
|
note: stripAnnotations(field.documentation)
|
|
58
74
|
};
|
|
59
75
|
}
|
|
60
|
-
function makeTableIndexes(model) {
|
|
61
|
-
return [...model.primaryKey?.fields && model.primaryKey.fields.length > 0 ? [{
|
|
62
|
-
columns: model.primaryKey.fields,
|
|
63
|
-
isPrimaryKey: true
|
|
64
|
-
}] : [], ...model.uniqueFields.filter((c) => c.length > 1).map((c) => ({
|
|
65
|
-
columns: c,
|
|
66
|
-
isUnique: true
|
|
67
|
-
}))];
|
|
68
|
-
}
|
|
69
76
|
function makeTables(models, mapToDbSchema = false) {
|
|
70
77
|
return models.map((model) => {
|
|
71
78
|
const modelName = mapToDbSchema && model.dbName ? model.dbName : model.name;
|
|
72
79
|
const columnLines = model.fields.map((field) => toDBMLColumn(field, models, mapToDbSchema)).map(makePrismaColumn).join("\n");
|
|
73
|
-
const indexes =
|
|
80
|
+
const indexes = [...model.primaryKey?.fields && model.primaryKey.fields.length > 0 ? [{
|
|
81
|
+
columns: model.primaryKey.fields,
|
|
82
|
+
isPrimaryKey: true
|
|
83
|
+
}] : [], ...model.uniqueFields.filter((c) => c.length > 1).map((c) => ({
|
|
84
|
+
columns: c,
|
|
85
|
+
isUnique: true
|
|
86
|
+
}))];
|
|
74
87
|
const indexBlock = indexes.length > 0 ? `\n\n indexes {\n${indexes.map(makeIndex).join("\n")}\n }` : "";
|
|
75
88
|
const strippedNote = stripAnnotations(model.documentation);
|
|
76
89
|
return `Table ${modelName} {\n${columnLines}${indexBlock}${strippedNote ? `\n\n Note: ${quote(escapeNote(strippedNote))}` : ""}\n}`;
|
|
@@ -84,14 +97,11 @@ function makeEnums(enums) {
|
|
|
84
97
|
});
|
|
85
98
|
});
|
|
86
99
|
}
|
|
87
|
-
function getRelationOperator(models, from, to) {
|
|
88
|
-
return (models.find((m) => m.name === to)?.fields.find((f) => f.type === from))?.isList ? ">" : "-";
|
|
89
|
-
}
|
|
90
100
|
function makeRelations(models, mapToDbSchema = false) {
|
|
91
101
|
return models.flatMap((model) => model.fields.filter((field) => field.relationName && field.relationToFields?.length && field.relationFromFields?.length).map((field) => {
|
|
92
102
|
const relationFrom = model.name;
|
|
93
103
|
const relationTo = field.type;
|
|
94
|
-
const operator =
|
|
104
|
+
const operator = (models.find((m) => m.name === relationTo)?.fields.find((f) => f.type === relationFrom))?.isList ? ">" : "-";
|
|
95
105
|
const relationFromName = mapToDbSchema && model.dbName ? model.dbName : model.name;
|
|
96
106
|
const relatedModel = models.find((m) => m.name === relationTo);
|
|
97
107
|
const relationToName = mapToDbSchema && relatedModel?.dbName ? relatedModel.dbName : relationTo;
|
|
@@ -118,25 +128,25 @@ function dbmlContent(datamodel, mapToDbSchema = false) {
|
|
|
118
128
|
...refs
|
|
119
129
|
].join("\n\n");
|
|
120
130
|
}
|
|
121
|
-
|
|
131
|
+
async function makeDbmlFile(outputDir, content, fileName) {
|
|
122
132
|
const writeResult = await writeFile(`${outputDir}/${fileName}`, content);
|
|
123
133
|
if (!writeResult.ok) return {
|
|
124
134
|
ok: false,
|
|
125
135
|
error: `Failed to write DBML: ${writeResult.error}`
|
|
126
136
|
};
|
|
127
137
|
return { ok: true };
|
|
128
|
-
}
|
|
129
|
-
|
|
138
|
+
}
|
|
139
|
+
async function makePng(outputDir, dbml, fileName) {
|
|
130
140
|
return makePngFile(`${outputDir}/${fileName}`, dbml);
|
|
131
|
-
}
|
|
132
|
-
|
|
141
|
+
}
|
|
142
|
+
async function makePngFile(outputPath, dbml) {
|
|
133
143
|
const writeResult = await writeFileBinary(outputPath, new Resvg(run(dbml, "svg"), { font: { loadSystemFonts: true } }).render().asPng());
|
|
134
144
|
if (!writeResult.ok) return {
|
|
135
145
|
ok: false,
|
|
136
146
|
error: `Failed to write PNG: ${writeResult.error}`
|
|
137
147
|
};
|
|
138
148
|
return { ok: true };
|
|
139
|
-
}
|
|
149
|
+
}
|
|
140
150
|
|
|
141
151
|
//#endregion
|
|
142
152
|
//#region src/generator/dbml/index.ts
|
|
@@ -1,19 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { t as fmt } from "../../format-CzXgkLDe.js";
|
|
3
|
-
import {
|
|
3
|
+
import { d as mkdir, f as writeFile, o as makeSnakeCase } from "../../utils-COHZyQue.js";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import pkg from "@prisma/generator-helper";
|
|
6
6
|
|
|
7
7
|
//#region src/helper/drizzle.ts
|
|
8
|
-
function resolveProvider(provider) {
|
|
9
|
-
switch (provider) {
|
|
10
|
-
case "postgresql":
|
|
11
|
-
case "cockroachdb": return "postgresql";
|
|
12
|
-
case "mysql": return "mysql";
|
|
13
|
-
case "sqlite": return "sqlite";
|
|
14
|
-
default: throw new Error(`Unsupported provider: ${provider}`);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
8
|
const PG_SCALAR_MAP = {
|
|
18
9
|
String: "text()",
|
|
19
10
|
Int: "integer()",
|
|
@@ -140,13 +131,6 @@ function toCamelCase(name) {
|
|
|
140
131
|
function isFieldDefault(v) {
|
|
141
132
|
return typeof v === "object" && v !== null && "name" in v;
|
|
142
133
|
}
|
|
143
|
-
function insertColName(expr, colName) {
|
|
144
|
-
const parenIdx = expr.indexOf("(");
|
|
145
|
-
if (parenIdx === -1) return expr;
|
|
146
|
-
const fnName = expr.slice(0, parenIdx);
|
|
147
|
-
const rest = expr.slice(parenIdx + 1);
|
|
148
|
-
return rest === ")" ? `${fnName}('${colName}')` : `${fnName}('${colName}', ${rest}`;
|
|
149
|
-
}
|
|
150
134
|
function resolveScalarType(field, provider) {
|
|
151
135
|
if (field.nativeType && provider !== "sqlite") {
|
|
152
136
|
const [nativeName, nativeArgs] = field.nativeType;
|
|
@@ -179,7 +163,11 @@ function makeColumnExpr(field, provider, imports, enums) {
|
|
|
179
163
|
const baseExpr = resolveScalarType(field, provider);
|
|
180
164
|
const fnName = baseExpr.match(/^(\w+)/)?.[1];
|
|
181
165
|
if (fnName) imports.addCore(fnName);
|
|
182
|
-
|
|
166
|
+
const parenIdx = baseExpr.indexOf("(");
|
|
167
|
+
if (parenIdx === -1) return baseExpr;
|
|
168
|
+
const baseFnName = baseExpr.slice(0, parenIdx);
|
|
169
|
+
const rest = baseExpr.slice(parenIdx + 1);
|
|
170
|
+
return rest === ")" ? `${baseFnName}('${colName}')` : `${baseFnName}('${colName}', ${rest}`;
|
|
183
171
|
}
|
|
184
172
|
function makeDefaultChain(dflt, fieldType, provider, imports) {
|
|
185
173
|
if (dflt === void 0 || dflt === null) return "";
|
|
@@ -228,9 +216,6 @@ function makeColumn(field, model, provider, imports, enums) {
|
|
|
228
216
|
].join("");
|
|
229
217
|
return `${field.name}: ${colExpr}${chain}`;
|
|
230
218
|
}
|
|
231
|
-
function makeEnums() {
|
|
232
|
-
return [];
|
|
233
|
-
}
|
|
234
219
|
function makeCompositeConstraints(model, imports, indexes) {
|
|
235
220
|
const pkLine = model.primaryKey ? (() => {
|
|
236
221
|
imports.addCore("primaryKey");
|
|
@@ -291,9 +276,16 @@ function makeRelations(models, imports) {
|
|
|
291
276
|
});
|
|
292
277
|
}
|
|
293
278
|
function drizzleSchema(datamodel, provider, indexes) {
|
|
294
|
-
const db =
|
|
279
|
+
const db = (() => {
|
|
280
|
+
switch (provider) {
|
|
281
|
+
case "postgresql":
|
|
282
|
+
case "cockroachdb": return "postgresql";
|
|
283
|
+
case "mysql": return "mysql";
|
|
284
|
+
case "sqlite": return "sqlite";
|
|
285
|
+
default: throw new Error(`Unsupported provider: ${provider}`);
|
|
286
|
+
}
|
|
287
|
+
})();
|
|
295
288
|
const imports = new ImportTracker(db);
|
|
296
|
-
const enumLines = makeEnums();
|
|
297
289
|
const tableLines = datamodel.models.map((model) => makeTable(model, db, imports, datamodel.enums, indexes));
|
|
298
290
|
const relationsLines = makeRelations(datamodel.models, imports);
|
|
299
291
|
const tableLinesWithGap = tableLines.flatMap((line, i) => i < tableLines.length - 1 ? [line, ""] : [line]);
|
|
@@ -301,7 +293,6 @@ function drizzleSchema(datamodel, provider, indexes) {
|
|
|
301
293
|
return [
|
|
302
294
|
imports.generate(),
|
|
303
295
|
"",
|
|
304
|
-
...enumLines.length > 0 ? [...enumLines, ""] : [],
|
|
305
296
|
...tableLinesWithGap,
|
|
306
297
|
...relationsLinesWithGap.length > 0 ? ["", ...relationsLinesWithGap] : []
|
|
307
298
|
].join("\n");
|
|
@@ -1,9 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { d as mkdir, f as writeFile, o as makeSnakeCase } from "../../utils-COHZyQue.js";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import pkg from "@prisma/generator-helper";
|
|
5
5
|
|
|
6
6
|
//#region src/helper/ecto.ts
|
|
7
|
+
function prismaTypeToEctoType(type) {
|
|
8
|
+
if (type === "Int") return "integer";
|
|
9
|
+
if (type === "BigInt") return "integer";
|
|
10
|
+
if (type === "Float") return "float";
|
|
11
|
+
if (type === "Decimal") return "decimal";
|
|
12
|
+
if (type === "String") return "string";
|
|
13
|
+
if (type === "Boolean") return "boolean";
|
|
14
|
+
if (type === "DateTime") return "utc_datetime";
|
|
15
|
+
if (type === "Json") return "map";
|
|
16
|
+
if (type === "Bytes") return "binary";
|
|
17
|
+
return "string";
|
|
18
|
+
}
|
|
19
|
+
function ectoTypeToTypespec(type) {
|
|
20
|
+
switch (type) {
|
|
21
|
+
case "string": return "String.t()";
|
|
22
|
+
case "integer": return "integer()";
|
|
23
|
+
case "float": return "float()";
|
|
24
|
+
case "boolean": return "boolean()";
|
|
25
|
+
case "binary_id": return "Ecto.UUID.t()";
|
|
26
|
+
case "naive_datetime": return "NaiveDateTime.t()";
|
|
27
|
+
case "utc_datetime": return "DateTime.t()";
|
|
28
|
+
case "decimal": return "Decimal.t()";
|
|
29
|
+
case "map": return "map()";
|
|
30
|
+
case "binary": return "binary()";
|
|
31
|
+
default: return "term()";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
7
34
|
function getPrimaryKeyConfig(field) {
|
|
8
35
|
const def = field.default;
|
|
9
36
|
const isFunctionDefault = def && typeof def === "object" && "name" in def;
|
|
@@ -26,13 +53,6 @@ function getPrimaryKeyConfig(field) {
|
|
|
26
53
|
useBinaryForeignKey: false
|
|
27
54
|
};
|
|
28
55
|
}
|
|
29
|
-
function getFieldDefaultOption(field) {
|
|
30
|
-
const def = field.default;
|
|
31
|
-
if (def === void 0 || def === null) return null;
|
|
32
|
-
if (typeof def === "string") return `default: "${def}"`;
|
|
33
|
-
if (typeof def === "number" || typeof def === "boolean") return `default: ${def}`;
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
56
|
function makeTimestampsLine(fields) {
|
|
37
57
|
const insertedAliases = [
|
|
38
58
|
"inserted_at",
|
|
@@ -183,7 +203,12 @@ function ectoSchemas(models, app, allModels, enums) {
|
|
|
183
203
|
}
|
|
184
204
|
const type = prismaTypeToEctoType(f.type);
|
|
185
205
|
const ectoType = f.isList ? `{:array, :${type}}` : `:${type}`;
|
|
186
|
-
const defaultOpt =
|
|
206
|
+
const defaultOpt = ((def) => {
|
|
207
|
+
if (def === void 0 || def === null) return null;
|
|
208
|
+
if (typeof def === "string") return `default: "${def}"`;
|
|
209
|
+
if (typeof def === "number" || typeof def === "boolean") return `default: ${def}`;
|
|
210
|
+
return null;
|
|
211
|
+
})(f.default);
|
|
187
212
|
return ` field(:${snakeName}, ${ectoType}${primary}${defaultOpt ? `, ${defaultOpt}` : ""}${sourceOpt})`;
|
|
188
213
|
});
|
|
189
214
|
const fkFieldLines = [];
|