@smartive/graphql-magic 15.3.1 → 15.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3 -3
- package/docs/docs/1-tutorial.md +1 -1
- package/docs/docs/2-models.md +19 -20
- package/docs/docs/3-fields.md +90 -46
- package/docs/docs/4-generation.md +14 -11
- package/docs/docs/5-migrations.md +13 -5
- package/docs/docs/6-graphql-server.md +242 -0
- package/docs/docs/{6-graphql.md → 7-graphql-client.md} +63 -42
- package/package.json +1 -1
- package/docs/docs/7-custom-resolvers.md +0 -3
- package/docs/docs/8-mutation-hooks.md +0 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
# [15.4.0](https://github.com/smartive/graphql-magic/compare/v15.3.1...v15.4.0) (2024-04-04)
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
###
|
|
4
|
+
### Features
|
|
5
5
|
|
|
6
|
-
*
|
|
6
|
+
* Docs! ([67446c8](https://github.com/smartive/graphql-magic/commit/67446c8a318a851ad86c37d34dca3633962a2b83))
|
package/docs/docs/1-tutorial.md
CHANGED
|
@@ -9,7 +9,7 @@ Let's create a blog with `graphql-magic`!
|
|
|
9
9
|
First create a `next.js` website:
|
|
10
10
|
|
|
11
11
|
```
|
|
12
|
-
npx create-next-app@latest magic-blog --ts --app --tailwind --eslint --src
|
|
12
|
+
npx create-next-app@latest magic-blog --ts --app --tailwind --eslint --src-dir
|
|
13
13
|
cd magic-blog
|
|
14
14
|
```
|
|
15
15
|
|
package/docs/docs/2-models.md
CHANGED
|
@@ -31,17 +31,17 @@ The most powerful model kind. Entities are models that are stored in database ta
|
|
|
31
31
|
}
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
These are the entity options
|
|
34
|
+
These are the entity options:
|
|
35
35
|
|
|
36
|
-
### description
|
|
36
|
+
### `description`
|
|
37
37
|
|
|
38
|
-
Will appear as graphql
|
|
38
|
+
Will appear as description in the graphql schema.
|
|
39
39
|
|
|
40
|
-
### plural
|
|
40
|
+
### `plural`
|
|
41
41
|
|
|
42
42
|
`graphql-magic` detects natural language plurals of model names with the `inflection` npm package. You can override this here.
|
|
43
43
|
|
|
44
|
-
### creatable
|
|
44
|
+
### `creatable`
|
|
45
45
|
|
|
46
46
|
When `creatable` is `true`, the entity can be created using a dedicated graphql `create<ModelName>` mutation.
|
|
47
47
|
|
|
@@ -49,7 +49,7 @@ For this to work, at least one entity field needs to be marked as `creatable`.
|
|
|
49
49
|
|
|
50
50
|
`creatable` also accepts an object to override properties of the implicitly generated `createdBy` and `createdAt` fields.
|
|
51
51
|
|
|
52
|
-
### updatable
|
|
52
|
+
### `updatable`
|
|
53
53
|
|
|
54
54
|
When `updatable` is `true`, the entity can be created using a dedicated graphql `delete<ModelName>` mutation.
|
|
55
55
|
|
|
@@ -59,7 +59,7 @@ For this to work, at least one entity field needs to be marked as `updatable`.
|
|
|
59
59
|
|
|
60
60
|
If a field is updatable, a `<ModelName>Revisions` table is created (containing only the updatable fields) and extended with each update.
|
|
61
61
|
|
|
62
|
-
### deletable
|
|
62
|
+
### `deletable`
|
|
63
63
|
|
|
64
64
|
When `deletable` is `true`, the entity can be created using a dedicated graphql `delete<ModelName>` mutation.
|
|
65
65
|
|
|
@@ -67,7 +67,7 @@ This is a soft delete (the `deleted` field is set to `true`), and the entity can
|
|
|
67
67
|
|
|
68
68
|
`deletable` also accepts an object to override properties of the implicitly generated `deleted`, `deletedBy` and `deletedAt` fields.
|
|
69
69
|
|
|
70
|
-
### queriable
|
|
70
|
+
### `queriable`
|
|
71
71
|
|
|
72
72
|
When `queriable` is `true` a graphql `Query` becomes available to fetch exactly one element by id.
|
|
73
73
|
|
|
@@ -92,7 +92,7 @@ query {
|
|
|
92
92
|
}
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
-
### listQueriable
|
|
95
|
+
### `listQueriable`
|
|
96
96
|
|
|
97
97
|
When `listQueriable` is `true` a graphql `Query` becomes available to fetch a list of elements of this model.
|
|
98
98
|
|
|
@@ -117,20 +117,19 @@ query {
|
|
|
117
117
|
}
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
-
### displayField
|
|
120
|
+
### `displayField`
|
|
121
121
|
|
|
122
122
|
The name of the field that ought to be used as display value, e.g. a `Post`'s `title`.
|
|
123
123
|
|
|
124
|
-
### defaultOrderBy
|
|
124
|
+
### `defaultOrderBy`
|
|
125
125
|
|
|
126
126
|
An array of orders with the same structure as the `orderBy` parameters in graphql queries. The implicit default order by is `[{ createdAt: 'DESC }]`.
|
|
127
127
|
|
|
128
|
-
### fields
|
|
128
|
+
### `fields`
|
|
129
129
|
|
|
130
|
-
An array of fields. See [fields](./fields
|
|
130
|
+
An array of fields. See [fields](./fields)
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
## Scalar
|
|
132
|
+
## Scalars
|
|
134
133
|
|
|
135
134
|
Used for graphql scalars, e.g.
|
|
136
135
|
|
|
@@ -141,7 +140,7 @@ Used for graphql scalars, e.g.
|
|
|
141
140
|
}
|
|
142
141
|
```
|
|
143
142
|
|
|
144
|
-
##
|
|
143
|
+
## Enums
|
|
145
144
|
|
|
146
145
|
An enum that is available as type in the database:
|
|
147
146
|
|
|
@@ -153,7 +152,7 @@ An enum that is available as type in the database:
|
|
|
153
152
|
}
|
|
154
153
|
```
|
|
155
154
|
|
|
156
|
-
## Raw
|
|
155
|
+
## Raw enums
|
|
157
156
|
|
|
158
157
|
An enum that is *not* available as type in the database:
|
|
159
158
|
|
|
@@ -165,7 +164,7 @@ An enum that is *not* available as type in the database:
|
|
|
165
164
|
}
|
|
166
165
|
```
|
|
167
166
|
|
|
168
|
-
##
|
|
167
|
+
## Interfaces
|
|
169
168
|
|
|
170
169
|
Types that can be inherited from, e.g.
|
|
171
170
|
|
|
@@ -193,7 +192,7 @@ Types that can be inherited from, e.g.
|
|
|
193
192
|
}
|
|
194
193
|
```
|
|
195
194
|
|
|
196
|
-
##
|
|
195
|
+
## Objects
|
|
197
196
|
|
|
198
197
|
Custom types that *don't* correspond to database tables. To be used e.g. as return types for custom resolvers or JSON fields. These can also be used to extend `Query` or `Mutation` which are themselves of that type. E.g.
|
|
199
198
|
|
|
@@ -237,7 +236,7 @@ query {
|
|
|
237
236
|
}
|
|
238
237
|
```
|
|
239
238
|
|
|
240
|
-
##
|
|
239
|
+
## Inputs
|
|
241
240
|
|
|
242
241
|
A custom input type. To be combined with custom mutations, e.g.
|
|
243
242
|
|
package/docs/docs/3-fields.md
CHANGED
|
@@ -1,10 +1,91 @@
|
|
|
1
1
|
# Fields
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Models of kind `'entity'`, `'object'`, `'interface'` (see the docs on [models](./models)) have an option called `fields`.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
```ts
|
|
6
|
+
const modelDefinitions: ModelDefinitions = [
|
|
7
|
+
{
|
|
8
|
+
kind: 'entity',
|
|
9
|
+
name: 'User',
|
|
10
|
+
fields: [
|
|
11
|
+
// Fields
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
export const models = new Models(modelDefinitions)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Kinds
|
|
20
|
+
|
|
21
|
+
Fields can have various kinds, based on the field `kind`:
|
|
22
|
+
|
|
23
|
+
### Primitive fields
|
|
24
|
+
|
|
25
|
+
Primitive fields are fields where `kind` is either undefined or set to `'primitive'`. They can have the following `type`:
|
|
26
|
+
|
|
27
|
+
* `ID`
|
|
28
|
+
* `Boolean`
|
|
29
|
+
* `String` with optional fields `stringType` and `maxLength`
|
|
30
|
+
* `Int` with optional fields `intType`
|
|
31
|
+
* `Float` with optional fields `floatType`, `double`, `precision`, `scale`
|
|
32
|
+
* `Upload`
|
|
33
|
+
|
|
34
|
+
Examples:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
{
|
|
38
|
+
name: 'Person',
|
|
39
|
+
fields: [
|
|
40
|
+
{
|
|
41
|
+
type: 'String',
|
|
42
|
+
name: 'name',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
type: 'Int',
|
|
46
|
+
name: 'name',
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Enums
|
|
53
|
+
|
|
54
|
+
When `kind` is `enum`. Requires as `type` the name of a separately defined model of kind `'enum'`. Has optional field `possibleValues` to allow only a subset of available values in mutations.
|
|
55
|
+
|
|
56
|
+
### Custom
|
|
57
|
+
|
|
58
|
+
When `kind` is `custom`. Requires as `type` the name of a separately defined model of kind `'object'`.
|
|
59
|
+
|
|
60
|
+
If this is an entity field, `graphql-magic` will not try to fetch the result from the database and instead assume the presence of a custom resolver for this field and .
|
|
61
|
+
|
|
62
|
+
### JSON
|
|
63
|
+
|
|
64
|
+
This kind is only available in entity fields. When `kind` is `json`, `graphql-magic` assumes that this is a `json` column in the database and returns the data as is. The `type` needs be the name of a separately defined model of kind `object` that describes the structure of the `JSON`.
|
|
65
|
+
|
|
66
|
+
### Relations
|
|
67
|
+
|
|
68
|
+
This kind is only available in entity fields. When `kind` is `relation`, the field describes a link to an entity table. The `type` therefore needs to be the name of a model of kind `'entity'`.
|
|
69
|
+
|
|
70
|
+
## Options
|
|
71
|
+
|
|
72
|
+
Fields generally have the following options:
|
|
73
|
+
|
|
74
|
+
### `kind`
|
|
75
|
+
|
|
76
|
+
Fields can have various kinds, which affect other available options. Available kinds:
|
|
77
|
+
|
|
78
|
+
* `undefined` or `'primitive'`
|
|
79
|
+
* `'enum'`
|
|
80
|
+
* `'custom'`
|
|
81
|
+
* `'json'`
|
|
82
|
+
* `'relation'`
|
|
6
83
|
|
|
7
|
-
|
|
84
|
+
For more details, see section on [kinds](#kinds) below.
|
|
85
|
+
|
|
86
|
+
### `type`
|
|
87
|
+
|
|
88
|
+
This represents the graphql "return type", which can be a primitive or a separate model (depending on the [kind](#kinds)).
|
|
8
89
|
|
|
9
90
|
### `description`
|
|
10
91
|
|
|
@@ -244,50 +325,13 @@ If `true` this field will be available in the update mutation for the entity.
|
|
|
244
325
|
|
|
245
326
|
Also accepts an object that defines a list of `roles` to restrict creation to specific roles.
|
|
246
327
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
Primitive fields can have various kinds, based on the field `kind`:
|
|
250
|
-
|
|
251
|
-
### Primitive fields
|
|
252
|
-
|
|
253
|
-
Primitive fields are fields where `kind` is either undefined or set to `"primitive"`.
|
|
254
|
-
|
|
255
|
-
Primitive fields can have various types based on the `type` field:
|
|
256
|
-
|
|
257
|
-
* `ID`
|
|
258
|
-
* `Boolean`
|
|
259
|
-
* `String` with optional fields `stringType` and `maxLength`
|
|
260
|
-
* `Int` with optional fields `intType`
|
|
261
|
-
* `Float` with optional fields `floatType`, `double`, `precision`, `scale`
|
|
262
|
-
* `Upload`
|
|
263
|
-
|
|
264
|
-
### Enums
|
|
265
|
-
|
|
266
|
-
When `kind` is `enum`. Requires as `type` the name of a separately defined model of kind `enum`. Has optional field `possibleValues` to allow only a subset of available values in mutations.
|
|
267
|
-
|
|
268
|
-
### Custom
|
|
269
|
-
|
|
270
|
-
When `kind` is `custom`. Requires as `type` the name of a separately defined model of kind `object`.
|
|
271
|
-
|
|
272
|
-
If this is an entity field, `graphql-magic` will not try to fetch the result from the database and instead assume the presence of a custom resolver for this field and .
|
|
273
|
-
|
|
274
|
-
### JSON
|
|
275
|
-
|
|
276
|
-
This kind is only available in entity fields. When `kind` is `json`, `graphql-magic` assumes that this is a `json` column in the database and returns the data as is. The `type` needs be the name of a separately defined model of kind `object` that describes the structure of the `JSON`.
|
|
277
|
-
|
|
278
|
-
### Relations
|
|
279
|
-
|
|
280
|
-
This kind is only available in entity fields. When `kind` is `relation`, the field describes a link to an entity table. The `type` therefore needs to be the name of a model of kind `entity`.
|
|
281
|
-
|
|
282
|
-
Relation fields accept the following fields:
|
|
283
|
-
|
|
284
|
-
### toOne
|
|
328
|
+
### `toOne`
|
|
285
329
|
|
|
286
|
-
If `toOne` is `true` this marks a one-to-one relation, meaning that the reverse relation will not point to an array as is the default.
|
|
330
|
+
Only available on relation fields. If `toOne` is `true` this marks a one-to-one relation, meaning that the reverse relation will not point to an array as is the default.
|
|
287
331
|
|
|
288
|
-
### reverse
|
|
332
|
+
### `reverse`
|
|
289
333
|
|
|
290
|
-
`graphql-magic` automatically generates a name for the reverse relation, e.g. for a `Comment` pointing to `Post`:
|
|
334
|
+
Only available on relation fiels. `graphql-magic` automatically generates a name for the reverse relation, e.g. for a `Comment` pointing to `Post`:
|
|
291
335
|
|
|
292
336
|
```ts
|
|
293
337
|
{
|
|
@@ -304,6 +348,6 @@ If `toOne` is `true` this marks a one-to-one relation, meaning that the reverse
|
|
|
304
348
|
|
|
305
349
|
the reverse relation will automatically be `Post.comments`. With `reverse` this name can be overridden.
|
|
306
350
|
|
|
307
|
-
### onDelete
|
|
351
|
+
### `onDelete`
|
|
308
352
|
|
|
309
|
-
|
|
353
|
+
Only available on relation fields. Can be `"cascade"` (default) or `"set-null"`.
|
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
# Code generation
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`graphql-magic` generates a lot of utility code for you based on the models, in particular typescript types.
|
|
4
4
|
|
|
5
|
-
This can be done directly with `npx gqm generate`.
|
|
5
|
+
This can be done directly with `npx gqm generate`.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
"scripts": {
|
|
9
|
-
"bootstrap": "npm ci && npm run generate",
|
|
10
|
-
"generate": "gqm generate"
|
|
11
|
-
}
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
First-time this applies the following changes to the repo:
|
|
7
|
+
During the first run, the tool applies the following changes to the repo:
|
|
15
8
|
|
|
16
9
|
* Generate `.gqmrc.json` file.
|
|
17
10
|
* Add local database connection variables to `.env` file.
|
|
@@ -23,9 +16,19 @@ First-time this applies the following changes to the repo:
|
|
|
23
16
|
With each application, it generates the following files in the configured "generated" folder:
|
|
24
17
|
|
|
25
18
|
* `schema.graphql` - the schema of the api, for reference
|
|
26
|
-
* `models.json` - the final models
|
|
19
|
+
* `models.json` - the final models array, including generated fields such as "id","createdBy"... for reference
|
|
27
20
|
* `api/index.ts` - the server-side model typescipt types
|
|
28
21
|
* `client/index.ts` - the client-side typescript types for the provided queries
|
|
29
22
|
* `client/mutations.ts` - standard mutation queries for all models
|
|
30
23
|
* `db/index.ts` - types for data from/to the database
|
|
31
24
|
* `db/knex.ts` - types to extend the `knex` query builder
|
|
25
|
+
|
|
26
|
+
Whenever the models have been changed, it is necessary regenerate this code.
|
|
27
|
+
It is recommended to create a `package.json` script and to always generate code after install (or with `npm run generate`):
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
"scripts": {
|
|
31
|
+
"bootstrap": "npm ci && npm run generate",
|
|
32
|
+
"generate": "gqm generate"
|
|
33
|
+
}
|
|
34
|
+
```
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
# Migrations
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Migrations are there to keep the database schema in sync with the models.
|
|
4
|
+
|
|
5
|
+
## Generating migrations
|
|
6
|
+
|
|
7
|
+
After changing the models with database-relevant changes (and after initial setup), you'll need to have an existing database running to compare the models with, then generate a migration like this:
|
|
4
8
|
|
|
5
9
|
```
|
|
6
10
|
npx gqm generate-migration
|
|
7
11
|
```
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
It is recommended to create a `package.json` script for this:
|
|
10
14
|
|
|
11
15
|
```
|
|
12
16
|
"generate-migration": "gqm generate-migration"
|
|
@@ -14,15 +18,19 @@ We recommend creating a `package.json` script for this:
|
|
|
14
18
|
|
|
15
19
|
Note: if you are in a `feat/<feature-name>` branch, the script will use that as name for the migration.
|
|
16
20
|
|
|
17
|
-
This will generate a migration in the `migrations` folder. Check whether it needs to be adapted.
|
|
21
|
+
This will generate a migration file in the `migrations` folder (without running the migration itself yet). Check whether it needs to be adapted.
|
|
22
|
+
|
|
23
|
+
## Running migrations
|
|
24
|
+
|
|
25
|
+
Migrations themselves are managed with `knex` (see the [knex migration docs](https://knexjs.org/guide/migrations.html)).
|
|
18
26
|
|
|
19
|
-
|
|
27
|
+
For example, to migrate to the latest version (using `env-cmd` to add db connection variables in `.env`):
|
|
20
28
|
|
|
21
29
|
```
|
|
22
30
|
npx env-cmd knex migrate:latest
|
|
23
31
|
```
|
|
24
32
|
|
|
25
|
-
|
|
33
|
+
It is recommended to create a `package.json` script and always running it before starting the development server.
|
|
26
34
|
|
|
27
35
|
```
|
|
28
36
|
"migrate: "env-cmd knex migrate:latest"
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# Graphql server
|
|
2
|
+
|
|
3
|
+
## `executeQuery`
|
|
4
|
+
|
|
5
|
+
`graphql-magic` generates an `execute.ts` file for you, with this structure:
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import knexConfig from "@/knexfile";
|
|
9
|
+
import { Context, User, execute } from "@smartive/graphql-magic";
|
|
10
|
+
import { randomUUID } from "crypto";
|
|
11
|
+
import { knex } from 'knex';
|
|
12
|
+
import { DateTime } from "luxon";
|
|
13
|
+
import { models } from "../config/models";
|
|
14
|
+
|
|
15
|
+
export const executeGraphql = async <T, V = undefined>(
|
|
16
|
+
body: {
|
|
17
|
+
query: string;
|
|
18
|
+
operationName?: string;
|
|
19
|
+
variables?: V;
|
|
20
|
+
options?: { email?: string };
|
|
21
|
+
}): Promise<{ data: T }> => {
|
|
22
|
+
const db = knex(knexConfig);
|
|
23
|
+
let user: User | undefined;
|
|
24
|
+
// TODO: get user
|
|
25
|
+
|
|
26
|
+
const result = await execute({
|
|
27
|
+
req: null as unknown as Context['req'],
|
|
28
|
+
body,
|
|
29
|
+
knex: db as unknown as Context['knex'],
|
|
30
|
+
locale: 'en',
|
|
31
|
+
locales: ['en'],
|
|
32
|
+
user,
|
|
33
|
+
models: models,
|
|
34
|
+
permissions: { ADMIN: true, UNAUTHENTICATED: true },
|
|
35
|
+
now: DateTime.local(),
|
|
36
|
+
});
|
|
37
|
+
await db.destroy();
|
|
38
|
+
|
|
39
|
+
// https://github.com/vercel/next.js/issues/47447#issuecomment-1500371732
|
|
40
|
+
return JSON.parse(JSON.stringify(result)) as { data: T };
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
This is where you can set up your graphql server with
|
|
45
|
+
|
|
46
|
+
* user authentication (see the [Tutorial](./tutorial) for an example with auth0)
|
|
47
|
+
* custom resolvers (see [Custom resolvers](#custom-resolvers))
|
|
48
|
+
* mutation hooks (see [Mutation hooks](#mutation-hooks))
|
|
49
|
+
|
|
50
|
+
## Graphql API
|
|
51
|
+
|
|
52
|
+
If you only need to execute graphql on the server (e.g. on `next.js` server components or server actions), you don't need a graphql endpoint.
|
|
53
|
+
If you need client side querying, use `executeGraphql` to create a graphql endpoint, e.g. in `src/app/api/graphql/route.ts`:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
export const POST = (req) => {
|
|
57
|
+
return await executeGraphql(req.body)
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Custom resolvers
|
|
62
|
+
|
|
63
|
+
Sometimes you'll need a custom resolver, at the level of root queries, mutations or existing models. For that you need to create a `additionalResolvers` object.
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
export const additionalResolvers = {
|
|
67
|
+
// custom resolvers go here
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Then feed it to `graphql-magic`'s `execute` function:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
const result = await execute({
|
|
75
|
+
...
|
|
76
|
+
additionalResolvers
|
|
77
|
+
})
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Custom queries
|
|
81
|
+
|
|
82
|
+
For that you'll need to extend the `Query` model (and define any needed additional models), e.g.:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
{
|
|
86
|
+
kind: 'object',
|
|
87
|
+
name: 'Stats',
|
|
88
|
+
fields: [
|
|
89
|
+
{
|
|
90
|
+
name: 'usersCount'
|
|
91
|
+
type: 'Int'
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: 'postsCount',
|
|
95
|
+
type: 'Int'
|
|
96
|
+
}
|
|
97
|
+
]
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
kind: 'object',
|
|
101
|
+
name: 'Query',
|
|
102
|
+
fields: [
|
|
103
|
+
// You'll need to define a custom resolver for this one
|
|
104
|
+
{
|
|
105
|
+
kind: 'custom',
|
|
106
|
+
name: 'stats',
|
|
107
|
+
type: 'Stats'
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
then implement the custom resolver as usual:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
const additionalResolvers = {
|
|
117
|
+
Query: {
|
|
118
|
+
stats: (parent, args, ctx, schema) => {
|
|
119
|
+
// Implement custom resolver here
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Custom mutations
|
|
126
|
+
|
|
127
|
+
For that you'll need to extend the `Mutation` model (and define any needed additional models), e.g.:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
{
|
|
131
|
+
kind: 'input',
|
|
132
|
+
name: 'BulkDeleteWhereInput'
|
|
133
|
+
fields: [
|
|
134
|
+
{
|
|
135
|
+
kind: 'ID',
|
|
136
|
+
name: 'ids',
|
|
137
|
+
list: true
|
|
138
|
+
}
|
|
139
|
+
]
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
kind: 'object',
|
|
143
|
+
name: 'Mutation',
|
|
144
|
+
fields: [
|
|
145
|
+
// You'll need to define a custom resolver for this one
|
|
146
|
+
{
|
|
147
|
+
kind: 'custom',
|
|
148
|
+
name: 'bulkDelete',
|
|
149
|
+
args: [
|
|
150
|
+
{
|
|
151
|
+
kind: 'custom',
|
|
152
|
+
name: 'where',
|
|
153
|
+
type: 'BulkDeleteWhereInput'
|
|
154
|
+
}
|
|
155
|
+
]
|
|
156
|
+
type: 'Boolean'
|
|
157
|
+
}
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
then implement the custom resolver as usual:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
const additionalResolvers = {
|
|
166
|
+
Mutation: {
|
|
167
|
+
bulkDelete: (parent, args, ctx, schema) => {
|
|
168
|
+
// Implement custom resolver here
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Custom model resolvers
|
|
175
|
+
|
|
176
|
+
Sometimes you need to add a custom field to an existing model. For that, add a field of `kind: 'custom'` to the model.
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
{
|
|
180
|
+
kind: 'entity',
|
|
181
|
+
name: 'User',
|
|
182
|
+
fields: [
|
|
183
|
+
// ...
|
|
184
|
+
{
|
|
185
|
+
kind: 'custom',
|
|
186
|
+
name: 'isAdmin',
|
|
187
|
+
type: 'Boolean'
|
|
188
|
+
}
|
|
189
|
+
]
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
then implement the custom resolver as usual:
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
const additionalResolvers = {
|
|
198
|
+
User: {
|
|
199
|
+
isAdmin: (parent, args, ctx, schema) => {
|
|
200
|
+
// Implement custom resolver here
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Mutation hooks
|
|
207
|
+
|
|
208
|
+
Sometimes you'll need some custom handling of mutations, before or after the change is committed to the database (e.g. for special data validation, or triggering cleanup work).
|
|
209
|
+
|
|
210
|
+
For this we can implement a global `mutationHook` function that will be called for all mutations with parameters describing the context:
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
import { MutationHook } from '@smartive/graphql-magic';
|
|
214
|
+
|
|
215
|
+
export const mutationHook: MutationHook = (model, action, when, data: { prev, input, normalizedInput, next }, ctx) => {
|
|
216
|
+
switch (model.name) {
|
|
217
|
+
// perform model specific tasks
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// perform global tasks
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Then feed it to `graphql-magic`'s `execute` function:
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
const result = await execute({
|
|
228
|
+
...
|
|
229
|
+
mutationHook
|
|
230
|
+
})
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
The mutation hook function takes the following arguments:
|
|
234
|
+
|
|
235
|
+
* `model` the model for the entity being mutated
|
|
236
|
+
* `action`: can be `'create'`, `'update'`, `'delete'` or `'restore'`
|
|
237
|
+
* `when`: either `"before"` or `"after"` the mutation is committed to the database
|
|
238
|
+
* `data` containing the entity in various states:
|
|
239
|
+
* `prev`: the previous entity (undefined in creation mutations)
|
|
240
|
+
* `input`: input from the user
|
|
241
|
+
* `normalizedInput`: input to feed to the database (e.g. including generated values such as `id`, `createdAt`...)
|
|
242
|
+
* `next`: the full next entity after changes are applied
|
|
@@ -14,59 +14,60 @@ module.exports = {
|
|
|
14
14
|
};
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
##
|
|
17
|
+
## Querying mechanisms
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
### Server side
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
On the server side, and with `next.js` server actions, a graphql api becomes unnecessary, and you can execute query directly using `executeQuery`:
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
TODO
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
## Queries
|
|
23
|
+
```tsx
|
|
24
|
+
import { GetMeQuery, GetPostsQuery } from "@/generated/client";
|
|
25
|
+
import { GET_POSTS } from "@/graphql/client/queries/get-posts";
|
|
26
|
+
import { executeGraphql } from "@/graphql/execute";
|
|
31
27
|
|
|
32
|
-
|
|
28
|
+
async function Posts({ me }: { me: GetMeQuery['me'] }) {
|
|
29
|
+
const { data: { posts } } = await executeGraphql<GetPostsQuery>({ query: GET_POSTS })
|
|
33
30
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
content
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
`;
|
|
31
|
+
return <div>
|
|
32
|
+
{posts.map(post => <div key={post.id}>
|
|
33
|
+
<article>
|
|
34
|
+
<h2>{post.title}</h2>
|
|
35
|
+
<div>by {post.createdBy.username}</div>
|
|
36
|
+
<p>{post.content}</p>
|
|
37
|
+
<h4>Comments</h4>
|
|
38
|
+
{post.comments.map(comment => (<div key={comment.id}>
|
|
39
|
+
<div>{comment.createdBy.username}</div>
|
|
40
|
+
<p>{comment.content}</p> by {comment.createdBy.username}
|
|
41
|
+
</div>)
|
|
42
|
+
)}
|
|
43
|
+
</article>
|
|
44
|
+
</div>)}
|
|
45
|
+
</div>
|
|
46
|
+
}
|
|
56
47
|
```
|
|
57
48
|
|
|
58
|
-
|
|
49
|
+
### Client side
|
|
50
|
+
|
|
51
|
+
On the client, you'd need to set up a graphql endpoint and then query it like any other graphql api, such as with [`@apollo/client`](https://www.apollographql.com/docs/react/get-started).
|
|
59
52
|
|
|
60
53
|
```tsx
|
|
61
54
|
import { GetMeQuery, GetPostsQuery } from "@/generated/client";
|
|
62
55
|
import { GET_POSTS } from "@/graphql/client/queries/get-posts";
|
|
63
56
|
import { executeGraphql } from "@/graphql/execute";
|
|
57
|
+
import { gql, useQuery } from '@apollo/client';
|
|
64
58
|
|
|
65
|
-
|
|
66
|
-
const {
|
|
59
|
+
function Posts({ me }: { me: GetMeQuery['me'] }) {
|
|
60
|
+
const { loading, error, data } = useQuery<GetPostsQuery>({ query: GET_POSTS })
|
|
61
|
+
|
|
62
|
+
if (loading) {
|
|
63
|
+
return 'Loading...';
|
|
64
|
+
}
|
|
65
|
+
if (error) {
|
|
66
|
+
return `Error! ${error.message}`;
|
|
67
|
+
}
|
|
67
68
|
|
|
68
69
|
return <div>
|
|
69
|
-
{posts.map(post => <div key={post.id}>
|
|
70
|
+
{res?.data?.posts.map(post => <div key={post.id}>
|
|
70
71
|
<article>
|
|
71
72
|
<h2>{post.title}</h2>
|
|
72
73
|
<div>by {post.createdBy.username}</div>
|
|
@@ -83,11 +84,31 @@ async function Posts({ me }: { me: GetMeQuery['me'] }) {
|
|
|
83
84
|
}
|
|
84
85
|
```
|
|
85
86
|
|
|
86
|
-
Client-side: TODO
|
|
87
|
-
|
|
88
87
|
## Mutations
|
|
89
88
|
|
|
90
|
-
Mutation queries are generated by `graphql-magic` directly so you don't need to write them.
|
|
89
|
+
Mutation queries are generated by `graphql-magic` directly so you don't need to write them. They have a very simple structure:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
export const CREATE_POST = gql`
|
|
93
|
+
mutation CreatePostMutation($data: CreatePost!) {
|
|
94
|
+
createPost(data: $data) { id }
|
|
95
|
+
}
|
|
96
|
+
`;
|
|
97
|
+
|
|
98
|
+
export const UPDATE_POST = gql`
|
|
99
|
+
mutation UpdatePostMutation($id: ID!, $data: UpdatePost!) {
|
|
100
|
+
updatePost(where: { id: $id }, data: $data) { id }
|
|
101
|
+
}
|
|
102
|
+
`;
|
|
103
|
+
|
|
104
|
+
export const DELETE_POST = gql`
|
|
105
|
+
mutation DeletePostMutation($id: ID!) {
|
|
106
|
+
deletePost(where: { id: $id })
|
|
107
|
+
}
|
|
108
|
+
`;
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Use like this:
|
|
91
112
|
|
|
92
113
|
```tsx
|
|
93
114
|
import { CreatePostMutationMutation, CreatePostMutationMutationVariables } from "@/generated/client";
|
|
@@ -127,4 +148,4 @@ async function CreatePost() {
|
|
|
127
148
|
}
|
|
128
149
|
```
|
|
129
150
|
|
|
130
|
-
|
|
151
|
+
Just like with queries, if is necessary to perform mutations on the client, use a graphql client instead of `executeGraphql`.
|
package/package.json
CHANGED