@smartive/graphql-magic 15.2.1 → 15.3.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/{tutorial.md → 1-tutorial.md} +26 -30
- package/docs/docs/10-admin-ui.md +3 -0
- package/docs/docs/2-models.md +283 -0
- package/docs/docs/3-fields.md +309 -0
- package/docs/docs/4-generation.md +31 -0
- package/docs/docs/5-migrations.md +31 -0
- package/docs/docs/6-graphql.md +130 -0
- package/docs/docs/7-custom-resolvers.md +3 -0
- package/docs/docs/8-mutation-hooks.md +3 -0
- package/docs/docs/9-polymorphism.md +3 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
# [15.3.0](https://github.com/smartive/graphql-magic/compare/v15.2.1...v15.3.0) (2024-04-03)
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
###
|
|
4
|
+
### Features
|
|
5
5
|
|
|
6
|
-
*
|
|
6
|
+
* Docs ([56d96ce](https://github.com/smartive/graphql-magic/commit/56d96ce14c44d486096f6695ebed38a6862c9129))
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
---
|
|
2
|
-
sidebar_position: 1
|
|
3
|
-
---
|
|
4
|
-
|
|
5
1
|
# Tutorial
|
|
6
2
|
|
|
7
3
|
Let's create a blog with `graphql-magic`!
|
|
@@ -19,7 +15,7 @@ cd magic-blog
|
|
|
19
15
|
|
|
20
16
|
Replace `src/app/globals.css`:
|
|
21
17
|
|
|
22
|
-
```
|
|
18
|
+
```css
|
|
23
19
|
@tailwind base;
|
|
24
20
|
@tailwind components;
|
|
25
21
|
@tailwind utilities;
|
|
@@ -71,7 +67,7 @@ label span {
|
|
|
71
67
|
|
|
72
68
|
Replace `src/app/page.tsx`:
|
|
73
69
|
|
|
74
|
-
```
|
|
70
|
+
```tsx
|
|
75
71
|
export default async function Home() {
|
|
76
72
|
return <main>
|
|
77
73
|
<nav>
|
|
@@ -83,7 +79,7 @@ export default async function Home() {
|
|
|
83
79
|
|
|
84
80
|
Start the website:
|
|
85
81
|
|
|
86
|
-
```
|
|
82
|
+
```bash
|
|
87
83
|
npm run dev
|
|
88
84
|
```
|
|
89
85
|
|
|
@@ -91,7 +87,7 @@ npm run dev
|
|
|
91
87
|
|
|
92
88
|
Add this setting to `next.config.mjs`:
|
|
93
89
|
|
|
94
|
-
```
|
|
90
|
+
```ts
|
|
95
91
|
const nextConfig = {
|
|
96
92
|
experimental: {
|
|
97
93
|
serverComponentsExternalPackages: ['knex'],
|
|
@@ -101,13 +97,13 @@ const nextConfig = {
|
|
|
101
97
|
|
|
102
98
|
Install `@smartive/graphql-magic`:
|
|
103
99
|
|
|
104
|
-
```
|
|
100
|
+
```bash
|
|
105
101
|
npm install @smartive/graphql-magic
|
|
106
102
|
```
|
|
107
103
|
|
|
108
104
|
Run the gqm cli:
|
|
109
105
|
|
|
110
|
-
```
|
|
106
|
+
```bash
|
|
111
107
|
npx gqm generate
|
|
112
108
|
```
|
|
113
109
|
|
|
@@ -116,7 +112,7 @@ npx gqm generate
|
|
|
116
112
|
Let's boot a local database instance.
|
|
117
113
|
Create the following `docker-compose.yml`:
|
|
118
114
|
|
|
119
|
-
```
|
|
115
|
+
```yml
|
|
120
116
|
version: '3.4'
|
|
121
117
|
services:
|
|
122
118
|
postgres:
|
|
@@ -136,7 +132,7 @@ Then start it with `docker-compose up`.
|
|
|
136
132
|
|
|
137
133
|
Generate the first migration:
|
|
138
134
|
|
|
139
|
-
```
|
|
135
|
+
```bash
|
|
140
136
|
npx gqm generate-migration
|
|
141
137
|
```
|
|
142
138
|
|
|
@@ -145,8 +141,8 @@ Enter a migration name, e.g. "setup".
|
|
|
145
141
|
|
|
146
142
|
Run the migration
|
|
147
143
|
|
|
148
|
-
```
|
|
149
|
-
npx env-cmd knex migrate:
|
|
144
|
+
```bash
|
|
145
|
+
npx env-cmd knex migrate:latest
|
|
150
146
|
```
|
|
151
147
|
|
|
152
148
|
### Auth setup
|
|
@@ -156,7 +152,7 @@ For example, follow [this tutorial](https://auth0.com/docs/quickstart/webapp/nex
|
|
|
156
152
|
|
|
157
153
|
Assuming you used auth0, here's a bare-bones version of what `src/app/page.tsx` could look like:
|
|
158
154
|
|
|
159
|
-
```
|
|
155
|
+
```tsx
|
|
160
156
|
import { getSession } from '@auth0/nextjs-auth0';
|
|
161
157
|
|
|
162
158
|
export default async function Page() {
|
|
@@ -179,7 +175,7 @@ Now, we need to ensure that the user is stored in the database.
|
|
|
179
175
|
|
|
180
176
|
First extend the user model in `src/config/models.ts` with the following fields:
|
|
181
177
|
|
|
182
|
-
```
|
|
178
|
+
```tsx
|
|
183
179
|
fields: [
|
|
184
180
|
{
|
|
185
181
|
name: 'authId',
|
|
@@ -196,25 +192,25 @@ First extend the user model in `src/config/models.ts` with the following fields:
|
|
|
196
192
|
|
|
197
193
|
The models have changed, generate the new types:
|
|
198
194
|
|
|
199
|
-
```
|
|
195
|
+
```bash
|
|
200
196
|
npx gqm generate
|
|
201
197
|
```
|
|
202
198
|
|
|
203
199
|
Generate the new migration:
|
|
204
200
|
|
|
205
|
-
```
|
|
201
|
+
```bash
|
|
206
202
|
npx gqm generate-migration
|
|
207
203
|
```
|
|
208
204
|
|
|
209
205
|
Edit the generated migration, then run it
|
|
210
206
|
|
|
211
|
-
```
|
|
212
|
-
npx env-cmd knex migrate:
|
|
207
|
+
```bash
|
|
208
|
+
npx env-cmd knex migrate:latest
|
|
213
209
|
```
|
|
214
210
|
|
|
215
211
|
Now let's implement the `// TODO: get user` part in the `src/graphql/execute.ts` file
|
|
216
212
|
|
|
217
|
-
```
|
|
213
|
+
```ts
|
|
218
214
|
const session = await getSession();
|
|
219
215
|
if (session) {
|
|
220
216
|
let dbUser = await db('User').where({ authId: session.user.sid }).first();
|
|
@@ -235,7 +231,7 @@ Now let's implement the `// TODO: get user` part in the `src/graphql/execute.ts`
|
|
|
235
231
|
|
|
236
232
|
Extend `src/graphql/client/queries/get-me.ts` to also fetch the user's username:
|
|
237
233
|
|
|
238
|
-
```
|
|
234
|
+
```ts
|
|
239
235
|
import { gql } from '@smartive/graphql-magic';
|
|
240
236
|
|
|
241
237
|
export const GET_ME = gql`
|
|
@@ -250,13 +246,13 @@ export const GET_ME = gql`
|
|
|
250
246
|
|
|
251
247
|
Generate the new types:
|
|
252
248
|
|
|
253
|
-
```
|
|
249
|
+
```bash
|
|
254
250
|
npx gqm generate
|
|
255
251
|
```
|
|
256
252
|
|
|
257
253
|
Now, let's modify `src/app/page.tsx` so that it fetches the user from the database:
|
|
258
254
|
|
|
259
|
-
```
|
|
255
|
+
```tsx
|
|
260
256
|
import { GetMeQuery } from "@/generated/client";
|
|
261
257
|
import { GET_ME } from "@/graphql/client/queries/get-me";
|
|
262
258
|
import { executeGraphql } from "@/graphql/execute";
|
|
@@ -279,7 +275,7 @@ export default async function Home() {
|
|
|
279
275
|
|
|
280
276
|
Let's make a blog out of this app by adding new models in `src/config/models.ts`:
|
|
281
277
|
|
|
282
|
-
```
|
|
278
|
+
```ts
|
|
283
279
|
{
|
|
284
280
|
kind: 'entity',
|
|
285
281
|
name: 'Post',
|
|
@@ -330,14 +326,14 @@ Let's make a blog out of this app by adding new models in `src/config/models.ts`
|
|
|
330
326
|
|
|
331
327
|
Generate and run the new migrations and generate the new models:
|
|
332
328
|
|
|
333
|
-
```
|
|
329
|
+
```bash
|
|
334
330
|
npx gqm generate-migration
|
|
335
|
-
npx env-cmd knex migrate:
|
|
331
|
+
npx env-cmd knex migrate:latest
|
|
336
332
|
```
|
|
337
333
|
|
|
338
334
|
Create a new query `src/graphql/client/queries/get-posts.ts`:
|
|
339
335
|
|
|
340
|
-
```
|
|
336
|
+
```ts
|
|
341
337
|
import { gql } from '@smartive/graphql-magic';
|
|
342
338
|
|
|
343
339
|
export const GET_POSTS = gql`
|
|
@@ -363,14 +359,14 @@ export const GET_POSTS = gql`
|
|
|
363
359
|
|
|
364
360
|
Generate the new types:
|
|
365
361
|
|
|
366
|
-
```
|
|
362
|
+
```bash
|
|
367
363
|
npx gqm generate
|
|
368
364
|
```
|
|
369
365
|
|
|
370
366
|
Now add all the logic to create and display posts and comments to `src/app/page.tsx`
|
|
371
367
|
|
|
372
368
|
|
|
373
|
-
```
|
|
369
|
+
```tsx
|
|
374
370
|
import { CreateCommentMutationMutation, CreateCommentMutationMutationVariables, CreatePostMutationMutation, CreatePostMutationMutationVariables, GetMeQuery, GetPostsQuery } from "@/generated/client";
|
|
375
371
|
import { CREATE_COMMENT, CREATE_POST } from "@/generated/client/mutations";
|
|
376
372
|
import { GET_ME } from "@/graphql/client/queries/get-me";
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# Models
|
|
2
|
+
|
|
3
|
+
The source of truth for `graphql-magic` is the `models` object, usually defined in `src/config/models.ts`. This is the minimal models:
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
const modelDefinitions: ModelDefinitions = [
|
|
7
|
+
{
|
|
8
|
+
kind: 'entity',
|
|
9
|
+
name: 'User',
|
|
10
|
+
fields: [
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
export const models = new Models(modelDefinitions)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Models can have the following kinds:
|
|
19
|
+
|
|
20
|
+
## Entities
|
|
21
|
+
|
|
22
|
+
The most powerful model kind. Entities are models that are stored in database tables, and are defined with `kind: 'entity'`:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
{
|
|
26
|
+
kind: 'entity',
|
|
27
|
+
name: 'Post',
|
|
28
|
+
fields: [
|
|
29
|
+
// ...
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
These are the entity options
|
|
35
|
+
|
|
36
|
+
### description
|
|
37
|
+
|
|
38
|
+
Will appear as graphql description
|
|
39
|
+
|
|
40
|
+
### plural
|
|
41
|
+
|
|
42
|
+
`graphql-magic` detects natural language plurals of model names with the `inflection` npm package. You can override this here.
|
|
43
|
+
|
|
44
|
+
### creatable
|
|
45
|
+
|
|
46
|
+
When `creatable` is `true`, the entity can be created using a dedicated graphql `create<ModelName>` mutation.
|
|
47
|
+
|
|
48
|
+
For this to work, at least one entity field needs to be marked as `creatable`.
|
|
49
|
+
|
|
50
|
+
`creatable` also accepts an object to override properties of the implicitly generated `createdBy` and `createdAt` fields.
|
|
51
|
+
|
|
52
|
+
### updatable
|
|
53
|
+
|
|
54
|
+
When `updatable` is `true`, the entity can be created using a dedicated graphql `delete<ModelName>` mutation.
|
|
55
|
+
|
|
56
|
+
For this to work, at least one entity field needs to be marked as `updatable`.
|
|
57
|
+
|
|
58
|
+
`updatable` also accepts an object to override properties of the implicitly generated `updatedBy` and `updatedAt` fields.
|
|
59
|
+
|
|
60
|
+
If a field is updatable, a `<ModelName>Revisions` table is created (containing only the updatable fields) and extended with each update.
|
|
61
|
+
|
|
62
|
+
### deletable
|
|
63
|
+
|
|
64
|
+
When `deletable` is `true`, the entity can be created using a dedicated graphql `delete<ModelName>` mutation.
|
|
65
|
+
|
|
66
|
+
This is a soft delete (the `deleted` field is set to `true`), and the entity can be restored with the graphql `restore<ModelName>` mutation.
|
|
67
|
+
|
|
68
|
+
`deletable` also accepts an object to override properties of the implicitly generated `deleted`, `deletedBy` and `deletedAt` fields.
|
|
69
|
+
|
|
70
|
+
### queriable
|
|
71
|
+
|
|
72
|
+
When `queriable` is `true` a graphql `Query` becomes available to fetch exactly one element by id.
|
|
73
|
+
|
|
74
|
+
For example, with
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
{
|
|
78
|
+
kind: 'entity',
|
|
79
|
+
name: 'Post',
|
|
80
|
+
queriable: true
|
|
81
|
+
...
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
the following graphql query becomes possible
|
|
86
|
+
|
|
87
|
+
```graphql
|
|
88
|
+
query {
|
|
89
|
+
post(where: { id: "bf9496bb-9302-4528-aebc-c97ae49c52fa"}) {
|
|
90
|
+
title
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### listQueriable
|
|
96
|
+
|
|
97
|
+
When `listQueriable` is `true` a graphql `Query` becomes available to fetch a list of elements of this model.
|
|
98
|
+
|
|
99
|
+
For example, with
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
{
|
|
103
|
+
kind: 'entity',
|
|
104
|
+
name: 'Post',
|
|
105
|
+
listQueriable: true
|
|
106
|
+
...
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
the following graphql query becomes possible
|
|
111
|
+
|
|
112
|
+
```graphql
|
|
113
|
+
query {
|
|
114
|
+
posts {
|
|
115
|
+
title
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### displayField
|
|
121
|
+
|
|
122
|
+
The name of the field that ought to be used as display value, e.g. a `Post`'s `title`.
|
|
123
|
+
|
|
124
|
+
### defaultOrderBy
|
|
125
|
+
|
|
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
|
+
|
|
128
|
+
### fields
|
|
129
|
+
|
|
130
|
+
An array of fields. See [fields](./fields.md)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
## Scalar
|
|
134
|
+
|
|
135
|
+
Used for graphql scalars, e.g.
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
{
|
|
139
|
+
kind: 'Scalar',
|
|
140
|
+
name: 'DateTime'
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Enum
|
|
145
|
+
|
|
146
|
+
An enum that is available as type in the database:
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
{
|
|
150
|
+
kind: 'enum',
|
|
151
|
+
name: 'Role',
|
|
152
|
+
values: ['ADMIN', 'MODERATOR', 'USER']
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Raw enum
|
|
157
|
+
|
|
158
|
+
An enum that is *not* available as type in the database:
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
{
|
|
162
|
+
kind: 'raw-enum',
|
|
163
|
+
name: 'Role',
|
|
164
|
+
values: ['ADMIN', 'MODERATOR', 'USER']
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Interface
|
|
169
|
+
|
|
170
|
+
Types that can be inherited from, e.g.
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
{
|
|
174
|
+
kind: 'interface',
|
|
175
|
+
name: 'WithContent',
|
|
176
|
+
fields: [
|
|
177
|
+
{
|
|
178
|
+
type: 'String',
|
|
179
|
+
name: 'content'
|
|
180
|
+
}
|
|
181
|
+
]
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
kind: 'entity',
|
|
185
|
+
name: 'Post',
|
|
186
|
+
interfaces: ['WithContent']
|
|
187
|
+
fields: [
|
|
188
|
+
{
|
|
189
|
+
type: 'String',
|
|
190
|
+
name: 'content'
|
|
191
|
+
}
|
|
192
|
+
]
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Object
|
|
197
|
+
|
|
198
|
+
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
|
+
|
|
200
|
+
```ts
|
|
201
|
+
{
|
|
202
|
+
kind: 'object',
|
|
203
|
+
name: 'Stats',
|
|
204
|
+
fields: [
|
|
205
|
+
{
|
|
206
|
+
name: 'usersCount'
|
|
207
|
+
type: 'Int'
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: 'postsCount',
|
|
211
|
+
type: 'Int'
|
|
212
|
+
}
|
|
213
|
+
]
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
kind: 'object',
|
|
217
|
+
name: 'Query',
|
|
218
|
+
fields: [
|
|
219
|
+
// You'll need to define a custom resolver for this one
|
|
220
|
+
{
|
|
221
|
+
kind: 'custom',
|
|
222
|
+
name: 'stats',
|
|
223
|
+
type: 'Stats'
|
|
224
|
+
}
|
|
225
|
+
]
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
will make this query possible:
|
|
230
|
+
|
|
231
|
+
```graphql
|
|
232
|
+
query {
|
|
233
|
+
stats {
|
|
234
|
+
usersCount
|
|
235
|
+
postsCount
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Input
|
|
241
|
+
|
|
242
|
+
A custom input type. To be combined with custom mutations, e.g.
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
{
|
|
246
|
+
kind: 'input',
|
|
247
|
+
name: 'BulkDeleteWhereInput'
|
|
248
|
+
fields: [
|
|
249
|
+
{
|
|
250
|
+
kind: 'ID',
|
|
251
|
+
name: 'ids',
|
|
252
|
+
list: true
|
|
253
|
+
}
|
|
254
|
+
]
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
kind: 'object',
|
|
258
|
+
name: 'Mutation',
|
|
259
|
+
fields: [
|
|
260
|
+
// You'll need to define a custom resolver for this one
|
|
261
|
+
{
|
|
262
|
+
kind: 'custom',
|
|
263
|
+
name: 'bulkDelete',
|
|
264
|
+
args: [
|
|
265
|
+
{
|
|
266
|
+
kind: 'custom',
|
|
267
|
+
name: 'where',
|
|
268
|
+
type: 'BulkDeleteWhereInput'
|
|
269
|
+
}
|
|
270
|
+
]
|
|
271
|
+
type: 'Boolean'
|
|
272
|
+
}
|
|
273
|
+
]
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
will make this mutation possible:
|
|
278
|
+
|
|
279
|
+
```graphql
|
|
280
|
+
mutation {
|
|
281
|
+
bulkDelete(where: { ids: [...]})
|
|
282
|
+
}
|
|
283
|
+
```
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# Fields
|
|
2
|
+
|
|
3
|
+
Note that some fields are generated implicitly, such as `id`, `createdAt`/`createdBy` if the model is creatable.
|
|
4
|
+
|
|
5
|
+
## Fields of fields
|
|
6
|
+
|
|
7
|
+
Fields generally have the following fields available for configuration:
|
|
8
|
+
|
|
9
|
+
### `description`
|
|
10
|
+
|
|
11
|
+
Will appear as description in the graphql schema.
|
|
12
|
+
|
|
13
|
+
### `list`
|
|
14
|
+
|
|
15
|
+
If `list` is `true` the result is an array.
|
|
16
|
+
|
|
17
|
+
### `nonNull`
|
|
18
|
+
|
|
19
|
+
Will make the field required both in the graphql schema and in the database.
|
|
20
|
+
|
|
21
|
+
### `defaultValue`
|
|
22
|
+
|
|
23
|
+
Will set this as default value in graphql mutations and in the database.
|
|
24
|
+
|
|
25
|
+
### `args`
|
|
26
|
+
|
|
27
|
+
An array of fields that can then be used as parameters, e.g. if this field is implemented as a custom resolver.
|
|
28
|
+
|
|
29
|
+
### `directives`
|
|
30
|
+
|
|
31
|
+
Graphql directives for this field.
|
|
32
|
+
|
|
33
|
+
### `primary`
|
|
34
|
+
|
|
35
|
+
If `true` this will generate a primary key in the database.
|
|
36
|
+
|
|
37
|
+
### `unique`
|
|
38
|
+
|
|
39
|
+
If `true` this will generate a unique key in the database.
|
|
40
|
+
|
|
41
|
+
### `filterable`
|
|
42
|
+
|
|
43
|
+
If true, this field will be available in the `where` parameter for queries of this entity.
|
|
44
|
+
|
|
45
|
+
E.g. with
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
{
|
|
49
|
+
name: 'Post',
|
|
50
|
+
fields: [
|
|
51
|
+
{
|
|
52
|
+
name: 'name',
|
|
53
|
+
type: 'String',
|
|
54
|
+
filterable: true
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
this becomes possible:
|
|
61
|
+
|
|
62
|
+
```graphql
|
|
63
|
+
query {
|
|
64
|
+
posts(where: { name: "Hello World" }) {
|
|
65
|
+
title
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
With relations, this enables sub-filters, e.g. with
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
{
|
|
74
|
+
name: 'Comment',
|
|
75
|
+
fields: [
|
|
76
|
+
{
|
|
77
|
+
kind: 'relation',
|
|
78
|
+
name: 'post',
|
|
79
|
+
type: 'Post',
|
|
80
|
+
filterable: true
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
this becomes possible:
|
|
87
|
+
|
|
88
|
+
```graphql
|
|
89
|
+
query {
|
|
90
|
+
comments(where: { post: { name: "Hello World" } }) {
|
|
91
|
+
content
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
### `reverseFilterable`
|
|
98
|
+
|
|
99
|
+
Only relevant on relation fields. On `true` makes the reverse relation filterable.
|
|
100
|
+
|
|
101
|
+
E.g. with
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
{
|
|
105
|
+
name: 'Comment',
|
|
106
|
+
fields: [
|
|
107
|
+
{
|
|
108
|
+
name: 'post',
|
|
109
|
+
type: 'String',
|
|
110
|
+
reverseFilterable: true
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
this becomes possible:
|
|
117
|
+
|
|
118
|
+
```graphql
|
|
119
|
+
query {
|
|
120
|
+
posts(where: { comments_SOME: { name: "Hello World" } }) {
|
|
121
|
+
title
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Available filter postfixes are `_SOME` and `_NONE`.
|
|
127
|
+
|
|
128
|
+
### `searchable`
|
|
129
|
+
|
|
130
|
+
On `true` makes the field searchable. Search always happens across all fields marked as searchable (only one has to match). Search is case insensitive.
|
|
131
|
+
|
|
132
|
+
E.g. with
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
{
|
|
136
|
+
name: 'Post',
|
|
137
|
+
fields: [
|
|
138
|
+
{
|
|
139
|
+
name: 'title',
|
|
140
|
+
type: 'String',
|
|
141
|
+
searchable: true
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: 'content',
|
|
145
|
+
type: 'String',
|
|
146
|
+
searchable: true
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
]
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
this becomes possible:
|
|
154
|
+
|
|
155
|
+
```graphql
|
|
156
|
+
query {
|
|
157
|
+
posts(search: "Hello") {
|
|
158
|
+
title
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### `orderable`
|
|
164
|
+
|
|
165
|
+
On `true` makes the field available to the `orderBy` parameter.
|
|
166
|
+
|
|
167
|
+
E.g. with
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
{
|
|
171
|
+
name: 'Post',
|
|
172
|
+
fields: [
|
|
173
|
+
{
|
|
174
|
+
name: 'title',
|
|
175
|
+
type: 'String',
|
|
176
|
+
orderable: true
|
|
177
|
+
},
|
|
178
|
+
]
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
this becomes possible:
|
|
183
|
+
|
|
184
|
+
```graphql
|
|
185
|
+
query {
|
|
186
|
+
posts(orderBy: [{ title: DESC }]) {
|
|
187
|
+
title
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
### `comparable`
|
|
194
|
+
|
|
195
|
+
On `true` makes the field comparable.
|
|
196
|
+
|
|
197
|
+
E.g. with
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
{
|
|
201
|
+
name: 'Post',
|
|
202
|
+
fields: [
|
|
203
|
+
{
|
|
204
|
+
name: 'rating',
|
|
205
|
+
type: 'Int',
|
|
206
|
+
comparable: true
|
|
207
|
+
},
|
|
208
|
+
]
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
this becomes possible:
|
|
213
|
+
|
|
214
|
+
```graphql
|
|
215
|
+
query {
|
|
216
|
+
posts(where: { rating_GTE 4 }) {
|
|
217
|
+
title
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Available postfixes are:
|
|
223
|
+
|
|
224
|
+
* `_GT`: greater than
|
|
225
|
+
* `_GTE`: greater than or equal
|
|
226
|
+
* `_LT`: less than
|
|
227
|
+
* `_LTE`: less than or equal
|
|
228
|
+
|
|
229
|
+
### `queriable`
|
|
230
|
+
|
|
231
|
+
`true` by default. If explicitly set to `false`, the field won't be queriable via graphql.
|
|
232
|
+
|
|
233
|
+
Also accepts an object that defines a list of `roles` to restrict access to specific roles.
|
|
234
|
+
|
|
235
|
+
### `creatable`
|
|
236
|
+
|
|
237
|
+
If `true` this field will be available in the create mutation for the entity.
|
|
238
|
+
|
|
239
|
+
Also accepts an object that defines a list of `roles` to restrict creation to specific roles.
|
|
240
|
+
|
|
241
|
+
### `updatable`
|
|
242
|
+
|
|
243
|
+
If `true` this field will be available in the update mutation for the entity.
|
|
244
|
+
|
|
245
|
+
Also accepts an object that defines a list of `roles` to restrict creation to specific roles.
|
|
246
|
+
|
|
247
|
+
## Kinds of fields
|
|
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
|
|
285
|
+
|
|
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.
|
|
287
|
+
|
|
288
|
+
### reverse
|
|
289
|
+
|
|
290
|
+
`graphql-magic` automatically generates a name for the reverse relation, e.g. for a `Comment` pointing to `Post`:
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
{
|
|
294
|
+
name: 'Comment',
|
|
295
|
+
fields: [
|
|
296
|
+
{
|
|
297
|
+
kind: 'relation',
|
|
298
|
+
name: 'post',
|
|
299
|
+
type: 'Post'
|
|
300
|
+
}
|
|
301
|
+
]
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
the reverse relation will automatically be `Post.comments`. With `reverse` this name can be overridden.
|
|
306
|
+
|
|
307
|
+
### onDelete
|
|
308
|
+
|
|
309
|
+
can be `"cascade"` (default) or `"set-null"`.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Code generation
|
|
2
|
+
|
|
3
|
+
Whenever the models have been changed it is necessary generate code use the `graphql-magic` cli.
|
|
4
|
+
|
|
5
|
+
This can be done directly with `npx gqm generate`. We recommend to create a `package.json` script and to always generate code after install (or with `npm run generate`):
|
|
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:
|
|
15
|
+
|
|
16
|
+
* Generate `.gqmrc.json` file.
|
|
17
|
+
* Add local database connection variables to `.env` file.
|
|
18
|
+
* Add generated folder to `.gitignore`
|
|
19
|
+
* Generate `models.ts` file (if not present).
|
|
20
|
+
* Generate a basic `get-me.ts` example graphql query.
|
|
21
|
+
* Generate the `execute.ts` file for the execution
|
|
22
|
+
|
|
23
|
+
With each application, it generates the following files in the configured "generated" folder:
|
|
24
|
+
|
|
25
|
+
* `schema.graphql` - the schema of the api, for reference
|
|
26
|
+
* `models.json` - the final models object including generated fields such as "id"... for reference
|
|
27
|
+
* `api/index.ts` - the server-side model typescipt types
|
|
28
|
+
* `client/index.ts` - the client-side typescript types for the provided queries
|
|
29
|
+
* `client/mutations.ts` - standard mutation queries for all models
|
|
30
|
+
* `db/index.ts` - types for data from/to the database
|
|
31
|
+
* `db/knex.ts` - types to extend the `knex` query builder
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Migrations
|
|
2
|
+
|
|
3
|
+
After changing the models, if you have an existing database running to compare the models with, you can generate a migration like this:
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
npx gqm generate-migration
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
We recommend creating a `package.json` script for this:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
"generate-migration": "gqm generate-migration"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Note: if you are in a `feat/<feature-name>` branch, the script will use that as name for the migration.
|
|
16
|
+
|
|
17
|
+
This will generate a migration in the `migrations` folder. Check whether it needs to be adapted.
|
|
18
|
+
|
|
19
|
+
You can then run it with `knex` migration commands (using `env-cmd` to add db connection variables in `.env`):
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
npx env-cmd knex migrate:latest
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
We recommend creating a `package.json` script and always running it before starting the development server.
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
"migrate: "env-cmd knex migrate:latest"
|
|
29
|
+
"predev: "npm run migrate"
|
|
30
|
+
"dev": "next dev"
|
|
31
|
+
```
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Graphql querying
|
|
2
|
+
|
|
3
|
+
For autocompletion of your queries, you can create the following `apollo.config.js` file:
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
module.exports = {
|
|
7
|
+
client: {
|
|
8
|
+
service: {
|
|
9
|
+
name: 'your-project',
|
|
10
|
+
localSchemaFile: './src/generated/schema.graphql',
|
|
11
|
+
},
|
|
12
|
+
includes: ['./src/**/*.ts', './src/**/*.tsx'],
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## `executeQuery`
|
|
18
|
+
|
|
19
|
+
`graphql-magic` generates an `executeQuery` function for you, which you can adapt to your needs.
|
|
20
|
+
|
|
21
|
+
It handles things such as:
|
|
22
|
+
|
|
23
|
+
* user authentication (see the [Tutorial](./tutorial) for an example with auth0)
|
|
24
|
+
* custom resolvers (see [Custom resolvers](./custom-resolvers))
|
|
25
|
+
* mutation hooks (see [Mutation hooks](./mutation-hooks))
|
|
26
|
+
|
|
27
|
+
TODO
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
## Queries
|
|
31
|
+
|
|
32
|
+
Typically, you'll put graphql queries in `src/graphql/client/queries`, e.g. `get-posts.ts`
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { gql } from '@smartive/graphql-magic';
|
|
36
|
+
|
|
37
|
+
export const GET_POSTS = gql`
|
|
38
|
+
query GetPosts {
|
|
39
|
+
posts {
|
|
40
|
+
id
|
|
41
|
+
title
|
|
42
|
+
content
|
|
43
|
+
createdBy {
|
|
44
|
+
username
|
|
45
|
+
}
|
|
46
|
+
comments {
|
|
47
|
+
id
|
|
48
|
+
createdBy {
|
|
49
|
+
username
|
|
50
|
+
}
|
|
51
|
+
content
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
`;
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The query can then be used like so:
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
import { GetMeQuery, GetPostsQuery } from "@/generated/client";
|
|
62
|
+
import { GET_POSTS } from "@/graphql/client/queries/get-posts";
|
|
63
|
+
import { executeGraphql } from "@/graphql/execute";
|
|
64
|
+
|
|
65
|
+
async function Posts({ me }: { me: GetMeQuery['me'] }) {
|
|
66
|
+
const { data: { posts } } = await executeGraphql<GetPostsQuery>({ query: GET_POSTS })
|
|
67
|
+
|
|
68
|
+
return <div>
|
|
69
|
+
{posts.map(post => <div key={post.id}>
|
|
70
|
+
<article>
|
|
71
|
+
<h2>{post.title}</h2>
|
|
72
|
+
<div>by {post.createdBy.username}</div>
|
|
73
|
+
<p>{post.content}</p>
|
|
74
|
+
<h4>Comments</h4>
|
|
75
|
+
{post.comments.map(comment => (<div key={comment.id}>
|
|
76
|
+
<div>{comment.createdBy.username}</div>
|
|
77
|
+
<p>{comment.content}</p> by {comment.createdBy.username}
|
|
78
|
+
</div>)
|
|
79
|
+
)}
|
|
80
|
+
</article>
|
|
81
|
+
</div>)}
|
|
82
|
+
</div>
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Client-side: TODO
|
|
87
|
+
|
|
88
|
+
## Mutations
|
|
89
|
+
|
|
90
|
+
Mutation queries are generated by `graphql-magic` directly so you don't need to write them. Use like this:
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
import { CreatePostMutationMutation, CreatePostMutationMutationVariables } from "@/generated/client";
|
|
94
|
+
import { CREATE_POST } from "@/generated/client/mutations";
|
|
95
|
+
import { executeGraphql } from "@/graphql/execute";
|
|
96
|
+
import { revalidatePath } from "next/cache";
|
|
97
|
+
|
|
98
|
+
async function CreatePost() {
|
|
99
|
+
async function createPost(formData: FormData) {
|
|
100
|
+
'use server'
|
|
101
|
+
await executeGraphql<CreatePostMutationMutation, CreatePostMutationMutationVariables>({
|
|
102
|
+
query: CREATE_POST,
|
|
103
|
+
variables: {
|
|
104
|
+
data: {
|
|
105
|
+
title: formData.get('title') as string,
|
|
106
|
+
content: formData.get('content') as string
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
revalidatePath('/')
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return <form action={createPost}>
|
|
114
|
+
<h2>New Post</h2>
|
|
115
|
+
<label>
|
|
116
|
+
<span>Title</span>
|
|
117
|
+
<input name="title" />
|
|
118
|
+
</label>
|
|
119
|
+
<label>
|
|
120
|
+
<span>Content</span>
|
|
121
|
+
<textarea rows={5} name="content" />
|
|
122
|
+
</label>
|
|
123
|
+
<div>
|
|
124
|
+
<button type="submit">Create</button>
|
|
125
|
+
</div>
|
|
126
|
+
</form>
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Client-side: TODO
|