Skip to content

Commit c6c5dbc

Browse files
committed
Added basic support for foreign keys, updated docs, dehardcoding
1 parent a7d9369 commit c6c5dbc

File tree

28 files changed

+958
-276
lines changed

28 files changed

+958
-276
lines changed

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ Other gunicorn flag variants had even lower performance.
6666
* Add support for model relations
6767
* ~~Add support for viewsets~~
6868
* Add support for complex pagination implementations
69-
* Add support for permissions (authorization)
7069
* Add support for automatic OpenAPI spec generation
7170

7271
* Add support for output formatters

docs/docs/models.md

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,28 +40,67 @@ The functions use reflection to convert between the types, so they are not the f
4040

4141
## Model fields
4242

43+
All the model fields must be tagged with a `json` tag, which is used to map the field name in `models.InternalValue`, and thus to (at least default) request and response JSON payloads. Saying that, GRF must know how to:
4344

44-
:::danger
45+
* Read the field from the storage (in case of GORM, the field should implement the sql.Scanner interface)
46+
* Write the field to the storage (in case of GORM, the field should implement the driver.Valuer interface)
47+
* Read the field from the request JSON payload (GRF uses some tricks to do that, read more in the [Serializers](./serializers) section)
48+
* Write the field to the response JSON payload, read more in the [Serializers](./serializers) section, like above.
4549

46-
Those types are not supported yet, but will be in the future:
4750

48-
* time.Time
49-
* time.Duration
51+
:::warning
52+
Those types are not supported yet, but will be in the future:
5053
* `slice<int>`
5154
* pointer fields, for example `*string`
5255
* JSON fields, as non-string, dedicated column types (eg. Postgres)
53-
5456
:::
5557

56-
All the model fields must be tagged with a `json` tag, which is used to map the field name in `models.InternalValue`, and thus to (at least default) request and response JSON payloads. Saying that, GRF must know how to:
58+
### Slice field
5759

58-
* Read the field from the storage (in case of GORM, the field should implement the sql.Scanner interface)
59-
* Write the field to the storage (in case of GORM, the field should implement the driver.Valuer interface)
60-
* Read the field from the request JSON payload (GRF uses some tricks to do that, read more in the [Serializers](./serializers) section)
61-
* Write the field to the response JSON payload, read more in the [Serializers](./serializers) section, like above.
60+
`models.SliceField` can be used to store slices, that are encoded to JSON string for storage (implement sql.Scanner and driver.Valuer interfaces). In request and response JSON payloads, the slice is represented as a JSON array. The types of the slice need to be golang built-in basic types. The field provides validation of all the elements in the slice.
6261

62+
## Model relations
6363

64-
### Slice field
64+
GRF models by themselves do not directly support relations, but:
65+
66+
* grf allows setting a `grf:"relation"` tag on the field, that instructs serializers to not treat a field as a basic type and skip initial parsing
67+
* GORM's query driver `WithPreload` method can be used to [preload](https://gorm.io/docs/preload.html) related models
68+
* `fields.SerializerField` can be used to include related models in the response JSON payload as a nested object
69+
70+
All of this together allows for a simple implementation of relations in GRF:
71+
72+
```go
6573

66-
`models.SliceField` can be used to store slices, that are encoded to JSON string for storage (implement sql.Scanner and driver.Valuer interfaces). In request and response JSON payloads, the slice is represented as a JSON array. The types of the slice need to be golang built-in types. The field provides validation of all the elements in the slice.
74+
type Profile struct {
75+
models.BaseModel
76+
Name string `json:"name" gorm:"size:191;column:name"`
77+
Photos []Photo `json:"photos" gorm:"foreignKey:profile_id" grf:"relation"`
78+
}
79+
80+
type Photo struct {
81+
models.BaseModel
82+
ProfileID uuid.UUID `json:"profile_id" gorm:"size:191;column:profile_id"`
83+
}
6784

85+
views.NewModelViewSet[Profile](
86+
"/profiles",
87+
queries.GORM[Profile](
88+
gormDB,
89+
).WithPreload(
90+
"photos", // Note JSON tag here, in original GORM API it's the field name
91+
).WithOrderBy(
92+
"`profiles`.`created_at` ASC",
93+
),
94+
).WithSerializer(
95+
serializers.NewModelSerializer[Profile]().WithNewField(
96+
serializers.NewSerializerField[Photo](
97+
"photos",
98+
serializers.NewModelSerializer[Photo](),
99+
),
100+
),
101+
).Register(router)
102+
```
103+
104+
:::warning
105+
GORM's Joins are not supported, as they are pretty useless anyway. If you need to join tables, you have no choice but to create a view in your SQL database and use it as a model.
106+
:::

docs/docs/query-drivers.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,29 @@ Driver configured in such way:
6060
* Sorts the list of products by name in ascending order
6161
* Uses limit/offset pagination provided by gorm query driver package
6262

63+
#### Transactions
64+
65+
All the default REST actions are performed in a single query, thus a transaction is not strictly needed. If however you'd like your action to have some side-effects (for example saving an entry in an audit log), you can use GORM query driver's transaction support.
66+
67+
```go
68+
queryDriver.CRUD().WithCreate(
69+
gormq.CreateTx(
70+
gormq.BeforeCreate(
71+
func(ctx *gin.Context, iv models.InternalValue, tx *gorm.DB) (models.InternalValue, error) {
72+
// do whatever you want with tx before creating
73+
return iv, nil
74+
},
75+
),
76+
)(queryDriver.CRUD().Create),
77+
)
78+
```
79+
80+
The API is a little bit complex (with functions returning functions creating functions 🤣), so it may be changed at some point, but for now it does the job.
81+
82+
#### Relationships
83+
84+
GORM query driver supports basic relationships between models. See more in [model relations section](./models#model-relations).
85+
6386
### InMemory `queries.InMemory()`
6487

6588
InMemory query driver is a simple implementation of QueryDriver interface, that stores all the data in memory. It's useful for testing and prototyping, but it definetly should not be used in production. It doesn't support any filtering, sorting or pagination.

docs/docs/serializers.md

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,72 @@
11
# Serializers
22

3-
Nothing here yet...
3+
Serializers in GRF are responsible for translation of data between the database and the API. They control which fields are accepted in the payload, which are served in the response, and how they are formatted. The best way to understand how serializers work is to see them in action.
4+
5+
```go
6+
type Serializer interface {
7+
// This function is called when transforming the request payload into an internal value
8+
// that will be later passed to QueryDriver (for example GORM) for processing.
9+
ToInternalValue(map[string]any, *gin.Context) (models.InternalValue, error)
10+
11+
// This function transforms data obtained from the QueryDriver into a response payload, that
12+
// will be then marshalled into JSON and sent to the client.
13+
ToRepresentation(models.InternalValue, *gin.Context) (Representation, error)
14+
}
15+
```
16+
17+
## ModelSerializer
18+
19+
`ModelSerializer` is the workhorse of GRF. It can be created with `serializers.NewModelSerializer[Model]()`. By default such serializer will include all struct fields (there's also `NewEmptyModelSerializer[Model]()` that will not include any fields).
20+
21+
22+
### Using existing fields in ModelSerializer
23+
24+
You can select which existing fields should be included in the response by using the `WithModelFields` method.
25+
26+
```go
27+
serializer := serializers.NewModelSerializer[Model]().WithModelFields("field1", "field2")
28+
```
29+
30+
Please note, that the fields here are identified by their JSON tags, not the struct field names.
31+
32+
### Adding completely new fields
33+
34+
For adding fields, that are not present in the model struct or you didn't want to include in the `WithModelFields` method, you can use the `WithField` method. More about fields can be found in the [Fields](#fields) section.
35+
36+
```go
37+
serializer := serializers.NewModelSerializer[Model]().WithNewField(
38+
fields.NewField("color").ReadOnly().WithRepresentationFunc(
39+
func(models.InternalValue, string, *gin.Context) (any, error) {
40+
return "blue", nil
41+
}
42+
)
43+
)
44+
```
45+
46+
### Customizing existing fields
47+
48+
You can also customize existing fields by using the `WithField` method.
49+
50+
```go
51+
52+
serializer := serializers.NewModelSerializer[Model]().WithField(
53+
"color",
54+
func(oldField fields.Field){
55+
return oldField.WithRepresentationFunc(
56+
func(models.InternalValue, string, *gin.Context) (any, error) {
57+
return "pink", nil
58+
}
59+
)
60+
}
61+
)
62+
```
63+
64+
## Fields
65+
66+
Fields are used by ModelSerializers to transform data between the database and the API on the single JSON field / SQL column level. They can be created with `fields.NewField("field_name")`. The API is pretty straightforward, please consult the [godoc](https://pkg.go.dev/github.com/glothriel/grf/pkg/fields).
67+
68+
TLDR; you can:
69+
70+
* Set the field as read-only, write-only or read-write
71+
* Set the InternalValue function, that will be used to transform the data from the API to format that can be stored in the database
72+
* Set the Representation function, that will be used to transform the data from the database to the API response

docs/docs/views.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,22 @@ type Person struct {
2929

3030
func main() {
3131
ginEngine := gin.Default()
32+
// For example's sake, we'll use the in-memory query driver
3233
queryDriver := queries.InMemory[Person]()
33-
personViewSet := views.NewViewSet("/people", queryDriver).WithActions(views.ActionsList)
34+
// NewModelViewSet creates a new ViewSet for the Person model, which uses
35+
// NewModelSerializer under the hood
36+
personViewSet := views.NewModelViewSet[Person](
37+
"/people",
38+
queryDriver,
39+
// Here we can override REST actions, that are served by the ViewSet
40+
).WithActions(views.ActionList)
3441

35-
// Register the ViewSet with your Gin engine
3642
personViewSet.Register(ginEngine)
3743

38-
// Start your Gin server
3944
ginEngine.Run(":8080")
4045
}
4146
```
4247

43-
In this example, we create a basic ViewSet for the `Person` model, and we register it with the Gin engine. The viewset will only respond to List action.
44-
4548
## Configuring Actions
4649

4750
ViewSets provide the following actions:
@@ -90,8 +93,6 @@ personViewSet.OnUpdate(customUpdateLogic)
9093
personViewSet.OnDestroy(customDestroyLogic)
9194
```
9295

93-
In this example, we add custom logic for the Create, Update, and Destroy operations.
94-
9596
## Registering the ViewSet
9697

9798
After configuring your ViewSet and Gin engine, make sure to call the `Register` method to register the ViewSet's routes:
@@ -110,6 +111,7 @@ It's possible to add a custom action for your ViewSet. This can be useful when y
110111
views.NewViewSet[CustomerProfile](
111112
"/me",
112113
qd,
114+
serializers.NewModelSerializer[CustomerProfile](),
113115
).WithExtraAction(
114116
views.NewExtraAction[CustomerProfile](
115117
"GET",

docs/docusaurus.config.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
// @ts-check
22
// Note: type annotations allow type checking and IDEs autocompletion
33

4-
const lightCodeTheme = require('prism-react-renderer/themes/github');
5-
const darkCodeTheme = require('prism-react-renderer/themes/dracula');
4+
const {themes} = require('prism-react-renderer');
5+
const lightTheme = themes.github;
6+
const darkTheme = themes.dracula;
67

78
/** @type {import('@docusaurus/types').Config} */
89
const config = {
@@ -117,8 +118,8 @@ const config = {
117118
copyright: `Copyright © ${new Date().getFullYear()} Gin REST Framework authors. Built with Docusaurus.`,
118119
},
119120
prism: {
120-
theme: lightCodeTheme,
121-
darkTheme: darkCodeTheme,
121+
theme: lightTheme,
122+
darkTheme: darkTheme,
122123
},
123124
}),
124125
};

docs/package.json

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "docs",
3-
"version": "0.0.0",
3+
"version": "1.0.0",
44
"private": true,
55
"scripts": {
66
"docusaurus": "docusaurus",
@@ -14,16 +14,17 @@
1414
"write-heading-ids": "docusaurus write-heading-ids"
1515
},
1616
"dependencies": {
17-
"@docusaurus/core": "2.4.1",
18-
"@docusaurus/preset-classic": "2.4.1",
19-
"@mdx-js/react": "^1.6.22",
17+
"@docusaurus/core": "3.7.0",
18+
"@docusaurus/preset-classic": "3.7.0",
19+
"@mdx-js/react": "^3.0.0",
2020
"clsx": "^1.2.1",
21-
"prism-react-renderer": "^1.3.5",
22-
"react": "^17.0.2",
23-
"react-dom": "^17.0.2"
21+
"prism-react-renderer": "^2.1.0",
22+
"react": "^18.2.0",
23+
"react-dom": "^18.2.0"
2424
},
2525
"devDependencies": {
26-
"@docusaurus/module-type-aliases": "2.4.1"
26+
"@docusaurus/module-type-aliases": "3.7.0",
27+
"@docusaurus/types": "3.7.0"
2728
},
2829
"browserslist": {
2930
"production": [
@@ -38,6 +39,6 @@
3839
]
3940
},
4041
"engines": {
41-
"node": ">=16.14"
42+
"node": ">=18.0"
4243
}
4344
}

docs/src/components/HomepageFeatures/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const FeatureList = [
1919
description: (
2020
<>
2121
Hate code generation? So do we. GRF uses generics to hide the boring stuff and let you focus on what brings value to your project.
22+
2223
</>
2324
),
2425
},

go.mod

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,63 @@
11
module github.com/glothriel/grf
22

3-
go 1.21
3+
go 1.21.0
4+
5+
toolchain go1.23.6
46

57
require (
6-
github.com/gin-gonic/gin v1.9.1
7-
github.com/go-playground/assert/v2 v2.2.0
8-
github.com/go-playground/validator/v10 v10.14.1
9-
github.com/google/uuid v1.3.0
8+
github.com/fergusstrange/embedded-postgres v1.30.0
9+
github.com/gin-gonic/gin v1.10.0
10+
github.com/go-playground/validator/v10 v10.25.0
11+
github.com/google/uuid v1.6.0
1012
github.com/mitchellh/mapstructure v1.5.0
1113
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
1214
github.com/shopspring/decimal v1.3.1
1315
github.com/sirupsen/logrus v1.9.3
14-
github.com/stretchr/testify v1.8.4
16+
github.com/stretchr/testify v1.10.0
17+
gorm.io/driver/postgres v1.5.11
1518
gorm.io/driver/sqlite v1.5.2
16-
gorm.io/gorm v1.25.2
19+
gorm.io/gorm v1.25.12
1720
)
1821

1922
require (
20-
github.com/bytedance/sonic v1.9.2 // indirect
21-
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
23+
github.com/bytedance/sonic v1.12.8 // indirect
24+
github.com/bytedance/sonic/loader v0.2.3 // indirect
25+
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
26+
github.com/cloudwego/base64x v0.1.5 // indirect
2227
github.com/davecgh/go-spew v1.1.1 // indirect
23-
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
24-
github.com/gin-contrib/sse v0.1.0 // indirect
28+
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
29+
github.com/gin-contrib/sse v1.0.0 // indirect
2530
github.com/go-playground/locales v0.14.1 // indirect
2631
github.com/go-playground/universal-translator v0.18.1 // indirect
27-
github.com/goccy/go-json v0.10.2 // indirect
32+
github.com/goccy/go-json v0.10.5 // indirect
33+
github.com/jackc/pgpassfile v1.0.0 // indirect
34+
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
35+
github.com/jackc/pgx/v5 v5.5.5 // indirect
36+
github.com/jackc/puddle/v2 v2.2.1 // indirect
2837
github.com/jinzhu/inflection v1.0.0 // indirect
2938
github.com/jinzhu/now v1.1.5 // indirect
3039
github.com/json-iterator/go v1.1.12 // indirect
31-
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
40+
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
3241
github.com/kr/pretty v0.3.1 // indirect
33-
github.com/leodido/go-urn v1.2.4 // indirect
34-
github.com/mattn/go-isatty v0.0.19 // indirect
35-
github.com/mattn/go-sqlite3 v1.14.17 // indirect
42+
github.com/leodido/go-urn v1.4.0 // indirect
43+
github.com/lib/pq v1.10.9 // indirect
44+
github.com/mattn/go-isatty v0.0.20 // indirect
45+
github.com/mattn/go-sqlite3 v1.14.22 // indirect
3646
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
3747
github.com/modern-go/reflect2 v1.0.2 // indirect
38-
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
48+
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
3949
github.com/pmezard/go-difflib v1.0.0 // indirect
4050
github.com/rogpeppe/go-internal v1.11.0 // indirect
4151
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
42-
github.com/ugorji/go/codec v1.2.11 // indirect
43-
golang.org/x/arch v0.3.0 // indirect
44-
golang.org/x/crypto v0.10.0 // indirect
45-
golang.org/x/net v0.11.0 // indirect
46-
golang.org/x/sys v0.9.0 // indirect
47-
golang.org/x/text v0.10.0 // indirect
52+
github.com/ugorji/go/codec v1.2.12 // indirect
53+
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
54+
golang.org/x/arch v0.14.0 // indirect
55+
golang.org/x/crypto v0.33.0 // indirect
56+
golang.org/x/net v0.35.0 // indirect
57+
golang.org/x/sync v0.11.0 // indirect
58+
golang.org/x/sys v0.30.0 // indirect
59+
golang.org/x/text v0.22.0 // indirect
4860
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
49-
google.golang.org/protobuf v1.31.0 // indirect
50-
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
61+
google.golang.org/protobuf v1.36.5 // indirect
5162
gopkg.in/yaml.v3 v3.0.1 // indirect
5263
)

0 commit comments

Comments
 (0)