Skip to content

Commit

Permalink
Add bobgen-prisma driver to generate models/factory from a prisma schema
Browse files Browse the repository at this point in the history
  • Loading branch information
stephenafamo committed Jan 4, 2023
1 parent 28efc5b commit 7d4ccce
Show file tree
Hide file tree
Showing 15 changed files with 5,988 additions and 6 deletions.
9 changes: 5 additions & 4 deletions gen/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,12 @@ like [sql-migrate](https://github.com/rubenv/sql-migrate) or some other migratio
There are no immediate plans for this.
The many edge cases make this extremely complex especially when relationships and cascading soft deletes are considered.

## Supported Databases
## Avaialable Drivers

| Database | Driver Location |
| ----------------- | --------------- |
| PostgreSQL | [LINK](bobgen-psql) |
| Database | Driver Location |
| ----------------- | --------------------- |
| PostgreSQL | [LINK](bobgen-psql) |
| Prisma | [LINK](bobgen-prisma) |

## Configuration

Expand Down
1 change: 1 addition & 0 deletions gen/bobgen-prisma/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.ignore
154 changes: 154 additions & 0 deletions gen/bobgen-prisma/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# Bob Gen for Prisma

Generates an ORM based on a prisma schema file

## How to Generate

1. Initialise a new Go project.
The generator must be run from inside a go module, so either the current directory or a parent directory should contain a `go.mod` file.
If you don't have a Go project yet, initialise one using Go modules:

```shell script
mkdir demo && cd demo
go mod init demo
```

1. Get the Bob Prisma Generator. Install the Go module in your project by running:

```shell script
go get github.com/stephenafamo/bob/gen/bobgen-prisma
```

1. Prepare your database schema in a `schema.prisma` file. For example, a simple schema with a postgres database and the Bob Prisma generator with two models would look like this:

```prisma
datasource db {
// only postgresql is supported for now
provider = "postgresql"
url = env("DATABASE_URL")
}
generator db {
provider = "go run github.com/stephenafamo/bob/gen/bobgen-prisma"
output = "./prisma" // Optional. Default: ./db
configFile = "./config/bobgen.yml" // Optional. Default ./bobgen.yaml
}
model Post {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
created_at DateTime @default(now()) @db.Timestamptz(6)
updated_at DateTime @db.Timestamptz(6)
title String
published Boolean
desc String?
}
```
1. Generate the models using `prisma generate`
## How to Use
[Detailed documentation on how to use the Bob ORM can be found here](..)
A small taste
```go
package main
import (
"context"
"encoding/json"
"fmt"
db "random/prisma/bob"
"github.com/aarondl/opt/omit"
"github.com/aarondl/opt/omitnull"
)
func main() {
if err := run(context.Background()); err != nil {
panic(err)
}
}
func run(ctx context.Context) error {
client, err := db.New()
if err != nil {
return err
}
defer client.Close()
// create a post
createdPost, err := db.PostsTable.Insert(ctx, client, &db.OptionalPost{
Title: omit.From("Hi from Prisma!"),
Published: omit.From(true),
Desc: omitnull.From("Prisma is a database toolkit and makes databases easy."),
})
if err != nil {
return err
}
result, _ := json.MarshalIndent(createdPost, "", " ")
fmt.Printf("created post: %s\n", result)
// find a single post
post, err := db.FindPost(ctx, client, createdPost.ID)
if err != nil {
return err
}
result, _ = json.MarshalIndent(post, "", " ")
fmt.Printf("post: %s\n", result)
// For optional/nullable values, the field will have a generic null wrapper
// with some helpful methods
if post.Desc.IsNull() {
return fmt.Errorf("post's description is null")
}
fmt.Printf("The posts's description is: %s\n", post.Desc.MustGet())
return nil
}
```
[Detailed documentation on how to use the Bob ORM can be found here](..)
## Driver Configuration
The configuration for the prisma driver must all be prefixed by the driver name.
You must use a configuration file or environment variables for configuring the database driver.
In the configuration file for prisma for example you would do:
```yaml
prisma:
schema: "public"
```
When you use an environment variable it must also be prefixed by the driver
name:
```sh
PRISMA_SCHEMA="public"
```
The values that exist for the drivers:
| Name | Required | Prisma Default |
| --------- | --------- | -------------- |
| schema | no | "public" |
| whitelist | no | [] |
| blacklist | no | [] |
Example of whitelist/blacklist:
```yaml
prisma:
# Removes migrations table, the name column from the addresses table, and
# secret_col of any table from being generated. Foreign keys that reference tables
# or columns that are no longer generated because of whitelists or blacklists may
# cause problems.
blacklist: ["migrations", "addresses.name", "*.secret_col"]
```
106 changes: 106 additions & 0 deletions gen/bobgen-prisma/driver/keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package driver

import (
"fmt"
"strings"

"github.com/stephenafamo/bob/gen/drivers"
)

func (d *Driver) Constraints(colFilter drivers.ColumnFilter) (drivers.DBConstraints, error) {
ret := drivers.DBConstraints{
PKs: map[string]*drivers.Constraint{},
FKs: map[string][]drivers.ForeignKey{},
Uniques: map[string][]drivers.Constraint{},
}

allfilter := colFilter["*"]

for _, model := range d.config.Datamodel.Models {
filter := colFilter[model.Name]
include := append(allfilter.Include, filter.Include...)
exclude := append(allfilter.Exclude, filter.Exclude...)

// If it is a composite primary key defined on the model
if len(model.PrimaryKey.Fields) > 0 {
shouldSkip := false
cols := make([]string, len(model.PrimaryKey.Fields))

for i, f := range model.PrimaryKey.Fields {
if skip(f, include, exclude) {
shouldSkip = true
}
cols[i] = f
}

if !shouldSkip {
pkName := model.PrimaryKey.Name
if pkName == "" {
pkName = "pk_" + model.Name
}
ret.PKs[model.Name] = &drivers.Constraint{
Name: pkName,
Columns: cols,
}
}
}

for _, unique := range model.UniqueIndexes {
shouldSkip := false
cols := make([]string, len(unique.Fields))

for i, f := range unique.Fields {
if skip(f, include, exclude) {
shouldSkip = true
}
cols[i] = f
}

if !shouldSkip {
keyName := unique.InternalName
if keyName == "" {
keyName = fmt.Sprintf("unique_%s_%s", model.Name, strings.Join(cols, "_"))
}

ret.Uniques[model.Name] = append(ret.Uniques[model.Name], drivers.Constraint{
Name: keyName,
Columns: cols,
})
}
}

// If one of the fields has an @id attribute
for _, field := range model.Fields {
if skip(field.Name, include, exclude) {
continue
}

if field.IsID {
ret.PKs[model.Name] = &drivers.Constraint{
Name: "pk_" + model.Name,
Columns: []string{field.Name},
}
}

if field.IsUnique {
ret.Uniques[model.Name] = append(ret.Uniques[model.Name], drivers.Constraint{
Name: fmt.Sprintf("unique_%s_%s", model.Name, field.Name),
Columns: []string{field.Name},
})
}

if field.Kind == FieldKindObject && len(field.RelationFromFields) > 0 {
ret.FKs[model.Name] = append(ret.FKs[model.Name], drivers.ForeignKey{
Constraint: drivers.Constraint{
Name: field.RelationName,
Columns: field.RelationFromFields,
},
ForeignTable: field.Type,
ForeignColumns: field.RelationToFields,
})
}
}
}

return ret, nil
}
Loading

0 comments on commit 7d4ccce

Please sign in to comment.