@tredy-ai/prisma-go-generator 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/NOTICE +34 -0
- package/README.md +327 -2
- package/bin/prisma-go-generator.js +2 -0
- package/dist/bin.js +7 -0
- package/dist/config.js +141 -0
- package/dist/generator.js +122 -0
- package/dist/inputs.js +222 -0
- package/dist/layout.js +101 -0
- package/dist/naming.js +40 -0
- package/dist/render.js +94 -0
- package/dist/types.js +90 -0
- package/package.json +52 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tredy AI, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/NOTICE
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
prisma-go-generator
|
|
2
|
+
Copyright (c) 2026 Tredy AI, Inc.
|
|
3
|
+
|
|
4
|
+
This project adapts the design and implementation approach of two generators by
|
|
5
|
+
Lexmata LLC, both released under the MIT License:
|
|
6
|
+
|
|
7
|
+
prisma-ent-generator https://github.com/Lexmata/prisma-ent-generator
|
|
8
|
+
prisma-rust-generator https://github.com/Lexmata/prisma-rust-generator
|
|
9
|
+
|
|
10
|
+
Their copyright and permission notice follows.
|
|
11
|
+
|
|
12
|
+
--------------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
MIT License
|
|
15
|
+
|
|
16
|
+
Copyright (c) 2026 Lexmata LLC
|
|
17
|
+
|
|
18
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
19
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
20
|
+
in the Software without restriction, including without limitation the rights
|
|
21
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
22
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
23
|
+
furnished to do so, subject to the following conditions:
|
|
24
|
+
|
|
25
|
+
The above copyright notice and this permission notice shall be included in all
|
|
26
|
+
copies or substantial portions of the Software.
|
|
27
|
+
|
|
28
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
29
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
30
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
31
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
32
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
33
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
34
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,328 @@
|
|
|
1
|
-
#
|
|
1
|
+
# prisma-go-generator
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A [Prisma](https://www.prisma.io/) generator that writes Go from your Prisma schema: a struct per model, a type per enum, and, if you ask for them, Prisma's query input types (`UserWhereInput`, `UserUncheckedCreateInput` and the rest).
|
|
4
|
+
|
|
5
|
+
The output is plain data. It has no runtime and no query engine, so it works with whichever database library you already use.
|
|
6
|
+
|
|
7
|
+
```prisma
|
|
8
|
+
generator go {
|
|
9
|
+
provider = "prisma-go-generator"
|
|
10
|
+
output = "../models"
|
|
11
|
+
}
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```go
|
|
15
|
+
// User - A person who can sign in.
|
|
16
|
+
type User struct {
|
|
17
|
+
Id int32 `json:"id,omitempty" db:"id"` // primary key
|
|
18
|
+
Email string `json:"email" db:"email"`
|
|
19
|
+
Name *string `json:"name,omitempty" db:"name"`
|
|
20
|
+
Role Role `json:"role,omitempty" db:"role"`
|
|
21
|
+
CreatedAt time.Time `json:"createdAt,omitempty" db:"created_at"`
|
|
22
|
+
|
|
23
|
+
// Relations (not persisted, populate manually)
|
|
24
|
+
Posts []Post `json:"posts,omitempty" db:"-"`
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
With no options set, the output is what [prisma2go](https://github.com/tredy-ai/prisma2go) writes. Every decision prisma2go made for you is an option here.
|
|
29
|
+
|
|
30
|
+
## Status
|
|
31
|
+
|
|
32
|
+
Pre-1.0: options and output may still change between minor versions. Tested against Prisma 7.10 and Go 1.27.
|
|
33
|
+
|
|
34
|
+
## Requirements
|
|
35
|
+
|
|
36
|
+
- Node.js 20.19 or later and Prisma 7.
|
|
37
|
+
- `gofmt` on your `PATH` when `prisma generate` runs. The generator formats its output with it, so the files never change when you run `gofmt` yourself. Set `gofmt = "false"` to do without.
|
|
38
|
+
- [enumer](https://github.com/dmarkham/enumer) on your `PATH` if you want `String`, JSON and SQL methods on integer enums. Without it the generator prints a warning and carries on. Under Go 1.27 use enumer v1.7.2 or later: v1.6.3 cannot load packages there and fails with `package "time" without types was imported`.
|
|
39
|
+
- Go 1.18 or later to compile the query inputs, which use generics.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pnpm add -D prisma @tredy-ai/prisma-go-generator
|
|
45
|
+
go install github.com/dmarkham/enumer@latest # optional
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Add the generator block shown above to your schema, then run `pnpm prisma generate`.
|
|
49
|
+
|
|
50
|
+
Prisma runs every generator block on each `prisma generate`. To run only some of them, name them: `prisma generate --generator client`.
|
|
51
|
+
|
|
52
|
+
## Options
|
|
53
|
+
|
|
54
|
+
Every option is a string, or a list of strings, in the generator block. `output` is Prisma's own option and is resolved relative to the schema file that declares the generator.
|
|
55
|
+
|
|
56
|
+
| Option | Default | What it does |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| `output` | `./models` | Directory the Go files are written to. |
|
|
59
|
+
| `package` | name of the output directory | Go package name. |
|
|
60
|
+
| `emitSingle` | `"true"` | Write everything to one file, named by `fileName`. |
|
|
61
|
+
| `emitPerFile` | `"false"` | Write one Go file per `.prisma` file. |
|
|
62
|
+
| `emitPerModel` | `"false"` | Write one Go file per model, holding the struct and its inputs. Enums go to `enums.go`. |
|
|
63
|
+
| `fileName` | `models.go` | File name for the single layout. |
|
|
64
|
+
| `structTags` | `["json", "db"]` | Struct tags to write, in order. See [Struct tags](#struct-tags). |
|
|
65
|
+
| `omitempty` | `["json"]` | Tags that get `,omitempty` on optional and defaulted columns. |
|
|
66
|
+
| `types` | `[]` | Go type overrides. See [Type mapping](#type-mapping). |
|
|
67
|
+
| `optionalType` | `pointer` | How an optional column is typed: `*T`, or a generic wrapper such as `database/sql.Null`. |
|
|
68
|
+
| `enumType` | `int` | Go type behind enums: any integer type (`int`, `uint8`, ...) or `string`. |
|
|
69
|
+
| `enumer` | `"true"` for integer enums | Run enumer for every enum after generating. |
|
|
70
|
+
| `enumerArgs` | `["-json", "-sql", "-text", "-trimprefix={name}"]` | Arguments passed to enumer after `-type={name}`. |
|
|
71
|
+
| `enumDirective` | `enumer -type={name}` plus `enumerArgs` | The `//go:generate` line above each enum. Empty for none. |
|
|
72
|
+
| `initialisms` | `[]` | Words to write in capitals in Go names, such as `["ID", "URL"]`. |
|
|
73
|
+
| `relations` | `"true"` | Include relation fields in model structs and inputs. |
|
|
74
|
+
| `tableNameMethod` | `"false"` | Add `func (User) TableName() string` to every model. |
|
|
75
|
+
| `inputs` | `[]` | Query input families to generate. See [Query inputs](#query-inputs). |
|
|
76
|
+
| `gofmt` | `"true"` | Format the output with `gofmt`. |
|
|
77
|
+
| `generateGoMod` | `"false"` | Write a `go.mod` into the output directory if there is none. |
|
|
78
|
+
| `goModule` | | Module path for the generated `go.mod`. Required with `generateGoMod`. |
|
|
79
|
+
| `goVersion` | version of the installed Go | `go` directive for the generated `go.mod`. |
|
|
80
|
+
| `runGoGenerate` | `"false"` | Run `go generate` in the output directory as the last step. |
|
|
81
|
+
|
|
82
|
+
Only one of the three `emit*` options can be `"true"`. An option name the generator does not know is an error, so a typo cannot be silently ignored.
|
|
83
|
+
|
|
84
|
+
Prisma passes `env("NAME")` in a custom option through as the bare name `NAME`, so use literal values here.
|
|
85
|
+
|
|
86
|
+
## Type mapping
|
|
87
|
+
|
|
88
|
+
| Prisma | Go |
|
|
89
|
+
|---|---|
|
|
90
|
+
| `String` | `string` |
|
|
91
|
+
| `Int` | `int32` |
|
|
92
|
+
| `BigInt` | `int64` |
|
|
93
|
+
| `Float` | `float64` |
|
|
94
|
+
| `Boolean` | `bool` |
|
|
95
|
+
| `DateTime` | `time.Time` |
|
|
96
|
+
| `Json` | `json.RawMessage` |
|
|
97
|
+
| `Bytes` | `[]byte` |
|
|
98
|
+
| `Decimal` | `string` |
|
|
99
|
+
| `@db.Uuid` | `uuid.UUID` from `github.com/google/uuid` |
|
|
100
|
+
| `@db.Real` | `float32` |
|
|
101
|
+
| an enum | the generated enum type |
|
|
102
|
+
| `T?` | `*T` |
|
|
103
|
+
| `T[]` | `[]T` |
|
|
104
|
+
| a to-one relation | `*Model` |
|
|
105
|
+
| a to-many relation | `[]Model` |
|
|
106
|
+
|
|
107
|
+
A type Prisma adds later, or one this table does not know, becomes `json.RawMessage`.
|
|
108
|
+
|
|
109
|
+
### Overriding a type
|
|
110
|
+
|
|
111
|
+
`types` takes entries of the form `Prisma=Go`. The left side is a Prisma scalar (`Int`) or a native type written as in the schema (`db.Uuid`). The right side is a Go type: a builtin as is, anything else as its import path, a dot, and the type name.
|
|
112
|
+
|
|
113
|
+
```prisma
|
|
114
|
+
generator go {
|
|
115
|
+
provider = "prisma-go-generator"
|
|
116
|
+
output = "../models"
|
|
117
|
+
types = [
|
|
118
|
+
"Int=int",
|
|
119
|
+
"Json=map[string]any",
|
|
120
|
+
"Decimal=github.com/shopspring/decimal.Decimal",
|
|
121
|
+
"db.Uuid=uuid.UUID", // the standard library package, Go 1.27 and later
|
|
122
|
+
"db.Timestamptz=github.com/jackc/pgx/v5/pgtype.Timestamptz",
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The generator works out the import and the package name from the path: `github.com/gofrs/uuid/v5.UUID` is written as `uuid.UUID`. A qualified type may start with `*` or `[]`.
|
|
128
|
+
|
|
129
|
+
For a column, the generator looks for a type in this order: your override for its native type, your override for its scalar, the built-in native mapping, the built-in scalar mapping.
|
|
130
|
+
|
|
131
|
+
## Struct tags
|
|
132
|
+
|
|
133
|
+
`structTags` lists the tags to write. Each entry is a tag key, optionally followed by a colon and a naming:
|
|
134
|
+
|
|
135
|
+
| Naming | Value for a field `createdAt @map("created_at")` |
|
|
136
|
+
|---|---|
|
|
137
|
+
| `field` | `createdAt`, the Prisma field name. The default for every tag but `db`. |
|
|
138
|
+
| `column` | `created_at`, the database column. The default for `db`. |
|
|
139
|
+
| `snake` | `created_at` |
|
|
140
|
+
| `camel` | `createdAt` |
|
|
141
|
+
| `pascal` | `CreatedAt`, the Go field name |
|
|
142
|
+
|
|
143
|
+
```prisma
|
|
144
|
+
structTags = ["json:snake", "db", "yaml", "bson:column"]
|
|
145
|
+
omitempty = ["json", "yaml"]
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
A relation field gets `-` in tags that use the `column` naming, and its name with `,omitempty` everywhere else. Set `structTags = []` for no tags.
|
|
149
|
+
|
|
150
|
+
## Enums
|
|
151
|
+
|
|
152
|
+
By default an enum becomes an integer type with `iota` constants, and enumer supplies the methods:
|
|
153
|
+
|
|
154
|
+
```go
|
|
155
|
+
//go:generate enumer -type=Role -json -sql -text -trimprefix=Role
|
|
156
|
+
type Role int
|
|
157
|
+
|
|
158
|
+
const (
|
|
159
|
+
RoleAdmin Role = iota
|
|
160
|
+
RoleUser
|
|
161
|
+
)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
With `enumType = "string"` the constants hold the value stored in the database (the `@map` name if there is one), and nothing else needs to run:
|
|
165
|
+
|
|
166
|
+
```go
|
|
167
|
+
type Role string
|
|
168
|
+
|
|
169
|
+
const (
|
|
170
|
+
RoleAdmin Role = "ADMIN"
|
|
171
|
+
RoleUser Role = "USER"
|
|
172
|
+
)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The generator's own enumer run and a later `go generate` write identical files, so you can use either or both.
|
|
176
|
+
|
|
177
|
+
To use a tool other than enumer, turn enumer off and set the directive: `enumer = "false"`, `enumDirective = "stringer -type={name}"`.
|
|
178
|
+
|
|
179
|
+
## Query inputs
|
|
180
|
+
|
|
181
|
+
`inputs` turns on Prisma's query input types as Go structs. They carry JSON tags with Prisma's field and operator names, so a value marshals to Prisma-style JSON. They are modelled on Prisma's client inputs but are not a drop-in for them: see [Differences from Prisma's inputs](#differences-from-prismas-inputs).
|
|
182
|
+
|
|
183
|
+
| Family | Types generated per model |
|
|
184
|
+
|---|---|
|
|
185
|
+
| `where` | `UserWhereInput`, `UserRelationFilter`, `UserListRelationFilter` |
|
|
186
|
+
| `whereUnique` | `UserWhereUniqueInput` |
|
|
187
|
+
| `orderBy` | `UserOrderByWithRelationInput` |
|
|
188
|
+
| `create` | `UserUncheckedCreateInput` |
|
|
189
|
+
| `update` | `UserUncheckedUpdateInput` |
|
|
190
|
+
| `select` | `UserSelect` |
|
|
191
|
+
| `include` | `UserInclude` |
|
|
192
|
+
|
|
193
|
+
Use `inputs = "all"` for every family.
|
|
194
|
+
|
|
195
|
+
```go
|
|
196
|
+
where := store.UserWhereInput{
|
|
197
|
+
Email: &store.StringFilter{Contains: new("@example.com"), Mode: store.QueryModeInsensitive},
|
|
198
|
+
Role: &store.Filter[store.Role]{In: []store.Role{store.RoleAdmin, store.RoleMember}},
|
|
199
|
+
Orders: &store.OrderListRelationFilter{
|
|
200
|
+
Some: &store.OrderWhereInput{TotalCents: &store.RangeFilter[int]{Gte: new(10_000)}},
|
|
201
|
+
},
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
`new` with a value, as used above, needs Go 1.26. On earlier versions take the address of a variable instead.
|
|
206
|
+
|
|
207
|
+
The filters are generic over the column's Go type, so they follow your `types` overrides: `Filter[T]` (equality), `RangeFilter[T]` (adds `lt`, `lte`, `gt`, `gte`), `StringFilter` (adds `contains`, `startsWith`, `endsWith`, `mode`), `ListFilter[T]` for scalar lists, and a `Nullable` variant of each with `IsNull`. Updates use `FieldUpdate[T]`, `NumericFieldUpdate[T]` (`increment`, `decrement`, `multiply`, `divide`), `ListFieldUpdate[T]` and their `Nullable` variants.
|
|
208
|
+
|
|
209
|
+
### Differences from Prisma's inputs
|
|
210
|
+
|
|
211
|
+
- Create and update inputs are the `Unchecked` forms, which take foreign keys directly. Nested relation writes (`create`, `connect`, `connectOrCreate` and `upsert` inside a create or update) are not generated.
|
|
212
|
+
- Aggregate and group-by inputs are not generated.
|
|
213
|
+
- A nullable filter has `isNull`. Prisma writes that as `equals: null`, which a Go pointer cannot tell apart from "not set".
|
|
214
|
+
- `WhereUniqueInput` lists the columns of a composite key or unique group as separate fields. Prisma nests them in one compound field.
|
|
215
|
+
- Order-by inputs cover columns and to-one relations, not counts of to-many relations.
|
|
216
|
+
- A nil slice and an empty slice both drop out of the JSON, so `in: []` cannot be expressed.
|
|
217
|
+
|
|
218
|
+
## Layouts
|
|
219
|
+
|
|
220
|
+
| Layout | Files |
|
|
221
|
+
|---|---|
|
|
222
|
+
| `emitSingle` | `models.go` with enums, models and inputs. |
|
|
223
|
+
| `emitPerFile` | `users.prisma` becomes `users.go`, holding that file's enums, models and inputs. Shared input types go to `inputs.go`. |
|
|
224
|
+
| `emitPerModel` | `user.go` per model with its struct and inputs, `enums.go`, and `inputs.go` for the shared input types. |
|
|
225
|
+
|
|
226
|
+
On each run the generator deletes the Go files it wrote earlier and no longer writes, such as the file of a model you removed. It recognises its own files by their first line and leaves every other file alone. With `enumer` on, it also deletes enumer output for enums that no longer exist.
|
|
227
|
+
|
|
228
|
+
A file name that the go tool would read as a build constraint gets `_gen` appended: a model called `UserWindows` is written to `user_windows_gen.go`.
|
|
229
|
+
|
|
230
|
+
## Moving from prisma2go
|
|
231
|
+
|
|
232
|
+
Add the generator block, then delete the DMMF script, the `prisma2go generate` step and `.prisma2go.yaml`.
|
|
233
|
+
|
|
234
|
+
| `.prisma2go.yaml` | Generator block |
|
|
235
|
+
|---|---|
|
|
236
|
+
| `output: gomodels/models.go` | `output = "../gomodels"` (relative to the schema file) and `fileName = "models.go"` |
|
|
237
|
+
| `package: prisma` | `package = "prisma"` |
|
|
238
|
+
| `json_tags: false` | `structTags = ["db"]` |
|
|
239
|
+
| `db_tags: false` | `structTags = ["json"]` |
|
|
240
|
+
| `json_omitempty: false` | `omitempty = []` |
|
|
241
|
+
| `uuid_package: ""` | `types = ["db.Uuid=string"]` |
|
|
242
|
+
| `decimal_package: "github.com/shopspring/decimal"` | `types = ["Decimal=github.com/shopspring/decimal.Decimal"]` |
|
|
243
|
+
| `no_enumer: true` | `enumer = "false"` |
|
|
244
|
+
|
|
245
|
+
The generated file is then identical to prisma2go's, apart from the name in its first line.
|
|
246
|
+
|
|
247
|
+
### Behaviour kept from prisma2go
|
|
248
|
+
|
|
249
|
+
These are unchanged so that existing output does not move. Each can be worked around with an option.
|
|
250
|
+
|
|
251
|
+
- A `@@unique` group is listed twice in the comment above a model, once from each place Prisma reports it.
|
|
252
|
+
- Enum constants are the enum name followed by the value in Pascal case, so `FDLStatus.FDL_STATUS_DRAFT` becomes `FDLStatusFdlStatusDraft`.
|
|
253
|
+
- An integer enum's first value is zero, and `omitempty` drops a zero from JSON. A defaulted enum column holding its first value is therefore left out when marshalled. Use `enumType = "string"` or `omitempty = []` if that matters to you.
|
|
254
|
+
- Names are not rewritten to Go initialisms unless you list them: `userId` becomes `UserId`, or `UserID` with `initialisms = ["ID"]`.
|
|
255
|
+
- The first line of the file is directly above the package clause, so Go reads it as the package comment.
|
|
256
|
+
|
|
257
|
+
One thing differs: when the generated code does not parse, prisma2go wrote it unformatted and warned. This generator stops with gofmt's error instead.
|
|
258
|
+
|
|
259
|
+
## Examples
|
|
260
|
+
|
|
261
|
+
Both examples are complete projects with their generated Go committed, and each has an `example_test.go` that uses the output.
|
|
262
|
+
|
|
263
|
+
- [`examples/basic`](examples/basic): one schema file, every option at its default.
|
|
264
|
+
- [`examples/crud-inputs`](examples/crud-inputs): a multi-file schema, one Go file per model, string enums and query inputs.
|
|
265
|
+
|
|
266
|
+
## Development
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
task install # pnpm install
|
|
270
|
+
task test # unit tests, then a real `prisma generate` over test/e2e/schema
|
|
271
|
+
task ci # everything CI runs
|
|
272
|
+
task --list
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
`task ci` needs Go 1.27 or later. It builds its own enumer into `.bin/`, at the version pinned in `Taskfile.yml`, so the examples do not depend on the enumer you have installed. It typechecks, runs the tests, compiles and vets the Go generated from the test schema, regenerates the examples, and fails if they differ from what is committed.
|
|
276
|
+
|
|
277
|
+
The tests are in three layers:
|
|
278
|
+
|
|
279
|
+
| Layer | Where | What it checks |
|
|
280
|
+
|---|---|---|
|
|
281
|
+
| Unit | `test/*.test.ts` | Naming, type mapping, option parsing, rendering and layouts, against small hand-built schemas. The prisma2go test cases are carried over. |
|
|
282
|
+
| End to end | `test/e2e.test.ts` | Runs the Prisma CLI over `test/e2e/schema` with four generator blocks and compares each file with `test/e2e/expected`. `task test:update` rewrites the expected files. |
|
|
283
|
+
| Go | `task test:go`, `task examples` | Builds and vets the generated code, and runs the examples' tests. |
|
|
284
|
+
|
|
285
|
+
## Releasing
|
|
286
|
+
|
|
287
|
+
Releases are cut by CI from commit messages. Nobody edits a version by hand.
|
|
288
|
+
|
|
289
|
+
1. Open a pull request against `main`. `ci.yml` runs the checks.
|
|
290
|
+
2. Squash-merge it with a [Conventional Commits](https://www.conventionalcommits.org/) message. That message decides the release:
|
|
291
|
+
|
|
292
|
+
| Commit message | Release |
|
|
293
|
+
|---|---|
|
|
294
|
+
| `fix:`, `perf:`, `hotfix:`, `patch:` | patch |
|
|
295
|
+
| `feat:` | minor |
|
|
296
|
+
| `feat!:`, `fix!:`, or a `BREAKING CHANGE:` footer | major (minor while the version is below 1.0.0) |
|
|
297
|
+
| `docs:`, `chore:`, `ci:`, `test:`, `refactor:`, anything else | none |
|
|
298
|
+
|
|
299
|
+
3. `release.yml` runs the checks again on `main`, then works out the next version from every commit since the last `vX.Y.Z` tag, taking the largest bump.
|
|
300
|
+
4. It publishes `@tredy-ai/prisma-go-generator` to npm with that version and the `latest` tag, then creates the `vX.Y.Z` tag and a GitHub release with generated notes.
|
|
301
|
+
|
|
302
|
+
The same version is also published to GitHub Packages. npmjs.com is the public home; the copy is for setups whose `.npmrc` sends the whole `@tredy-ai` scope to `npm.pkg.github.com`, where an npmjs-only package would not be found.
|
|
303
|
+
|
|
304
|
+
Put `[ci skip]` in the commit message to merge without starting the release workflow at all. GitHub does the skipping, and a commit marked that way is never counted towards a later release either.
|
|
305
|
+
|
|
306
|
+
The version is kept in git tags. `package.json` says `0.0.0` in the repository and is stamped with the real version when the package is published, because CI cannot commit to `main`.
|
|
307
|
+
|
|
308
|
+
A failed release can be re-run: publishing is skipped when the version is already on npm, and tagging when the release already exists.
|
|
309
|
+
|
|
310
|
+
### One-time setup
|
|
311
|
+
|
|
312
|
+
- **npm.** The workflow publishes by [trusted publishing](https://docs.npmjs.com/trusted-publishers/), which needs the package to exist first. For the first release, add an npm access token that may publish to the `@tredy-ai` scope as the repository secret `NPM_TOKEN`. After that release, add a trusted publisher to the package on npmjs.com (organisation `tredy-ai`, repository `prisma-go-generator`, workflow `release.yml`) and delete the secret.
|
|
313
|
+
- **GitHub.** The repository has to be public for npm to attach provenance.
|
|
314
|
+
|
|
315
|
+
## Acknowledgements
|
|
316
|
+
|
|
317
|
+
This project stands on two generators by [Lexmata](https://github.com/Lexmata):
|
|
318
|
+
|
|
319
|
+
- [prisma-ent-generator](https://github.com/Lexmata/prisma-ent-generator) is the inspiration for this generator and the source of how it is built: a TypeScript generator on `@prisma/generator-helper` that reads Prisma's DMMF and writes Go source, with its mapping, field and file-writing steps kept apart and tested one by one.
|
|
320
|
+
- [prisma-rust-generator](https://github.com/Lexmata/prisma-rust-generator) is the source of the query input surface (which input types exist for a model and what goes in each), the three output layouts, mapping declarations back to their `.prisma` files, and pruning stale output by its header line.
|
|
321
|
+
|
|
322
|
+
Both are MIT licensed; see [NOTICE](NOTICE).
|
|
323
|
+
|
|
324
|
+
The mapping from Prisma to Go, the naming rules and the default struct tags come from [prisma2go](https://github.com/tredy-ai/prisma2go).
|
|
325
|
+
|
|
326
|
+
## License
|
|
327
|
+
|
|
328
|
+
MIT. See [LICENSE](LICENSE).
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// @prisma/generator-helper is CommonJS and Node cannot see its named exports from an ES module.
|
|
2
|
+
import helper from '@prisma/generator-helper';
|
|
3
|
+
import { generate } from './generator.js';
|
|
4
|
+
helper.generatorHandler({
|
|
5
|
+
onManifest: () => ({ prettyName: 'Prisma Go Generator', defaultOutput: './models' }),
|
|
6
|
+
onGenerate: async (options) => generate(options),
|
|
7
|
+
});
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { basename } from 'node:path';
|
|
2
|
+
export const NAMINGS = ['field', 'column', 'snake', 'camel', 'pascal'];
|
|
3
|
+
export const INPUT_FAMILIES = ['where', 'whereUnique', 'orderBy', 'create', 'update', 'select', 'include'];
|
|
4
|
+
export const PRISMA_SCALARS = ['String', 'Int', 'BigInt', 'Float', 'Boolean', 'DateTime', 'Json', 'Bytes', 'Decimal'];
|
|
5
|
+
const DEFAULT_ENUMER_ARGS = ['-json', '-sql', '-text', '-trimprefix={name}'];
|
|
6
|
+
const INTEGER_TYPES = ['int', 'int8', 'int16', 'int32', 'int64', 'uint', 'uint8', 'uint16', 'uint32', 'uint64'];
|
|
7
|
+
const LAYOUT_FLAGS = { emitSingle: 'single', emitPerFile: 'perFile', emitPerModel: 'perModel' };
|
|
8
|
+
const OPTIONS = [
|
|
9
|
+
'emitSingle',
|
|
10
|
+
'emitPerFile',
|
|
11
|
+
'emitPerModel',
|
|
12
|
+
'fileName',
|
|
13
|
+
'package',
|
|
14
|
+
'structTags',
|
|
15
|
+
'omitempty',
|
|
16
|
+
'types',
|
|
17
|
+
'optionalType',
|
|
18
|
+
'enumType',
|
|
19
|
+
'enumDirective',
|
|
20
|
+
'enumer',
|
|
21
|
+
'enumerArgs',
|
|
22
|
+
'runGoGenerate',
|
|
23
|
+
'initialisms',
|
|
24
|
+
'relations',
|
|
25
|
+
'tableNameMethod',
|
|
26
|
+
'gofmt',
|
|
27
|
+
'inputs',
|
|
28
|
+
'generateGoMod',
|
|
29
|
+
'goModule',
|
|
30
|
+
'goVersion',
|
|
31
|
+
];
|
|
32
|
+
const IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
33
|
+
function fail(message) {
|
|
34
|
+
return new Error(`prisma-go-generator: ${message}`);
|
|
35
|
+
}
|
|
36
|
+
export function parseConfig(generator) {
|
|
37
|
+
const raw = generator.config;
|
|
38
|
+
for (const key of Object.keys(raw)) {
|
|
39
|
+
if (!OPTIONS.includes(key))
|
|
40
|
+
throw fail(`unknown option "${key}". Known options: output, ${OPTIONS.join(', ')}`);
|
|
41
|
+
}
|
|
42
|
+
const str = (key, fallback) => {
|
|
43
|
+
const value = raw[key];
|
|
44
|
+
if (value === undefined)
|
|
45
|
+
return fallback;
|
|
46
|
+
if (Array.isArray(value))
|
|
47
|
+
throw fail(`${key} must be a single string`);
|
|
48
|
+
return value;
|
|
49
|
+
};
|
|
50
|
+
const bool = (key, fallback) => {
|
|
51
|
+
const value = str(key, String(fallback));
|
|
52
|
+
if (value !== 'true' && value !== 'false')
|
|
53
|
+
throw fail(`${key} must be "true" or "false", got "${value}"`);
|
|
54
|
+
return value === 'true';
|
|
55
|
+
};
|
|
56
|
+
const list = (key, fallback) => {
|
|
57
|
+
const value = raw[key];
|
|
58
|
+
if (value === undefined)
|
|
59
|
+
return fallback;
|
|
60
|
+
return Array.isArray(value) ? value : [value];
|
|
61
|
+
};
|
|
62
|
+
const goFile = (key, fallback) => {
|
|
63
|
+
const value = str(key, fallback);
|
|
64
|
+
if (!/^[^/\\]+\.go$/.test(value))
|
|
65
|
+
throw fail(`${key} must be a .go file name without a directory, got "${value}"`);
|
|
66
|
+
return value;
|
|
67
|
+
};
|
|
68
|
+
const dir = generator.output?.value;
|
|
69
|
+
if (!dir)
|
|
70
|
+
throw fail('no output directory');
|
|
71
|
+
const pkg = str('package', basename(dir));
|
|
72
|
+
if (!IDENT.test(pkg)) {
|
|
73
|
+
throw fail(`"${pkg}" is not a valid Go package name; set package = "..." in the generator block`);
|
|
74
|
+
}
|
|
75
|
+
const structTags = list('structTags', ['json', 'db']).map((entry) => {
|
|
76
|
+
const [key = '', naming = key === 'db' ? 'column' : 'field', ...rest] = entry.split(':');
|
|
77
|
+
if (!IDENT.test(key) || rest.length > 0)
|
|
78
|
+
throw fail(`structTags entry "${entry}" must look like "json" or "json:snake"`);
|
|
79
|
+
if (!NAMINGS.includes(naming)) {
|
|
80
|
+
throw fail(`structTags entry "${entry}": naming must be one of ${NAMINGS.join(', ')}`);
|
|
81
|
+
}
|
|
82
|
+
return { key, naming: naming };
|
|
83
|
+
});
|
|
84
|
+
const types = {};
|
|
85
|
+
for (const entry of list('types', [])) {
|
|
86
|
+
const eq = entry.indexOf('=');
|
|
87
|
+
const key = entry.slice(0, eq);
|
|
88
|
+
const goType = entry.slice(eq + 1);
|
|
89
|
+
if (eq < 1 || goType === '')
|
|
90
|
+
throw fail(`types entry "${entry}" must look like Prisma=Go, for example "Int=int" or "db.Uuid=string"`);
|
|
91
|
+
if (!PRISMA_SCALARS.includes(key) && !/^db\.\w+$/.test(key)) {
|
|
92
|
+
throw fail(`types entry "${entry}": "${key}" is neither a Prisma scalar (${PRISMA_SCALARS.join(', ')}) nor a native type such as db.Uuid`);
|
|
93
|
+
}
|
|
94
|
+
types[key] = goType;
|
|
95
|
+
}
|
|
96
|
+
const enumType = str('enumType', 'int');
|
|
97
|
+
const integerEnums = INTEGER_TYPES.includes(enumType);
|
|
98
|
+
if (!integerEnums && enumType !== 'string')
|
|
99
|
+
throw fail(`enumType must be "string" or one of ${INTEGER_TYPES.join(', ')}, got "${enumType}"`);
|
|
100
|
+
const enumer = bool('enumer', integerEnums);
|
|
101
|
+
if (enumer && !integerEnums)
|
|
102
|
+
throw fail('enumer = "true" needs an integer enumType: enumer only handles integer enums');
|
|
103
|
+
const enumerArgs = list('enumerArgs', DEFAULT_ENUMER_ARGS);
|
|
104
|
+
const layouts = Object.keys(LAYOUT_FLAGS).filter((flag) => bool(flag, false));
|
|
105
|
+
if (layouts.length > 1)
|
|
106
|
+
throw fail(`${layouts.join(' and ')} are mutually exclusive; set only one to "true"`);
|
|
107
|
+
const requested = list('inputs', []);
|
|
108
|
+
const inputs = requested.includes('all') ? [...INPUT_FAMILIES] : requested;
|
|
109
|
+
for (const family of inputs) {
|
|
110
|
+
if (!INPUT_FAMILIES.includes(family)) {
|
|
111
|
+
throw fail(`inputs entry "${family}" must be "all" or one of ${INPUT_FAMILIES.join(', ')}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const generateGoMod = bool('generateGoMod', false);
|
|
115
|
+
const goModule = str('goModule', '');
|
|
116
|
+
if (generateGoMod && goModule === '')
|
|
117
|
+
throw fail('generateGoMod = "true" needs goModule = "<module path>"');
|
|
118
|
+
return {
|
|
119
|
+
dir,
|
|
120
|
+
layout: layouts[0] ? LAYOUT_FLAGS[layouts[0]] : 'single',
|
|
121
|
+
fileName: goFile('fileName', 'models.go'),
|
|
122
|
+
package: pkg,
|
|
123
|
+
structTags,
|
|
124
|
+
omitempty: new Set(list('omitempty', ['json'])),
|
|
125
|
+
types,
|
|
126
|
+
optionalType: str('optionalType', 'pointer'),
|
|
127
|
+
enumType,
|
|
128
|
+
enumDirective: str('enumDirective', integerEnums ? ['enumer -type={name}', ...enumerArgs].join(' ') : ''),
|
|
129
|
+
enumer,
|
|
130
|
+
enumerArgs,
|
|
131
|
+
runGoGenerate: bool('runGoGenerate', false),
|
|
132
|
+
initialisms: new Set(list('initialisms', []).map((s) => s.toUpperCase())),
|
|
133
|
+
relations: bool('relations', true),
|
|
134
|
+
tableNameMethod: bool('tableNameMethod', false),
|
|
135
|
+
gofmt: bool('gofmt', true),
|
|
136
|
+
inputs: inputs,
|
|
137
|
+
generateGoMod,
|
|
138
|
+
goModule,
|
|
139
|
+
goVersion: str('goVersion', ''),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { basename, extname, join } from 'node:path';
|
|
4
|
+
import { parseConfig } from './config.js';
|
|
5
|
+
import { HEADER, fileFor, generateFiles } from './layout.js';
|
|
6
|
+
import { toPascalCase } from './naming.js';
|
|
7
|
+
const fail = (message) => new Error(`prisma-go-generator: ${message}`);
|
|
8
|
+
// Prisma reads the generator's stderr as its RPC channel, so messages for the user go to stdout.
|
|
9
|
+
const warn = (message) => console.log(`prisma-go-generator: warning: ${message}`);
|
|
10
|
+
function prismaFiles(path) {
|
|
11
|
+
if (statSync(path).isFile())
|
|
12
|
+
return [path];
|
|
13
|
+
return readdirSync(path, { withFileTypes: true })
|
|
14
|
+
.flatMap((entry) => {
|
|
15
|
+
const full = join(path, entry.name);
|
|
16
|
+
if (entry.isDirectory())
|
|
17
|
+
return prismaFiles(full);
|
|
18
|
+
return entry.isFile() && entry.name.endsWith('.prisma') ? [full] : [];
|
|
19
|
+
})
|
|
20
|
+
.sort();
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Maps every model and enum to the name of the .prisma file that declares it. DMMF does not carry
|
|
24
|
+
* this, so the schema files are scanned for their `model X {` and `enum X {` lines.
|
|
25
|
+
*/
|
|
26
|
+
export function declarationFiles(schemaPath) {
|
|
27
|
+
const sources = new Map();
|
|
28
|
+
for (const file of prismaFiles(schemaPath)) {
|
|
29
|
+
const stem = basename(file, extname(file));
|
|
30
|
+
for (const match of readFileSync(file, 'utf8').matchAll(/^[ \t]*(?:model|enum|view)\s+(\w+)\s*\{/gm)) {
|
|
31
|
+
sources.set(match[1] ?? '', stem);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return sources;
|
|
35
|
+
}
|
|
36
|
+
function gofmt(source, fileName) {
|
|
37
|
+
const result = spawnSync('gofmt', { input: source, encoding: 'utf8', maxBuffer: Infinity });
|
|
38
|
+
if (result.error) {
|
|
39
|
+
throw fail(`could not run gofmt (${result.error.message}). Install Go, or set gofmt = "false" to write unformatted code.`);
|
|
40
|
+
}
|
|
41
|
+
if (result.status !== 0)
|
|
42
|
+
throw fail(`gofmt rejected ${fileName}:\n${result.stderr}`);
|
|
43
|
+
return result.stdout;
|
|
44
|
+
}
|
|
45
|
+
const firstLine = (path) => readFileSync(path, 'utf8').split('\n', 1)[0] ?? '';
|
|
46
|
+
/**
|
|
47
|
+
* Deletes what an earlier run wrote and this run did not: files carrying this generator's header,
|
|
48
|
+
* and enumer output for enums that no longer exist. Anything else in the directory is left alone.
|
|
49
|
+
*/
|
|
50
|
+
function prune(dir, written, enumerFiles) {
|
|
51
|
+
for (const name of readdirSync(dir)) {
|
|
52
|
+
if (!name.endsWith('.go') || written.has(name))
|
|
53
|
+
continue;
|
|
54
|
+
const path = join(dir, name);
|
|
55
|
+
const head = firstLine(path);
|
|
56
|
+
const staleEnumer = enumerFiles !== null && name.endsWith('_enumer.go') && !enumerFiles.has(name) && head.startsWith('// Code generated by "enumer');
|
|
57
|
+
if (head === HEADER || staleEnumer)
|
|
58
|
+
rmSync(path);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function writeGoMod(cfg) {
|
|
62
|
+
const path = join(cfg.dir, 'go.mod');
|
|
63
|
+
// go.mod belongs to the user once it exists: `go mod tidy` adds their requirements to it.
|
|
64
|
+
if (existsSync(path))
|
|
65
|
+
return;
|
|
66
|
+
let version = cfg.goVersion;
|
|
67
|
+
if (version === '') {
|
|
68
|
+
const result = spawnSync('go', ['env', 'GOVERSION'], { encoding: 'utf8' });
|
|
69
|
+
version = /^go(\d+\.\d+)/.exec(result.stdout ?? '')?.[1] ?? '';
|
|
70
|
+
if (version === '')
|
|
71
|
+
throw fail('generateGoMod could not ask `go env GOVERSION` for a Go version; set goVersion = "1.xx"');
|
|
72
|
+
}
|
|
73
|
+
writeFileSync(path, `module ${cfg.goModule}\n\ngo ${version}\n`);
|
|
74
|
+
}
|
|
75
|
+
const enumerOutput = (goName) => `${goName.toLowerCase()}_enumer.go`;
|
|
76
|
+
function runEnumer(cfg, enums) {
|
|
77
|
+
for (const [goName, file] of enums) {
|
|
78
|
+
const args = [`-type=${goName}`, ...cfg.enumerArgs.map((arg) => arg.replaceAll('{name}', goName))];
|
|
79
|
+
const output = enumerOutput(goName);
|
|
80
|
+
// Naming the source file keeps this working in a directory that is not inside a Go module.
|
|
81
|
+
const result = spawnSync('enumer', [...args, `-output=${output}`, file], { cwd: cfg.dir, encoding: 'utf8' });
|
|
82
|
+
if (result.error) {
|
|
83
|
+
warn('enumer is not on PATH, so no enum methods were generated. Install it with `go install github.com/dmarkham/enumer@latest`, or set enumer = "false".');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (result.status !== 0) {
|
|
87
|
+
warn(`enumer failed for ${goName}:\n${result.stderr}${result.stdout}`);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
// enumer records its command line in the file's first line. Write the line the go:generate
|
|
91
|
+
// directive would produce, so this run and a later `go generate` leave identical files.
|
|
92
|
+
const path = join(cfg.dir, output);
|
|
93
|
+
const lines = readFileSync(path, 'utf8').split('\n');
|
|
94
|
+
if (lines[0]?.startsWith('// Code generated by "enumer ')) {
|
|
95
|
+
lines[0] = `// Code generated by "enumer ${args.join(' ')}"; DO NOT EDIT.`;
|
|
96
|
+
writeFileSync(path, lines.join('\n'));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export function generate(options) {
|
|
101
|
+
const cfg = parseConfig(options.generator);
|
|
102
|
+
const { datamodel } = options.dmmf;
|
|
103
|
+
const sources = cfg.layout === 'perFile' ? declarationFiles(options.schemaPath) : new Map();
|
|
104
|
+
const files = generateFiles(datamodel, cfg, sources);
|
|
105
|
+
mkdirSync(cfg.dir, { recursive: true });
|
|
106
|
+
for (const [name, source] of files) {
|
|
107
|
+
writeFileSync(join(cfg.dir, name), cfg.gofmt ? gofmt(source, name) : source);
|
|
108
|
+
}
|
|
109
|
+
const enums = datamodel.enums.map((e) => [toPascalCase(e.name, cfg.initialisms), fileFor('enum', e.name, cfg, sources)]);
|
|
110
|
+
prune(cfg.dir, new Set(files.keys()), cfg.enumer ? new Set(enums.map(([goName]) => enumerOutput(goName))) : null);
|
|
111
|
+
if (cfg.generateGoMod)
|
|
112
|
+
writeGoMod(cfg);
|
|
113
|
+
if (cfg.enumer)
|
|
114
|
+
runEnumer(cfg, enums);
|
|
115
|
+
if (cfg.runGoGenerate) {
|
|
116
|
+
const result = spawnSync('go', ['generate', '.'], { cwd: cfg.dir, encoding: 'utf8' });
|
|
117
|
+
if (result.error)
|
|
118
|
+
throw fail(`runGoGenerate could not run go (${result.error.message})`);
|
|
119
|
+
if (result.status !== 0)
|
|
120
|
+
throw fail(`go generate failed:\n${result.stderr}${result.stdout}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
package/dist/inputs.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { INPUT_FAMILIES } from './config.js';
|
|
2
|
+
import { toPascalCase } from './naming.js';
|
|
3
|
+
import { baseType } from './types.js';
|
|
4
|
+
const optional = (name) => `${name},omitempty`;
|
|
5
|
+
function struct(doc, name, fields) {
|
|
6
|
+
const taken = new Set();
|
|
7
|
+
for (const [goName] of fields) {
|
|
8
|
+
if (taken.has(goName))
|
|
9
|
+
throw new Error(`prisma-go-generator: ${name} would have two fields named ${goName}`);
|
|
10
|
+
taken.add(goName);
|
|
11
|
+
}
|
|
12
|
+
return [`// ${doc}`, `type ${name} struct {`, ...fields.map(([n, type, tag]) => `\t${n} ${type} \`json:"${tag}"\``), '}'].join('\n');
|
|
13
|
+
}
|
|
14
|
+
function stringEnum(doc, name, values) {
|
|
15
|
+
const constants = values.map((v) => `\t${name}${toPascalCase(v)} ${name} = "${v}"`);
|
|
16
|
+
return [`// ${doc}`, `type ${name} string`, '', 'const (', ...constants, ')'].join('\n');
|
|
17
|
+
}
|
|
18
|
+
function filter(doc, name, t, kind) {
|
|
19
|
+
const self = name.replace('[T any]', '[T]');
|
|
20
|
+
const fields = [
|
|
21
|
+
['Equals', `*${t}`, optional('equals')],
|
|
22
|
+
['In', `[]${t}`, optional('in')],
|
|
23
|
+
['NotIn', `[]${t}`, optional('notIn')],
|
|
24
|
+
];
|
|
25
|
+
if (kind.nullable)
|
|
26
|
+
fields.push(['IsNull', '*bool', optional('isNull')]);
|
|
27
|
+
if (kind.range) {
|
|
28
|
+
for (const op of ['Lt', 'Lte', 'Gt', 'Gte'])
|
|
29
|
+
fields.push([op, `*${t}`, optional(op.toLowerCase())]);
|
|
30
|
+
}
|
|
31
|
+
if (kind.string) {
|
|
32
|
+
fields.push(['Contains', '*string', optional('contains')]);
|
|
33
|
+
fields.push(['StartsWith', '*string', optional('startsWith')]);
|
|
34
|
+
fields.push(['EndsWith', '*string', optional('endsWith')]);
|
|
35
|
+
fields.push(['Mode', 'QueryMode', optional('mode')]);
|
|
36
|
+
}
|
|
37
|
+
fields.push(['Not', `*${self}`, optional('not')]);
|
|
38
|
+
return struct(doc, name, fields);
|
|
39
|
+
}
|
|
40
|
+
function numericUpdate(doc, name, set) {
|
|
41
|
+
const ops = ['Increment', 'Decrement', 'Multiply', 'Divide'].map((op) => [op, '*T', optional(op.toLowerCase())]);
|
|
42
|
+
return struct(doc, name, [['Set', set, optional('set')], ...ops]);
|
|
43
|
+
}
|
|
44
|
+
/** Types shared by every model, per input family: [Go type name, declaration]. */
|
|
45
|
+
const SHARED = {
|
|
46
|
+
where: [
|
|
47
|
+
['QueryMode', stringEnum('QueryMode is the case sensitivity of a string filter.', 'QueryMode', ['default', 'insensitive'])],
|
|
48
|
+
['Filter', filter('Filter matches a column of Go type T by equality. Every operator that is set must hold.', 'Filter[T any]', 'T', {})],
|
|
49
|
+
['NullableFilter', filter('NullableFilter is Filter for a nullable column.', 'NullableFilter[T any]', 'T', { nullable: true })],
|
|
50
|
+
['RangeFilter', filter('RangeFilter is Filter plus the ordering operators.', 'RangeFilter[T any]', 'T', { range: true })],
|
|
51
|
+
[
|
|
52
|
+
'NullableRangeFilter',
|
|
53
|
+
filter('NullableRangeFilter is RangeFilter for a nullable column.', 'NullableRangeFilter[T any]', 'T', { range: true, nullable: true }),
|
|
54
|
+
],
|
|
55
|
+
['StringFilter', filter('StringFilter is RangeFilter[string] plus the substring operators.', 'StringFilter', 'string', { range: true, string: true })],
|
|
56
|
+
[
|
|
57
|
+
'StringNullableFilter',
|
|
58
|
+
filter('StringNullableFilter is StringFilter for a nullable column.', 'StringNullableFilter', 'string', {
|
|
59
|
+
range: true,
|
|
60
|
+
string: true,
|
|
61
|
+
nullable: true,
|
|
62
|
+
}),
|
|
63
|
+
],
|
|
64
|
+
[
|
|
65
|
+
'ListFilter',
|
|
66
|
+
struct('ListFilter matches a scalar list column.', 'ListFilter[T any]', [
|
|
67
|
+
['Equals', '[]T', optional('equals')],
|
|
68
|
+
['Has', '*T', optional('has')],
|
|
69
|
+
['HasEvery', '[]T', optional('hasEvery')],
|
|
70
|
+
['HasSome', '[]T', optional('hasSome')],
|
|
71
|
+
['IsEmpty', '*bool', optional('isEmpty')],
|
|
72
|
+
]),
|
|
73
|
+
],
|
|
74
|
+
],
|
|
75
|
+
orderBy: [['SortOrder', stringEnum('SortOrder is the direction of an order-by clause.', 'SortOrder', ['asc', 'desc'])]],
|
|
76
|
+
update: [
|
|
77
|
+
['FieldUpdate', struct('FieldUpdate sets a column of Go type T.', 'FieldUpdate[T any]', [['Set', 'T', 'set']])],
|
|
78
|
+
[
|
|
79
|
+
'NullableFieldUpdate',
|
|
80
|
+
struct('NullableFieldUpdate sets a nullable column. A nil Set stores NULL.', 'NullableFieldUpdate[T any]', [['Set', '*T', 'set']]),
|
|
81
|
+
],
|
|
82
|
+
[
|
|
83
|
+
'NumericFieldUpdate',
|
|
84
|
+
numericUpdate('NumericFieldUpdate sets a numeric column or changes it in place. Set one operation.', 'NumericFieldUpdate[T any]', '*T'),
|
|
85
|
+
],
|
|
86
|
+
[
|
|
87
|
+
'NullableNumericFieldUpdate',
|
|
88
|
+
numericUpdate('NullableNumericFieldUpdate is NumericFieldUpdate for a nullable column. Set points at the new\n// value; a pointer to a nil *T stores NULL.', 'NullableNumericFieldUpdate[T any]', '**T'),
|
|
89
|
+
],
|
|
90
|
+
[
|
|
91
|
+
'ListFieldUpdate',
|
|
92
|
+
struct('ListFieldUpdate replaces a scalar list column or appends to it.', 'ListFieldUpdate[T any]', [
|
|
93
|
+
['Set', '[]T', optional('set')],
|
|
94
|
+
['Push', '[]T', optional('push')],
|
|
95
|
+
]),
|
|
96
|
+
],
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
const selected = (cfg) => INPUT_FAMILIES.filter((family) => cfg.inputs.includes(family));
|
|
100
|
+
/** Declarations every model's inputs depend on. */
|
|
101
|
+
export function sharedInputs(cfg) {
|
|
102
|
+
return selected(cfg).flatMap((family) => (SHARED[family] ?? []).map(([, code]) => code));
|
|
103
|
+
}
|
|
104
|
+
const ORDERED = new Set(['Int', 'BigInt', 'Float', 'Decimal', 'DateTime']);
|
|
105
|
+
const NUMERIC = new Set(['Int', 'BigInt', 'Float', 'Decimal']);
|
|
106
|
+
function whereFilter(field, t) {
|
|
107
|
+
if (field.isList)
|
|
108
|
+
return `ListFilter[${t}]`;
|
|
109
|
+
const nullable = !field.isRequired;
|
|
110
|
+
const isText = field.kind === 'scalar' && field.type === 'String' && field.nativeType?.[0] !== 'Uuid';
|
|
111
|
+
if (isText && t === 'string')
|
|
112
|
+
return nullable ? 'StringNullableFilter' : 'StringFilter';
|
|
113
|
+
const range = isText || (field.kind === 'scalar' && ORDERED.has(field.type));
|
|
114
|
+
return `${nullable ? 'Nullable' : ''}${range ? 'Range' : ''}Filter[${t}]`;
|
|
115
|
+
}
|
|
116
|
+
function updateOperation(field, t) {
|
|
117
|
+
if (field.isList)
|
|
118
|
+
return `ListFieldUpdate[${t}]`;
|
|
119
|
+
const numeric = field.kind === 'scalar' && NUMERIC.has(field.type);
|
|
120
|
+
return `${field.isRequired ? '' : 'Nullable'}${numeric ? 'Numeric' : ''}FieldUpdate[${t}]`;
|
|
121
|
+
}
|
|
122
|
+
function uniqueColumns(m) {
|
|
123
|
+
const names = new Set(m.primaryKey?.fields ?? []);
|
|
124
|
+
for (const group of m.uniqueFields ?? [])
|
|
125
|
+
group.forEach((name) => names.add(name));
|
|
126
|
+
for (const index of m.uniqueIndexes ?? [])
|
|
127
|
+
index.fields.forEach((name) => names.add(name));
|
|
128
|
+
for (const field of m.fields)
|
|
129
|
+
if (field.isId || field.isUnique)
|
|
130
|
+
names.add(field.name);
|
|
131
|
+
return names;
|
|
132
|
+
}
|
|
133
|
+
/** Go type names modelInputs declares for a model. */
|
|
134
|
+
export function modelInputNames(m, cfg) {
|
|
135
|
+
const model = toPascalCase(m.name, cfg.initialisms);
|
|
136
|
+
const names = {
|
|
137
|
+
where: [`${model}WhereInput`, ...(cfg.relations ? [`${model}RelationFilter`, `${model}ListRelationFilter`] : [])],
|
|
138
|
+
whereUnique: [`${model}WhereUniqueInput`],
|
|
139
|
+
orderBy: [`${model}OrderByWithRelationInput`],
|
|
140
|
+
create: [`${model}UncheckedCreateInput`],
|
|
141
|
+
update: [`${model}UncheckedUpdateInput`],
|
|
142
|
+
select: [`${model}Select`],
|
|
143
|
+
include: [`${model}Include`],
|
|
144
|
+
};
|
|
145
|
+
return selected(cfg).flatMap((family) => names[family]);
|
|
146
|
+
}
|
|
147
|
+
/** Go type names sharedInputs declares. */
|
|
148
|
+
export function sharedInputNames(cfg) {
|
|
149
|
+
return selected(cfg).flatMap((family) => (SHARED[family] ?? []).map(([name]) => name));
|
|
150
|
+
}
|
|
151
|
+
/** The input types of one model, for the families the config asks for. */
|
|
152
|
+
export function modelInputs(m, cfg) {
|
|
153
|
+
const model = toPascalCase(m.name, cfg.initialisms);
|
|
154
|
+
const imports = [];
|
|
155
|
+
const goName = (field) => toPascalCase(field.name, cfg.initialisms);
|
|
156
|
+
const goType = (field) => {
|
|
157
|
+
const type = baseType(field, cfg);
|
|
158
|
+
imports.push(...type.imports);
|
|
159
|
+
return type.expr;
|
|
160
|
+
};
|
|
161
|
+
const columns = m.fields.filter((field) => field.kind !== 'object');
|
|
162
|
+
const relations = cfg.relations ? m.fields.filter((field) => field.kind === 'object') : [];
|
|
163
|
+
const target = (field) => toPascalCase(field.type, cfg.initialisms);
|
|
164
|
+
const families = {
|
|
165
|
+
where: () => {
|
|
166
|
+
const out = [
|
|
167
|
+
struct(`${model}WhereInput filters ${model} rows. Every condition that is set must hold.`, `${model}WhereInput`, [
|
|
168
|
+
...columns.map((f) => [goName(f), `*${whereFilter(f, goType(f))}`, optional(f.name)]),
|
|
169
|
+
...relations.map((f) => [goName(f), `*${target(f)}${f.isList ? 'ListRelationFilter' : 'RelationFilter'}`, optional(f.name)]),
|
|
170
|
+
['AND', `[]${model}WhereInput`, optional('AND')],
|
|
171
|
+
['OR', `[]${model}WhereInput`, optional('OR')],
|
|
172
|
+
['NOT', `[]${model}WhereInput`, optional('NOT')],
|
|
173
|
+
]),
|
|
174
|
+
];
|
|
175
|
+
if (cfg.relations) {
|
|
176
|
+
out.push(struct(`${model}RelationFilter filters through a to-one relation to ${model}.`, `${model}RelationFilter`, [
|
|
177
|
+
['Is', `*${model}WhereInput`, optional('is')],
|
|
178
|
+
['IsNot', `*${model}WhereInput`, optional('isNot')],
|
|
179
|
+
]), struct(`${model}ListRelationFilter filters through a to-many relation to ${model}.`, `${model}ListRelationFilter`, [
|
|
180
|
+
['Some', `*${model}WhereInput`, optional('some')],
|
|
181
|
+
['Every', `*${model}WhereInput`, optional('every')],
|
|
182
|
+
['None', `*${model}WhereInput`, optional('none')],
|
|
183
|
+
]));
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
},
|
|
187
|
+
whereUnique: () => {
|
|
188
|
+
const unique = uniqueColumns(m);
|
|
189
|
+
return [
|
|
190
|
+
struct(`${model}WhereUniqueInput identifies one ${model} row by its id or a unique column.`, `${model}WhereUniqueInput`, columns.filter((f) => unique.has(f.name) && !f.isList).map((f) => [goName(f), `*${goType(f)}`, optional(f.name)])),
|
|
191
|
+
];
|
|
192
|
+
},
|
|
193
|
+
orderBy: () => [
|
|
194
|
+
struct(`${model}OrderByWithRelationInput orders ${model} rows. Use a slice of these to order by several columns.`, `${model}OrderByWithRelationInput`, [
|
|
195
|
+
...columns.map((f) => [goName(f), 'SortOrder', optional(f.name)]),
|
|
196
|
+
...relations.filter((f) => !f.isList).map((f) => [goName(f), `*${target(f)}OrderByWithRelationInput`, optional(f.name)]),
|
|
197
|
+
]),
|
|
198
|
+
],
|
|
199
|
+
create: () => [
|
|
200
|
+
struct(`${model}UncheckedCreateInput is the data for a new ${model} row. Columns with a default are optional.`, `${model}UncheckedCreateInput`, columns.map((f) => {
|
|
201
|
+
if (f.isList)
|
|
202
|
+
return [goName(f), `[]${goType(f)}`, optional(f.name)];
|
|
203
|
+
const required = f.isRequired && !f.hasDefaultValue && !f.isUpdatedAt;
|
|
204
|
+
return required ? [goName(f), goType(f), f.name] : [goName(f), `*${goType(f)}`, optional(f.name)];
|
|
205
|
+
})),
|
|
206
|
+
],
|
|
207
|
+
update: () => [
|
|
208
|
+
struct(`${model}UncheckedUpdateInput changes the columns that are set and leaves the rest alone.`, `${model}UncheckedUpdateInput`, columns.map((f) => [goName(f), `*${updateOperation(f, goType(f))}`, optional(f.name)])),
|
|
209
|
+
],
|
|
210
|
+
select: () => [
|
|
211
|
+
struct(`${model}Select picks the fields of ${model} to return.`, `${model}Select`, [...columns, ...relations].map((f) => [goName(f), 'bool', optional(f.name)])),
|
|
212
|
+
],
|
|
213
|
+
include: () => relations.length === 0
|
|
214
|
+
? []
|
|
215
|
+
: [
|
|
216
|
+
struct(`${model}Include picks the relations of ${model} to load.`, `${model}Include`, relations.map((f) => [goName(f), 'bool', optional(f.name)])),
|
|
217
|
+
],
|
|
218
|
+
};
|
|
219
|
+
return selected(cfg)
|
|
220
|
+
.flatMap((family) => families[family]())
|
|
221
|
+
.map((code) => ({ code, imports }));
|
|
222
|
+
}
|
package/dist/layout.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { modelInputNames, modelInputs, sharedInputNames, sharedInputs } from './inputs.js';
|
|
2
|
+
import { toPascalCase, toSnakeCase } from './naming.js';
|
|
3
|
+
import { renderEnum, renderModel } from './render.js';
|
|
4
|
+
/** First line of every file this generator writes; pruning only deletes files that start with it. */
|
|
5
|
+
export const HEADER = '// Code generated by prisma-go-generator. DO NOT EDIT.';
|
|
6
|
+
// A trailing _test, _<GOOS> or _<GOARCH> in a file name is a build constraint to the go tool.
|
|
7
|
+
const CONSTRAINT_SUFFIXES = new Set(('test aix android darwin dragonfly freebsd hurd illumos ios js linux nacl netbsd openbsd plan9 solaris wasip1 windows zos ' +
|
|
8
|
+
'386 amd64 amd64p32 arm arm64 arm64be armbe loong64 mips mips64 mips64le mips64p32 mips64p32le mipsle ppc ppc64 ppc64le ' +
|
|
9
|
+
'riscv riscv64 s390 s390x sparc sparc64 wasm').split(' '));
|
|
10
|
+
/** Turns a model or schema-file name into a Go file name that the go tool always compiles. */
|
|
11
|
+
export function goFileName(stem) {
|
|
12
|
+
let base = stem
|
|
13
|
+
.toLowerCase()
|
|
14
|
+
.replaceAll(/[^a-z0-9_]+/g, '_')
|
|
15
|
+
.replace(/^_+/, '');
|
|
16
|
+
if (base === '')
|
|
17
|
+
base = 'models';
|
|
18
|
+
if (CONSTRAINT_SUFFIXES.has(base.split('_').pop() ?? ''))
|
|
19
|
+
base += '_gen';
|
|
20
|
+
return `${base}.go`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The file a model or enum is written to. `sources` maps Prisma declaration names to the name of
|
|
24
|
+
* the .prisma file they come from, and is only needed for the per-file layout.
|
|
25
|
+
*/
|
|
26
|
+
export function fileFor(kind, name, cfg, sources) {
|
|
27
|
+
if (cfg.layout === 'perModel')
|
|
28
|
+
return kind === 'enum' ? 'enums.go' : goFileName(toSnakeCase(name));
|
|
29
|
+
if (cfg.layout === 'perFile') {
|
|
30
|
+
const source = sources.get(name);
|
|
31
|
+
return source === undefined ? cfg.fileName : goFileName(source);
|
|
32
|
+
}
|
|
33
|
+
return cfg.fileName;
|
|
34
|
+
}
|
|
35
|
+
function assemble(pkg, parts) {
|
|
36
|
+
const imports = [...parts.imports].sort();
|
|
37
|
+
const blocks = [
|
|
38
|
+
`${HEADER}\npackage ${pkg}`,
|
|
39
|
+
...(imports.length > 0 ? [`import (\n${imports.map((path) => `\t"${path}"`).join('\n')}\n)`] : []),
|
|
40
|
+
...(parts.enums.length > 0 ? ['// Enums', ...parts.enums] : []),
|
|
41
|
+
...(parts.models.length > 0 ? ['// Models', ...parts.models] : []),
|
|
42
|
+
...(parts.inputs.length > 0 ? ['// Inputs', ...parts.inputs] : []),
|
|
43
|
+
];
|
|
44
|
+
return `${blocks.join('\n\n')}\n`;
|
|
45
|
+
}
|
|
46
|
+
function checkTypeNames(datamodel, cfg) {
|
|
47
|
+
const taken = new Map();
|
|
48
|
+
const claim = (goName, owner) => {
|
|
49
|
+
const other = taken.get(goName);
|
|
50
|
+
if (other !== undefined)
|
|
51
|
+
throw new Error(`prisma-go-generator: ${other} and ${owner} both become the Go type ${goName}`);
|
|
52
|
+
taken.set(goName, owner);
|
|
53
|
+
};
|
|
54
|
+
for (const e of datamodel.enums)
|
|
55
|
+
claim(toPascalCase(e.name, cfg.initialisms), `enum ${e.name}`);
|
|
56
|
+
for (const m of datamodel.models)
|
|
57
|
+
claim(toPascalCase(m.name, cfg.initialisms), `model ${m.name}`);
|
|
58
|
+
if (cfg.inputs.length === 0)
|
|
59
|
+
return;
|
|
60
|
+
for (const name of sharedInputNames(cfg))
|
|
61
|
+
claim(name, 'the shared input types');
|
|
62
|
+
for (const m of datamodel.models) {
|
|
63
|
+
for (const name of modelInputNames(m, cfg))
|
|
64
|
+
claim(name, `the inputs of model ${m.name}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Renders the whole datamodel: Go file name -> unformatted Go source. */
|
|
68
|
+
export function generateFiles(datamodel, cfg, sources = new Map()) {
|
|
69
|
+
checkTypeNames(datamodel, cfg);
|
|
70
|
+
const files = new Map();
|
|
71
|
+
const file = (name) => {
|
|
72
|
+
let parts = files.get(name);
|
|
73
|
+
if (!parts)
|
|
74
|
+
files.set(name, (parts = { imports: new Set(), enums: [], models: [], inputs: [] }));
|
|
75
|
+
return parts;
|
|
76
|
+
};
|
|
77
|
+
if (cfg.layout === 'single')
|
|
78
|
+
file(cfg.fileName);
|
|
79
|
+
for (const e of datamodel.enums)
|
|
80
|
+
file(fileFor('enum', e.name, cfg, sources)).enums.push(renderEnum(e, cfg));
|
|
81
|
+
const withInputs = cfg.inputs.length > 0 && datamodel.models.length > 0;
|
|
82
|
+
if (withInputs)
|
|
83
|
+
file(cfg.layout === 'single' ? cfg.fileName : 'inputs.go').inputs.push(...sharedInputs(cfg));
|
|
84
|
+
for (const m of datamodel.models) {
|
|
85
|
+
const parts = file(fileFor('model', m.name, cfg, sources));
|
|
86
|
+
const model = renderModel(m, cfg);
|
|
87
|
+
parts.models.push(model.code);
|
|
88
|
+
model.imports.forEach((path) => parts.imports.add(path));
|
|
89
|
+
if (!withInputs)
|
|
90
|
+
continue;
|
|
91
|
+
for (const input of modelInputs(m, cfg)) {
|
|
92
|
+
parts.inputs.push(input.code);
|
|
93
|
+
input.imports.forEach((path) => parts.imports.add(path));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return new Map([...files].map(([name, parts]) => [name, assemble(cfg.package, parts)]));
|
|
97
|
+
}
|
|
98
|
+
/** The single-file layout's one file. */
|
|
99
|
+
export function render(datamodel, cfg) {
|
|
100
|
+
return generateFiles(datamodel, cfg).get(cfg.fileName) ?? '';
|
|
101
|
+
}
|
package/dist/naming.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const NO_INITIALISMS = new Set();
|
|
2
|
+
/**
|
|
3
|
+
* Converts a Prisma identifier to an exported Go identifier, the way prisma2go does: the first
|
|
4
|
+
* letter and any letter after `_`, `-` or a space is upper-cased and everything else is kept, so
|
|
5
|
+
* `user_name` and `userName` both become `UserName` and `USER_NAME` becomes `USERNAME`.
|
|
6
|
+
*
|
|
7
|
+
* Words that match one of the upper-case initialisms are upper-cased whole (`UserId` -> `UserID`).
|
|
8
|
+
*/
|
|
9
|
+
export function toPascalCase(s, initialisms = NO_INITIALISMS) {
|
|
10
|
+
let out = '';
|
|
11
|
+
let upperNext = true;
|
|
12
|
+
for (const ch of s) {
|
|
13
|
+
if (ch === '_' || ch === '-' || ch === ' ') {
|
|
14
|
+
upperNext = true;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
out += upperNext ? ch.toUpperCase() : ch;
|
|
18
|
+
upperNext = false;
|
|
19
|
+
}
|
|
20
|
+
if (initialisms.size === 0)
|
|
21
|
+
return out;
|
|
22
|
+
return out.replace(/[A-Z][a-z0-9]*/g, (word) => (initialisms.has(word.toUpperCase()) ? word.toUpperCase() : word));
|
|
23
|
+
}
|
|
24
|
+
/** Names an enum constant: `Role` + `SUPER_USER` -> `RoleSuperUser`. */
|
|
25
|
+
export function toEnumConstName(enumName, value, initialisms = NO_INITIALISMS) {
|
|
26
|
+
return enumName + toPascalCase(value.toLowerCase(), initialisms);
|
|
27
|
+
}
|
|
28
|
+
function words(s) {
|
|
29
|
+
return s.match(/[A-Z]+(?![a-z])|[A-Z]?[a-z]+|[0-9]+/g) ?? [];
|
|
30
|
+
}
|
|
31
|
+
export function toSnakeCase(s) {
|
|
32
|
+
return words(s)
|
|
33
|
+
.map((w) => w.toLowerCase())
|
|
34
|
+
.join('_');
|
|
35
|
+
}
|
|
36
|
+
export function toCamelCase(s) {
|
|
37
|
+
return words(s)
|
|
38
|
+
.map((w, i) => (i === 0 ? w.toLowerCase() : w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()))
|
|
39
|
+
.join('');
|
|
40
|
+
}
|
package/dist/render.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { toCamelCase, toEnumConstName, toPascalCase, toSnakeCase } from './naming.js';
|
|
2
|
+
import { fieldType } from './types.js';
|
|
3
|
+
export function renderEnum(e, cfg) {
|
|
4
|
+
const name = toPascalCase(e.name, cfg.initialisms);
|
|
5
|
+
const lines = [];
|
|
6
|
+
if (cfg.enumDirective)
|
|
7
|
+
lines.push(`//go:generate ${cfg.enumDirective.replaceAll('{name}', name)}`);
|
|
8
|
+
lines.push(`type ${name} ${cfg.enumType}`, '', 'const (');
|
|
9
|
+
e.values.forEach((value, i) => {
|
|
10
|
+
const constant = toEnumConstName(name, value.name, cfg.initialisms);
|
|
11
|
+
if (cfg.enumType === 'string')
|
|
12
|
+
lines.push(`\t${constant} ${name} = ${JSON.stringify(value.dbName ?? value.name)}`);
|
|
13
|
+
else
|
|
14
|
+
lines.push(i === 0 ? `\t${constant} ${name} = iota` : `\t${constant}`);
|
|
15
|
+
});
|
|
16
|
+
lines.push(')');
|
|
17
|
+
return lines.join('\n');
|
|
18
|
+
}
|
|
19
|
+
function tagName(tag, field, goName) {
|
|
20
|
+
switch (tag.naming) {
|
|
21
|
+
case 'field':
|
|
22
|
+
return field.name;
|
|
23
|
+
case 'column':
|
|
24
|
+
return field.dbName ?? field.name;
|
|
25
|
+
case 'snake':
|
|
26
|
+
return toSnakeCase(field.name);
|
|
27
|
+
case 'camel':
|
|
28
|
+
return toCamelCase(field.name);
|
|
29
|
+
case 'pascal':
|
|
30
|
+
return goName;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function structTag(tag, field, goName, cfg) {
|
|
34
|
+
if (field.kind === 'object') {
|
|
35
|
+
// A relation is not a column, and is only present once the caller has loaded it.
|
|
36
|
+
return tag.naming === 'column' ? `${tag.key}:"-"` : `${tag.key}:"${tagName(tag, field, goName)},omitempty"`;
|
|
37
|
+
}
|
|
38
|
+
const omit = cfg.omitempty.has(tag.key) && (!field.isRequired || field.hasDefaultValue);
|
|
39
|
+
return `${tag.key}:"${tagName(tag, field, goName)}${omit ? ',omitempty' : ''}"`;
|
|
40
|
+
}
|
|
41
|
+
// prisma2go lists @@unique groups from both uniqueIndexes and uniqueFields, so a group shows up
|
|
42
|
+
// twice. Kept as is: the comment is part of the output existing users diff against.
|
|
43
|
+
function indexComments(m) {
|
|
44
|
+
const lines = (m.uniqueIndexes ?? []).map((index) => `// Index: ${index.name ?? 'unique'} (${index.fields.join(', ')})`);
|
|
45
|
+
for (const fields of m.uniqueFields ?? []) {
|
|
46
|
+
if (fields.length > 0)
|
|
47
|
+
lines.push(`// Index: unique (${fields.join(', ')})`);
|
|
48
|
+
}
|
|
49
|
+
return lines;
|
|
50
|
+
}
|
|
51
|
+
export function renderModel(m, cfg) {
|
|
52
|
+
const name = toPascalCase(m.name, cfg.initialisms);
|
|
53
|
+
const table = m.dbName ?? m.name;
|
|
54
|
+
const imports = [];
|
|
55
|
+
const columns = [];
|
|
56
|
+
const relations = [];
|
|
57
|
+
const taken = new Map();
|
|
58
|
+
for (const field of m.fields) {
|
|
59
|
+
const isRelation = field.kind === 'object';
|
|
60
|
+
if (isRelation && !cfg.relations)
|
|
61
|
+
continue;
|
|
62
|
+
const goName = toPascalCase(field.name, cfg.initialisms);
|
|
63
|
+
const clash = taken.get(goName);
|
|
64
|
+
if (clash !== undefined) {
|
|
65
|
+
throw new Error(`prisma-go-generator: ${m.name}: fields "${clash}" and "${field.name}" both become ${goName} in Go`);
|
|
66
|
+
}
|
|
67
|
+
taken.set(goName, field.name);
|
|
68
|
+
const type = fieldType(field, cfg);
|
|
69
|
+
imports.push(...type.imports);
|
|
70
|
+
const tags = cfg.structTags.map((tag) => structTag(tag, field, goName, cfg));
|
|
71
|
+
const line = `\t${goName} ${type.expr}${tags.length > 0 ? ` \`${tags.join(' ')}\`` : ''}`;
|
|
72
|
+
if (isRelation) {
|
|
73
|
+
relations.push(line);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (field.documentation)
|
|
77
|
+
columns.push(`\t// ${field.documentation.replaceAll('\n', '\n\t// ')}`);
|
|
78
|
+
columns.push(field.isId ? `${line} // primary key` : line);
|
|
79
|
+
}
|
|
80
|
+
const lines = [
|
|
81
|
+
m.documentation ? `// ${name} - ${m.documentation.replaceAll('\n', '\n// ')}` : `// ${name} represents the ${table} table.`,
|
|
82
|
+
...indexComments(m),
|
|
83
|
+
`type ${name} struct {`,
|
|
84
|
+
...columns,
|
|
85
|
+
];
|
|
86
|
+
if (relations.length > 0)
|
|
87
|
+
lines.push('', '\t// Relations (not persisted, populate manually)', ...relations);
|
|
88
|
+
lines.push('}');
|
|
89
|
+
if (cfg.tableNameMethod) {
|
|
90
|
+
lines.push('', `// TableName returns the name of the table ${name} rows are stored in.`);
|
|
91
|
+
lines.push(`func (${name}) TableName() string { return ${JSON.stringify(table)} }`);
|
|
92
|
+
}
|
|
93
|
+
return { code: lines.join('\n'), imports };
|
|
94
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { toPascalCase } from './naming.js';
|
|
2
|
+
// Go types are written the way the `types` option takes them: a builtin as is, anything else as
|
|
3
|
+
// `<import path>.<Name>`.
|
|
4
|
+
const SCALARS = {
|
|
5
|
+
String: 'string',
|
|
6
|
+
Int: 'int32',
|
|
7
|
+
BigInt: 'int64',
|
|
8
|
+
Float: 'float64',
|
|
9
|
+
Boolean: 'bool',
|
|
10
|
+
DateTime: 'time.Time',
|
|
11
|
+
Json: 'encoding/json.RawMessage',
|
|
12
|
+
Bytes: '[]byte',
|
|
13
|
+
Decimal: 'string',
|
|
14
|
+
};
|
|
15
|
+
const NATIVE_TYPES = {
|
|
16
|
+
Uuid: 'github.com/google/uuid.UUID',
|
|
17
|
+
JsonB: 'encoding/json.RawMessage',
|
|
18
|
+
Json: 'encoding/json.RawMessage',
|
|
19
|
+
Text: 'string',
|
|
20
|
+
VarChar: 'string',
|
|
21
|
+
Char: 'string',
|
|
22
|
+
Citext: 'string',
|
|
23
|
+
SmallInt: 'int32',
|
|
24
|
+
Integer: 'int32',
|
|
25
|
+
Int2: 'int32',
|
|
26
|
+
Int4: 'int32',
|
|
27
|
+
BigInt: 'int64',
|
|
28
|
+
Int8: 'int64',
|
|
29
|
+
Real: 'float32',
|
|
30
|
+
Float4: 'float32',
|
|
31
|
+
DoublePrecision: 'float64',
|
|
32
|
+
Float8: 'float64',
|
|
33
|
+
Decimal: 'string',
|
|
34
|
+
Numeric: 'string',
|
|
35
|
+
Money: 'string',
|
|
36
|
+
Timestamp: 'time.Time',
|
|
37
|
+
Timestamptz: 'time.Time',
|
|
38
|
+
Date: 'time.Time',
|
|
39
|
+
Time: 'time.Time',
|
|
40
|
+
Timetz: 'time.Time',
|
|
41
|
+
ByteA: '[]byte',
|
|
42
|
+
};
|
|
43
|
+
const FALLBACK = 'encoding/json.RawMessage';
|
|
44
|
+
/** The package name Go gives an import path: its last element, ignoring a major-version suffix. */
|
|
45
|
+
function packageName(importPath) {
|
|
46
|
+
const parts = importPath.split('/');
|
|
47
|
+
let last = parts.pop() ?? importPath;
|
|
48
|
+
if (/^v\d+$/.test(last) && parts.length > 0)
|
|
49
|
+
last = parts.pop() ?? last;
|
|
50
|
+
return last.replace(/\.v\d+$/, '');
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Parses a type spec: `string`, `[]byte` and `map[string]any` are used as written, while
|
|
54
|
+
* `github.com/google/uuid.UUID` becomes `uuid.UUID` plus an import. A qualified type may be
|
|
55
|
+
* prefixed with `*` or `[]`.
|
|
56
|
+
*/
|
|
57
|
+
export function parseGoType(spec) {
|
|
58
|
+
const match = /^((?:\[\]|\*)*)([\w./~-]+)\.([A-Za-z_]\w*)$/.exec(spec);
|
|
59
|
+
if (!match)
|
|
60
|
+
return { expr: spec, imports: [] };
|
|
61
|
+
const [, prefix = '', importPath = '', name = ''] = match;
|
|
62
|
+
return { expr: `${prefix}${packageName(importPath)}.${name}`, imports: [importPath] };
|
|
63
|
+
}
|
|
64
|
+
/** The Go type of a field before optional, list and relation wrapping. */
|
|
65
|
+
export function baseType(field, cfg) {
|
|
66
|
+
if (field.kind === 'object' || field.kind === 'enum') {
|
|
67
|
+
return { expr: toPascalCase(field.type, cfg.initialisms), imports: [] };
|
|
68
|
+
}
|
|
69
|
+
const native = field.nativeType?.[0];
|
|
70
|
+
const spec = (native ? cfg.types[`db.${native}`] : undefined) ??
|
|
71
|
+
cfg.types[field.type] ??
|
|
72
|
+
(native ? NATIVE_TYPES[native] : undefined) ??
|
|
73
|
+
SCALARS[field.type] ??
|
|
74
|
+
FALLBACK;
|
|
75
|
+
return parseGoType(spec);
|
|
76
|
+
}
|
|
77
|
+
/** The Go type of a model struct field. */
|
|
78
|
+
export function fieldType(field, cfg) {
|
|
79
|
+
const base = baseType(field, cfg);
|
|
80
|
+
if (field.isList)
|
|
81
|
+
return { expr: `[]${base.expr}`, imports: base.imports };
|
|
82
|
+
if (field.kind === 'object')
|
|
83
|
+
return { expr: `*${base.expr}`, imports: base.imports };
|
|
84
|
+
if (field.isRequired)
|
|
85
|
+
return base;
|
|
86
|
+
if (cfg.optionalType === 'pointer')
|
|
87
|
+
return { expr: `*${base.expr}`, imports: base.imports };
|
|
88
|
+
const wrapper = parseGoType(cfg.optionalType);
|
|
89
|
+
return { expr: `${wrapper.expr}[${base.expr}]`, imports: [...base.imports, ...wrapper.imports] };
|
|
90
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,54 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tredy-ai/prisma-go-generator",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Prisma generator that emits Go structs and enums from your Prisma schema",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Tredy AI, Inc.",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/tredy-ai/prisma-go-generator.git"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"prisma",
|
|
13
|
+
"generator",
|
|
14
|
+
"prisma-generator",
|
|
15
|
+
"go",
|
|
16
|
+
"golang",
|
|
17
|
+
"codegen",
|
|
18
|
+
"structs"
|
|
19
|
+
],
|
|
20
|
+
"type": "module",
|
|
21
|
+
"bin": {
|
|
22
|
+
"prisma-go-generator": "bin/prisma-go-generator.js"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"bin",
|
|
26
|
+
"dist",
|
|
27
|
+
"NOTICE"
|
|
28
|
+
],
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public",
|
|
31
|
+
"@tredy-ai:registry": "https://registry.npmjs.org/"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=20.19"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc -p tsconfig.build.json",
|
|
38
|
+
"prepack": "tsc -p tsconfig.build.json",
|
|
39
|
+
"typecheck": "tsc --noEmit",
|
|
40
|
+
"test": "tsc -p tsconfig.build.json && vitest run"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@prisma/generator-helper": "^7.0.0"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"prisma": ">=7.0.0"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "24.10.1",
|
|
50
|
+
"prisma": "7.10.1",
|
|
51
|
+
"typescript": "7.0.2",
|
|
52
|
+
"vitest": "5.1.0"
|
|
53
|
+
}
|
|
54
|
+
}
|