├── .gitignore ├── .golint_exclude ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── buildsources.go ├── definitions.go ├── doc.go ├── dsl ├── doc.go ├── dsl_suite_test.go ├── relationalfield.go ├── relationalfield_test.go ├── relationalmodel.go ├── relationalmodel_test.go ├── relationalstore.go ├── relationalstore_test.go ├── runner.go ├── storagegroup.go └── storagegroup_test.go ├── generator.go ├── init.go ├── manytomany.go ├── mapdefinition.go ├── relationalfield.go ├── relationalfield_test.go ├── relationalmodel.go ├── relationalmodel_test.go ├── relationalstore.go ├── relationalstore_test.go ├── storagegroup.go ├── storagegroup_test.go ├── validate.go └── writers.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | public/ 3 | .DS_Store 4 | go.sum 5 | -------------------------------------------------------------------------------- /.golint_exclude: -------------------------------------------------------------------------------- 1 | ^example 2 | ^example/app 3 | ^example/client 4 | ^example/swagger 5 | ^example/design 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial # For new TLS version compatible with bitbucket 2 | language: go 3 | go: 4 | - 1.11.x 5 | sudo: false 6 | install: 7 | - export PATH=$PATH:$HOME/gopath/bin 8 | - curl https://bitbucket.org/birkenfeld/pygments-main/get/2.1.tar.gz -L -o 2.1.tar.gz 9 | - mkdir -p ${HOME}/bin 10 | - tar -xf 2.1.tar.gz 11 | - mv birkenfeld-pygments-main-34530db252d3/* ${HOME}/bin 12 | - rm -rf birkenfeld-pygments-main-34530db252d3 13 | script: 14 | - export PATH=${PATH}:${HOME}/bin 15 | - make 16 | - make docs 17 | notifications: 18 | slack: 19 | secure: bMYXaoSEGoNdqR0t1VnMAv/4V9PSOhEWyekdJM7p9WmKjJi2yKy0k77uRmwf+5Mrz5GLs3CkZnDha/8cSFld3KEN9SC6QYmIBF/1Pd/5mKHFQOI81i7sTlhrdMv897+6sofEtbBNq1jffhVGVttbMrMWwCTNZu0NrCGBVsDmb44= 20 | deploy: 21 | provider: gcs 22 | access_key_id: GOOGDIIIVPY7O6DG3PSZ 23 | secret_access_key: 24 | secure: JRgLO+aCMRgMEQHujG9Xjxez6CmTiSxE14dNGc+iG16jcgUjmRnyY1adNcp/gxzmi274qRC8OYT10+NVNVRl4lK7HTtCcWuCWOI3N1o77RZqNA+e2k4GrNrsAmfnlbUu2Eg8XCrlQfctwJmN6058oQ8r/hdq36JUk0xDPgA8hws= 25 | bucket: goa.design 26 | local-dir: public/reference 27 | upload-dir: reference 28 | skip-cleanup: true 29 | acl: public-read 30 | cache-control: max-age=300 31 | on: 32 | repo: goadesign/gorma 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Brian Ketelsen 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #! /usr/bin/make 2 | # 3 | # Makefile for gorma 4 | # 5 | # Targets: 6 | # - "lint" runs the linter and checks the code format using goimports 7 | # - "test" runs the tests 8 | # 9 | # Meta targets: 10 | # - "all" is the default target, it runs all the targets in the order above. 11 | # 12 | DIRS=$(shell go list -f {{.Dir}} ./... | grep -v /example) 13 | DEPEND=\ 14 | bitbucket.org/pkg/inflect \ 15 | github.com/goadesign/goa/... \ 16 | github.com/goadesign/goa.design/tools/mdc \ 17 | github.com/golang/lint/golint \ 18 | github.com/onsi/ginkgo \ 19 | github.com/onsi/ginkgo/ginkgo \ 20 | github.com/onsi/gomega \ 21 | github.com/jinzhu/inflection \ 22 | github.com/kr/pretty \ 23 | golang.org/x/tools/cmd/goimports 24 | 25 | .PHONY: goagen 26 | 27 | all: depend lint test 28 | 29 | docs: 30 | @go get -v github.com/spf13/hugo 31 | @cd /tmp && git clone https://github.com/goadesign/goa.design && cd goa.design && rm -rf content/reference public && make docs && hugo 32 | @rm -rf public 33 | @mv /tmp/goa.design/public public 34 | @rm -rf /tmp/goa.design 35 | 36 | depend: 37 | @export GO111MODULE=on && go get -v github.com/goadesign/goa.design/tools/godoc2md 38 | @go get -v $(DEPEND) 39 | @go install $(DEPEND) 40 | 41 | 42 | lint: 43 | @for d in $(DIRS) ; do \ 44 | if [ "`goimports -l $$d/*.go | grep -vf .golint_exclude | tee /dev/stderr`" ]; then \ 45 | echo "^ - Repo contains improperly formatted go files" && echo && exit 1; \ 46 | fi \ 47 | done 48 | @if [ "`golint ./... | grep -vf .golint_exclude | tee /dev/stderr`" ]; then \ 49 | echo "^ - Lint errors!" && echo && exit 1; \ 50 | fi 51 | 52 | 53 | test: 54 | @ginkgo -r --failOnPending --race -skipPackage vendor 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # gorma 3 | Gorma is a storage generator for [goa](http://goa.design). 4 | 5 | >Note: Gorma is not compatible with Goa v2 or v3 and requires [v1](https://github.com/goadesign/goa/tree/v1). 6 | 7 | [![GoDoc](https://godoc.org/github.com/goadesign/gorma?status.svg)](http://godoc.org/github.com/goadesign/gorma) [![Build Status](https://travis-ci.org/goadesign/gorma.svg?branch=master)](https://travis-ci.org/goadesign/gorma) [![Go Report Card](https://goreportcard.com/badge/github.com/goadesign/gorma)](https://goreportcard.com/report/github.com/goadesign/gorma) 8 | 9 | ## Table of Contents 10 | 11 | - [Purpose](#purpose) 12 | - [Opinionated](#opinionated) 13 | - [Translations](#translations) 14 | - [Use](#use) 15 | 16 | 17 | ## Purpose 18 | Gorma uses a custom `goa` DSL to generate a working storage system for your API. 19 | 20 | 21 | ## Opinionated 22 | Gorma generates Go code that uses [gorm](https://github.com/jinzhu/gorm) to access your database, therefore it is quite opinionated about how the data access layer is generated. 23 | 24 | By default, a primary key field is created as type `int` with name ID. Also Gorm's magic date stamp fields `created_at`, `updated_at` and `deleted_at` are created. Override this behavior with the Automatic* DSL functions on the Store. 25 | 26 | 27 | ## Translations 28 | Use the `BuildsFrom` and `RendersTo` DSL to have Gorma generate translation functions to translate your model 29 | to Media Types and from Payloads (User Types). If you don't have any complex business logic in your controllers, this makes a typical controller function 3-4 lines long. 30 | 31 | ## Use 32 | Write a storage definition using DSL from the `dsl` package. Example: 33 | 34 | ```go 35 | var sg = StorageGroup("MyStorageGroup", func() { 36 | Description("This is the global storage group") 37 | Store("mysql", gorma.MySQL, func() { 38 | Description("This is the mysql relational store") 39 | Model("Bottle", func() { 40 | BuildsFrom(func() { 41 | Payload("myresource","actionname") // e.g. "bottle", "create" resource definition 42 | }) 43 | 44 | RendersTo(Bottle) // a Media Type definition 45 | Description("This is the bottle model") 46 | 47 | Field("ID", gorma.Integer, func() { // Required for CRUD getters to take a PK argument! 48 | PrimaryKey() 49 | Description("This is the ID PK field") 50 | }) 51 | 52 | Field("Vintage", gorma.Integer, func() { 53 | SQLTag("index") // Add an index 54 | }) 55 | 56 | Field("CreatedAt", gorma.Timestamp) 57 | Field("UpdatedAt", gorma.Timestamp) // Shown for demonstration 58 | Field("DeletedAt", gorma.NullableTimestamp) // These are added by default 59 | }) 60 | }) 61 | }) 62 | ``` 63 | 64 | See the [`dsl` GoDoc](https://godoc.org/github.com/goadesign/gorma/dsl) for all the details and options. 65 | 66 | From the root of your application, issue the `goagen` command as follows: 67 | 68 | ``` 69 | $ goagen --design=github.com/gopheracademy/congo/design gen --pkg-path=github.com/goadesign/gorma 70 | ``` 71 | 72 | Be sure to replace `github.com/gopheracademy/congo/design` with the design package of your `goa` application. 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /buildsources.go: -------------------------------------------------------------------------------- 1 | package gorma 2 | 3 | import "fmt" 4 | 5 | // NewBuildSource returns an initialized BuildSource 6 | func NewBuildSource() *BuildSource { 7 | bs := &BuildSource{} 8 | return bs 9 | } 10 | 11 | // Context returns the generic definition name used in error messages. 12 | func (f *BuildSource) Context() string { 13 | if f.BuildSourceName != "" { 14 | return fmt.Sprintf("BuildSource %#v", f.BuildSourceName) 15 | } 16 | return "unnamed BuildSource" 17 | } 18 | 19 | // DSL returns this object's DSL. 20 | func (f *BuildSource) DSL() func() { 21 | return f.DefinitionDSL 22 | } 23 | -------------------------------------------------------------------------------- /definitions.go: -------------------------------------------------------------------------------- 1 | package gorma 2 | 3 | import ( 4 | "github.com/goadesign/goa/design" 5 | "github.com/goadesign/goa/dslengine" 6 | ) 7 | 8 | // RelationalStorageType is the type of database. 9 | type RelationalStorageType string 10 | 11 | // FieldType is the storage data type for a database field. 12 | type FieldType string 13 | 14 | // StorageGroupDefinition is the parent configuration structure for Gorma definitions. 15 | type StorageGroupDefinition struct { 16 | dslengine.Definition 17 | DefinitionDSL func() 18 | Name string 19 | Description string 20 | RelationalStores map[string]*RelationalStoreDefinition 21 | } 22 | 23 | // RelationalStoreDefinition is the parent configuration structure for Gorm relational model definitions. 24 | type RelationalStoreDefinition struct { 25 | dslengine.Definition 26 | DefinitionDSL func() 27 | Name string 28 | Description string 29 | Parent *StorageGroupDefinition 30 | Type RelationalStorageType 31 | RelationalModels map[string]*RelationalModelDefinition 32 | NoAutoIDFields bool 33 | NoAutoTimestamps bool 34 | NoAutoSoftDelete bool 35 | } 36 | 37 | // RelationalModelDefinition implements the storage of a domain model into a 38 | // table in a relational database. 39 | type RelationalModelDefinition struct { 40 | dslengine.Definition 41 | *design.UserTypeDefinition 42 | DefinitionDSL func() 43 | ModelName string 44 | Description string 45 | GoaType *design.MediaTypeDefinition 46 | Parent *RelationalStoreDefinition 47 | BuiltFrom map[string]*design.UserTypeDefinition 48 | BuildSources []*BuildSource 49 | RenderTo map[string]*design.MediaTypeDefinition 50 | BelongsTo map[string]*RelationalModelDefinition 51 | HasMany map[string]*RelationalModelDefinition 52 | HasOne map[string]*RelationalModelDefinition 53 | ManyToMany map[string]*ManyToManyDefinition 54 | Alias string // gorm:tablename 55 | Cached bool 56 | CacheDuration int 57 | Roler bool 58 | DynamicTableName bool 59 | SQLTag string 60 | RelationalFields map[string]*RelationalFieldDefinition 61 | PrimaryKeys []*RelationalFieldDefinition 62 | many2many []string 63 | } 64 | 65 | // BuildSource stores the BuildsFrom sources 66 | // for parsing. 67 | type BuildSource struct { 68 | dslengine.Definition 69 | DefinitionDSL func() 70 | Parent *RelationalModelDefinition 71 | BuildSourceName string 72 | } 73 | 74 | // MapDefinition represents field mapping to and from 75 | // Gorma models. 76 | type MapDefinition struct { 77 | RemoteType *design.UserTypeDefinition 78 | RemoteField string 79 | } 80 | 81 | // MediaTypeAdapterDefinition represents the transformation of a 82 | // Goa media type into a Gorma Model. 83 | // 84 | // Unimplemented at this time. 85 | type MediaTypeAdapterDefinition struct { 86 | dslengine.Definition 87 | DefinitionDSL func() 88 | Name string 89 | Description string 90 | Left *design.MediaTypeDefinition 91 | Right *RelationalModelDefinition 92 | } 93 | 94 | // UserTypeAdapterDefinition represents the transformation of a Goa 95 | // user type into a Gorma Model. 96 | // 97 | // Unimplemented at this time. 98 | type UserTypeAdapterDefinition struct { 99 | dslengine.Definition 100 | DefinitionDSL func() 101 | Name string 102 | Description string 103 | Left *RelationalModelDefinition 104 | Right *RelationalModelDefinition 105 | } 106 | 107 | // PayloadAdapterDefinition represents the transformation of a Goa 108 | // Payload (which is really a UserTypeDefinition) 109 | // into a Gorma model. 110 | // 111 | // Unimplemented at this time. 112 | type PayloadAdapterDefinition struct { 113 | dslengine.Definition 114 | DefinitionDSL func() 115 | Name string 116 | Description string 117 | Left *design.UserTypeDefinition 118 | Right *RelationalModelDefinition 119 | } 120 | 121 | // RelationalFieldDefinition represents 122 | // a field in a relational database. 123 | type RelationalFieldDefinition struct { 124 | dslengine.Definition 125 | DefinitionDSL func() 126 | Parent *RelationalModelDefinition 127 | a *design.AttributeDefinition 128 | FieldName string 129 | TableName string 130 | Datatype FieldType 131 | SQLTag string 132 | DatabaseFieldName string // gorm:column 133 | Description string 134 | Nullable bool 135 | PrimaryKey bool 136 | Timestamp bool 137 | Size int // string field size 138 | BelongsTo string 139 | HasOne string 140 | HasMany string 141 | Many2Many string 142 | Mappings map[string]*MapDefinition 143 | } 144 | 145 | // ManyToManyDefinition stores information about a ManyToMany 146 | // relationship between two domain objects. 147 | type ManyToManyDefinition struct { 148 | dslengine.Definition 149 | DefinitionDSL func() 150 | Left *RelationalModelDefinition 151 | Right *RelationalModelDefinition 152 | RelationshipName string // ?? 153 | DatabaseField string 154 | } 155 | 156 | // StoreIterator is a function that iterates over Relational Stores in a 157 | // StorageGroup. 158 | type StoreIterator func(m *RelationalStoreDefinition) error 159 | 160 | // ModelIterator is a function that iterates over Models in a 161 | // RelationalStore. 162 | type ModelIterator func(m *RelationalModelDefinition) error 163 | 164 | // FieldIterator is a function that iterates over Fields 165 | // in a RelationalModel. 166 | type FieldIterator func(m *RelationalFieldDefinition) error 167 | 168 | // BuildSourceIterator is a function that iterates over Fields 169 | // in a RelationalModel. 170 | type BuildSourceIterator func(m *BuildSource) error 171 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package gorma is a plugin generator for Goa (http://goa.design). 3 | See the documentation in the `dsl` package for details 4 | on how to create a definition for your API. 5 | 6 | The `example` folder contains an example Goa design package. 7 | The `models.go` file is the Gorma definition, which is responsible 8 | for generating all the files in the `example\models` folder. 9 | 10 | See specific documentation in the `dsl` package. 11 | */ 12 | package gorma 13 | -------------------------------------------------------------------------------- /dsl/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package dsl uses the Goa DSL engine to generate a data storage layer 3 | for your Goa API. 4 | 5 | Using a few DSL definitions you can extend the Goa API to include 6 | database persistence. 7 | 8 | An example: 9 | 10 | var sg = StorageGroup("MyStorageGroup", func() { 11 | Description("This is the global storage group") 12 | Store("mysql", gorma.MySQL, func() { 13 | Description("This is the mysql relational store") 14 | Model("Bottle", func() { 15 | BuildsFrom(func() { 16 | Payload("myresource","actionname") // e.g. "bottle", "create" resource definition 17 | }) 18 | RendersTo(Bottle) // a Media Type definition 19 | Description("This is the bottle model") 20 | Field("ID", gorma.Integer, func() { // redundant 21 | PrimaryKey() 22 | Description("This is the ID PK field") 23 | }) 24 | Field("Vintage", gorma.Integer, func() { 25 | SQLTag("index") // Add an index 26 | }) 27 | Field("CreatedAt", gorma.Timestamp) 28 | Field("UpdatedAt", gorma.Timestamp) // Shown for demonstration 29 | Field("DeletedAt", gorma.NullableTimestamp) // These are added by default 30 | }) 31 | }) 32 | }) 33 | 34 | Gorma uses Gorm (https://github.com/jinzhu/gorm) for database access. Gorm was chosen 35 | as the best of the 'light-ORM' libraries available for Go. It does the mundane work and 36 | allows you to do anything manually if you choose. 37 | 38 | The base Gorma definition is a `StorageGroup` which represents all the storage needs for an 39 | application. A StorageGroup will contain one or more `Store`, which represends a database or 40 | other persistence mechanism. Gorma supports all the databases that Gorm supports, and 41 | it is possible in the future to support others -- like Key/Value stores. 42 | 43 | Every `Store` will have one or more `Model`s which maps a Go structure to a table in the database. 44 | Use the `BuildsFrom` and `RendersTo` DSL to build generated functions to convert the model to Media 45 | Type definitions and from User Type definitions. 46 | 47 | A `Model` will contain one or more fields. Gorma will use the `BuildsFrom` definition to populate 48 | a base set of fields. Custom DSL is provided to add additional fields: 49 | 50 | Each table will likely want a primary key. Gorma automatically adds one to your table called "ID" if 51 | there isn't one already. Gorma supports Integer primary keys currently, but support for UUID and string 52 | primary keys is in the plan for the future. [github](https://github.com/goadesign/gorma/issues/57) 53 | 54 | In the event that the `BuildsFrom` types don't contain all the fields that you want to include in your 55 | model, you can add extra fields using the `Field` DSL: 56 | 57 | Field("foo", gorma.String, func() { 58 | Description("This is the foo field") 59 | }) 60 | 61 | You can also specify modifications to fields that you know will be inherited from the `BuildsFrom` DSL. For 62 | example if the type specified in `BuildsFrom` contains a field called `Author` and you want to ensure that 63 | it gets indexed, you can specify the field explicitly and add a `SQLTag` declaration: 64 | 65 | Field("author", gorma.String, func() { 66 | SQLTag("index") 67 | }) 68 | 69 | In general the only time you need to declare fields explicitly is if you want to modify the type or attributes 70 | of the fields that are inherited from the `BuildsFrom` type, or if you want to add extra fields not included 71 | in the `BuildsFrom` types. 72 | 73 | You may specify more than one `BuildsFrom` type. 74 | 75 | You can control naming between the `BuildsFrom` and `RendersTo` models by using the `MapsTo` and `MapsFrom` DSL: 76 | 77 | Field("Title", func(){ 78 | MapsFrom(UserPayload, "position") 79 | }) 80 | 81 | This creates a field in the Gorma model called "Title", which is populated from the "position" field in the UserPayload. 82 | 83 | The `MapsTo` DSL can be used similarly to change output field mapping to Media Types. 84 | 85 | 86 | Gorma generates all the helpers you need to translate to and from the Goa types (media types and payloads). 87 | This makes wiring up your Goa controllers almost too easy to be considered programming. 88 | 89 | */ 90 | package dsl 91 | -------------------------------------------------------------------------------- /dsl/dsl_suite_test.go: -------------------------------------------------------------------------------- 1 | package dsl_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestDsl(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Dsl Suite") 13 | } 14 | -------------------------------------------------------------------------------- /dsl/relationalfield.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "unicode" 7 | 8 | "github.com/goadesign/goa/design" 9 | "github.com/goadesign/goa/dslengine" 10 | "github.com/goadesign/goa/goagen/codegen" 11 | "github.com/goadesign/gorma" 12 | ) 13 | 14 | // DatabaseFieldName allows for customization of the column name 15 | // by seting the struct tags. This is necessary to create correlate 16 | // non standard column naming conventions in gorm. 17 | func DatabaseFieldName(name string) { 18 | if f, ok := relationalFieldDefinition(true); ok { 19 | f.DatabaseFieldName = name 20 | } 21 | } 22 | 23 | // Field is a DSL definition for a field in a Relational Model. 24 | // Parameter Options: 25 | // 26 | // // A field called "Title" with default type `String`. 27 | // Field("Title") 28 | // 29 | // // Explicitly specify the type. 30 | // Field("Title", gorma.String) 31 | // 32 | // // "Title" field, as `String` with other DSL included. 33 | // Field("Title", func(){... other field level dsl ...}) 34 | // 35 | // // All options specified: name, type and dsl. 36 | // Field("Title", gorma.String, func(){... other field level dsl ...}) 37 | func Field(name string, args ...interface{}) { 38 | name = codegen.Goify(name, true) 39 | name = SanitizeFieldName(name) 40 | fieldType, dsl := parseFieldArgs(args...) 41 | if s, ok := relationalModelDefinition(true); ok { 42 | if s.RelationalFields == nil { 43 | s.RelationalFields = make(map[string]*gorma.RelationalFieldDefinition) 44 | } 45 | field, ok := s.RelationalFields[name] 46 | if !ok { 47 | field = gorma.NewRelationalFieldDefinition() 48 | field.FieldName = name 49 | field.DefinitionDSL = dsl 50 | field.Parent = s 51 | field.Datatype = fieldType 52 | } else { 53 | // the field was auto-added by the model parser 54 | // so we need to update whatever we can from this new definition 55 | field.DefinitionDSL = dsl 56 | field.Datatype = fieldType 57 | 58 | } 59 | 60 | if fieldType == gorma.Timestamp { 61 | field.Timestamp = true 62 | field.Description = "timestamp" 63 | } 64 | if fieldType == gorma.NullableTimestamp { 65 | field.Timestamp = true 66 | field.Nullable = true 67 | field.Description = "nullable timestamp (soft delete)" 68 | } 69 | field.DatabaseFieldName = SanitizeDBFieldName(name) 70 | 71 | s.RelationalFields[name] = field 72 | } 73 | } 74 | 75 | // MapsFrom establishes a mapping relationship between a source 76 | // Type field and this model. The source type must be a UserTypeDefinition "Type" 77 | // in goa. These are typically Payloads. 78 | func MapsFrom(utd *design.UserTypeDefinition, field string) { 79 | if f, ok := relationalFieldDefinition(true); ok { 80 | md := gorma.NewMapDefinition() 81 | md.RemoteField = field 82 | md.RemoteType = utd 83 | f.Mappings[utd.TypeName] = md 84 | 85 | } 86 | } 87 | 88 | // MapsTo establishes a mapping relationship between a field in model and 89 | // a MediaType in goa. 90 | func MapsTo(mtd *design.MediaTypeDefinition, field string) { 91 | if f, ok := relationalFieldDefinition(true); ok { 92 | md := gorma.NewMapDefinition() 93 | md.RemoteField = field 94 | md.RemoteType = mtd.UserTypeDefinition 95 | f.Mappings[mtd.UserTypeDefinition.TypeName] = md 96 | } 97 | } 98 | 99 | func fixID(s string) string { 100 | if s == "i_d" { 101 | return "id" 102 | } 103 | return s 104 | 105 | } 106 | 107 | // Nullable sets the fields nullability 108 | // A Nullable field will be stored as a pointer. A field that is 109 | // not Nullable won't be stored as a pointer. 110 | func Nullable() { 111 | if f, ok := relationalFieldDefinition(false); ok { 112 | f.Nullable = true 113 | } 114 | } 115 | 116 | // PrimaryKey establishes a field as a Primary Key by 117 | // seting the struct tags necessary to create the PK in gorm. 118 | // Valid only for `Integer` datatypes currently 119 | func PrimaryKey() { 120 | if f, ok := relationalFieldDefinition(true); ok { 121 | if f.Datatype != gorma.Integer && f.Datatype != gorma.UUID { 122 | dslengine.ReportError("Integer and UUID are the only supported Primary Key field types.") 123 | } 124 | 125 | f.PrimaryKey = true 126 | f.Nullable = false 127 | f.Description = "primary key" 128 | f.Parent.PrimaryKeys = append(f.Parent.PrimaryKeys, f) 129 | } 130 | } 131 | 132 | // SanitizeFieldName is exported for testing purposes 133 | func SanitizeFieldName(name string) string { 134 | name = codegen.Goify(name, true) 135 | if strings.HasSuffix(name, "Id") { 136 | name = strings.TrimSuffix(name, "Id") 137 | name = name + "ID" 138 | } 139 | 140 | return name 141 | } 142 | 143 | // SanitizeDBFieldName is exported for testing purposes 144 | func SanitizeDBFieldName(name string) string { 145 | name = goifyToCamelCase(name) 146 | name = codegen.SnakeCase(name) 147 | if strings.HasSuffix(name, "_i_d") { 148 | name = strings.TrimSuffix(name, "_i_d") 149 | name = name + "_id" 150 | } 151 | if name == "i_d" { 152 | name = "id" 153 | 154 | } 155 | return name 156 | } 157 | func parseFieldArgs(args ...interface{}) (gorma.FieldType, func()) { 158 | var ( 159 | fieldType gorma.FieldType 160 | dslp func() 161 | ok bool 162 | ) 163 | 164 | parseFieldType := func(expected string, index int) { 165 | if fieldType, ok = args[index].(gorma.FieldType); !ok { 166 | dslengine.InvalidArgError(expected, args[index]) 167 | } 168 | } 169 | parseDSL := func(index int, success, failure func()) { 170 | if dslp, ok = args[index].(func()); ok { 171 | success() 172 | } else { 173 | failure() 174 | } 175 | } 176 | 177 | success := func() {} 178 | 179 | switch len(args) { 180 | case 0: 181 | fieldType = gorma.NotFound 182 | case 1: 183 | parseDSL(0, success, func() { parseFieldType("DataType or func()", 0) }) 184 | case 2: 185 | parseFieldType("FieldType", 0) 186 | parseDSL(1, success, func() { dslengine.InvalidArgError("DSL", args[1]) }) 187 | 188 | default: 189 | dslengine.ReportError("too many arguments in call to Attribute") 190 | } 191 | 192 | return fieldType, dslp 193 | } 194 | 195 | // goifyToCamelCase converts the output of Goify to CamelCase, for example: "APIFoo" to "ApiFoo" 196 | func goifyToCamelCase(val string) string { 197 | if len(val) == 0 { 198 | return "" 199 | } 200 | var b bytes.Buffer 201 | var prev rune 202 | for i, v := range val { 203 | if unicode.IsUpper(v) && unicode.IsUpper(prev) { 204 | b.WriteRune(unicode.ToLower(v)) 205 | } else { 206 | if unicode.IsUpper(prev) { 207 | if i != 0 { 208 | b.Truncate(i - 1) 209 | b.WriteRune(unicode.ToUpper(prev)) 210 | } 211 | } 212 | b.WriteRune(v) 213 | } 214 | prev = v 215 | } 216 | return b.String() 217 | } 218 | -------------------------------------------------------------------------------- /dsl/relationalfield_test.go: -------------------------------------------------------------------------------- 1 | package dsl_test 2 | 3 | import ( 4 | "github.com/goadesign/gorma" 5 | gdsl "github.com/goadesign/gorma/dsl" 6 | 7 | . "github.com/goadesign/goa/design" 8 | . "github.com/goadesign/goa/dslengine" 9 | . "github.com/onsi/ginkgo" 10 | . "github.com/onsi/gomega" 11 | ) 12 | 13 | var _ = Describe("RelationalField", func() { 14 | var sgname, storename, modelname, name string 15 | var ft gorma.FieldType 16 | var dsl, modeldsl func() 17 | BeforeEach(func() { 18 | Reset() 19 | sgname = "production" 20 | dsl = nil 21 | modeldsl = nil 22 | storename = "mysql" 23 | modelname = "Users" 24 | name = "" 25 | ft = gorma.String 26 | }) 27 | 28 | JustBeforeEach(func() { 29 | 30 | modeldsl = func() { 31 | gdsl.Field(name, ft, dsl) 32 | gdsl.Field("id", gorma.Integer, dsl) // use lowercase "id" to test sanitizer 33 | gdsl.Field("API", gorma.String) 34 | gdsl.Field("url", gorma.String) 35 | gdsl.Field("APIType", gorma.String) 36 | gdsl.Field("xml_type", gorma.String) 37 | gdsl.Field("", gorma.String) 38 | gdsl.Field("MiddleName", gorma.String) 39 | gdsl.Field("CreatedAt", gorma.Timestamp) 40 | gdsl.Field("UpdatedAt", gorma.Timestamp) 41 | gdsl.Field("DeletedAt", gorma.NullableTimestamp) 42 | 43 | } 44 | gdsl.StorageGroup(sgname, func() { 45 | gdsl.Store(storename, gorma.MySQL, func() { 46 | gdsl.Model(modelname, modeldsl) 47 | }) 48 | }) 49 | Run() 50 | 51 | }) 52 | 53 | Context("with no DSL", func() { 54 | BeforeEach(func() { 55 | name = "FirstName" 56 | }) 57 | 58 | It("produces a valid Relational Field definition", func() { 59 | Ω(Design.Validate()).ShouldNot(HaveOccurred()) 60 | sg := gorma.GormaDesign 61 | rs := sg.RelationalStores[storename] 62 | rm := rs.RelationalModels[modelname] 63 | Ω(rm.RelationalFields[name].FieldName).Should(Equal(name)) 64 | }) 65 | }) 66 | 67 | Context("with an already defined Relational Field with the same name", func() { 68 | BeforeEach(func() { 69 | name = "FirstName" 70 | }) 71 | 72 | It("does not produce an error", func() { 73 | 74 | modeldsl = func() { 75 | gdsl.Field(name, ft, dsl) 76 | } 77 | 78 | Ω(Errors).Should(Not(HaveOccurred())) 79 | }) 80 | }) 81 | 82 | Context("with valid DSL", func() { 83 | JustBeforeEach(func() { 84 | Ω(Errors).ShouldNot(HaveOccurred()) 85 | Ω(Design.Validate()).ShouldNot(HaveOccurred()) 86 | }) 87 | 88 | Context("sets appropriate fields and flags", func() { 89 | const description = "description" 90 | 91 | BeforeEach(func() { 92 | name = "FirstName" 93 | dsl = func() { 94 | gdsl.Description(description) 95 | } 96 | }) 97 | 98 | It("sanitizes the ID field", func() { 99 | sg := gorma.GormaDesign 100 | rs := sg.RelationalStores[storename] 101 | rm := rs.RelationalModels[modelname] 102 | Ω(rm.RelationalFields).Should(HaveKey("ID")) 103 | }) 104 | 105 | It("sets the relational field description", func() { 106 | sg := gorma.GormaDesign 107 | rs := sg.RelationalStores[storename] 108 | rm := rs.RelationalModels[modelname] 109 | Ω(rm.RelationalFields[name].Description).Should(Equal(description)) 110 | }) 111 | 112 | It("sets the field name", func() { 113 | sg := gorma.GormaDesign 114 | rs := sg.RelationalStores[storename] 115 | rm := rs.RelationalModels[modelname] 116 | Ω(rm.RelationalFields["ID"].FieldName).Should(Equal("ID")) 117 | Ω(rm.RelationalFields["API"].FieldName).Should(Equal("API")) 118 | Ω(rm.RelationalFields["URL"].FieldName).Should(Equal("URL")) 119 | Ω(rm.RelationalFields["APIType"].FieldName).Should(Equal("APIType")) 120 | Ω(rm.RelationalFields["XMLType"].FieldName).Should(Equal("XMLType")) 121 | Ω(rm.RelationalFields[""].FieldName).Should(Equal("")) 122 | Ω(rm.RelationalFields["ID"].FieldName).Should(Equal("ID")) 123 | }) 124 | It("sets the field type", func() { 125 | sg := gorma.GormaDesign 126 | rs := sg.RelationalStores[storename] 127 | rm := rs.RelationalModels[modelname] 128 | Ω(rm.RelationalFields["ID"].Datatype).Should(Equal(gorma.Integer)) 129 | }) 130 | It("sets the pk flag", func() { 131 | sg := gorma.GormaDesign 132 | rs := sg.RelationalStores[storename] 133 | rm := rs.RelationalModels[modelname] 134 | Ω(rm.RelationalFields["ID"].PrimaryKey).Should(Equal(true)) 135 | }) 136 | It("sets has a created at field", func() { 137 | sg := gorma.GormaDesign 138 | rs := sg.RelationalStores[storename] 139 | rm := rs.RelationalModels[modelname] 140 | Ω(rm.RelationalFields["CreatedAt"].FieldName).Should(Equal("CreatedAt")) 141 | Ω(rm.RelationalFields["CreatedAt"].Datatype).Should(Equal(gorma.Timestamp)) 142 | Ω(rm.RelationalFields["CreatedAt"].Nullable).Should(Equal(false)) 143 | }) 144 | It("sets has a deleted at field", func() { 145 | sg := gorma.GormaDesign 146 | rs := sg.RelationalStores[storename] 147 | rm := rs.RelationalModels[modelname] 148 | Ω(rm.RelationalFields["DeletedAt"].FieldName).Should(Equal("DeletedAt")) 149 | Ω(rm.RelationalFields["DeletedAt"].Datatype).Should(Equal(gorma.NullableTimestamp)) 150 | Ω(rm.RelationalFields["DeletedAt"].Nullable).Should(Equal(true)) 151 | }) 152 | It("has the right number of fields", func() { 153 | sg := gorma.GormaDesign 154 | rs := sg.RelationalStores[storename] 155 | rm := rs.RelationalModels[modelname] 156 | length := len(rm.RelationalFields) 157 | Ω(length).Should(Equal(11)) 158 | }) 159 | }) 160 | 161 | }) 162 | 163 | Context("Primary Keys", func() { 164 | JustBeforeEach(func() { 165 | 166 | gdsl.StorageGroup(sgname, func() { 167 | gdsl.Store(storename, gorma.MySQL, func() { 168 | gdsl.NoAutomaticIDFields() 169 | gdsl.Model(modelname, func() { 170 | gdsl.Field(name, ft, dsl) 171 | gdsl.Field("id", gorma.Integer, dsl) // use lowercase "id" to test sanitizer 172 | gdsl.Field("MiddleName", gorma.String) 173 | gdsl.Field("CreatedAt", gorma.Timestamp) 174 | gdsl.Field("UpdatedAt", gorma.Timestamp) 175 | gdsl.Field("DeletedAt", gorma.NullableTimestamp) 176 | }) 177 | }) 178 | }) 179 | Run() 180 | 181 | Ω(Design.Validate()).ShouldNot(HaveOccurred()) 182 | }) 183 | 184 | Context("sets Primary Key flags for an integer field", func() { 185 | 186 | BeforeEach(func() { 187 | name = "random" 188 | ft = gorma.Integer 189 | dsl = func() { 190 | gdsl.PrimaryKey() 191 | } 192 | }) 193 | It("sets the pk flag", func() { 194 | sg := gorma.GormaDesign 195 | rs := sg.RelationalStores[storename] 196 | rm := rs.RelationalModels[modelname] 197 | Ω(Errors).ShouldNot(HaveOccurred()) 198 | Ω(rm.RelationalFields["ID"].PrimaryKey).Should(Equal(true)) 199 | }) 200 | }) 201 | Context("won't set Primary Key flags for string", func() { 202 | 203 | BeforeEach(func() { 204 | name = "random" 205 | dsl = func() { 206 | gdsl.PrimaryKey() 207 | } 208 | }) 209 | It("doesnt set the pk flag", func() { 210 | Ω(Errors).Should(HaveOccurred()) 211 | }) 212 | }) 213 | Context("doesn't set Primary Key flag with no PrimaryKey() dsl", func() { 214 | 215 | BeforeEach(func() { 216 | name = "random" 217 | dsl = func() { 218 | gdsl.Description("Test description") 219 | } 220 | 221 | }) 222 | It("the pk flag", func() { 223 | sg := gorma.GormaDesign 224 | rs := sg.RelationalStores[storename] 225 | rm := rs.RelationalModels[modelname] 226 | Ω(Errors).ShouldNot(HaveOccurred()) 227 | Ω(rm.RelationalFields["Random"].PrimaryKey).Should(Equal(false)) 228 | }) 229 | }) 230 | 231 | }) 232 | 233 | }) 234 | -------------------------------------------------------------------------------- /dsl/relationalmodel.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | 7 | "bitbucket.org/pkg/inflect" 8 | 9 | "github.com/goadesign/goa/design" 10 | "github.com/goadesign/goa/dslengine" 11 | "github.com/goadesign/goa/goagen/codegen" 12 | "github.com/goadesign/gorma" 13 | "github.com/jinzhu/inflection" 14 | ) 15 | 16 | // Model is the DSL that represents a Relational Model. 17 | // Model name should be Title cased. Use BuildsFrom() and RendersTo() DSL 18 | // to define the mapping between a Model and a Goa Type. 19 | // Models may contain multiple instances of the `Field` DSL to 20 | // add fields to the model. 21 | // 22 | // To control whether the ID field is auto-generated, use `NoAutomaticIDFields()` 23 | // Similarly, use NoAutomaticTimestamps() and NoAutomaticSoftDelete() to 24 | // prevent CreatedAt, UpdatedAt and DeletedAt fields from being created. 25 | func Model(name string, dsl func()) { 26 | if s, ok := relationalStoreDefinition(true); ok { 27 | var model *gorma.RelationalModelDefinition 28 | var ok bool 29 | model, ok = s.RelationalModels[name] 30 | if !ok { 31 | model = gorma.NewRelationalModelDefinition() 32 | model.ModelName = name 33 | model.DefinitionDSL = dsl 34 | model.Parent = s 35 | model.RelationalFields = make(map[string]*gorma.RelationalFieldDefinition) 36 | } else { 37 | dslengine.ReportError("Model %s already exists", name) 38 | return 39 | } 40 | s.RelationalModels[name] = model 41 | model.UserTypeDefinition.TypeName = model.ModelName 42 | // much stutter here 43 | // TODO(BJK) refactor 44 | if !s.NoAutoIDFields { 45 | field := gorma.NewRelationalFieldDefinition() 46 | field.FieldName = SanitizeFieldName("ID") 47 | field.Parent = model 48 | field.Datatype = gorma.Integer 49 | field.PrimaryKey = true 50 | field.Nullable = false 51 | field.DatabaseFieldName = SanitizeDBFieldName("ID") 52 | model.RelationalFields[field.FieldName] = field 53 | } 54 | if !s.NoAutoTimestamps { 55 | // add createdat 56 | field := gorma.NewRelationalFieldDefinition() 57 | field.FieldName = SanitizeFieldName("CreatedAt") 58 | field.Parent = model 59 | field.Datatype = gorma.Timestamp 60 | field.DatabaseFieldName = SanitizeDBFieldName("CreatedAt") 61 | model.RelationalFields[field.FieldName] = field 62 | 63 | // add updatedat 64 | field = gorma.NewRelationalFieldDefinition() 65 | field.FieldName = SanitizeFieldName("UpdatedAt") 66 | field.Parent = model 67 | field.Datatype = gorma.Timestamp 68 | field.DatabaseFieldName = SanitizeDBFieldName("UpdatedAt") 69 | model.RelationalFields[field.FieldName] = field 70 | } 71 | if !s.NoAutoSoftDelete { 72 | // Add softdelete 73 | field := gorma.NewRelationalFieldDefinition() 74 | field.FieldName = SanitizeFieldName("DeletedAt") 75 | field.Parent = model 76 | field.Nullable = true 77 | field.Datatype = gorma.NullableTimestamp 78 | field.DatabaseFieldName = SanitizeDBFieldName("DeletedAt") 79 | model.RelationalFields[field.FieldName] = field 80 | } 81 | } 82 | } 83 | 84 | // RendersTo informs Gorma that this model will need to be 85 | // rendered to a Goa type. Conversion functions 86 | // will be generated to convert to/from the model. 87 | // 88 | // Usage: RendersTo(MediaType) 89 | func RendersTo(rt interface{}) { 90 | if m, ok := relationalModelDefinition(false); ok { 91 | mts, ok := rt.(*design.MediaTypeDefinition) 92 | if ok { 93 | m.RenderTo[mts.TypeName] = mts 94 | } 95 | 96 | } 97 | } 98 | 99 | // BuildsFrom informs Gorma that this model will be populated 100 | // from a Goa UserType. Conversion functions 101 | // will be generated to convert from the payload to the model. 102 | // 103 | // Usage: BuildsFrom(YourType) 104 | // 105 | // Fields not in `YourType` that you want in your model must be 106 | // added explicitly with the `Field` DSL. 107 | func BuildsFrom(dsl func()) { 108 | if m, ok := relationalModelDefinition(false); ok { 109 | /* mts, ok := bf.(*design.UserTypeDefinition) 110 | if ok { 111 | m.BuiltFrom[mts.TypeName] = mts 112 | } else if mts, ok := bf.(*design.MediaTypeDefinition); ok { 113 | m.BuiltFrom[mts.TypeName] = mts.UserTypeDefinition 114 | } 115 | m.PopulateFromModeledType() 116 | */ 117 | bf := gorma.NewBuildSource() 118 | bf.DefinitionDSL = dsl 119 | bf.Parent = m 120 | m.BuildSources = append(m.BuildSources, bf) 121 | } 122 | 123 | } 124 | 125 | // Payload specifies the Resource and Action containing 126 | // a User Type (Payload). 127 | // Gorma will generate a conversion function for the Payload to 128 | // the Model. 129 | func Payload(r interface{}, act string) { 130 | if bs, ok := buildSourceDefinition(true); ok { 131 | var res *design.ResourceDefinition 132 | var resName string 133 | if n, ok := r.(string); ok { 134 | res = design.Design.Resources[n] 135 | resName = n 136 | } else { 137 | res, _ = r.(*design.ResourceDefinition) 138 | } 139 | if res == nil { 140 | dslengine.ReportError("There is no resource %q", resName) 141 | return 142 | } 143 | a, ok := res.Actions[act] 144 | if !ok { 145 | dslengine.ReportError("There is no action") 146 | return 147 | } 148 | payload := a.Payload 149 | 150 | // Set UTD in BuildsFrom parent context 151 | 152 | bs.Parent.BuiltFrom[payload.TypeName] = payload 153 | bs.Parent.PopulateFromModeledType() 154 | } 155 | } 156 | 157 | // BelongsTo signifies a relationship between this model and a 158 | // Parent. The Parent has the child, and the Child belongs 159 | // to the Parent. 160 | // 161 | // Usage: BelongsTo("User") 162 | func BelongsTo(parent string) { 163 | if r, ok := relationalModelDefinition(false); ok { 164 | idfield := gorma.NewRelationalFieldDefinition() 165 | idfield.FieldName = codegen.Goify(inflect.Singularize(parent), true) + "ID" 166 | idfield.Description = "Belongs To " + codegen.Goify(inflect.Singularize(parent), true) 167 | idfield.Parent = r 168 | idfield.Datatype = gorma.BelongsTo 169 | idfield.DatabaseFieldName = SanitizeDBFieldName(codegen.Goify(inflect.Singularize(parent), true) + "ID") 170 | r.RelationalFields[idfield.FieldName] = idfield 171 | bt, ok := r.Parent.RelationalModels[codegen.Goify(inflect.Singularize(parent), true)] 172 | if ok { 173 | r.BelongsTo[bt.ModelName] = bt 174 | } else { 175 | model := gorma.NewRelationalModelDefinition() 176 | model.ModelName = codegen.Goify(inflect.Singularize(parent), true) 177 | model.Parent = r.Parent 178 | r.BelongsTo[model.ModelName] = model 179 | } 180 | } 181 | } 182 | 183 | // HasOne signifies a relationship between this model and another model. 184 | // If this model HasOne(OtherModel), then OtherModel is expected 185 | // to have a ThisModelID field as a Foreign Key to this model's 186 | // Primary Key. ThisModel will have a field named OtherModel of type 187 | // OtherModel. 188 | // 189 | // Usage: HasOne("Proposal") 190 | func HasOne(child string) { 191 | if r, ok := relationalModelDefinition(false); ok { 192 | field := gorma.NewRelationalFieldDefinition() 193 | field.FieldName = codegen.Goify(inflect.Singularize(child), true) 194 | field.HasOne = child 195 | field.Description = "has one " + child 196 | field.Datatype = gorma.HasOne 197 | field.Parent = r 198 | r.RelationalFields[field.FieldName] = field 199 | bt, ok := r.Parent.RelationalModels[child] 200 | 201 | // Refactor (BJK) 202 | if ok { 203 | r.HasOne[child] = bt 204 | // create the fk field 205 | f := gorma.NewRelationalFieldDefinition() 206 | f.FieldName = codegen.Goify(inflect.Singularize(r.ModelName), true) + "ID" 207 | f.HasOne = child 208 | f.Description = "has one " + child 209 | f.Datatype = gorma.HasOneKey 210 | f.Parent = bt 211 | f.DatabaseFieldName = SanitizeDBFieldName(codegen.Goify(inflect.Singularize(r.ModelName), true) + "ID") 212 | bt.RelationalFields[f.FieldName] = f 213 | } else { 214 | model := gorma.NewRelationalModelDefinition() 215 | model.ModelName = child 216 | model.Parent = r.Parent 217 | r.HasOne[child] = model 218 | 219 | // create the fk field 220 | f := gorma.NewRelationalFieldDefinition() 221 | f.FieldName = codegen.Goify(inflect.Singularize(r.ModelName), true) + "ID" 222 | f.HasOne = child 223 | f.Description = "has one " + child 224 | f.Datatype = gorma.HasOneKey 225 | f.Parent = bt 226 | f.DatabaseFieldName = SanitizeDBFieldName(codegen.Goify(inflect.Singularize(r.ModelName), true) + "ID") 227 | model.RelationalFields[f.FieldName] = f 228 | } 229 | } 230 | } 231 | 232 | // HasMany signifies a relationship between this model and a 233 | // set of Children. The Parent has the children, and the Children belong 234 | // to the Parent. The first parameter becomes the name of the 235 | // field in the model struct, the second parameter is the name 236 | // of the child model. The Child model will have a ParentID field 237 | // appended to the field list. The Parent model definition will use 238 | // the first parameter as the field name in the struct definition. 239 | // 240 | // Usage: HasMany("Orders", "Order") 241 | // 242 | // Generated struct field definition: Children []Child 243 | func HasMany(name, child string) { 244 | if r, ok := relationalModelDefinition(false); ok { 245 | field := gorma.NewRelationalFieldDefinition() 246 | field.FieldName = codegen.Goify(name, true) 247 | field.HasMany = child 248 | field.Description = "has many " + inflection.Plural(child) 249 | field.Datatype = gorma.HasMany 250 | field.Parent = r 251 | r.RelationalFields[field.FieldName] = field 252 | 253 | var model *gorma.RelationalModelDefinition 254 | model, ok := r.Parent.RelationalModels[child] 255 | if ok { 256 | r.HasMany[child] = model 257 | // create the fk field 258 | f := gorma.NewRelationalFieldDefinition() 259 | f.FieldName = codegen.Goify(inflect.Singularize(r.ModelName), true) + "ID" 260 | f.HasMany = child 261 | f.Description = "has many " + child 262 | f.Datatype = gorma.HasManyKey 263 | f.Parent = model 264 | f.DatabaseFieldName = SanitizeDBFieldName(codegen.Goify(inflect.Singularize(r.ModelName), true) + "ID") 265 | model.RelationalFields[f.FieldName] = f 266 | } else { 267 | model = gorma.NewRelationalModelDefinition() 268 | model.ModelName = child 269 | model.Parent = r.Parent 270 | } 271 | r.HasMany[child] = model 272 | // create the fk field 273 | f := gorma.NewRelationalFieldDefinition() 274 | f.FieldName = codegen.Goify(inflect.Singularize(r.ModelName), true) + "ID" 275 | f.HasMany = child 276 | f.Description = "has many " + child 277 | f.Datatype = gorma.HasManyKey 278 | f.Parent = model 279 | f.DatabaseFieldName = SanitizeDBFieldName(codegen.Goify(inflect.Singularize(r.ModelName), true) + "ID") 280 | model.RelationalFields[f.FieldName] = f 281 | } 282 | } 283 | 284 | // ManyToMany creates a join table to store the intersection relationship 285 | // between this model and another model. For example, in retail an Order can 286 | // contain many products, and a product can belong to many orders. To express 287 | // this relationship use the following syntax: 288 | // 289 | // Model("Order", func(){ 290 | // ManyToMany("Product", "order_lines") 291 | // }) 292 | // 293 | // This specifies that the Order and Product tables have a "junction" table 294 | // called `order_lines` that contains the order and product information. 295 | // The generated model will have a field called `Products` that will 296 | // be an array of type `Product`. 297 | func ManyToMany(other, tablename string) { 298 | if r, ok := relationalModelDefinition(false); ok { 299 | field := gorma.NewRelationalFieldDefinition() 300 | field.FieldName = inflection.Plural(other) 301 | field.TableName = tablename 302 | field.Many2Many = other 303 | field.Datatype = gorma.Many2Many 304 | field.Description = "many to many " + r.ModelName + "/" + strings.Title(other) 305 | field.Parent = r 306 | r.RelationalFields[field.FieldName] = field 307 | var model *gorma.RelationalModelDefinition 308 | model, ok := r.Parent.RelationalModels[other] 309 | var m2m *gorma.ManyToManyDefinition 310 | 311 | // refactor (BJK) 312 | if ok { 313 | m2m = &gorma.ManyToManyDefinition{ 314 | Left: r, 315 | Right: model, 316 | DatabaseField: tablename, 317 | } 318 | r.ManyToMany[other] = m2m 319 | } else { 320 | model := gorma.NewRelationalModelDefinition() 321 | model.ModelName = other 322 | model.Parent = r.Parent 323 | m2m = &gorma.ManyToManyDefinition{ 324 | Left: r, 325 | Right: model, 326 | DatabaseField: tablename, 327 | } 328 | r.ManyToMany[other] = m2m 329 | } 330 | } 331 | } 332 | 333 | // Alias overrides the name of the SQL store's table or field. 334 | func Alias(d string) { 335 | if r, ok := relationalModelDefinition(false); ok { 336 | r.Alias = d 337 | } else if f, ok := relationalFieldDefinition(false); ok { 338 | f.DatabaseFieldName = d 339 | } 340 | } 341 | 342 | // Cached caches the models for `duration` seconds. 343 | // Not fully implemented yet, and not guaranteed to stay 344 | // in Gorma long-term because of the complex rendering 345 | // that happens in the conversion functions. 346 | func Cached(d string) { 347 | if r, ok := relationalModelDefinition(false); ok { 348 | r.Cached = true 349 | dur, err := strconv.Atoi(d) 350 | if err != nil { 351 | dslengine.ReportError("Duration %s couldn't be parsed as integer", d) 352 | } 353 | r.CacheDuration = dur 354 | } 355 | } 356 | 357 | // Roler sets a boolean flag that cause the generation of a 358 | // Role() function that returns the model's Role value 359 | // Creates a "Role" field in the table if it doesn't already exist 360 | // as a string type 361 | func Roler() { 362 | if r, ok := relationalModelDefinition(false); ok { 363 | r.Roler = true 364 | if _, ok := r.RelationalFields["Role"]; !ok { 365 | field := gorma.NewRelationalFieldDefinition() 366 | field.FieldName = "Role" 367 | field.Datatype = gorma.String 368 | r.RelationalFields["Role"] = field 369 | } 370 | } 371 | } 372 | 373 | // DynamicTableName sets a boolean flag that causes the generator to 374 | // generate function definitions in the database models that specify 375 | // the name of the database table. Useful when using multiple tables 376 | // with different names but same schema e.g. Users, AdminUsers. 377 | func DynamicTableName() { 378 | if r, ok := relationalModelDefinition(false); ok { 379 | r.DynamicTableName = true 380 | } 381 | } 382 | 383 | // SQLTag sets the model's struct tag `sql` value 384 | // for indexing and other purposes. 385 | func SQLTag(d string) { 386 | if r, ok := relationalModelDefinition(false); ok { 387 | r.SQLTag = d 388 | } else if f, ok := relationalFieldDefinition(false); ok { 389 | f.SQLTag = d 390 | } 391 | } 392 | -------------------------------------------------------------------------------- /dsl/relationalmodel_test.go: -------------------------------------------------------------------------------- 1 | package dsl_test 2 | 3 | import ( 4 | "github.com/goadesign/gorma" 5 | gdsl "github.com/goadesign/gorma/dsl" 6 | 7 | . "github.com/goadesign/goa/design" 8 | . "github.com/goadesign/goa/design/apidsl" 9 | . "github.com/goadesign/goa/dslengine" 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/gomega" 12 | ) 13 | 14 | var _ = Describe("RelationalModel", func() { 15 | var sgname, storename, name string 16 | var dsl, storedsl func() 17 | var ChildPayload *UserTypeDefinition 18 | var HasManyPayload *UserTypeDefinition 19 | var ChildMedia *MediaTypeDefinition 20 | 21 | var TestResource *ResourceDefinition 22 | 23 | BeforeEach(func() { 24 | Reset() 25 | sgname = "production" 26 | dsl = nil 27 | storename = "mysql" 28 | name = "" 29 | 30 | TestResource = Resource("testresource", func() { 31 | BasePath("/tests") 32 | Action("create", func() { 33 | Routing( 34 | POST(""), 35 | ) 36 | Payload(ChildPayload, func() { 37 | Required("first_name") 38 | }) 39 | }) 40 | Action("update", func() { 41 | Routing( 42 | POST(""), 43 | ) 44 | Payload(HasManyPayload, func() { 45 | Required("first_name") 46 | }) 47 | }) 48 | 49 | }) 50 | Ω(TestResource).ShouldNot(BeNil()) 51 | 52 | ChildPayload = Type("ChildPayload", func() { 53 | Attribute("first_name", String) 54 | Attribute("last_name", String) 55 | }) 56 | ChildMedia = MediaType("application/vnd.childmedia+json", func() { 57 | Description("Child Media Type") 58 | Attribute("first_name", String) 59 | Attribute("last_name", String) 60 | View("default", func() { 61 | Attribute("first_name") 62 | Attribute("last_name") 63 | }) 64 | }) 65 | HasManyPayload = Type("HasManyPayload", func() { 66 | Attribute("first_name", String) 67 | Attribute("last_name", String) 68 | }) 69 | 70 | }) 71 | 72 | JustBeforeEach(func() { 73 | storedsl = func() { 74 | gdsl.Model(name, dsl) 75 | gdsl.Model("Child", func() { 76 | gdsl.BuildsFrom(func() { 77 | gdsl.Payload("testresource", "create") 78 | }) 79 | gdsl.RendersTo(ChildMedia) 80 | gdsl.BelongsTo(name) 81 | }) 82 | gdsl.Model("One", func() { 83 | gdsl.BuildsFrom(func() { 84 | gdsl.Payload("testresource", "create") 85 | }) 86 | gdsl.HasOne("Child") 87 | }) 88 | gdsl.Model("Many", func() { 89 | gdsl.BuildsFrom(func() { 90 | gdsl.Payload("testresource", "update") 91 | }) 92 | gdsl.HasMany("Children", "Child") 93 | }) 94 | 95 | } 96 | gdsl.StorageGroup(sgname, func() { 97 | gdsl.Store(storename, gorma.MySQL, storedsl) 98 | }) 99 | 100 | Run() 101 | 102 | }) 103 | 104 | Context("with no DSL", func() { 105 | BeforeEach(func() { 106 | name = "Users" 107 | }) 108 | 109 | It("produces a valid Relational Model definition", func() { 110 | Ω(Design.Validate()).ShouldNot(HaveOccurred()) 111 | sg := gorma.GormaDesign 112 | rs := sg.RelationalStores[storename] 113 | Ω(rs.RelationalModels[name].ModelName).Should(Equal(name)) 114 | }) 115 | }) 116 | Context("with an valid name", func() { 117 | BeforeEach(func() { 118 | name = "good" 119 | }) 120 | 121 | It("does not produce an error", func() { 122 | storedsl = func() { 123 | gdsl.Model(name, dsl) 124 | } 125 | Ω(Errors).ShouldNot(HaveOccurred()) 126 | }) 127 | }) 128 | Context("with an already defined Relational Model with the same name", func() { 129 | BeforeEach(func() { 130 | name = "Child" 131 | }) 132 | 133 | It("produce an error", func() { 134 | 135 | Ω(Errors).Should(HaveOccurred()) 136 | }) 137 | }) 138 | 139 | Context("with an already defined Relational model with a different name", func() { 140 | BeforeEach(func() { 141 | name = "Users" 142 | }) 143 | 144 | It("does not return an error", func() { 145 | storedsl = func() { 146 | gdsl.Model(name, dsl) 147 | } 148 | Ω(Errors).Should(Not(HaveOccurred())) 149 | }) 150 | }) 151 | 152 | Context("with valid DSL", func() { 153 | JustBeforeEach(func() { 154 | Ω(Errors).ShouldNot(HaveOccurred()) 155 | Ω(Design.Validate()).ShouldNot(HaveOccurred()) 156 | }) 157 | 158 | Context("with a description", func() { 159 | const description = "description" 160 | 161 | BeforeEach(func() { 162 | name = "Users" 163 | dsl = func() { 164 | gdsl.Description(description) 165 | } 166 | }) 167 | 168 | It("sets the relational model description", func() { 169 | sg := gorma.GormaDesign 170 | rs := sg.RelationalStores[storename] 171 | Ω(rs.RelationalModels[name].Description).Should(Equal(description)) 172 | }) 173 | }) 174 | Context("with a table alias name", func() { 175 | const alias = "user_table" 176 | 177 | BeforeEach(func() { 178 | name = "Users" 179 | dsl = func() { 180 | gdsl.Alias(alias) 181 | } 182 | }) 183 | 184 | It("sets the relational store table name", func() { 185 | sg := gorma.GormaDesign 186 | rs := sg.RelationalStores[storename] 187 | Ω(rs.RelationalModels[name].Alias).Should(Equal(alias)) 188 | }) 189 | }) 190 | Context("with an alias", func() { 191 | const alias = "user_table" 192 | 193 | BeforeEach(func() { 194 | name = "Users" 195 | dsl = func() { 196 | gdsl.Alias(alias) 197 | } 198 | }) 199 | 200 | It("sets the relational store alias", func() { 201 | sg := gorma.GormaDesign 202 | rs := sg.RelationalStores[storename] 203 | Ω(rs.RelationalModels[name].Alias).Should(Equal(alias)) 204 | }) 205 | }) 206 | 207 | Context("cached", func() { 208 | const duration = "50" 209 | 210 | BeforeEach(func() { 211 | name = "Users" 212 | dsl = func() { 213 | gdsl.Cached(duration) 214 | } 215 | }) 216 | 217 | It("sets the relational store cache values", func() { 218 | sg := gorma.GormaDesign 219 | rs := sg.RelationalStores[storename] 220 | Ω(rs.RelationalModels[name].Cached).Should(Equal(true)) 221 | Ω(rs.RelationalModels[name].CacheDuration).Should(Equal(50)) 222 | 223 | }) 224 | }) 225 | 226 | Context("with roler", func() { 227 | 228 | BeforeEach(func() { 229 | name = "Users" 230 | dsl = func() { 231 | gdsl.Roler() 232 | } 233 | }) 234 | 235 | It("Creates a Role field", func() { 236 | sg := gorma.GormaDesign 237 | rs := sg.RelationalStores[storename] 238 | Ω(rs.RelationalModels[name].Roler).Should(Equal(true)) 239 | }) 240 | }) 241 | 242 | Context("with dynamic table name", func() { 243 | 244 | BeforeEach(func() { 245 | name = "Users" 246 | dsl = func() { 247 | gdsl.DynamicTableName() 248 | } 249 | }) 250 | 251 | It("sets the relational store alias", func() { 252 | sg := gorma.GormaDesign 253 | rs := sg.RelationalStores[storename] 254 | Ω(rs.RelationalModels[name].DynamicTableName).Should(Equal(true)) 255 | }) 256 | }) 257 | Context("with an sql tag", func() { 258 | const tag = "unique" 259 | 260 | BeforeEach(func() { 261 | name = "Users" 262 | dsl = func() { 263 | gdsl.SQLTag(tag) 264 | } 265 | }) 266 | 267 | It("sets the relational store alias", func() { 268 | sg := gorma.GormaDesign 269 | rs := sg.RelationalStores[storename] 270 | Ω(rs.RelationalModels[name].SQLTag).Should(Equal(tag)) 271 | }) 272 | }) 273 | 274 | Context("with a has one relationaship", func() { 275 | 276 | It("sets the creates the foreign key in the child model", func() { 277 | sg := gorma.GormaDesign 278 | rs := sg.RelationalStores[storename] 279 | f, ok := rs.RelationalModels["Child"].RelationalFields["OneID"] 280 | 281 | Ω(ok).Should(Equal(true)) 282 | Ω(f.DatabaseFieldName).Should(Equal("one_id")) 283 | }) 284 | It("the relationship is added to the HasOne list", func() { 285 | sg := gorma.GormaDesign 286 | rs := sg.RelationalStores[storename] 287 | ch, ok := rs.RelationalModels["One"].HasOne["Child"] 288 | 289 | Ω(ok).Should(Equal(true)) 290 | Ω(ch).Should(Equal(rs.RelationalModels["Child"])) 291 | }) 292 | 293 | It("sets the field definition correctly for the owning model", func() { 294 | sg := gorma.GormaDesign 295 | rs := sg.RelationalStores[storename] 296 | f, ok := rs.RelationalModels["One"].RelationalFields["Child"] 297 | 298 | Ω(ok).Should(Equal(true)) 299 | Ω(f.FieldName).Should(Equal("Child")) 300 | }) 301 | }) 302 | 303 | Context("with a belongs to relationship", func() { 304 | 305 | BeforeEach(func() { 306 | name = "User" 307 | }) 308 | 309 | It("sets the creates the foreign key in the child model", func() { 310 | sg := gorma.GormaDesign 311 | rs := sg.RelationalStores[storename] 312 | f, ok := rs.RelationalModels["Child"].RelationalFields["UserID"] 313 | 314 | Ω(ok).Should(Equal(true)) 315 | Ω(f.DatabaseFieldName).Should(Equal("user_id")) 316 | }) 317 | It("the relationship is added to the BelongsTo list", func() { 318 | sg := gorma.GormaDesign 319 | rs := sg.RelationalStores[storename] 320 | ch, ok := rs.RelationalModels["Child"].BelongsTo["User"] 321 | 322 | Ω(ok).Should(Equal(true)) 323 | Ω(ch).Should(Equal(rs.RelationalModels["User"])) 324 | }) 325 | 326 | It("sets the field definition correctly for the child model", func() { 327 | sg := gorma.GormaDesign 328 | rs := sg.RelationalStores[storename] 329 | f, ok := rs.RelationalModels["Child"].RelationalFields["UserID"] 330 | 331 | Ω(ok).Should(Equal(true)) 332 | Ω(f.FieldName).Should(Equal("UserID")) 333 | }) 334 | }) 335 | 336 | Context("with a has many relationship", func() { 337 | 338 | It("sets the creates the foreign key in the child model", func() { 339 | sg := gorma.GormaDesign 340 | rs := sg.RelationalStores[storename] 341 | f, ok := rs.RelationalModels["Child"].RelationalFields["ManyID"] 342 | 343 | Ω(ok).Should(Equal(true)) 344 | Ω(f.DatabaseFieldName).Should(Equal("many_id")) 345 | }) 346 | It("the relationship is added to the Has Many list", func() { 347 | sg := gorma.GormaDesign 348 | rs := sg.RelationalStores[storename] 349 | ch, ok := rs.RelationalModels["Many"].HasMany["Child"] 350 | 351 | Ω(ok).Should(Equal(true)) 352 | Ω(ch).Should(Equal(rs.RelationalModels["Child"])) 353 | }) 354 | 355 | It("sets the field definition correctly for the child model", func() { 356 | sg := gorma.GormaDesign 357 | rs := sg.RelationalStores[storename] 358 | f, ok := rs.RelationalModels["Many"].RelationalFields["Children"] 359 | 360 | Ω(ok).Should(Equal(true)) 361 | Ω(f.FieldName).Should(Equal("Children")) 362 | }) 363 | }) 364 | 365 | }) 366 | }) 367 | 368 | var _ = Describe("RelationalModel with auto fields enabled and auto fields set in dsl", func() { 369 | var sgname, storename, name string 370 | var dsl func() 371 | var ChildPayload func() 372 | var HasOnePayload func() 373 | var HasManyPayload func() 374 | 375 | BeforeEach(func() { 376 | Reset() 377 | sgname = "production" 378 | dsl = nil 379 | storename = "mysql" 380 | name = "" 381 | 382 | ChildPayload = func() { 383 | Attribute("first_name", String) 384 | Attribute("last_name", String) 385 | } 386 | HasOnePayload = func() { 387 | Attribute("first_name", String) 388 | Attribute("last_name", String) 389 | } 390 | 391 | HasManyPayload = func() { 392 | Attribute("first_name", String) 393 | Attribute("last_name", String) 394 | } 395 | 396 | }) 397 | 398 | JustBeforeEach(func() { 399 | gdsl.StorageGroup(sgname, func() { 400 | gdsl.Store(storename, gorma.MySQL, func() { 401 | gdsl.Model(name, dsl) 402 | gdsl.Model("Child", func() { 403 | gdsl.BuildsFrom(ChildPayload) 404 | gdsl.BelongsTo(name) 405 | }) 406 | gdsl.Model("One", func() { 407 | gdsl.BuildsFrom(HasOnePayload) 408 | gdsl.HasOne("Child") 409 | }) 410 | gdsl.Model("Many", func() { 411 | gdsl.BuildsFrom(HasManyPayload) 412 | gdsl.HasMany("Children", "Child") 413 | }) 414 | 415 | }) 416 | }) 417 | 418 | Run() 419 | 420 | }) 421 | 422 | Context("with no DSL", func() { 423 | BeforeEach(func() { 424 | name = "Users" 425 | dsl = func() { 426 | gdsl.Field("ID", gorma.Integer) 427 | gdsl.Field("CreatedAt", gorma.Timestamp) 428 | gdsl.Field("UpdatedAt", gorma.Timestamp) 429 | gdsl.Field("DeletedAt", gorma.NullableTimestamp) 430 | } 431 | }) 432 | 433 | It("generates auto fields", func() { 434 | Ω(Design.Validate()).ShouldNot(HaveOccurred()) 435 | sg := gorma.GormaDesign 436 | rs := sg.RelationalStores[storename] 437 | Ω(rs.RelationalModels[name].ModelName).Should(Equal(name)) 438 | _, ok := rs.RelationalModels[name].RelationalFields["ID"] 439 | Ω(ok).Should(Equal(true)) 440 | _, ok = rs.RelationalModels[name].RelationalFields["UpdatedAt"] 441 | Ω(ok).Should(Equal(true)) 442 | _, ok = rs.RelationalModels[name].RelationalFields["CreatedAt"] 443 | Ω(ok).Should(Equal(true)) 444 | _, ok = rs.RelationalModels[name].RelationalFields["DeletedAt"] 445 | Ω(ok).Should(Equal(true)) 446 | Ω(rs.RelationalModels[name].ModelName).Should(Equal(name)) 447 | Ω(len(rs.RelationalModels[name].RelationalFields)).Should(Equal(4)) 448 | 449 | }) 450 | }) 451 | }) 452 | var _ = Describe("RelationalModel with auto fields explicitly enabled", func() { 453 | var sgname, storename, name string 454 | var dsl func() 455 | var ChildPayload func() 456 | var HasOnePayload func() 457 | var HasManyPayload func() 458 | 459 | BeforeEach(func() { 460 | Reset() 461 | sgname = "production" 462 | dsl = nil 463 | storename = "mysql" 464 | name = "" 465 | 466 | ChildPayload = func() { 467 | Attribute("first_name", String) 468 | Attribute("last_name", String) 469 | } 470 | HasOnePayload = func() { 471 | Attribute("first_name", String) 472 | Attribute("last_name", String) 473 | } 474 | 475 | HasManyPayload = func() { 476 | Attribute("first_name", String) 477 | Attribute("last_name", String) 478 | } 479 | 480 | }) 481 | 482 | JustBeforeEach(func() { 483 | gdsl.StorageGroup(sgname, func() { 484 | gdsl.Store(storename, gorma.MySQL, func() { 485 | gdsl.Model(name, dsl) 486 | gdsl.Model("Child", func() { 487 | gdsl.BuildsFrom(ChildPayload) 488 | gdsl.BelongsTo(name) 489 | }) 490 | gdsl.Model("One", func() { 491 | gdsl.BuildsFrom(HasOnePayload) 492 | gdsl.HasOne("Child") 493 | }) 494 | gdsl.Model("Many", func() { 495 | gdsl.BuildsFrom(HasManyPayload) 496 | gdsl.HasMany("Children", "Child") 497 | }) 498 | 499 | }) 500 | }) 501 | 502 | Run() 503 | 504 | }) 505 | 506 | Context("with no DSL", func() { 507 | BeforeEach(func() { 508 | name = "Users" 509 | }) 510 | 511 | It("generates auto fields", func() { 512 | Ω(Design.Validate()).ShouldNot(HaveOccurred()) 513 | sg := gorma.GormaDesign 514 | rs := sg.RelationalStores[storename] 515 | Ω(rs.RelationalModels[name].ModelName).Should(Equal(name)) 516 | _, ok := rs.RelationalModels[name].RelationalFields["ID"] 517 | Ω(ok).Should(Equal(true)) 518 | _, ok = rs.RelationalModels[name].RelationalFields["UpdatedAt"] 519 | Ω(ok).Should(Equal(true)) 520 | _, ok = rs.RelationalModels[name].RelationalFields["CreatedAt"] 521 | Ω(ok).Should(Equal(true)) 522 | _, ok = rs.RelationalModels[name].RelationalFields["DeletedAt"] 523 | Ω(ok).Should(Equal(true)) 524 | Ω(rs.RelationalModels[name].ModelName).Should(Equal(name)) 525 | Ω(len(rs.RelationalModels[name].RelationalFields)).Should(Equal(4)) 526 | }) 527 | }) 528 | }) 529 | 530 | var _ = Describe("RelationalModel with auto fields disabled", func() { 531 | var sgname, storename, name string 532 | var dsl func() 533 | var ChildPayload func() 534 | var HasOnePayload func() 535 | var HasManyPayload func() 536 | 537 | BeforeEach(func() { 538 | Reset() 539 | sgname = "production" 540 | dsl = nil 541 | storename = "mysql" 542 | name = "" 543 | 544 | ChildPayload = func() { 545 | Attribute("first_name", String) 546 | Attribute("last_name", String) 547 | } 548 | HasOnePayload = func() { 549 | Attribute("first_name", String) 550 | Attribute("last_name", String) 551 | } 552 | HasManyPayload = func() { 553 | Attribute("first_name", String) 554 | Attribute("last_name", String) 555 | } 556 | }) 557 | 558 | JustBeforeEach(func() { 559 | gdsl.StorageGroup(sgname, func() { 560 | gdsl.Store(storename, gorma.MySQL, func() { 561 | gdsl.NoAutomaticIDFields() 562 | gdsl.NoAutomaticTimestamps() 563 | gdsl.NoAutomaticSoftDelete() 564 | gdsl.Model(name, dsl) 565 | gdsl.Model("Child", func() { 566 | gdsl.BuildsFrom(ChildPayload) 567 | gdsl.BelongsTo(name) 568 | }) 569 | gdsl.Model("One", func() { 570 | gdsl.BuildsFrom(HasOnePayload) 571 | gdsl.HasOne("Child") 572 | }) 573 | gdsl.Model("Many", func() { 574 | gdsl.BuildsFrom(HasManyPayload) 575 | gdsl.HasMany("Children", "Child") 576 | }) 577 | 578 | }) 579 | }) 580 | 581 | Run() 582 | 583 | }) 584 | 585 | Context("with no DSL", func() { 586 | BeforeEach(func() { 587 | name = "Users" 588 | }) 589 | 590 | It("doesn't generate auto fields", func() { 591 | Ω(Design.Validate()).ShouldNot(HaveOccurred()) 592 | sg := gorma.GormaDesign 593 | rs := sg.RelationalStores[storename] 594 | Ω(rs.RelationalModels[name].ModelName).Should(Equal(name)) 595 | _, ok := rs.RelationalModels[name].RelationalFields["ID"] 596 | Ω(ok).Should(Equal(false)) 597 | _, ok = rs.RelationalModels[name].RelationalFields["UpdatedAt"] 598 | Ω(ok).Should(Equal(false)) 599 | _, ok = rs.RelationalModels[name].RelationalFields["CreatedAt"] 600 | Ω(ok).Should(Equal(false)) 601 | _, ok = rs.RelationalModels[name].RelationalFields["DeletedAt"] 602 | Ω(ok).Should(Equal(false)) 603 | Ω(rs.RelationalModels[name].ModelName).Should(Equal(name)) 604 | Ω(len(rs.RelationalModels[name].RelationalFields)).Should(Equal(0)) 605 | }) 606 | }) 607 | }) 608 | 609 | var _ = Describe("RelationalModel with auto fields unset", func() { 610 | var sgname, storename, name string 611 | var dsl func() 612 | var ChildPayload func() 613 | var HasOnePayload func() 614 | var HasManyPayload func() 615 | 616 | BeforeEach(func() { 617 | Reset() 618 | sgname = "production" 619 | dsl = nil 620 | storename = "mysql" 621 | name = "" 622 | 623 | ChildPayload = func() { 624 | Attribute("first_name", String) 625 | Attribute("last_name", String) 626 | } 627 | HasOnePayload = func() { 628 | Attribute("first_name", String) 629 | Attribute("last_name", String) 630 | } 631 | HasManyPayload = func() { 632 | Attribute("first_name", String) 633 | Attribute("last_name", String) 634 | } 635 | 636 | }) 637 | 638 | JustBeforeEach(func() { 639 | gdsl.StorageGroup(sgname, func() { 640 | gdsl.Store(storename, gorma.MySQL, func() { 641 | gdsl.Model(name, dsl) 642 | gdsl.Model("Child", func() { 643 | gdsl.BuildsFrom(ChildPayload) 644 | gdsl.BelongsTo(name) 645 | }) 646 | gdsl.Model("One", func() { 647 | gdsl.BuildsFrom(HasOnePayload) 648 | gdsl.HasOne("Child") 649 | }) 650 | gdsl.Model("Many", func() { 651 | gdsl.BuildsFrom(HasManyPayload) 652 | gdsl.HasMany("Children", "Child") 653 | }) 654 | 655 | }) 656 | }) 657 | 658 | Run() 659 | 660 | }) 661 | 662 | Context("with no DSL", func() { 663 | BeforeEach(func() { 664 | name = "Users" 665 | }) 666 | 667 | It("generates auto fields", func() { 668 | Ω(Design.Validate()).ShouldNot(HaveOccurred()) 669 | sg := gorma.GormaDesign 670 | rs := sg.RelationalStores[storename] 671 | Ω(rs.RelationalModels[name].ModelName).Should(Equal(name)) 672 | f, ok := rs.RelationalModels[name].RelationalFields["ID"] 673 | Ω(ok).Should(Equal(true)) 674 | Ω(f.Datatype).Should(Equal(gorma.Integer)) 675 | f, ok = rs.RelationalModels[name].RelationalFields["UpdatedAt"] 676 | Ω(ok).Should(Equal(true)) 677 | Ω(f.Datatype).Should(Equal(gorma.Timestamp)) 678 | f, ok = rs.RelationalModels[name].RelationalFields["CreatedAt"] 679 | Ω(ok).Should(Equal(true)) 680 | Ω(f.Datatype).Should(Equal(gorma.Timestamp)) 681 | f, ok = rs.RelationalModels[name].RelationalFields["DeletedAt"] 682 | Ω(ok).Should(Equal(true)) 683 | Ω(f.Datatype).Should(Equal(gorma.NullableTimestamp)) 684 | Ω(rs.RelationalModels[name].ModelName).Should(Equal(name)) 685 | }) 686 | }) 687 | }) 688 | -------------------------------------------------------------------------------- /dsl/relationalstore.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "github.com/goadesign/goa/dslengine" 5 | "github.com/goadesign/gorma" 6 | ) 7 | 8 | // Store represents a database. Gorma lets you specify 9 | // a database type, but it's currently not used for any generation 10 | // logic. 11 | func Store(name string, storeType gorma.RelationalStorageType, dsl func()) { 12 | if name == "" || len(name) == 0 { 13 | dslengine.ReportError("Relational Store requires a name.") 14 | return 15 | } 16 | if len(storeType) == 0 { 17 | dslengine.ReportError("Relational Store requires a RelationalStoreType.") 18 | return 19 | } 20 | if dsl == nil { 21 | dslengine.ReportError("Relational Store requires a dsl.") 22 | return 23 | } 24 | if s, ok := storageGroupDefinition(true); ok { 25 | if s.RelationalStores == nil { 26 | s.RelationalStores = make(map[string]*gorma.RelationalStoreDefinition) 27 | } 28 | store, ok := s.RelationalStores[name] 29 | if !ok { 30 | store = &gorma.RelationalStoreDefinition{ 31 | Name: name, 32 | DefinitionDSL: dsl, 33 | Parent: s, 34 | Type: storeType, 35 | RelationalModels: make(map[string]*gorma.RelationalModelDefinition), 36 | } 37 | } else { 38 | dslengine.ReportError("Relational Store %s can only be declared once.", name) 39 | } 40 | s.RelationalStores[name] = store 41 | } 42 | 43 | } 44 | 45 | // NoAutomaticIDFields applies to a `Store` or `Model` type. It allows you 46 | // to turn off the default behavior that will automatically create 47 | // an ID/int Primary Key for each model. 48 | func NoAutomaticIDFields() { 49 | if s, ok := relationalStoreDefinition(false); ok { 50 | s.NoAutoIDFields = true 51 | } else if m, ok := relationalModelDefinition(false); ok { 52 | delete(m.RelationalFields, "ID") 53 | } 54 | } 55 | 56 | // NoAutomaticTimestamps applies to a `Store` or `Model` type. It allows you 57 | // to turn off the default behavior that will automatically create 58 | // an `CreatedAt` and `UpdatedAt` fields for each model. 59 | func NoAutomaticTimestamps() { 60 | if s, ok := relationalStoreDefinition(false); ok { 61 | s.NoAutoTimestamps = true 62 | } else if m, ok := relationalModelDefinition(false); ok { 63 | delete(m.RelationalFields, "CreatedAt") 64 | delete(m.RelationalFields, "UpdatedAt") 65 | } 66 | } 67 | 68 | // NoAutomaticSoftDelete applies to a `Store` or `Model` type. It allows 69 | // you to turn off the default behavior that will automatically 70 | // create a `DeletedAt` field (*time.Time) that acts as a 71 | // soft-delete filter for your models. 72 | func NoAutomaticSoftDelete() { 73 | if s, ok := relationalStoreDefinition(false); ok { 74 | s.NoAutoSoftDelete = true 75 | } else if m, ok := relationalModelDefinition(false); ok { 76 | delete(m.RelationalFields, "DeletedAt") 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /dsl/relationalstore_test.go: -------------------------------------------------------------------------------- 1 | package dsl_test 2 | 3 | import ( 4 | "github.com/goadesign/gorma" 5 | gdsl "github.com/goadesign/gorma/dsl" 6 | 7 | . "github.com/goadesign/goa/design" 8 | . "github.com/goadesign/goa/dslengine" 9 | . "github.com/onsi/ginkgo" 10 | . "github.com/onsi/gomega" 11 | ) 12 | 13 | var _ = Describe("RelationalStore", func() { 14 | var sgname, name string 15 | var storetype gorma.RelationalStorageType 16 | var dsl, storedsl func() 17 | 18 | BeforeEach(func() { 19 | Reset() 20 | sgname = "production" 21 | storedsl = nil 22 | dsl = nil 23 | name = "" 24 | }) 25 | 26 | JustBeforeEach(func() { 27 | 28 | gdsl.StorageGroup(sgname, func() { 29 | gdsl.Store(name, storetype, dsl) 30 | }) 31 | 32 | Run() 33 | }) 34 | 35 | Context("with no name", func() { 36 | BeforeEach(func() { 37 | name = "" 38 | }) 39 | 40 | It("does not produce a valid Relational Store definition", func() { 41 | Ω(Design.Validate()).ShouldNot(HaveOccurred()) 42 | sg := gorma.GormaDesign 43 | Ω(sg.RelationalStores).Should(BeEmpty()) 44 | }) 45 | }) 46 | 47 | Context("with no DSL and no type", func() { 48 | BeforeEach(func() { 49 | name = "mysql" 50 | }) 51 | 52 | It("does not produce a valid Relational Store definition", func() { 53 | Ω(Design.Validate()).ShouldNot(HaveOccurred()) 54 | sg := gorma.GormaDesign 55 | Ω(sg.RelationalStores).Should(BeEmpty()) 56 | }) 57 | }) 58 | 59 | Context("with an already defined Relational Store with the same name", func() { 60 | BeforeEach(func() { 61 | name = "mysql" 62 | }) 63 | 64 | It("produce an error", func() { 65 | gdsl.StorageGroup(sgname, func() { 66 | gdsl.Store(name, gorma.MySQL, dsl) 67 | }) 68 | Ω(Errors).Should(HaveOccurred()) 69 | }) 70 | }) 71 | Context("with an already defined Relational Store with a different name", func() { 72 | BeforeEach(func() { 73 | sgname = "mysql" 74 | storetype = gorma.Postgres 75 | name = "model" 76 | dsl = func() {} 77 | storedsl = func() { 78 | gdsl.Store("media", gorma.MySQL, dsl) 79 | } 80 | }) 81 | 82 | It("doesn't return an error", func() { 83 | gdsl.StorageGroup("news", storedsl) 84 | Ω(Errors).Should(Not(HaveOccurred())) 85 | }) 86 | }) 87 | 88 | Context("with valid DSL", func() { 89 | JustBeforeEach(func() { 90 | Ω(Errors).ShouldNot(HaveOccurred()) 91 | Ω(Design.Validate()).ShouldNot(HaveOccurred()) 92 | }) 93 | 94 | Context("with a description", func() { 95 | const description = "description" 96 | 97 | BeforeEach(func() { 98 | name = "mysql" 99 | dsl = func() { 100 | gdsl.Description(description) 101 | } 102 | }) 103 | 104 | It("sets the relational store description", func() { 105 | sg := gorma.GormaDesign 106 | Ω(sg.RelationalStores[name].Description).Should(Equal(description)) 107 | }) 108 | It("auto id generation defaults to true", func() { 109 | sg := gorma.GormaDesign 110 | Ω(sg.RelationalStores[name].NoAutoIDFields).Should(BeFalse()) 111 | }) 112 | It("auto timestamps defaults to true", func() { 113 | sg := gorma.GormaDesign 114 | Ω(sg.RelationalStores[name].NoAutoTimestamps).Should(BeFalse()) 115 | }) 116 | It("auto soft delete defaults to true", func() { 117 | sg := gorma.GormaDesign 118 | Ω(sg.RelationalStores[name].NoAutoSoftDelete).Should(BeFalse()) 119 | }) 120 | }) 121 | 122 | Context("with NoAutomaticIDFields", func() { 123 | BeforeEach(func() { 124 | name = "mysql" 125 | dsl = func() { 126 | gdsl.NoAutomaticIDFields() 127 | } 128 | }) 129 | 130 | It("auto id generation should be off", func() { 131 | sg := gorma.GormaDesign 132 | Ω(sg.RelationalStores[name].NoAutoIDFields).Should(BeTrue()) 133 | }) 134 | It("auto timestamps defaults to true", func() { 135 | sg := gorma.GormaDesign 136 | Ω(sg.RelationalStores[name].NoAutoTimestamps).Should(BeFalse()) 137 | }) 138 | It("auto soft delete defaults to true", func() { 139 | sg := gorma.GormaDesign 140 | Ω(sg.RelationalStores[name].NoAutoSoftDelete).Should(BeFalse()) 141 | }) 142 | }) 143 | 144 | Context("with NoAutomaticTimestamps", func() { 145 | BeforeEach(func() { 146 | name = "mysql" 147 | dsl = func() { 148 | gdsl.NoAutomaticTimestamps() 149 | } 150 | }) 151 | 152 | It("auto id generation should be on", func() { 153 | sg := gorma.GormaDesign 154 | Ω(sg.RelationalStores[name].NoAutoIDFields).Should(BeFalse()) 155 | }) 156 | It("auto timestamps should be off", func() { 157 | sg := gorma.GormaDesign 158 | Ω(sg.RelationalStores[name].NoAutoTimestamps).Should(BeTrue()) 159 | }) 160 | It("auto soft delete should be on", func() { 161 | sg := gorma.GormaDesign 162 | Ω(sg.RelationalStores[name].NoAutoSoftDelete).Should(BeFalse()) 163 | }) 164 | }) 165 | 166 | }) 167 | 168 | }) 169 | -------------------------------------------------------------------------------- /dsl/runner.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "github.com/goadesign/goa/design" 5 | "github.com/goadesign/goa/dslengine" 6 | "github.com/goadesign/gorma" 7 | ) 8 | 9 | func init() { 10 | gorma.GormaDesign = gorma.NewStorageGroupDefinition() 11 | dslengine.Register(gorma.GormaDesign) 12 | } 13 | 14 | // storageDefinition returns true and current context if it is an StorageGroupDefinition, 15 | // nil and false otherwise. 16 | func storageGroupDefinition(failIfNotSD bool) (*gorma.StorageGroupDefinition, bool) { 17 | a, ok := dslengine.CurrentDefinition().(*gorma.StorageGroupDefinition) 18 | if !ok && failIfNotSD { 19 | dslengine.IncompatibleDSL() 20 | } 21 | return a, ok 22 | } 23 | 24 | // relationalStoreDefinition returns true and current context if it is an RelationalStoreDefinition, 25 | // nil and false otherwise. 26 | func relationalStoreDefinition(failIfNotSD bool) (*gorma.RelationalStoreDefinition, bool) { 27 | a, ok := dslengine.CurrentDefinition().(*gorma.RelationalStoreDefinition) 28 | if !ok && failIfNotSD { 29 | dslengine.IncompatibleDSL() 30 | } 31 | return a, ok 32 | } 33 | 34 | // relationalModelDefinition returns true and current context if it is an RelationalModelDefinition, 35 | // nil and false otherwise. 36 | func relationalModelDefinition(failIfNotSD bool) (*gorma.RelationalModelDefinition, bool) { 37 | a, ok := dslengine.CurrentDefinition().(*gorma.RelationalModelDefinition) 38 | if !ok && failIfNotSD { 39 | dslengine.IncompatibleDSL() 40 | } 41 | return a, ok 42 | } 43 | 44 | // relationalFieldDefinition returns true and current context if it is an RelationalFieldDefinition, 45 | // nil and false otherwise. 46 | func relationalFieldDefinition(failIfNotSD bool) (*gorma.RelationalFieldDefinition, bool) { 47 | a, ok := dslengine.CurrentDefinition().(*gorma.RelationalFieldDefinition) 48 | if !ok && failIfNotSD { 49 | dslengine.IncompatibleDSL() 50 | } 51 | return a, ok 52 | } 53 | 54 | // buildSourceDefinition returns true and current context if it is an BuildSource 55 | // nil and false otherwise. 56 | func buildSourceDefinition(failIfNotSD bool) (*gorma.BuildSource, bool) { 57 | a, ok := dslengine.CurrentDefinition().(*gorma.BuildSource) 58 | if !ok && failIfNotSD { 59 | dslengine.IncompatibleDSL() 60 | } 61 | return a, ok 62 | } 63 | 64 | // attributeDefinition returns true and current context if it is an AttributeDefinition 65 | // nil and false otherwise. 66 | func attributeDefinition(failIfNotSD bool) (*design.AttributeDefinition, bool) { 67 | a, ok := dslengine.CurrentDefinition().(*design.AttributeDefinition) 68 | if !ok && failIfNotSD { 69 | dslengine.IncompatibleDSL() 70 | } 71 | return a, ok 72 | } 73 | -------------------------------------------------------------------------------- /dsl/storagegroup.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "github.com/goadesign/goa/dslengine" 5 | "github.com/goadesign/gorma" 6 | ) 7 | 8 | // StorageGroup implements the top level Gorma DSL 9 | // There should be one StorageGroup per Goa application. 10 | func StorageGroup(name string, dsli func()) *gorma.StorageGroupDefinition { 11 | if !dslengine.IsTopLevelDefinition() { 12 | return nil 13 | } 14 | if name == "" { 15 | dslengine.ReportError("Storage Group name cannot be empty") 16 | } 17 | 18 | if gorma.GormaDesign != nil { 19 | if gorma.GormaDesign.Name == name { 20 | dslengine.ReportError("Only one StorageGroup is allowed") 21 | } 22 | } 23 | gorma.GormaDesign.Name = name 24 | gorma.GormaDesign.RelationalStores = make(map[string]*gorma.RelationalStoreDefinition) 25 | gorma.GormaDesign.DefinitionDSL = dsli 26 | return gorma.GormaDesign 27 | } 28 | 29 | // Description sets the definition description. 30 | // Description can be called inside StorageGroup, RelationalStore, RelationalModel, RelationalField 31 | func Description(d string) { 32 | if a, ok := storageGroupDefinition(false); ok { 33 | a.Description = d 34 | } else if v, ok := relationalStoreDefinition(false); ok { 35 | v.Description = d 36 | } else if r, ok := relationalModelDefinition(false); ok { 37 | r.Description = d 38 | } else if f, ok := relationalFieldDefinition(false); ok { 39 | f.Description = d 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /dsl/storagegroup_test.go: -------------------------------------------------------------------------------- 1 | package dsl_test 2 | 3 | import ( 4 | "github.com/goadesign/gorma" 5 | gdsl "github.com/goadesign/gorma/dsl" 6 | 7 | . "github.com/goadesign/goa/design" 8 | . "github.com/goadesign/goa/dslengine" 9 | . "github.com/onsi/ginkgo" 10 | . "github.com/onsi/gomega" 11 | ) 12 | 13 | var _ = Describe("StorageGroup", func() { 14 | var name string 15 | var dsl func() 16 | 17 | BeforeEach(func() { 18 | Reset() 19 | name = "mysql" 20 | dsl = nil 21 | }) 22 | 23 | JustBeforeEach(func() { 24 | 25 | gdsl.StorageGroup(name, dsl) 26 | 27 | Run() 28 | 29 | }) 30 | 31 | Context("with no DSL", func() { 32 | BeforeEach(func() { 33 | name = "mysql" 34 | }) 35 | 36 | It("produces a valid Storage Group definition", func() { 37 | Ω(Design.Validate()).ShouldNot(HaveOccurred()) 38 | Ω(gorma.GormaDesign.Name).Should(Equal(name)) 39 | }) 40 | }) 41 | 42 | Context("with an already defined Storage Group with the same name", func() { 43 | BeforeEach(func() { 44 | name = "mysql" 45 | }) 46 | 47 | It("produces an error", func() { 48 | gdsl.StorageGroup(name, dsl) 49 | Ω(Errors).Should(HaveOccurred()) 50 | }) 51 | }) 52 | 53 | Context("with an already defined Storage Group with a different name", func() { 54 | BeforeEach(func() { 55 | name = "mysql" 56 | }) 57 | 58 | It("return an error", func() { 59 | gdsl.StorageGroup("news", dsl) 60 | Ω(Errors).Should(Not(HaveOccurred())) 61 | }) 62 | }) 63 | 64 | Context("with valid DSL", func() { 65 | JustBeforeEach(func() { 66 | Ω(Errors).ShouldNot(HaveOccurred()) 67 | Ω(Design.Validate()).ShouldNot(HaveOccurred()) 68 | }) 69 | 70 | Context("with a description", func() { 71 | const description = "description" 72 | 73 | BeforeEach(func() { 74 | dsl = func() { 75 | gdsl.Description(description) 76 | } 77 | }) 78 | 79 | It("sets the storage group description", func() { 80 | Ω(gorma.GormaDesign.Description).Should(Equal(description)) 81 | }) 82 | }) 83 | 84 | }) 85 | }) 86 | -------------------------------------------------------------------------------- /generator.go: -------------------------------------------------------------------------------- 1 | package gorma 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/goadesign/goa/design" 11 | "github.com/goadesign/goa/goagen/codegen" 12 | "github.com/goadesign/goa/goagen/utils" 13 | ) 14 | 15 | // Generator is the application code generator. 16 | type Generator struct { 17 | genfiles []string // Generated files 18 | outDir string // Absolute path to output directory 19 | target string // Target package name - "models" by default 20 | appPkg string // Generated goa app package name - "app" by default 21 | appPkgPath string // Generated goa app package import path 22 | } 23 | 24 | // Generate is the generator entry point called by the meta generator. 25 | func Generate() (files []string, err error) { 26 | var outDir, target, appPkg, ver string 27 | 28 | set := flag.NewFlagSet("gorma", flag.PanicOnError) 29 | set.String("design", "", "") 30 | set.StringVar(&outDir, "out", "", "") 31 | set.StringVar(&ver, "version", "", "") 32 | set.StringVar(&target, "pkg", "models", "") 33 | set.StringVar(&appPkg, "app", "app", "") 34 | set.Parse(os.Args[2:]) 35 | 36 | // First check compatibility 37 | if err := codegen.CheckVersion(ver); err != nil { 38 | return nil, err 39 | } 40 | 41 | // Now proceed 42 | appPkgPath, err := codegen.PackagePath(filepath.Join(outDir, appPkg)) 43 | if err != nil { 44 | return nil, fmt.Errorf("invalid app package: %s", err) 45 | } 46 | outDir = filepath.Join(outDir, target) 47 | 48 | g := &Generator{outDir: outDir, target: target, appPkg: appPkg, appPkgPath: appPkgPath} 49 | 50 | return g.Generate(design.Design) 51 | } 52 | 53 | // Generate the application code, implement codegen.Generator. 54 | func (g *Generator) Generate(api *design.APIDefinition) (_ []string, err error) { 55 | if api == nil { 56 | return nil, fmt.Errorf("missing API definition, make sure design.Design is properly initialized") 57 | } 58 | go utils.Catch(nil, func() { g.Cleanup() }) 59 | defer func() { 60 | if err != nil { 61 | g.Cleanup() 62 | } 63 | }() 64 | if err := os.MkdirAll(g.outDir, 0755); err != nil { 65 | return nil, err 66 | } 67 | 68 | if err := g.generateUserTypes(g.outDir, api); err != nil { 69 | return g.genfiles, err 70 | } 71 | if err := g.generateUserHelpers(g.outDir, api); err != nil { 72 | return g.genfiles, err 73 | } 74 | 75 | return g.genfiles, nil 76 | } 77 | 78 | // Cleanup removes the entire "app" directory if it was created by this generator. 79 | func (g *Generator) Cleanup() { 80 | if len(g.genfiles) == 0 { 81 | return 82 | } 83 | //os.RemoveAll(ModelOutputDir()) 84 | g.genfiles = nil 85 | } 86 | 87 | // generateUserTypes iterates through the user types and generates the data structures and 88 | // marshaling code. 89 | func (g *Generator) generateUserTypes(outdir string, api *design.APIDefinition) error { 90 | var modelname, filename string 91 | err := GormaDesign.IterateStores(func(store *RelationalStoreDefinition) error { 92 | err := store.IterateModels(func(model *RelationalModelDefinition) error { 93 | modelname = strings.ToLower(codegen.Goify(model.ModelName, false)) 94 | 95 | filename = fmt.Sprintf("%s.go", modelname) 96 | utFile := filepath.Join(outdir, filename) 97 | err := os.RemoveAll(utFile) 98 | if err != nil { 99 | fmt.Println(err) 100 | } 101 | utWr, err := NewUserTypesWriter(utFile) 102 | if err != nil { 103 | panic(err) // bug 104 | } 105 | title := fmt.Sprintf("%s: Models", api.Context()) 106 | imports := []*codegen.ImportSpec{ 107 | codegen.SimpleImport(g.appPkgPath), 108 | codegen.SimpleImport("context"), 109 | codegen.SimpleImport("time"), 110 | codegen.SimpleImport("github.com/goadesign/goa"), 111 | codegen.SimpleImport("github.com/jinzhu/gorm"), 112 | codegen.SimpleImport("github.com/gofrs/uuid"), 113 | } 114 | 115 | if model.Cached { 116 | imp := codegen.NewImport("cache", "github.com/patrickmn/go-cache") 117 | imports = append(imports, imp) 118 | imp = codegen.SimpleImport("strconv") 119 | imports = append(imports, imp) 120 | } 121 | utWr.WriteHeader(title, g.target, imports) 122 | data := &UserTypeTemplateData{ 123 | APIDefinition: api, 124 | UserType: model, 125 | DefaultPkg: g.target, 126 | AppPkg: g.appPkgPath, 127 | } 128 | err = utWr.Execute(data) 129 | g.genfiles = append(g.genfiles, utFile) 130 | if err != nil { 131 | fmt.Println(err) 132 | return err 133 | } 134 | err = utWr.FormatCode() 135 | if err != nil { 136 | fmt.Println(err) 137 | } 138 | return err 139 | }) 140 | return err 141 | }) 142 | return err 143 | } 144 | 145 | // This var is set as a local packagewide variable to store the name of a 146 | // mediatype (Mt1) that may be needed in anohter mediatype (Mt2) in a different call 147 | // scopes of generateUserTypes. (if Mt1 is used in an array like manner in Mt2) 148 | var helperFuncMediaTypeNames = map[string]string{} 149 | 150 | // generateUserHelpers iterates through the user types and generates the data structures and 151 | // marshaling code. 152 | func (g *Generator) generateUserHelpers(outdir string, api *design.APIDefinition) error { 153 | var modelname, filename string 154 | err := GormaDesign.IterateStores(func(store *RelationalStoreDefinition) error { 155 | err := store.IterateModels(func(model *RelationalModelDefinition) error { 156 | modelname = strings.ToLower(codegen.Goify(model.ModelName, false)) 157 | 158 | filename = fmt.Sprintf("%s_helper.go", modelname) 159 | utFile := filepath.Join(outdir, filename) 160 | err := os.RemoveAll(utFile) 161 | if err != nil { 162 | fmt.Println(err) 163 | } 164 | utWr, err := NewUserHelperWriter(utFile) 165 | if err != nil { 166 | panic(err) // bug 167 | } 168 | title := fmt.Sprintf("%s: Model Helpers", api.Context()) 169 | imports := []*codegen.ImportSpec{ 170 | codegen.SimpleImport(g.appPkgPath), 171 | codegen.SimpleImport("context"), 172 | codegen.SimpleImport("time"), 173 | codegen.SimpleImport("github.com/goadesign/goa"), 174 | codegen.SimpleImport("github.com/jinzhu/gorm"), 175 | codegen.NewImport("uuid", "github.com/satori/go.uuid"), 176 | } 177 | 178 | if model.Cached { 179 | imp := codegen.NewImport("cache", "github.com/patrickmn/go-cache") 180 | imports = append(imports, imp) 181 | imp = codegen.SimpleImport("strconv") 182 | imports = append(imports, imp) 183 | } 184 | utWr.WriteHeader(title, g.target, imports) 185 | data := &UserTypeTemplateData{ 186 | APIDefinition: api, 187 | UserType: model, 188 | DefaultPkg: g.target, 189 | AppPkg: g.appPkgPath, 190 | } 191 | err = utWr.Execute(data) 192 | g.genfiles = append(g.genfiles, utFile) 193 | if err != nil { 194 | fmt.Println(err) 195 | return err 196 | } 197 | err = utWr.FormatCode() 198 | if err != nil { 199 | fmt.Println(err) 200 | } 201 | return err 202 | }) 203 | return err 204 | }) 205 | return err 206 | } 207 | -------------------------------------------------------------------------------- /init.go: -------------------------------------------------------------------------------- 1 | package gorma 2 | 3 | // GormaDesign is the root definition for Gorma 4 | var GormaDesign *StorageGroupDefinition 5 | 6 | const ( 7 | // StorageGroup is the constant string used as the index in the 8 | // GormaConstructs map 9 | StorageGroup = "storagegroup" 10 | // MySQL is the StorageType for MySQL databases 11 | MySQL RelationalStorageType = "mysql" 12 | // Postgres is the StorageType for Postgres 13 | Postgres RelationalStorageType = "postgres" 14 | // SQLite3 is the StorageType for SQLite3 databases 15 | SQLite3 RelationalStorageType = "sqlite3" 16 | // None is For tests 17 | None RelationalStorageType = "" 18 | // Boolean is a bool field type 19 | Boolean FieldType = "bool" 20 | // Integer is an integer field type 21 | Integer FieldType = "integer" 22 | // BigInteger is a large integer field type 23 | BigInteger FieldType = "biginteger" 24 | // AutoInteger is not implemented 25 | AutoInteger FieldType = "auto_integer" 26 | // AutoBigInteger is not implemented 27 | AutoBigInteger FieldType = "auto_biginteger" 28 | // Decimal is a float field type 29 | Decimal FieldType = "decimal" 30 | // BigDecimal is a large float field type 31 | BigDecimal FieldType = "bigdecimal" 32 | // String is a varchar field type 33 | String FieldType = "string" 34 | // Text is a large string field type 35 | Text FieldType = "text" 36 | // UUID is not implemented yet 37 | UUID FieldType = "uuid" 38 | // Timestamp is a date/time field in the database 39 | Timestamp FieldType = "timestamp" 40 | // NullableTimestamp is a timestamp that may not be 41 | // populated. Fields with no value will be null in the database 42 | NullableTimestamp FieldType = "nulltimestamp" 43 | // NotFound is used internally 44 | NotFound FieldType = "notfound" 45 | // HasOne is used internally 46 | HasOne FieldType = "hasone" 47 | // HasOneKey is used internally 48 | HasOneKey FieldType = "hasonekey" 49 | // HasMany is used internally 50 | HasMany FieldType = "hasmany" 51 | // HasManyKey is used internally 52 | HasManyKey FieldType = "hasmanykey" 53 | // Many2Many is used internally 54 | Many2Many FieldType = "many2many" 55 | // Many2ManyKey is used internally 56 | Many2ManyKey FieldType = "many2manykey" 57 | // BelongsTo is used internally 58 | BelongsTo FieldType = "belongsto" 59 | ) 60 | -------------------------------------------------------------------------------- /manytomany.go: -------------------------------------------------------------------------------- 1 | package gorma 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/jinzhu/inflection" 7 | ) 8 | 9 | // LeftNamePlural returns the pluralized version of 10 | // the "owner" of the m2m relationship. 11 | func (m *ManyToManyDefinition) LeftNamePlural() string { 12 | return inflection.Plural(m.Left.ModelName) 13 | } 14 | 15 | // RightNamePlural returns the pluralized version 16 | // of the "child" of the m2m relationship. 17 | func (m *ManyToManyDefinition) RightNamePlural() string { 18 | return inflection.Plural(m.Right.ModelName) 19 | } 20 | 21 | // LeftName returns the name of the "owner" of the 22 | // m2m relationship. 23 | func (m *ManyToManyDefinition) LeftName() string { 24 | return m.Left.ModelName 25 | } 26 | 27 | // RightName returns the name of the "child" of the 28 | // m2m relationship. 29 | func (m *ManyToManyDefinition) RightName() string { 30 | return m.Right.ModelName 31 | } 32 | 33 | // LowerLeftName returns the lower case name of the "owner" of the 34 | // m2m relationship. 35 | func (m *ManyToManyDefinition) LowerLeftName() string { 36 | return strings.ToLower(m.Left.ModelName) 37 | } 38 | 39 | // LowerRightName returns the name of the "child" of the 40 | // m2m relationship. 41 | func (m *ManyToManyDefinition) LowerRightName() string { 42 | return strings.ToLower(m.Right.ModelName) 43 | } 44 | -------------------------------------------------------------------------------- /mapdefinition.go: -------------------------------------------------------------------------------- 1 | package gorma 2 | 3 | // NewMapDefinition returns an initialized 4 | // MapDefinition. 5 | func NewMapDefinition() *MapDefinition { 6 | return &MapDefinition{} 7 | } 8 | -------------------------------------------------------------------------------- /relationalfield.go: -------------------------------------------------------------------------------- 1 | package gorma 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "unicode" 7 | 8 | "github.com/goadesign/goa/design" 9 | "github.com/goadesign/goa/dslengine" 10 | ) 11 | 12 | // NewRelationalFieldDefinition returns an initialized 13 | // RelationalFieldDefinition. 14 | func NewRelationalFieldDefinition() *RelationalFieldDefinition { 15 | m := &RelationalFieldDefinition{ 16 | Mappings: make(map[string]*MapDefinition), 17 | } 18 | return m 19 | } 20 | 21 | // Context returns the generic definition name used in error messages. 22 | func (f *RelationalFieldDefinition) Context() string { 23 | if f.FieldName != "" { 24 | return fmt.Sprintf("RelationalField %#v", f.FieldName) 25 | } 26 | return "unnamed RelationalField" 27 | } 28 | 29 | // DSL returns this object's DSL. 30 | func (f *RelationalFieldDefinition) DSL() func() { 31 | return f.DefinitionDSL 32 | } 33 | 34 | // Children returns a slice of this objects children. 35 | func (f RelationalFieldDefinition) Children() []dslengine.Definition { 36 | // no children yet 37 | return []dslengine.Definition{} 38 | } 39 | 40 | // Attribute implements the Container interface of the goa Attribute 41 | // model. 42 | func (f *RelationalFieldDefinition) Attribute() *design.AttributeDefinition { 43 | return f.a 44 | } 45 | 46 | // FieldDefinition returns the field's struct definition. 47 | func (f *RelationalFieldDefinition) FieldDefinition() string { 48 | var comment string 49 | if f.Description != "" { 50 | comment = "// " + f.Description 51 | } 52 | def := fmt.Sprintf("%s\t%s %s %s\n", f.FieldName, goDatatype(f, true), tags(f), comment) 53 | return def 54 | } 55 | 56 | // Tags returns the sql and gorm struct tags for the Definition. 57 | func (f *RelationalFieldDefinition) Tags() string { 58 | return tags(f) 59 | } 60 | 61 | // LowerName returns the field name as a lowercase string. 62 | func (f *RelationalFieldDefinition) LowerName() string { 63 | return strings.ToLower(f.FieldName) 64 | } 65 | 66 | // Underscore returns the field name as a lowercase string in snake case. 67 | func (f *RelationalFieldDefinition) Underscore() string { 68 | runes := []rune(f.FieldName) 69 | length := len(runes) 70 | 71 | var out []rune 72 | for i := 0; i < length; i++ { 73 | if i > 0 && unicode.IsUpper(runes[i]) && ((i+1 < length && unicode.IsLower(runes[i+1])) || unicode.IsLower(runes[i-1])) { 74 | out = append(out, '_') 75 | } 76 | out = append(out, unicode.ToLower(runes[i])) 77 | } 78 | 79 | return string(out) 80 | } 81 | 82 | func goDatatype(f *RelationalFieldDefinition, includePtr bool) string { 83 | var ptr string 84 | if f.Nullable && includePtr { 85 | ptr = "*" 86 | } 87 | switch f.Datatype { 88 | case Boolean: 89 | return ptr + "bool" 90 | case Integer: 91 | return ptr + "int" 92 | case BigInteger: 93 | return ptr + "int64" 94 | case AutoInteger, AutoBigInteger: 95 | return ptr + "int " // sql/gorm tags later 96 | case Decimal: 97 | return ptr + "float32" 98 | case BigDecimal: 99 | return ptr + "float64" 100 | case String: 101 | return ptr + "string" 102 | case Text: 103 | return ptr + "string" 104 | case UUID: 105 | return ptr + "uuid.UUID" 106 | case Timestamp, NullableTimestamp: 107 | return ptr + "time.Time" 108 | case BelongsTo: 109 | return ptr + belongsToIDType(f, includePtr) 110 | case HasMany: 111 | return fmt.Sprintf("[]%s", f.HasMany) 112 | case HasManyKey: 113 | return ptr + hasManyIDType(f, includePtr) 114 | case HasOneKey: 115 | return ptr + hasOneIDType(f, includePtr) 116 | case HasOne: 117 | return fmt.Sprintf("%s", f.HasOne) 118 | default: 119 | 120 | if f.Many2Many != "" { 121 | return fmt.Sprintf("[]%s", f.Many2Many) 122 | } 123 | } 124 | 125 | return "UNKNOWN TYPE" 126 | } 127 | 128 | func goDatatypeByModel(m *RelationalModelDefinition, belongsToModelName string) string { 129 | f := m.RelationalFields[belongsToModelName+"ID"] 130 | if f == nil { 131 | return "int" 132 | } 133 | return belongsToIDType(f, true) 134 | } 135 | 136 | func belongsToIDType(f *RelationalFieldDefinition, includePtr bool) string { 137 | if f.Parent == nil { 138 | return "int" 139 | } 140 | modelName := strings.Replace(f.FieldName, "ID", "", -1) 141 | model := f.Parent.BelongsTo[modelName] 142 | return relatedIDType(model, includePtr) 143 | } 144 | 145 | func hasOneIDType(f *RelationalFieldDefinition, includePtr bool) string { 146 | if f.Parent == nil { 147 | return "int" 148 | } 149 | modelName := strings.Replace(f.FieldName, "ID", "", -1) 150 | model := f.Parent.HasOne[modelName] 151 | return relatedIDType(model, includePtr) 152 | } 153 | 154 | func hasManyIDType(f *RelationalFieldDefinition, includePtr bool) string { 155 | if f.Parent == nil { 156 | return "int" 157 | } 158 | modelName := strings.Replace(f.FieldName, "ID", "", -1) 159 | model := f.Parent.HasMany[modelName] 160 | return relatedIDType(model, includePtr) 161 | } 162 | 163 | func relatedIDType(m *RelationalModelDefinition, includePtr bool) string { 164 | if m == nil { 165 | return "int" 166 | } 167 | if len(m.PrimaryKeys) > 1 { 168 | panic("Can't determine field Type when using multiple primary keys") 169 | } 170 | return goDatatype(m.PrimaryKeys[0], includePtr) 171 | } 172 | 173 | func tags(f *RelationalFieldDefinition) string { 174 | var sqltags []string 175 | if f.SQLTag != "" { 176 | sqltags = append(sqltags, f.SQLTag) 177 | } 178 | 179 | var gormtags []string 180 | if f.DatabaseFieldName != "" && f.DatabaseFieldName != f.Underscore() { 181 | gormtags = append(gormtags, "column:"+f.DatabaseFieldName) 182 | } 183 | if f.PrimaryKey { 184 | gormtags = append(gormtags, "primary_key") 185 | } 186 | if f.Many2Many != "" { 187 | gormtags = append(gormtags, "many2many:"+f.TableName) 188 | } 189 | 190 | var tags []string 191 | if len(sqltags) > 0 { 192 | sqltag := "sql:\"" + strings.Join(sqltags, ";") + "\"" 193 | tags = append(tags, sqltag) 194 | } 195 | if len(gormtags) > 0 { 196 | gormtag := "gorm:\"" + strings.Join(gormtags, ";") + "\"" 197 | tags = append(tags, gormtag) 198 | } 199 | 200 | if len(tags) > 0 { 201 | return "`" + strings.Join(tags, " ") + "`" 202 | } 203 | return "" 204 | } 205 | -------------------------------------------------------------------------------- /relationalfield_test.go: -------------------------------------------------------------------------------- 1 | package gorma_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/goadesign/gorma" 8 | "github.com/goadesign/gorma/dsl" 9 | ) 10 | 11 | func TestFieldContext(t *testing.T) { 12 | sg := &gorma.RelationalFieldDefinition{} 13 | sg.FieldName = "SG" 14 | 15 | c := sg.Context() 16 | exp := fmt.Sprintf("RelationalField %#v", sg.FieldName) 17 | if c != exp { 18 | t.Errorf("Expected %s, got %s", exp, c) 19 | } 20 | 21 | sg.FieldName = "" 22 | 23 | c = sg.Context() 24 | exp = "unnamed RelationalField" 25 | if c != exp { 26 | t.Errorf("Expected %s, got %s", exp, c) 27 | } 28 | } 29 | 30 | func TestFieldDSL(t *testing.T) { 31 | sg := &gorma.RelationalFieldDefinition{} 32 | f := func() { 33 | return 34 | } 35 | sg.DefinitionDSL = f 36 | c := sg.DSL() 37 | if c == nil { 38 | t.Errorf("Expected %T, got nil", f) 39 | } 40 | 41 | } 42 | 43 | func TestFieldDefinitions(t *testing.T) { 44 | 45 | var fieldtests = []struct { 46 | name string 47 | datatype gorma.FieldType 48 | description string 49 | nullable bool 50 | belongsto string 51 | hasmany string 52 | hasone string 53 | many2many string 54 | expected string 55 | }{ 56 | {"id", gorma.Integer, "description", false, "", "", "", "", "ID\tint // description\n"}, 57 | {"id", gorma.UUID, "description", false, "", "", "", "", "ID\tuuid.UUID // description\n"}, 58 | {"id", gorma.BigInteger, "description", false, "", "", "", "", "ID\tint64 // description\n"}, 59 | {"name", gorma.String, "name", true, "", "", "", "", "Name\t*string // name\n"}, 60 | {"user", gorma.HasOne, "has one", false, "", "", "User", "", "User\tUser // has one\n"}, 61 | {"user_id", gorma.BelongsTo, "belongs to", false, "", "", "", "", "UserID\tint // belongs to\n"}, 62 | } 63 | for _, tt := range fieldtests { 64 | f := &gorma.RelationalFieldDefinition{} 65 | f.FieldName = dsl.SanitizeFieldName(tt.name) 66 | f.Datatype = tt.datatype 67 | f.Description = tt.description 68 | f.Nullable = tt.nullable 69 | f.BelongsTo = tt.belongsto 70 | f.HasMany = tt.hasmany 71 | f.HasOne = tt.hasone 72 | f.Many2Many = tt.many2many 73 | def := f.FieldDefinition() 74 | 75 | if def != tt.expected { 76 | t.Errorf("expected %s,got %s", tt.expected, def) 77 | } 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /relationalmodel.go: -------------------------------------------------------------------------------- 1 | package gorma 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strings" 7 | "unicode" 8 | 9 | "bitbucket.org/pkg/inflect" 10 | 11 | "github.com/goadesign/goa/design" 12 | "github.com/goadesign/goa/dslengine" 13 | "github.com/goadesign/goa/goagen/codegen" 14 | "github.com/jinzhu/inflection" 15 | ) 16 | 17 | // NewRelationalModelDefinition returns an initialized 18 | // RelationalModelDefinition. 19 | func NewRelationalModelDefinition() *RelationalModelDefinition { 20 | baseAttr := &design.AttributeDefinition{} 21 | utd := &design.UserTypeDefinition{ 22 | AttributeDefinition: baseAttr, 23 | } 24 | utd.Type = design.Object{} 25 | m := &RelationalModelDefinition{ 26 | RelationalFields: make(map[string]*RelationalFieldDefinition), 27 | BuiltFrom: make(map[string]*design.UserTypeDefinition), 28 | RenderTo: make(map[string]*design.MediaTypeDefinition), 29 | BelongsTo: make(map[string]*RelationalModelDefinition), 30 | HasMany: make(map[string]*RelationalModelDefinition), 31 | HasOne: make(map[string]*RelationalModelDefinition), 32 | ManyToMany: make(map[string]*ManyToManyDefinition), 33 | UserTypeDefinition: &design.UserTypeDefinition{ 34 | AttributeDefinition: baseAttr, 35 | }, 36 | } 37 | return m 38 | } 39 | 40 | // Context returns the generic definition name used in error messages. 41 | func (f *RelationalModelDefinition) Context() string { 42 | if f.ModelName != "" { 43 | return fmt.Sprintf("RelationalModel %#v", f.Name()) 44 | } 45 | return "unnamed RelationalModel" 46 | } 47 | 48 | // DSL returns this object's DSL. 49 | func (f *RelationalModelDefinition) DSL() func() { 50 | return f.DefinitionDSL 51 | } 52 | 53 | // TableName returns the TableName of the struct. 54 | func (f RelationalModelDefinition) TableName() string { 55 | return inflect.Underscore(inflection.Plural(f.ModelName)) 56 | } 57 | 58 | // Children returns a slice of this objects children. 59 | func (f RelationalModelDefinition) Children() []dslengine.Definition { 60 | var stores []dslengine.Definition 61 | for _, s := range f.RelationalFields { 62 | stores = append(stores, s) 63 | } 64 | return stores 65 | } 66 | 67 | // PKAttributes constructs a pair of field + definition strings 68 | // useful for method parameters. 69 | func (f *RelationalModelDefinition) PKAttributes() string { 70 | var attr []string 71 | for _, pk := range f.PrimaryKeys { 72 | attr = append(attr, fmt.Sprintf("%s %s", codegen.Goify(pk.DatabaseFieldName, false), goDatatype(pk, true))) 73 | } 74 | return strings.Join(attr, ",") 75 | } 76 | 77 | // PKWhere returns an array of strings representing the where clause 78 | // of a retrieval by primary key(s) -- x = ? and y = ?. 79 | func (f *RelationalModelDefinition) PKWhere() string { 80 | var pkwhere []string 81 | for _, pk := range f.PrimaryKeys { 82 | def := fmt.Sprintf("%s = ?", pk.DatabaseFieldName) 83 | pkwhere = append(pkwhere, def) 84 | } 85 | return strings.Join(pkwhere, " and ") 86 | } 87 | 88 | // PKWhereFields returns the fields for a where clause for the primary 89 | // keys of a model. 90 | func (f *RelationalModelDefinition) PKWhereFields() string { 91 | var pkwhere []string 92 | for _, pk := range f.PrimaryKeys { 93 | def := fmt.Sprintf("%s", codegen.Goify(pk.DatabaseFieldName, false)) 94 | pkwhere = append(pkwhere, def) 95 | } 96 | return strings.Join(pkwhere, ",") 97 | } 98 | 99 | // PKUpdateFields returns something? This function doesn't look useful in 100 | // current form. Perhaps it isn't. 101 | func (f *RelationalModelDefinition) PKUpdateFields(modelname string) string { 102 | var pkwhere []string 103 | for _, pk := range f.PrimaryKeys { 104 | def := fmt.Sprintf("%s.%s", modelname, codegen.Goify(pk.FieldName, true)) 105 | pkwhere = append(pkwhere, def) 106 | } 107 | 108 | pkw := strings.Join(pkwhere, ",") 109 | return pkw 110 | } 111 | 112 | // StructDefinition returns the struct definition for the model. 113 | func (f *RelationalModelDefinition) StructDefinition() string { 114 | header := fmt.Sprintf("type %s struct {\n", f.ModelName) 115 | var output string 116 | f.IterateFields(func(field *RelationalFieldDefinition) error { 117 | output = output + field.FieldDefinition() 118 | return nil 119 | }) 120 | 121 | // Get a sortable slice of BelongsTo relationships 122 | var keys []string 123 | for k := range f.BelongsTo { 124 | keys = append(keys, k) 125 | } 126 | sort.Strings(keys) 127 | 128 | for _, k := range keys { 129 | output = output + f.BelongsTo[k].ModelName + "\t" + f.BelongsTo[k].ModelName + "\n" 130 | } 131 | footer := "}\n" 132 | return header + output + footer 133 | } 134 | 135 | // Attribute implements the Container interface of goa. 136 | func (f *RelationalModelDefinition) Attribute() *design.AttributeDefinition { 137 | return f.AttributeDefinition 138 | } 139 | 140 | // Project does something interesting, and I don't remember if I use it 141 | // anywhere. 142 | // 143 | // TODO find out 144 | func (f *RelationalModelDefinition) Project(name, v string) *design.MediaTypeDefinition { 145 | p, _, _ := f.RenderTo[name].Project(v) 146 | return p 147 | } 148 | 149 | // LowerName returns the model name as a lowercase string. 150 | func (f *RelationalModelDefinition) LowerName() string { 151 | return codegen.Goify(strings.ToLower(f.ModelName), false) 152 | } 153 | 154 | // Underscore returns the model name as a lowercase string in snake case. 155 | func (f *RelationalModelDefinition) Underscore() string { 156 | runes := []rune(f.ModelName) 157 | length := len(runes) 158 | 159 | var out []rune 160 | for i := 0; i < length; i++ { 161 | if i > 0 && unicode.IsUpper(runes[i]) && ((i+1 < length && unicode.IsLower(runes[i+1])) || unicode.IsLower(runes[i-1])) { 162 | out = append(out, '_') 163 | } 164 | out = append(out, unicode.ToLower(runes[i])) 165 | } 166 | 167 | return string(out) 168 | } 169 | 170 | // IterateBuildSources runs an iterator function once per Model in the Store's model list. 171 | func (f *RelationalModelDefinition) IterateBuildSources(it BuildSourceIterator) error { 172 | 173 | for _, bs := range f.BuildSources { 174 | if err := it(bs); err != nil { 175 | return err 176 | } 177 | } 178 | return nil 179 | } 180 | 181 | // IterateFields returns an iterator function useful for iterating through 182 | // this model's field list. 183 | func (f *RelationalModelDefinition) IterateFields(it FieldIterator) error { 184 | // Break out each type of fields 185 | 186 | var pkkeys []string 187 | pks := make(map[string]string) 188 | for n := range f.RelationalFields { 189 | if f.RelationalFields[n].PrimaryKey { 190 | pks[n] = n 191 | pkkeys = append(pkkeys, n) 192 | } 193 | } 194 | sort.Strings(pkkeys) 195 | 196 | var namekeys []string 197 | names := make(map[string]string) 198 | for n := range f.RelationalFields { 199 | if !f.RelationalFields[n].PrimaryKey && !f.RelationalFields[n].Timestamp { 200 | names[n] = n 201 | namekeys = append(namekeys, n) 202 | } 203 | } 204 | sort.Strings(namekeys) 205 | 206 | var datekeys []string 207 | dates := make(map[string]string) 208 | for n := range f.RelationalFields { 209 | if f.RelationalFields[n].Timestamp { 210 | dates[n] = n 211 | datekeys = append(datekeys, n) 212 | } 213 | } 214 | sort.Strings(datekeys) 215 | 216 | // Combine the sorted slices 217 | var fields []string 218 | fields = append(fields, pkkeys...) 219 | fields = append(fields, namekeys...) 220 | fields = append(fields, datekeys...) 221 | 222 | // Iterate them 223 | for _, n := range fields { 224 | if err := it(f.RelationalFields[n]); err != nil { 225 | return err 226 | } 227 | } 228 | return nil 229 | } 230 | 231 | // PopulateFromModeledType creates fields for the model 232 | // based on the goa UserTypeDefinition it models, which is 233 | // set using BuildsFrom(). 234 | // This happens before fields are processed, so it's 235 | // ok to just assign without testing. 236 | func (f *RelationalModelDefinition) PopulateFromModeledType() { 237 | if f.BuiltFrom == nil { 238 | return 239 | } 240 | for _, utd := range f.BuiltFrom { 241 | obj := utd.ToObject() 242 | obj.IterateAttributes(func(name string, att *design.AttributeDefinition) error { 243 | rf, ok := f.RelationalFields[codegen.Goify(name, true)] 244 | if ok { 245 | // We already have a mapping for this field. What to do? 246 | if rf.Datatype != "" { 247 | return nil 248 | } 249 | // we may have seen the field but don't know its type 250 | // TODO(BJK) refactor this into separate func later 251 | switch att.Type.Kind() { 252 | case design.BooleanKind: 253 | rf.Datatype = Boolean 254 | case design.IntegerKind: 255 | rf.Datatype = Integer 256 | case design.NumberKind: 257 | rf.Datatype = BigDecimal 258 | case design.StringKind: 259 | rf.Datatype = String 260 | case design.DateTimeKind: 261 | rf.Datatype = Timestamp 262 | case design.MediaTypeKind: 263 | // Embedded MediaType 264 | // Skip for now? 265 | return nil 266 | 267 | default: 268 | dslengine.ReportError("Unsupported type: %#v %s", att.Type.Kind(), att.Type.Name()) 269 | } 270 | if !utd.IsRequired(name) { 271 | rf.Nullable = true 272 | } 273 | } 274 | 275 | rf = &RelationalFieldDefinition{} 276 | rf.Parent = f 277 | rf.FieldName = codegen.Goify(name, true) 278 | 279 | if strings.HasSuffix(rf.FieldName, "Id") { 280 | rf.FieldName = strings.TrimSuffix(rf.FieldName, "Id") 281 | rf.FieldName = rf.FieldName + "ID" 282 | } 283 | switch att.Type.Kind() { 284 | case design.BooleanKind: 285 | rf.Datatype = Boolean 286 | case design.IntegerKind: 287 | rf.Datatype = Integer 288 | case design.NumberKind: 289 | rf.Datatype = BigDecimal 290 | case design.StringKind: 291 | rf.Datatype = String 292 | case design.DateTimeKind: 293 | rf.Datatype = Timestamp 294 | case design.MediaTypeKind: 295 | // Embedded MediaType 296 | // Skip for now? 297 | return nil 298 | 299 | default: 300 | dslengine.ReportError("Unsupported type: %#v %s", att.Type.Kind(), att.Type.Name()) 301 | } 302 | if !utd.IsRequired(name) { 303 | rf.Nullable = true 304 | } 305 | // might need this later? 306 | rf.a = att 307 | f.RelationalFields[rf.FieldName] = rf 308 | 309 | addAttributeToModel(name, att, f) 310 | 311 | return nil 312 | }) 313 | } 314 | return 315 | } 316 | 317 | func addAttributeToModel(name string, att *design.AttributeDefinition, m *RelationalModelDefinition) { 318 | var parent *design.AttributeDefinition 319 | parent = m.AttributeDefinition 320 | if parent != nil { 321 | if parent.Type == nil { 322 | parent.Type = design.Object{} 323 | } 324 | if _, ok := parent.Type.(design.Object); !ok { 325 | dslengine.ReportError("can't define child attributes on attribute of type %s", parent.Type.Name()) 326 | return 327 | } 328 | 329 | parent.Type.(design.Object)[name] = att 330 | } 331 | 332 | } 333 | 334 | // copied from Goa 335 | func parseAttributeArgs(baseAttr *design.AttributeDefinition, args ...interface{}) (design.DataType, string, func()) { 336 | var ( 337 | dataType design.DataType 338 | description string 339 | dsl func() 340 | ok bool 341 | ) 342 | 343 | parseDataType := func(expected string, index int) { 344 | if name, ok := args[index].(string); ok { 345 | // Lookup type by name 346 | if dataType, ok = design.Design.Types[name]; !ok { 347 | if dataType = design.Design.MediaTypeWithIdentifier(name); dataType == nil { 348 | dslengine.InvalidArgError(expected, args[index]) 349 | } 350 | } 351 | return 352 | } 353 | if dataType, ok = args[index].(design.DataType); !ok { 354 | dslengine.InvalidArgError(expected, args[index]) 355 | } 356 | } 357 | parseDescription := func(expected string, index int) { 358 | if description, ok = args[index].(string); !ok { 359 | dslengine.InvalidArgError(expected, args[index]) 360 | } 361 | } 362 | parseDSL := func(index int, success, failure func()) { 363 | if dsl, ok = args[index].(func()); ok { 364 | success() 365 | } else { 366 | failure() 367 | } 368 | } 369 | 370 | success := func() {} 371 | 372 | switch len(args) { 373 | case 0: 374 | if baseAttr != nil { 375 | dataType = baseAttr.Type 376 | } else { 377 | dataType = design.String 378 | } 379 | case 1: 380 | success = func() { 381 | if baseAttr != nil { 382 | dataType = baseAttr.Type 383 | } 384 | } 385 | parseDSL(0, success, func() { parseDataType("type, type name or func()", 0) }) 386 | case 2: 387 | parseDataType("type or type name", 0) 388 | parseDSL(1, success, func() { parseDescription("string or func()", 1) }) 389 | case 3: 390 | parseDataType("type or type name", 0) 391 | parseDescription("string", 1) 392 | parseDSL(2, success, func() { dslengine.InvalidArgError("func()", args[2]) }) 393 | default: 394 | dslengine.ReportError("too many arguments in call to Attribute") 395 | } 396 | 397 | return dataType, description, dsl 398 | } 399 | -------------------------------------------------------------------------------- /relationalmodel_test.go: -------------------------------------------------------------------------------- 1 | package gorma_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/goadesign/goa/design" 8 | "github.com/goadesign/gorma" 9 | "github.com/goadesign/gorma/dsl" 10 | ) 11 | 12 | func TestModelContext(t *testing.T) { 13 | sg := &gorma.RelationalModelDefinition{ 14 | UserTypeDefinition: &design.UserTypeDefinition{ 15 | AttributeDefinition: &design.AttributeDefinition{}, 16 | }, 17 | } 18 | 19 | sg.Type = design.String 20 | sg.ModelName = "SG" 21 | 22 | c := sg.Context() 23 | exp := fmt.Sprintf("RelationalModel %#v", sg.Name()) 24 | if c != exp { 25 | t.Errorf("Expected %s, got %s", exp, c) 26 | } 27 | 28 | sg.ModelName = "" 29 | 30 | c = sg.Context() 31 | exp = "unnamed RelationalModel" 32 | if c != exp { 33 | t.Errorf("Expected %s, got %s", exp, c) 34 | } 35 | } 36 | 37 | func TestModelDSL(t *testing.T) { 38 | sg := &gorma.RelationalModelDefinition{} 39 | f := func() { 40 | return 41 | } 42 | sg.DefinitionDSL = f 43 | c := sg.DSL() 44 | if c == nil { 45 | t.Errorf("Expected %T, got nil", f) 46 | } 47 | 48 | } 49 | 50 | func TestPKAttributesSingle(t *testing.T) { 51 | sg := &gorma.RelationalModelDefinition{} 52 | sg.RelationalFields = make(map[string]*gorma.RelationalFieldDefinition) 53 | f := makePK("id") 54 | sg.PrimaryKeys = append(sg.PrimaryKeys, f) 55 | sg.RelationalFields[f.FieldName] = f 56 | 57 | pka := sg.PKAttributes() 58 | 59 | if pka != "id int" { 60 | t.Errorf("Expected %s, got %s", "id int", pka) 61 | } 62 | 63 | } 64 | func TestPKAttributesMultiple(t *testing.T) { 65 | sg := &gorma.RelationalModelDefinition{} 66 | sg.RelationalFields = make(map[string]*gorma.RelationalFieldDefinition) 67 | f := makePK("Field1") 68 | sg.PrimaryKeys = append(sg.PrimaryKeys, f) 69 | sg.RelationalFields[f.FieldName] = f 70 | 71 | f2 := makePK("Field2") 72 | sg.RelationalFields[f2.FieldName] = f2 73 | sg.PrimaryKeys = append(sg.PrimaryKeys, f2) 74 | 75 | pka := sg.PKAttributes() 76 | 77 | if pka != "field1 int,field2 int" { 78 | t.Errorf("Expected %s, got %s", "field1 int,field2 int", pka) 79 | } 80 | 81 | } 82 | func makePK(name string) *gorma.RelationalFieldDefinition { 83 | 84 | f := &gorma.RelationalFieldDefinition{} 85 | f.FieldName = dsl.SanitizeFieldName(name) 86 | f.DatabaseFieldName = dsl.SanitizeDBFieldName(f.FieldName) 87 | f.Datatype = gorma.Integer 88 | f.PrimaryKey = true 89 | return f 90 | 91 | } 92 | func TestPKWhereSingle(t *testing.T) { 93 | sg := &gorma.RelationalModelDefinition{} 94 | sg.RelationalFields = make(map[string]*gorma.RelationalFieldDefinition) 95 | f := &gorma.RelationalFieldDefinition{} 96 | f.FieldName = dsl.SanitizeFieldName("ID") 97 | f.DatabaseFieldName = dsl.SanitizeDBFieldName(f.FieldName) 98 | f.Datatype = gorma.Integer 99 | f.PrimaryKey = true 100 | 101 | sg.RelationalFields[f.FieldName] = f 102 | sg.PrimaryKeys = append(sg.PrimaryKeys, f) 103 | 104 | pkw := sg.PKWhere() 105 | 106 | if pkw != "id = ?" { 107 | t.Errorf("Expected %s, got %s", "id = ?", pkw) 108 | } 109 | 110 | } 111 | 112 | func TestPKWhereMultiple(t *testing.T) { 113 | sg := &gorma.RelationalModelDefinition{} 114 | sg.RelationalFields = make(map[string]*gorma.RelationalFieldDefinition) 115 | f := makePK("Field1") 116 | sg.RelationalFields[f.FieldName] = f 117 | sg.PrimaryKeys = append(sg.PrimaryKeys, f) 118 | 119 | f2 := makePK("Field2") 120 | sg.RelationalFields[f2.FieldName] = f2 121 | sg.PrimaryKeys = append(sg.PrimaryKeys, f2) 122 | 123 | pkw := sg.PKWhere() 124 | 125 | if pkw != "field1 = ? and field2 = ?" { 126 | t.Errorf("Expected %s, got %s", "field1 = ? and field2 = ?", pkw) 127 | } 128 | 129 | } 130 | 131 | func TestPKWhereFieldsSingle(t *testing.T) { 132 | sg := &gorma.RelationalModelDefinition{} 133 | sg.RelationalFields = make(map[string]*gorma.RelationalFieldDefinition) 134 | f := &gorma.RelationalFieldDefinition{} 135 | f.FieldName = dsl.SanitizeFieldName("ID") 136 | f.DatabaseFieldName = dsl.SanitizeDBFieldName(f.FieldName) 137 | f.Datatype = gorma.Integer 138 | f.PrimaryKey = true 139 | 140 | sg.RelationalFields[f.FieldName] = f 141 | sg.PrimaryKeys = append(sg.PrimaryKeys, f) 142 | 143 | pkw := sg.PKWhereFields() 144 | 145 | if pkw != "id" { 146 | t.Errorf("Expected %s, got %s", "id", pkw) 147 | } 148 | 149 | } 150 | 151 | func TestPKWhereFieldsMultiple(t *testing.T) { 152 | sg := &gorma.RelationalModelDefinition{} 153 | sg.RelationalFields = make(map[string]*gorma.RelationalFieldDefinition) 154 | f := makePK("Field1") 155 | sg.RelationalFields[f.FieldName] = f 156 | sg.PrimaryKeys = append(sg.PrimaryKeys, f) 157 | 158 | f2 := makePK("Field2") 159 | sg.RelationalFields[f2.FieldName] = f2 160 | sg.PrimaryKeys = append(sg.PrimaryKeys, f2) 161 | 162 | pkw := sg.PKWhereFields() 163 | 164 | if pkw != "field1,field2" { 165 | t.Errorf("Expected %s, got %s", "field1,field2", pkw) 166 | } 167 | 168 | } 169 | 170 | func TestPKUpdateFieldsSingle(t *testing.T) { 171 | sg := &gorma.RelationalModelDefinition{} 172 | sg.RelationalFields = make(map[string]*gorma.RelationalFieldDefinition) 173 | f := &gorma.RelationalFieldDefinition{} 174 | f.FieldName = dsl.SanitizeFieldName("ID") 175 | f.DatabaseFieldName = dsl.SanitizeDBFieldName(f.FieldName) 176 | f.Datatype = gorma.Integer 177 | f.PrimaryKey = true 178 | 179 | sg.RelationalFields[f.FieldName] = f 180 | sg.PrimaryKeys = append(sg.PrimaryKeys, f) 181 | 182 | pkw := sg.PKUpdateFields("model") 183 | 184 | if pkw != "model.ID" { 185 | t.Errorf("Expected %s, got %s", "model.ID", pkw) 186 | } 187 | 188 | } 189 | 190 | func TestPKUpdateFieldsMultiple(t *testing.T) { 191 | sg := &gorma.RelationalModelDefinition{} 192 | sg.RelationalFields = make(map[string]*gorma.RelationalFieldDefinition) 193 | f := makePK("Field1") 194 | sg.RelationalFields[f.FieldName] = f 195 | sg.PrimaryKeys = append(sg.PrimaryKeys, f) 196 | 197 | f2 := makePK("Field2") 198 | sg.RelationalFields[f2.FieldName] = f2 199 | sg.PrimaryKeys = append(sg.PrimaryKeys, f2) 200 | 201 | pkw := sg.PKUpdateFields("model") 202 | 203 | if pkw != "model.Field1,model.Field2" { 204 | t.Errorf("Expected %s, got %s", "model.Field1,model.Field2", pkw) 205 | } 206 | 207 | } 208 | 209 | func TestTableName(t *testing.T) { 210 | sg := &gorma.RelationalModelDefinition{} 211 | sg.ModelName = "status" 212 | if sg.TableName() != "statuses" { 213 | t.Errorf("Expected %s, got %s", "statuses", sg.TableName()) 214 | } 215 | sg.ModelName = "study" 216 | if sg.TableName() != "studies" { 217 | t.Errorf("Expected %s, got %s", "studies", sg.TableName()) 218 | } 219 | sg.ModelName = "user" 220 | if sg.TableName() != "users" { 221 | t.Errorf("Expected %s, got %s", "users", sg.TableName()) 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /relationalstore.go: -------------------------------------------------------------------------------- 1 | package gorma 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | 7 | "github.com/goadesign/goa/dslengine" 8 | ) 9 | 10 | // NewRelationalStoreDefinition returns an initialized 11 | // RelationalStoreDefinition. 12 | func NewRelationalStoreDefinition() *RelationalStoreDefinition { 13 | m := &RelationalStoreDefinition{ 14 | RelationalModels: make(map[string]*RelationalModelDefinition), 15 | } 16 | return m 17 | } 18 | 19 | // Context returns the generic definition name used in error messages. 20 | func (sd *RelationalStoreDefinition) Context() string { 21 | if sd.Name != "" { 22 | return fmt.Sprintf("RelationalStore %#v", sd.Name) 23 | } 24 | return "unnamed RelationalStore" 25 | } 26 | 27 | // DSL returns this object's DSL. 28 | func (sd *RelationalStoreDefinition) DSL() func() { 29 | return sd.DefinitionDSL 30 | } 31 | 32 | // Children returns a slice of this objects children. 33 | func (sd RelationalStoreDefinition) Children() []dslengine.Definition { 34 | var stores []dslengine.Definition 35 | for _, s := range sd.RelationalModels { 36 | stores = append(stores, s) 37 | } 38 | return stores 39 | } 40 | 41 | // IterateModels runs an iterator function once per Model in the Store's model list. 42 | func (sd *RelationalStoreDefinition) IterateModels(it ModelIterator) error { 43 | names := make([]string, len(sd.RelationalModels)) 44 | i := 0 45 | for n := range sd.RelationalModels { 46 | names[i] = n 47 | i++ 48 | } 49 | sort.Strings(names) 50 | for _, n := range names { 51 | if err := it(sd.RelationalModels[n]); err != nil { 52 | return err 53 | } 54 | } 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /relationalstore_test.go: -------------------------------------------------------------------------------- 1 | package gorma_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/goadesign/gorma" 8 | ) 9 | 10 | func TestStoreContext(t *testing.T) { 11 | sg := &gorma.RelationalStoreDefinition{} 12 | sg.Name = "SG" 13 | 14 | c := sg.Context() 15 | exp := fmt.Sprintf("RelationalStore %#v", sg.Name) 16 | if c != exp { 17 | t.Errorf("Expected %s, got %s", exp, c) 18 | } 19 | 20 | sg.Name = "" 21 | 22 | c = sg.Context() 23 | exp = "unnamed RelationalStore" 24 | if c != exp { 25 | t.Errorf("Expected %s, got %s", exp, c) 26 | } 27 | } 28 | 29 | func TestStoreDSL(t *testing.T) { 30 | sg := &gorma.RelationalStoreDefinition{} 31 | f := func() { 32 | return 33 | } 34 | sg.DefinitionDSL = f 35 | c := sg.DSL() 36 | if c == nil { 37 | t.Errorf("Expected %T, got nil", f) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /storagegroup.go: -------------------------------------------------------------------------------- 1 | package gorma 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | 7 | "github.com/goadesign/goa/design" 8 | "github.com/goadesign/goa/dslengine" 9 | ) 10 | 11 | // NewStorageGroupDefinition returns an initialized 12 | // StorageGroupDefinition. 13 | func NewStorageGroupDefinition() *StorageGroupDefinition { 14 | m := &StorageGroupDefinition{ 15 | RelationalStores: make(map[string]*RelationalStoreDefinition), 16 | } 17 | return m 18 | } 19 | 20 | // IterateStores runs an iterator function once per Relational Store in the 21 | // StorageGroup's Store list. 22 | func (sd *StorageGroupDefinition) IterateStores(it StoreIterator) error { 23 | if sd == nil { 24 | return nil 25 | } 26 | if sd.RelationalStores != nil { 27 | names := make([]string, len(sd.RelationalStores)) 28 | i := 0 29 | for n := range sd.RelationalStores { 30 | names[i] = n 31 | i++ 32 | } 33 | sort.Strings(names) 34 | for _, n := range names { 35 | if err := it(sd.RelationalStores[n]); err != nil { 36 | return err 37 | } 38 | } 39 | } 40 | return nil 41 | } 42 | 43 | // Context returns the generic definition name used in error messages. 44 | func (sd StorageGroupDefinition) Context() string { 45 | if sd.Name != "" { 46 | return fmt.Sprintf("StorageGroup %#v", sd.Name) 47 | } 48 | return "unnamed Storage Group" 49 | } 50 | 51 | // DSL returns this object's DSL. 52 | func (sd StorageGroupDefinition) DSL() func() { 53 | return sd.DefinitionDSL 54 | } 55 | 56 | // Children returns a slice of this objects children. 57 | func (sd StorageGroupDefinition) Children() []dslengine.Definition { 58 | var stores []dslengine.Definition 59 | for _, s := range sd.RelationalStores { 60 | stores = append(stores, s) 61 | } 62 | return stores 63 | } 64 | 65 | // DSLName is displayed to the user when the DSL executes. 66 | func (sd *StorageGroupDefinition) DSLName() string { 67 | return "Gorma storage group" 68 | } 69 | 70 | // DependsOn return the DSL roots the Gorma DSL root depends on, that's the goa API DSL. 71 | func (sd *StorageGroupDefinition) DependsOn() []dslengine.Root { 72 | return []dslengine.Root{design.Design, design.GeneratedMediaTypes} 73 | } 74 | 75 | // IterateSets goes over all the definition sets of the StorageGroup: the 76 | // StorageGroup definition itself, each store definition, models and fields. 77 | func (sd *StorageGroupDefinition) IterateSets(iterator dslengine.SetIterator) { 78 | // First run the top level StorageGroup 79 | 80 | iterator([]dslengine.Definition{sd}) 81 | sd.IterateStores(func(store *RelationalStoreDefinition) error { 82 | iterator([]dslengine.Definition{store}) 83 | store.IterateModels(func(model *RelationalModelDefinition) error { 84 | iterator([]dslengine.Definition{model}) 85 | model.IterateFields(func(field *RelationalFieldDefinition) error { 86 | iterator([]dslengine.Definition{field}) 87 | return nil 88 | }) 89 | model.IterateBuildSources(func(bs *BuildSource) error { 90 | iterator([]dslengine.Definition{bs}) 91 | return nil 92 | }) 93 | 94 | return nil 95 | }) 96 | return nil 97 | }) 98 | } 99 | 100 | // Reset resets the storage group to pre DSL execution state. 101 | func (sd *StorageGroupDefinition) Reset() { 102 | n := NewStorageGroupDefinition() 103 | *sd = *n 104 | } 105 | -------------------------------------------------------------------------------- /storagegroup_test.go: -------------------------------------------------------------------------------- 1 | package gorma_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/goadesign/gorma" 8 | ) 9 | 10 | func TestStorageGroupContext(t *testing.T) { 11 | sg := &gorma.StorageGroupDefinition{} 12 | sg.Name = "SG" 13 | 14 | c := sg.Context() 15 | exp := fmt.Sprintf("StorageGroup %#v", sg.Name) 16 | if c != exp { 17 | t.Errorf("Expected %s, got %s", exp, c) 18 | } 19 | 20 | sg.Name = "" 21 | 22 | c = sg.Context() 23 | exp = "unnamed Storage Group" 24 | if c != exp { 25 | t.Errorf("Expected %s, got %s", exp, c) 26 | } 27 | } 28 | 29 | func TestStorageGroupDSL(t *testing.T) { 30 | sg := &gorma.StorageGroupDefinition{} 31 | f := func() { 32 | return 33 | } 34 | sg.DefinitionDSL = f 35 | c := sg.DSL() 36 | if c == nil { 37 | t.Errorf("Expected %T, got nil", f) 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /validate.go: -------------------------------------------------------------------------------- 1 | package gorma 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/goadesign/goa/dslengine" 7 | ) 8 | 9 | // Validate tests whether the StorageGroup definition is consistent. 10 | func (a *StorageGroupDefinition) Validate() *dslengine.ValidationErrors { 11 | fmt.Println("Validating Group") 12 | verr := new(dslengine.ValidationErrors) 13 | if a.Name == "" { 14 | verr.Add(a, "storage group name not defined") 15 | } 16 | a.IterateStores(func(store *RelationalStoreDefinition) error { 17 | verr.Merge(store.Validate()) 18 | return nil 19 | }) 20 | 21 | return verr.AsError() 22 | } 23 | 24 | // Validate tests whether the RelationalStore definition is consistent. 25 | func (a *RelationalStoreDefinition) Validate() *dslengine.ValidationErrors { 26 | fmt.Println("Validating Store") 27 | verr := new(dslengine.ValidationErrors) 28 | if a.Name == "" { 29 | verr.Add(a, "store name not defined") 30 | } 31 | if a.Parent == nil { 32 | verr.Add(a, "missing storage group parent") 33 | } 34 | a.IterateModels(func(model *RelationalModelDefinition) error { 35 | verr.Merge(model.Validate()) 36 | return nil 37 | }) 38 | 39 | return verr.AsError() 40 | } 41 | 42 | // Validate tests whether the RelationalModel definition is consistent. 43 | func (a *RelationalModelDefinition) Validate() *dslengine.ValidationErrors { 44 | fmt.Println("Validating Model") 45 | verr := new(dslengine.ValidationErrors) 46 | if a.ModelName == "" { 47 | verr.Add(a, "model name not defined") 48 | } 49 | if a.Parent == nil { 50 | verr.Add(a, "missing relational store parent") 51 | } 52 | a.IterateFields(func(field *RelationalFieldDefinition) error { 53 | verr.Merge(field.Validate()) 54 | return nil 55 | }) 56 | 57 | return verr.AsError() 58 | } 59 | 60 | // Validate tests whether the RelationalField definition is consistent. 61 | func (field *RelationalFieldDefinition) Validate() *dslengine.ValidationErrors { 62 | fmt.Println("Validing Field") 63 | verr := new(dslengine.ValidationErrors) 64 | 65 | if field.Parent == nil { 66 | verr.Add(field, "missing relational model parent") 67 | } 68 | if field.FieldName == "" { 69 | verr.Add(field, "field name not defined") 70 | } 71 | return verr.AsError() 72 | } 73 | -------------------------------------------------------------------------------- /writers.go: -------------------------------------------------------------------------------- 1 | package gorma 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strings" 7 | "text/template" 8 | 9 | "bitbucket.org/pkg/inflect" 10 | 11 | "github.com/goadesign/goa/design" 12 | "github.com/goadesign/goa/goagen/codegen" 13 | "github.com/jinzhu/inflection" 14 | "github.com/kr/pretty" 15 | ) 16 | 17 | type ( 18 | // UserTypeTemplateData contains all the information used by the template to redner the 19 | // media types code. 20 | UserTypeTemplateData struct { 21 | APIDefinition *design.APIDefinition 22 | UserType *RelationalModelDefinition 23 | DefaultPkg string 24 | AppPkg string 25 | } 26 | // UserTypesWriter generate code for a goa application user types. 27 | // User types are data structures defined in the DSL with "Type". 28 | UserTypesWriter struct { 29 | *codegen.SourceFile 30 | UserTypeTmpl *template.Template 31 | UserHelperTmpl *template.Template 32 | } 33 | 34 | // UserHelperWriter generate code for a goa application user types. 35 | // User types are data structures defined in the DSL with "Type". 36 | UserHelperWriter struct { 37 | *codegen.SourceFile 38 | UserHelperTmpl *template.Template 39 | } 40 | ) 41 | 42 | func fieldAssignmentPayloadToModel(model *RelationalModelDefinition, ut *design.UserTypeDefinition, verpkg, v, mtype, utype string) string { 43 | // Get a sortable slice of field names 44 | var keys []string 45 | for k := range model.RelationalFields { 46 | keys = append(keys, k) 47 | } 48 | sort.Strings(keys) 49 | 50 | var fieldAssignments []string 51 | for _, fname := range keys { 52 | field := model.RelationalFields[fname] 53 | 54 | var mpointer, upointer bool 55 | mpointer = field.Nullable 56 | obj := ut.Type.ToObject() 57 | definition := ut.Definition() 58 | 59 | if field.Datatype == "" { 60 | continue 61 | } 62 | 63 | for key := range obj { 64 | gfield := obj[key] 65 | 66 | if field.Underscore() == key || field.DatabaseFieldName == key { 67 | // this is our field 68 | if gfield.Type.IsObject() || definition.IsPrimitivePointer(key) { 69 | upointer = true 70 | } else { 71 | // set it explicitly because we're reusing the same bool 72 | upointer = false 73 | } 74 | 75 | prefix := "" 76 | if upointer && !mpointer { 77 | // ufield = &mfield 78 | prefix = "*" 79 | } else if mpointer && !upointer { 80 | // ufield = *mfield (rare if never?) 81 | prefix = "&" 82 | } else if !upointer && !mpointer { 83 | prefix = "" 84 | } 85 | 86 | if upointer { 87 | ifa := fmt.Sprintf("if %s.%s != nil {", v, codegen.Goify(key, true)) 88 | fieldAssignments = append(fieldAssignments, ifa) 89 | } 90 | 91 | fa := fmt.Sprintf("\t%s.%s = %s%s.%s", utype, fname, prefix, v, codegen.Goify(key, true)) 92 | fieldAssignments = append(fieldAssignments, fa) 93 | 94 | if upointer { 95 | ifa := fmt.Sprintf("}") 96 | fieldAssignments = append(fieldAssignments, ifa) 97 | } 98 | } 99 | } 100 | } 101 | return strings.Join(fieldAssignments, "\n") 102 | } 103 | 104 | func fieldAssignmentModelToType(model *RelationalModelDefinition, ut *design.ViewDefinition, v, mtype, utype string) string { 105 | tmp := 1 106 | var fieldAssignments []string 107 | 108 | if !strings.Contains(ut.Name, "link") { 109 | if len(ut.Parent.Links) > 0 { 110 | ifa := fmt.Sprintf("%s.Links = &app.%sLinks{}", utype, codegen.Goify(utype, true)) 111 | fieldAssignments = append(fieldAssignments, ifa) 112 | } 113 | 114 | for ln, lnd := range ut.Parent.Links { 115 | ln = codegen.Goify(ln, true) 116 | s := inflect.Singularize(ln) 117 | var ifb string 118 | if lnd.MediaType().IsArray() { 119 | mt := codegen.Goify(lnd.MediaType().ToArray().ElemType.Type.(*design.MediaTypeDefinition).TypeName, true) + "LinkCollection" 120 | fa := make([]string, 4) 121 | fa[0] = fmt.Sprintf("tmp%d := make(app.%s, len(%s.%s))", tmp, mt, v, ln) 122 | fa[1] = fmt.Sprintf("for i, elem := range %s.%s {", v, ln) 123 | fa[2] = fmt.Sprintf(" tmp%d[i] = elem.%sTo%sLink()", tmp, s, s) 124 | fa[3] = fmt.Sprintf("}") 125 | ifb = strings.Join(fa, "\n") 126 | } else { 127 | ifb = fmt.Sprintf("tmp%d := %s.%s.%sTo%sLink()", tmp, v, ln, s, s) 128 | } 129 | 130 | fieldAssignments = append(fieldAssignments, ifb) 131 | ifd := fmt.Sprintf("%s.Links.%s = tmp%d", utype, codegen.Goify(ln, true), tmp) 132 | fieldAssignments = append(fieldAssignments, ifd) 133 | tmp++ 134 | } 135 | } 136 | 137 | // Get a sortable slice of field names 138 | var keys []string 139 | for k := range model.RelationalFields { 140 | keys = append(keys, k) 141 | } 142 | sort.Strings(keys) 143 | 144 | for _, fname := range keys { 145 | field := model.RelationalFields[fname] 146 | 147 | var mpointer, upointer bool 148 | mpointer = field.Nullable 149 | obj := ut.Type.ToObject() 150 | definition := ut.Parent.Definition() 151 | 152 | if field.Datatype == "" { 153 | continue 154 | } 155 | 156 | for key := range obj { 157 | helperFuncMediaTypeNames[utype] = ut.Parent.TypeName 158 | gfield := obj[key] 159 | if field.Underscore() == key || field.DatabaseFieldName == key { 160 | // this is our field 161 | if gfield.Type.IsObject() || definition.IsPrimitivePointer(key) { 162 | upointer = true 163 | } else { 164 | // set it explicitly because we're reusing the same bool 165 | upointer = false 166 | } 167 | 168 | if field.Datatype == HasOne { 169 | fa := fmt.Sprintf("%s.%s = %s.%s.%sTo%s()", utype, codegen.Goify(field.FieldName, true), v, codegen.Goify(field.FieldName, true), codegen.Goify(field.FieldName, true), codegen.Goify(field.FieldName, true)) 170 | fieldAssignments = append(fieldAssignments, fa) 171 | continue 172 | } 173 | 174 | prefix := "" 175 | if upointer && !mpointer { 176 | // ufield = &mfield 177 | prefix = "&" 178 | } else if mpointer && !upointer { 179 | // ufield = *mfield (rare if never?) 180 | prefix = "*" 181 | } else if !upointer && !mpointer { 182 | prefix = "" 183 | } 184 | /// test to see if it's a go object here and add the appending stuff 185 | 186 | if gfield.Type.IsObject() || gfield.Type.IsArray() { 187 | tmp++ 188 | ifa := fmt.Sprintf("for i%d := range %s.%s {", tmp, v, codegen.Goify(fname, true)) 189 | fieldAssignments = append(fieldAssignments, ifa) 190 | ifd := fmt.Sprintf("tmp%d := &%s.%s[i%d]", tmp, v, codegen.Goify(fname, true), tmp) 191 | fieldAssignments = append(fieldAssignments, ifd) 192 | ifb := fmt.Sprintf("%s.%s = append(%s.%s, tmp%d.%sTo%s())", utype, codegen.Goify(key, true), utype, codegen.Goify(key, true), tmp, inflect.Singularize(codegen.Goify(key, true)), helperFuncMediaTypeNames[inflect.Singularize(key)]) 193 | fieldAssignments = append(fieldAssignments, ifb) 194 | ifc := fmt.Sprintf("}") 195 | fieldAssignments = append(fieldAssignments, ifc) 196 | 197 | } else { 198 | fa := fmt.Sprintf("\t%s.%s = %s%s.%s", utype, codegen.Goify(key, true), prefix, v, codegen.Goify(fname, true)) 199 | fieldAssignments = append(fieldAssignments, fa) 200 | } 201 | } else { 202 | fn := codegen.Goify(strings.Replace(field.FieldName, "ID", "", -1), false) 203 | if fn == key { 204 | gfield, ok := obj[fn] 205 | if ok { 206 | fa := fmt.Sprintf("tmp%d := &%s.%s", tmp, mtype, codegen.Goify(fn, true)) 207 | fieldAssignments = append(fieldAssignments, fa) 208 | var view string 209 | if gfield.View != "" { 210 | view = gfield.View 211 | } 212 | fa = fmt.Sprintf("%s.%s = tmp%d.%sTo%s%s()", utype, codegen.Goify(fn, true), tmp, codegen.Goify(fn, true), codegen.Goify(fn, true), codegen.Goify(view, true)) 213 | 214 | fieldAssignments = append(fieldAssignments, fa) 215 | tmp++ 216 | } 217 | } 218 | } 219 | } 220 | } 221 | return strings.Join(fieldAssignments, "\n") 222 | } 223 | 224 | func fieldAssignmentTypeToModel(model *RelationalModelDefinition, ut *design.UserTypeDefinition, utype, mtype string) string { 225 | // Get a sortable slice of field names 226 | var keys []string 227 | for k := range model.RelationalFields { 228 | keys = append(keys, k) 229 | } 230 | sort.Strings(keys) 231 | 232 | var fieldAssignments []string 233 | for _, fname := range keys { 234 | field := model.RelationalFields[fname] 235 | 236 | var mpointer, upointer bool 237 | mpointer = field.Nullable 238 | obj := ut.ToObject() 239 | definition := ut.Definition() 240 | if field.Datatype == "" { 241 | continue 242 | } 243 | for key := range obj { 244 | gfield := obj[key] 245 | if field.Underscore() == key || field.DatabaseFieldName == key { 246 | // this is our field 247 | if gfield.Type.IsObject() || definition.IsPrimitivePointer(key) { 248 | upointer = true 249 | } else { 250 | // set it explicitly because we're reusing the same bool 251 | upointer = false 252 | } 253 | 254 | var prefix string 255 | if upointer != mpointer { 256 | prefix = "*" 257 | } 258 | 259 | fa := fmt.Sprintf("\t%s.%s = %s%s.%s", mtype, fname, prefix, utype, codegen.Goify(key, true)) 260 | fieldAssignments = append(fieldAssignments, fa) 261 | } 262 | } 263 | 264 | } 265 | return strings.Join(fieldAssignments, "\n") 266 | } 267 | 268 | func viewSelect(ut *RelationalModelDefinition, v *design.ViewDefinition) string { 269 | obj := v.Type.(design.Object) 270 | var fields []string 271 | for name := range obj { 272 | if obj[name].Type.IsPrimitive() { 273 | if strings.TrimSpace(name) != "" && name != "links" { 274 | bf, ok := ut.RelationalFields[codegen.Goify(name, true)] 275 | if ok { 276 | fields = append(fields, bf.DatabaseFieldName) 277 | } 278 | } 279 | } 280 | } 281 | sort.Strings(fields) 282 | return strings.Join(fields, ",") 283 | } 284 | 285 | func viewFields(ut *RelationalModelDefinition, v *design.ViewDefinition) []*RelationalFieldDefinition { 286 | obj := v.Type.(design.Object) 287 | var fields []*RelationalFieldDefinition 288 | for name := range obj { 289 | if obj[name].Type.IsPrimitive() { 290 | if strings.TrimSpace(name) != "" && name != "links" { 291 | bf, ok := ut.RelationalFields[codegen.Goify(name, true)] 292 | if ok { 293 | fields = append(fields, bf) 294 | } 295 | } else if name == "links" { 296 | for _, ld := range v.Parent.Links { 297 | pretty.Println(ld.Name, ld.View) 298 | } 299 | } 300 | } 301 | } 302 | 303 | return fields 304 | } 305 | 306 | func viewFieldNames(ut *RelationalModelDefinition, v *design.ViewDefinition) []string { 307 | obj := v.Type.(design.Object) 308 | var fields []string 309 | for name := range obj { 310 | if obj[name].Type.IsPrimitive() { 311 | if strings.TrimSpace(name) != "" && name != "links" { 312 | bf, ok := ut.RelationalFields[codegen.Goify(name, true)] 313 | 314 | if ok { 315 | fields = append(fields, "&"+codegen.Goify(bf.FieldName, false)) 316 | } 317 | } 318 | } 319 | } 320 | 321 | sort.Strings(fields) 322 | return fields 323 | } 324 | 325 | // NewUserHelperWriter returns a contexts code writer. 326 | // User types contain custom data structured defined in the DSL with "Type". 327 | func NewUserHelperWriter(filename string) (*UserHelperWriter, error) { 328 | file, err := codegen.SourceFileFor(filename) 329 | if err != nil { 330 | return nil, err 331 | } 332 | return &UserHelperWriter{SourceFile: file}, nil 333 | } 334 | 335 | // Execute writes the code for the context types to the writer. 336 | func (w *UserHelperWriter) Execute(data *UserTypeTemplateData) error { 337 | fm := make(map[string]interface{}) 338 | fm["famt"] = fieldAssignmentModelToType 339 | fm["fatm"] = fieldAssignmentTypeToModel 340 | fm["viewSelect"] = viewSelect 341 | fm["viewFields"] = viewFields 342 | fm["viewFieldNames"] = viewFieldNames 343 | fm["goDatatype"] = goDatatype 344 | fm["goDatatypeByModel"] = goDatatypeByModel 345 | fm["plural"] = inflection.Plural 346 | fm["gtt"] = codegen.GoTypeTransform 347 | fm["gttn"] = codegen.GoTypeTransformName 348 | fm["gptn"] = codegen.GoTypeName 349 | fm["newMediaTemplate"] = newMediaTemplate 350 | return w.ExecuteTemplate("types", userHelperT, fm, data) 351 | } 352 | 353 | // NewUserTypesWriter returns a contexts code writer. 354 | // User types contain custom data structured defined in the DSL with "Type". 355 | func NewUserTypesWriter(filename string) (*UserTypesWriter, error) { 356 | file, err := codegen.SourceFileFor(filename) 357 | if err != nil { 358 | return nil, err 359 | } 360 | return &UserTypesWriter{SourceFile: file}, nil 361 | } 362 | 363 | // Execute writes the code for the context types to the writer. 364 | func (w *UserTypesWriter) Execute(data *UserTypeTemplateData) error { 365 | fm := make(map[string]interface{}) 366 | fm["famt"] = fieldAssignmentModelToType 367 | fm["fatm"] = fieldAssignmentTypeToModel 368 | fm["fapm"] = fieldAssignmentPayloadToModel 369 | fm["viewSelect"] = viewSelect 370 | fm["viewFields"] = viewFields 371 | fm["viewFieldNames"] = viewFieldNames 372 | fm["goDatatype"] = goDatatype 373 | fm["goDatatypeByModel"] = goDatatypeByModel 374 | fm["plural"] = inflection.Plural 375 | fm["gtt"] = codegen.GoTypeTransform 376 | fm["gttn"] = codegen.GoTypeTransformName 377 | return w.ExecuteTemplate("types", userTypeT, fm, data) 378 | } 379 | 380 | // arrayAttribute returns the array element attribute definition. 381 | func arrayAttribute(a *design.AttributeDefinition) *design.AttributeDefinition { 382 | return a.Type.(*design.Array).ElemType 383 | } 384 | 385 | type mediaTemplate struct { 386 | Media *design.MediaTypeDefinition 387 | ViewName string 388 | Model *RelationalModelDefinition 389 | View *design.ViewDefinition 390 | } 391 | 392 | // {{ template "Media" (newMediaTemplate $rmt $vname $ut $vp $vpn)}} 393 | func newMediaTemplate(mtd *design.MediaTypeDefinition, vn string, view *design.ViewDefinition, model *RelationalModelDefinition) *mediaTemplate { 394 | return &mediaTemplate{ 395 | Media: mtd, 396 | ViewName: vn, 397 | View: view, 398 | Model: model, 399 | } 400 | } 401 | 402 | const ( 403 | // userTypeT generates the code for a user type. 404 | // template input: UserTypeTemplateData 405 | userTypeT = `{{$ut := .UserType}}{{$ap := .AppPkg}}// {{if $ut.Description}}{{$ut.Description}}{{else}}{{$ut.ModelName}} Relational Model{{end}} 406 | {{$ut.StructDefinition}} 407 | // TableName overrides the table name settings in Gorm to force a specific table name 408 | // in the database. 409 | func (m {{$ut.ModelName}}) TableName() string { 410 | {{ if ne $ut.Alias "" }} 411 | return "{{ $ut.Alias}}" {{ else }} return "{{ $ut.TableName }}" 412 | {{end}} 413 | } 414 | // {{$ut.ModelName}}DB is the implementation of the storage interface for 415 | // {{$ut.ModelName}}. 416 | type {{$ut.ModelName}}DB struct { 417 | Db *gorm.DB 418 | {{ if $ut.Cached }}cache *cache.Cache{{end}} 419 | } 420 | // New{{$ut.ModelName}}DB creates a new storage type. 421 | func New{{$ut.ModelName}}DB(db *gorm.DB) *{{$ut.ModelName}}DB { 422 | {{ if $ut.Cached }}return &{{$ut.ModelName}}DB{ 423 | Db: db, 424 | cache: cache.New(5*time.Minute, 30*time.Second), 425 | } 426 | {{ else }}return &{{$ut.ModelName}}DB{Db: db}{{ end }} 427 | } 428 | // DB returns the underlying database. 429 | func (m *{{$ut.ModelName}}DB) DB() interface{} { 430 | return m.Db 431 | } 432 | 433 | // {{$ut.ModelName}}Storage represents the storage interface. 434 | type {{$ut.ModelName}}Storage interface { 435 | DB() interface{} 436 | List(ctx context.Context{{ if $ut.DynamicTableName}}, tableName string{{ end }}) ([]*{{$ut.ModelName}}, error) 437 | Get(ctx context.Context{{ if $ut.DynamicTableName }}, tableName string{{ end }}, {{$ut.PKAttributes}}) (*{{$ut.ModelName}}, error) 438 | Add(ctx context.Context{{ if $ut.DynamicTableName }}, tableName string{{ end }}, {{$ut.LowerName}} *{{$ut.ModelName}}) (error) 439 | Update(ctx context.Context{{ if $ut.DynamicTableName }}, tableName string{{ end }}, {{$ut.LowerName}} *{{$ut.ModelName}}) (error) 440 | Delete(ctx context.Context{{ if $ut.DynamicTableName }}, tableName string{{ end }}, {{ $ut.PKAttributes}}) (error) 441 | DeleteByModel(ctx context.Context{{ if $ut.DynamicTableName }}, tableName string{{ end }}, obj *{{$ut.ModelName}}) (error) 442 | {{range $rname, $rmt := $ut.RenderTo}}{{/* 443 | 444 | */}}{{range $vname, $view := $rmt.Views}}{{ $mtd := $ut.Project $rname $vname }} 445 | List{{goify $rmt.TypeName true}}{{if not (eq $vname "default")}}{{goify $vname true}}{{end}} (ctx context.Context{{ if $ut.DynamicTableName}}, tableName string{{ end }}{{/* 446 | */}}{{range $nm, $bt := $ut.BelongsTo}}, {{goify (printf "%s%s" $bt.ModelName "ID") false}} int{{end}}) []*app.{{goify $rmt.TypeName true}}{{if not (eq $vname "default")}}{{goify $vname true}}{{end}} 447 | One{{goify $rmt.TypeName true}}{{if not (eq $vname "default")}}{{goify $vname true}}{{end}} (ctx context.Context{{ if $ut.DynamicTableName}}, tableName string{{ end }}{{/* 448 | */}}, {{$ut.PKAttributes}}{{range $nm, $bt := $ut.BelongsTo}},{{goify (printf "%s%s" $bt.ModelName "ID") false}} int{{end}}){{/* 449 | */}} (*app.{{goify $rmt.TypeName true}}{{if not (eq $vname "default")}}{{goify $vname true}}{{end}}, error) 450 | {{end}}{{/* 451 | 452 | */}}{{end}} 453 | {{range $bfn, $bf := $ut.BuiltFrom}} 454 | UpdateFrom{{$bfn}}(ctx context.Context{{ if $ut.DynamicTableName}}, tableName string{{ end }},payload *app.{{goify $bfn true}}, {{$ut.PKAttributes}}) error 455 | {{end}} 456 | } 457 | 458 | // TableName overrides the table name settings in Gorm to force a specific table name 459 | // in the database. 460 | func (m *{{$ut.ModelName}}DB) TableName() string { 461 | {{ if ne $ut.Alias "" }} 462 | return "{{ $ut.Alias}}" {{ else }} return "{{ $ut.TableName }}" 463 | {{end}} 464 | } 465 | 466 | {{ range $idx, $bt := $ut.BelongsTo}} 467 | // Belongs To Relationships 468 | 469 | // {{$ut.ModelName}}FilterBy{{$bt.ModelName}} is a gorm filter for a Belongs To relationship. 470 | func {{$ut.ModelName}}FilterBy{{$bt.ModelName}}({{goify (printf "%s%s" $bt.ModelName "ID") false}} {{ goDatatypeByModel $ut $bt.ModelName }}, originaldb *gorm.DB) func(db *gorm.DB) *gorm.DB { 471 | 472 | {{ range $l, $pk := $ut.PrimaryKeys }} 473 | {{ if eq $pk.Datatype "uuid" }} 474 | if {{goify (printf "%s%s" $bt.ModelName "ID") false}}.String() != "" { 475 | {{ else }} 476 | if {{goify (printf "%s%s" $bt.ModelName "ID") false}} > 0 { 477 | {{ end }} 478 | {{ end }} 479 | return func(db *gorm.DB) *gorm.DB { 480 | return db.Where("{{if $bt.RelationalFields.ID.DatabaseFieldName}}{{ if ne $bt.RelationalFields.ID.DatabaseFieldName "id" }}{{$bt.RelationalFields.ID.DatabaseFieldName}} = ?", {{goify (printf "%s%s" $bt.ModelName "ID") false}}){{else}}{{$bt.Underscore}}_id = ?", {{goify (printf "%s%s" $bt.ModelName "ID") false}}){{end}} 481 | {{ else }}{{$bt.Underscore}}_id = ?", {{goify (printf "%s%s" $bt.ModelName "ID") false}}){{ end }} 482 | } 483 | } 484 | return func(db *gorm.DB) *gorm.DB { return db } 485 | } 486 | {{end}} 487 | 488 | // CRUD Functions 489 | 490 | // Get returns a single {{$ut.ModelName}} as a Database Model 491 | // This is more for use internally, and probably not what you want in your controllers 492 | func (m *{{$ut.ModelName}}DB) Get(ctx context.Context{{ if $ut.DynamicTableName}}, tableName string{{ end }}, {{$ut.PKAttributes}}) (*{{$ut.ModelName}}, error){ 493 | defer goa.MeasureSince([]string{"goa","db","{{goify $ut.ModelName false}}", "get"}, time.Now()) 494 | 495 | var native {{$ut.ModelName}} 496 | err := m.Db.Table({{ if $ut.DynamicTableName }}tableName{{else}}m.TableName(){{ end }}).Where("{{$ut.PKWhere}}",{{$ut.PKWhereFields}} ).Find(&native).Error 497 | if err == gorm.ErrRecordNotFound { 498 | return nil, err 499 | } 500 | {{ if $ut.Cached }}go m.cache.Set(strconv.Itoa(native.ID), &native, cache.DefaultExpiration) 501 | {{end}} 502 | return &native, err 503 | } 504 | 505 | // List returns an array of {{$ut.ModelName}} 506 | func (m *{{$ut.ModelName}}DB) List(ctx context.Context{{ if $ut.DynamicTableName}}, tableName string{{ end }}) ([]*{{$ut.ModelName}}, error) { 507 | defer goa.MeasureSince([]string{"goa","db","{{goify $ut.ModelName false}}", "list"}, time.Now()) 508 | 509 | var objs []*{{$ut.ModelName}} 510 | err := m.Db.Table({{ if $ut.DynamicTableName }}tableName{{else}}m.TableName(){{ end }}).Find(&objs).Error 511 | if err != nil && err != gorm.ErrRecordNotFound { 512 | return nil, err 513 | } 514 | 515 | return objs, nil 516 | } 517 | 518 | // Add creates a new record. 519 | func (m *{{$ut.ModelName}}DB) Add(ctx context.Context{{ if $ut.DynamicTableName }}, tableName string{{ end }}, model *{{$ut.ModelName}}) (error) { 520 | defer goa.MeasureSince([]string{"goa","db","{{goify $ut.ModelName false}}", "add"}, time.Now()) 521 | 522 | {{ range $l, $pk := $ut.PrimaryKeys }} 523 | {{ if eq $pk.Datatype "uuid" }}model.{{$pk.FieldName}} = uuid.Must(uuid.NewV4()){{ end }} 524 | {{ end }} 525 | err := m.Db{{ if $ut.DynamicTableName }}.Table(tableName){{ end }}.Create(model).Error 526 | if err != nil { 527 | goa.LogError(ctx, "error adding {{$ut.ModelName}}", "error", err.Error()) 528 | return err 529 | } 530 | {{ if $ut.Cached }} 531 | go m.cache.Set(strconv.Itoa(model.ID), model, cache.DefaultExpiration) {{ end }} 532 | return nil 533 | } 534 | 535 | // Update modifies a single record. 536 | func (m *{{$ut.ModelName}}DB) Update(ctx context.Context{{ if $ut.DynamicTableName }}, tableName string{{ end }}, model *{{$ut.ModelName}}) error { 537 | defer goa.MeasureSince([]string{"goa","db","{{goify $ut.ModelName false}}", "update"}, time.Now()) 538 | 539 | obj, err := m.Get(ctx{{ if $ut.DynamicTableName }}, tableName{{ end }}, {{$ut.PKUpdateFields "model"}}) 540 | if err != nil { 541 | goa.LogError(ctx, "error updating {{$ut.ModelName}}", "error", err.Error()) 542 | return err 543 | } 544 | err = m.Db{{ if $ut.DynamicTableName }}.Table(tableName){{ end }}.Model(obj).Updates(model).Error 545 | {{ if $ut.Cached }}go func(){ 546 | m.cache.Set(strconv.Itoa(model.ID), obj, cache.DefaultExpiration) 547 | }() 548 | {{ end }} 549 | return err 550 | } 551 | 552 | // Delete removes a single record. 553 | func (m *{{$ut.ModelName}}DB) Delete(ctx context.Context{{ if $ut.DynamicTableName }}, tableName string{{ end }}, {{$ut.PKAttributes}}) error { 554 | defer goa.MeasureSince([]string{"goa","db","{{goify $ut.ModelName false}}", "delete"}, time.Now()) 555 | 556 | var obj {{$ut.ModelName}}{{ $l := len $ut.PrimaryKeys }} 557 | {{ if eq $l 1 }} 558 | err := m.Db{{ if $ut.DynamicTableName }}.Table(tableName){{ end }}.Delete(&obj, {{$ut.PKWhereFields}}).Error 559 | {{ else }}err := m.Db{{ if $ut.DynamicTableName }}.Table(tableName){{ end }}.Delete(&obj).Where("{{$ut.PKWhere}}", {{$ut.PKWhereFields}}).Error 560 | {{ end }} 561 | if err != nil { 562 | goa.LogError(ctx, "error deleting {{$ut.ModelName}}", "error", err.Error()) 563 | return err 564 | } 565 | {{ if $ut.Cached }} go m.cache.Delete(strconv.Itoa(id)) {{ end }} 566 | return nil 567 | } 568 | 569 | // DeleteModel removes a single record given the corresponding model. 570 | // Exisiting gorm deletion callbacks are executed if a record is deleted. 571 | func (m *{{$ut.ModelName}}DB) DeleteByModel(ctx context.Context{{ if $ut.DynamicTableName }}, tableName string{{ end }}, obj *{{$ut.ModelName}}) error { 572 | defer goa.MeasureSince([]string{"goa","db","{{goify $ut.ModelName false}}", "deleteByModel"}, time.Now()) 573 | 574 | err := m.Db{{ if $ut.DynamicTableName }}.Table(tableName){{ end }}.Delete(obj).Error 575 | if err != nil { 576 | goa.LogError(ctx, "error deleting {{$ut.ModelName}}", "error", err.Error()) 577 | return err 578 | } 579 | {{ if $ut.Cached }} go m.cache.Delete(strconv.Itoa(id)) {{ end }} 580 | return nil 581 | } 582 | 583 | {{ range $bfn, $bf := $ut.BuiltFrom }} 584 | // {{$ut.ModelName}}From{{$bfn}} Converts source {{goify $bfn true}} to target {{$ut.ModelName}} model 585 | // only copying the non-nil fields from the source. 586 | func {{$ut.ModelName}}From{{$bfn}}(payload *app.{{goify $bfn true}}) *{{$ut.ModelName}} { 587 | {{$ut.LowerName}} := &{{$ut.ModelName}}{} 588 | {{ fapm $ut $bf "app" "payload" "payload" $ut.LowerName}} 589 | 590 | return {{$ut.LowerName}} 591 | } 592 | 593 | // UpdateFrom{{$bfn}} applies non-nil changes from {{goify $bfn true}} to the model and saves it 594 | func (m *{{$ut.ModelName}}DB)UpdateFrom{{$bfn}}(ctx context.Context{{ if $ut.DynamicTableName}}, tableName string{{ end }},payload *app.{{goify $bfn true}}, {{$ut.PKAttributes}}) error { 595 | defer goa.MeasureSince([]string{"goa","db","{{goify $ut.ModelName false}}", "updatefrom{{goify $bfn false}}"}, time.Now()) 596 | 597 | var obj {{$ut.ModelName}} 598 | err := m.Db.Table({{ if $ut.DynamicTableName }}tableName{{else}}m.TableName(){{ end }}).Where("{{$ut.PKWhere}}",{{$ut.PKWhereFields}} ).Find(&obj).Error 599 | if err != nil { 600 | goa.LogError(ctx, "error retrieving {{$ut.ModelName}}", "error", err.Error()) 601 | return err 602 | } 603 | {{ fapm $ut $bf "app" "payload" "payload" "obj"}} 604 | 605 | err = m.Db.Save(&obj).Error 606 | return err 607 | } 608 | {{ end }} 609 | 610 | 611 | ` 612 | 613 | userHelperT = `{{define "Media"}}` + mediaT + `{{end}}` + `{{$ut := .UserType}}{{$ap := .AppPkg}} 614 | {{ if $ut.Roler }} 615 | // GetRole returns the value of the role field and satisfies the Roler interface. 616 | func (m {{$ut.ModelName}}) GetRole() string { 617 | return {{$f := $ut.Fields.role}}{{if $f.Nullable}}*{{end}}m.Role 618 | } 619 | {{end}} 620 | 621 | {{ range $rname, $rmt := $ut.RenderTo }} 622 | {{ range $vname, $view := $rmt.Views}} 623 | {{ $mtd := $ut.Project $rname $vname }} 624 | 625 | {{template "Media" (newMediaTemplate $rmt $vname $view $ut)}} 626 | {{end}}{{end}} 627 | 628 | ` 629 | 630 | mediaT = `// MediaType Retrieval Functions 631 | 632 | // List{{goify .Media.TypeName true}}{{if not (eq .ViewName "default")}}{{goify .ViewName true}}{{end}} returns an array of view: {{.ViewName}}. 633 | func (m *{{.Model.ModelName}}DB) List{{goify .Media.TypeName true}}{{if not (eq .ViewName "default")}}{{goify .ViewName true}}{{end}}{{/* 634 | */}} (ctx context.Context{{ if .Model.DynamicTableName}}, tableName string{{ end }}{{/* 635 | */}} {{$mod:=.Model}}{{range $nm, $bt := .Model.BelongsTo}},{{goify (printf "%s%s" $bt.ModelName "ID") false}} {{ goDatatypeByModel $mod $bt.ModelName }}{{end}}){{/* 636 | */}} []*app.{{goify .Media.TypeName true}}{{if not (eq .ViewName "default")}}{{goify .ViewName true}}{{end}}{ 637 | defer goa.MeasureSince([]string{"goa","db","{{goify .Media.TypeName false}}", "list{{goify .Media.TypeName false}}{{if eq .ViewName "default"}}{{else}}{{goify .ViewName false}}{{end}}"}, time.Now()) 638 | 639 | var native []*{{goify .Model.ModelName true}} 640 | var objs []*app.{{goify .Media.TypeName true}}{{if not (eq .ViewName "default")}}{{goify .ViewName true}}{{end}}{{$ctx:= .}} 641 | err := m.Db.Scopes({{range $nm, $bt := .Model.BelongsTo}}{{/* 642 | */}}{{$ctx.Model.ModelName}}FilterBy{{goify $bt.ModelName true}}({{goify (printf "%s%s" $bt.ModelName "ID") false}}, m.Db), {{end}}){{/* 643 | */}}.Table({{ if .Model.DynamicTableName }}tableName{{else}}m.TableName(){{ end }}).{{ range $ln, $lv := .Media.Links }}Preload("{{goify $ln true}}").{{end}}Find(&native).Error 644 | {{/* // err := m.Db.Table({{ if .Model.DynamicTableName }}tableName{{else}}m.TableName(){{ end }}).{{ range $ln, $lv := .Media.Links }}Preload("{{goify $ln true}}").{{end}}Find(&objs).Error */}} 645 | if err != nil { 646 | goa.LogError(ctx, "error listing {{.Model.ModelName}}", "error", err.Error()) 647 | return objs 648 | } 649 | 650 | for _, t := range native { 651 | objs = append(objs, t.{{.Model.ModelName}}To{{goify .Media.UserTypeDefinition.TypeName true}}{{if eq .ViewName "default"}}{{else}}{{goify .ViewName true}}{{end}}()) 652 | } 653 | 654 | return objs 655 | } 656 | 657 | // {{$.Model.ModelName}}To{{goify .Media.UserTypeDefinition.TypeName true}}{{if not (eq .ViewName "default")}}{{goify .ViewName true}}{{end}}{{/* 658 | */}} loads a {{.Model.ModelName}} and builds the {{.ViewName}} view of media type {{.Media.TypeName}}. 659 | func (m *{{.Model.ModelName}}) {{$.Model.ModelName}}To{{goify .Media.UserTypeDefinition.TypeName true}}{{if not (eq .ViewName "default")}}{{goify .ViewName true}}{{end}}(){{/* 660 | */}} *app.{{goify .Media.TypeName true}}{{if not (eq .ViewName "default")}}{{goify .ViewName true}}{{end}} { 661 | {{.Model.LowerName}} := &app.{{goify .Media.TypeName true}}{{if not (eq .ViewName "default")}}{{goify .ViewName true}}{{end}}{} 662 | {{ famt .Model .View "m" "m" .Model.LowerName}} 663 | 664 | return {{.Model.LowerName}} 665 | } 666 | 667 | // One{{goify .Media.TypeName true}}{{if not (eq .ViewName "default")}}{{goify .ViewName true}}{{end}} loads a {{.Model.ModelName}} and builds the {{.ViewName}} view of media type {{.Media.TypeName}}. 668 | func (m *{{.Model.ModelName}}DB) One{{goify .Media.TypeName true}}{{if not (eq .ViewName "default")}}{{goify .ViewName true}}{{end}}{{/* 669 | */}} (ctx context.Context{{ if .Model.DynamicTableName}}, tableName string{{ end }},{{.Model.PKAttributes}}{{/* 670 | */}}{{$mod:=.Model}}{{range $nm, $bt := .Model.BelongsTo}},{{goify (printf "%s%s" $bt.ModelName "ID") false}} {{ goDatatypeByModel $mod $bt.ModelName }}{{end}}){{/* 671 | */}} (*app.{{goify .Media.TypeName true}}{{if not (eq .ViewName "default")}}{{goify .ViewName true}}{{end}}, error){ 672 | defer goa.MeasureSince([]string{"goa","db","{{goify .Media.TypeName false}}", "one{{goify .Media.TypeName false}}{{if not (eq .ViewName "default")}}{{goify .ViewName false}}{{end}}"}, time.Now()) 673 | 674 | var native {{.Model.ModelName}} 675 | err := m.Db.Scopes({{range $nm, $bt := .Model.BelongsTo}}{{$ctx.Model.ModelName}}FilterBy{{goify $bt.ModelName true}}({{goify (printf "%s%s" $bt.ModelName "ID") false}}, m.Db), {{end}}).Table({{ if .Model.DynamicTableName }}tableName{{else}}m.TableName(){{ end }}){{range $na, $hm:= .Model.HasMany}}.Preload("{{plural $hm.ModelName}}"){{end}}{{range $nm, $bt := .Model.BelongsTo}}.Preload("{{$bt.ModelName}}"){{end}}.Where("{{.Model.PKWhere}}",{{.Model.PKWhereFields}}).Find(&native).Error 676 | 677 | if err != nil && err != gorm.ErrRecordNotFound { 678 | goa.LogError(ctx, "error getting {{.Model.ModelName}}", "error", err.Error()) 679 | return nil, err 680 | } 681 | {{ if .Model.Cached }} go func(){ 682 | m.cache.Set(strconv.Itoa(native.ID), &native, cache.DefaultExpiration) 683 | }() {{ end }} 684 | view := *native.{{.Model.ModelName}}To{{goify .Media.UserTypeDefinition.TypeName true}}{{if not (eq .ViewName "default")}}{{goify .ViewName true}}{{end}}() 685 | return &view, err 686 | } 687 | ` 688 | ) 689 | --------------------------------------------------------------------------------