buro26-strapi-graphql 0.0.26 → 0.1.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/README.md +353 -446
- package/dist/server/index.d.ts +13 -7
- package/dist/server/index.js +1 -0
- package/dist/server/nexus/index.d.ts +1 -0
- package/dist/server/nexus/index.js +17 -0
- package/dist/server/nexus/plugins/field-authorize.plugin.d.ts +15 -0
- package/dist/server/nexus/plugins/field-authorize.plugin.js +89 -0
- package/dist/server/nexus/plugins/index.d.ts +2 -0
- package/dist/server/nexus/plugins/index.js +18 -0
- package/dist/server/nexus/plugins/middleware.plugin.d.ts +17 -0
- package/dist/server/{utils/middleware.js → nexus/plugins/middleware.plugin.js} +2 -3
- package/dist/server/register.js +6 -7
- package/dist/server/services/file-service.js +7 -1
- package/dist/server/types.d.ts +10 -10
- package/dist/server/utils/field-builder/builder.d.ts +24 -0
- package/dist/server/utils/field-builder/builder.js +510 -0
- package/dist/server/utils/field-builder/helpers.d.ts +19 -0
- package/dist/server/utils/field-builder/helpers.js +56 -0
- package/dist/server/utils/field-builder/index.d.ts +3 -0
- package/dist/server/utils/field-builder/index.js +19 -0
- package/dist/server/utils/field-builder/types.d.ts +160 -0
- package/dist/server/utils/field-builder/types.js +2 -0
- package/dist/server/utils/graphql-extension.d.ts +5 -0
- package/dist/server/utils/graphql-extension.js +26 -24
- package/dist/server/utils/index.d.ts +1 -1
- package/dist/server/utils/index.js +1 -1
- package/dist/server/utils/override.d.ts +2 -1
- package/dist/server/utils/registry/global-registry.d.ts +19 -0
- package/dist/server/utils/registry/global-registry.js +21 -0
- package/dist/server/utils/registry/index.d.ts +1 -0
- package/dist/server/utils/registry/index.js +17 -0
- package/dist/tsconfig.server.tsbuildinfo +1 -1
- package/package.json +5 -3
- package/dist/server/utils/field-builder.d.ts +0 -31
- package/dist/server/utils/field-builder.js +0 -113
- package/dist/server/utils/middleware.d.ts +0 -17
package/README.md
CHANGED
|
@@ -1,589 +1,496 @@
|
|
|
1
1
|
# Strapi GraphQL Extension
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
as co-location of GraphQL schema and resolvers, field overriding, and field authorization.
|
|
3
|
+
**Buro26’s Strapi GraphQL Extension** supercharges your Strapi GraphQL API with:
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
- Field-level authorization and middleware
|
|
6
|
+
- Co-location of schema and resolvers
|
|
7
|
+
- Fluent, type-safe builder APIs
|
|
8
|
+
- Powerful testing, validation, and tracing utilities
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
> **Supports Strapi v4 and v5**
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 🚀 Getting Started
|
|
12
15
|
|
|
13
16
|
```bash
|
|
14
17
|
npm i buro26-strapi-graphql
|
|
15
18
|
```
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
### Configure Strapi GraphQL plugin
|
|
20
|
+
---
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
the [Strapi GraphQL plugin](https://docs.strapi.io/dev-docs/plugins/graphql).
|
|
22
|
+
## 🏗️ Recommended: The Field Builder Pattern
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
The **recommended way** to build GraphQL fields is with `createGraphQLFieldBuilder()`.
|
|
25
|
+
This fluent, type-safe builder gives you the best DX, composability, and testability.
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
### **Example: A Custom Query Field**
|
|
27
28
|
|
|
28
29
|
```typescript
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
import { createGraphQLFieldBuilder } from 'buro26-strapi-graphql';
|
|
31
|
+
import { nonNull, stringArg } from 'nexus';
|
|
32
|
+
|
|
33
|
+
export default createGraphQLFieldBuilder('Query')
|
|
34
|
+
.fieldName('helloWorld')
|
|
35
|
+
.args({ name: nonNull(stringArg()) })
|
|
36
|
+
.outputType(nonNull('String'))
|
|
37
|
+
.description('Returns a personalized greeting')
|
|
38
|
+
.resolver(async (parent, args) => `Hello World ${args.name}`)
|
|
39
|
+
.resolverConfig({ auth: false });
|
|
36
40
|
```
|
|
37
41
|
|
|
38
|
-
|
|
42
|
+
---
|
|
39
43
|
|
|
40
|
-
|
|
41
|
-
extensions folder (in case of extending a plugin).
|
|
42
|
-
Inside the `graphql` folder you can create files with any name. This plugin will scan the `graphql` folder register your
|
|
43
|
-
extensions with Strapi.
|
|
44
|
+
### **Why use the Field Builder?**
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
- **Chainable, immutable API** for safe, readable code
|
|
47
|
+
- **Type-safe context and args** for resolvers, middleware, and auth
|
|
48
|
+
- **Built-in support for:**
|
|
49
|
+
- Middleware and authorization (local and global)
|
|
50
|
+
- Argument validation (Zod)
|
|
51
|
+
- Tracing/profiling
|
|
52
|
+
- Deprecation and description
|
|
53
|
+
- Field hooks (before/after resolve)
|
|
54
|
+
- Testing in isolation
|
|
55
|
+
- Batch/group registration
|
|
56
|
+
- Composable presets (`.use()`)
|
|
47
57
|
|
|
48
|
-
|
|
49
|
-
|-----------------|--------------------------------------------------|
|
|
50
|
-
| init | Called when the extension is initialized |
|
|
51
|
-
| types | Returns an array of GraphQL types |
|
|
52
|
-
| typeDefs | Returns a string with the type definition in SDL |
|
|
53
|
-
| resolvers | Returns an object with resolvers |
|
|
54
|
-
| resolversConfig | Returns an object with resolver configuration |
|
|
58
|
+
---
|
|
55
59
|
|
|
56
|
-
|
|
57
|
-
options: https://docs.strapi.io/dev-docs/plugins/graphql#customization. The return values here will be registered as-is
|
|
58
|
-
with Strapi in the register hook.
|
|
60
|
+
### **Composable Presets**
|
|
59
61
|
|
|
60
|
-
|
|
61
|
-
import { createGraphQLExtension } from 'buro26-strapi-graphql';
|
|
62
|
-
import { extendType, nonNull, stringArg } from 'nexus';
|
|
62
|
+
Create reusable sets of middleware/auth logic and apply them to any field:
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
```typescript
|
|
65
|
+
const adminPreset = createGraphQLFieldPreset(builder =>
|
|
66
|
+
builder
|
|
67
|
+
.middleware(requireAdmin)
|
|
68
|
+
.authorize(adminAuth)
|
|
69
|
+
);
|
|
65
70
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
.disableActions(['find', 'findOne', 'create', 'update', 'delete']);
|
|
73
|
-
},
|
|
71
|
+
export default createGraphQLFieldBuilder('Query')
|
|
72
|
+
.use(adminPreset)
|
|
73
|
+
.fieldName('adminHello')
|
|
74
|
+
.outputType('String')
|
|
75
|
+
.resolver(() => 'Hello, admin!');
|
|
76
|
+
```
|
|
74
77
|
|
|
75
|
-
|
|
76
|
-
return `
|
|
77
|
-
type HelloWorldResponse {
|
|
78
|
-
greeting: String!
|
|
79
|
-
}
|
|
80
|
-
`;
|
|
81
|
-
},
|
|
78
|
+
---
|
|
82
79
|
|
|
83
|
-
|
|
84
|
-
return [
|
|
85
|
-
// Extend an existing type
|
|
86
|
-
extendType({
|
|
87
|
-
type: 'MyType',
|
|
88
|
-
definition(t) {
|
|
89
|
-
t.field('foo', {
|
|
90
|
-
type: 'Boolean',
|
|
91
|
-
resolve: async (root) => {
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
t.field('bar', {
|
|
96
|
-
type: 'Boolean',
|
|
97
|
-
resolve: async (root) => {
|
|
98
|
-
return false;
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
}),
|
|
80
|
+
## 🔒 Field-Level Authorization and Middleware
|
|
103
81
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
type: 'Query',
|
|
107
|
-
definition(t) {
|
|
108
|
-
t.field('helloWorld', {
|
|
109
|
-
type: 'HelloWorldResponse',
|
|
110
|
-
args: {
|
|
111
|
-
greeting: nonNull(stringArg())
|
|
112
|
-
},
|
|
113
|
-
// either resolve here or add it in resolvers method
|
|
114
|
-
resolve: async (root, args) => {
|
|
115
|
-
// resolve logic
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
})
|
|
120
|
-
];
|
|
121
|
-
},
|
|
82
|
+
Every field defined with `createGraphQLFieldBuilder()` or via Nexus’s `t.field` can use the `extensions` property to
|
|
83
|
+
attach **authorization** and **middleware** logic.
|
|
122
84
|
|
|
123
|
-
|
|
124
|
-
return {
|
|
125
|
-
Query: {
|
|
126
|
-
// Override an existing query
|
|
127
|
-
helloWorld: async (root, args) => {
|
|
128
|
-
// resolve logic
|
|
129
|
-
}
|
|
130
|
-
},
|
|
131
|
-
Mutation: {
|
|
132
|
-
// Override an existing mutation
|
|
133
|
-
updateMyType: async (root, args) => {
|
|
134
|
-
// resolve logic
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
},
|
|
85
|
+
### **Field-Level Authorization**
|
|
139
86
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
'Query.helloWorld': {
|
|
143
|
-
policies: [
|
|
144
|
-
async ({ state, args }, { strapi }) => {
|
|
145
|
-
// Validate request
|
|
146
|
-
}
|
|
147
|
-
],
|
|
148
|
-
middlewares: [
|
|
149
|
-
async (next, root, args, context, info) => {
|
|
150
|
-
// Do something here
|
|
151
|
-
|
|
152
|
-
return next(root, args, context, info);
|
|
153
|
-
}
|
|
154
|
-
]
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
}));
|
|
159
|
-
```
|
|
87
|
+
- Use the `authorize` field in `extensions` to add per-field authorization logic.
|
|
88
|
+
- The function should return `true` (allow), `false` (deny), or throw an error.
|
|
160
89
|
|
|
161
|
-
|
|
162
|
-
The plugin also supports a builder pattern so that you can configure your extension using a fluent API. This is achieved by calling createGraphQLExtension() with no arguments. For example, you can write:
|
|
90
|
+
**Example:**
|
|
163
91
|
|
|
164
92
|
```typescript
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
hello: String
|
|
93
|
+
t.field('myField', {
|
|
94
|
+
type: 'String',
|
|
95
|
+
extensions: {
|
|
96
|
+
authorize: async (parent, args, ctx, info) => {
|
|
97
|
+
// Only allow admins
|
|
98
|
+
return ctx.state.user?.role === 'admin';
|
|
172
99
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
hello: () => 'Hello from builder!'
|
|
177
|
-
}
|
|
178
|
-
});
|
|
100
|
+
},
|
|
101
|
+
resolve: () => 'Secret data'
|
|
102
|
+
});
|
|
179
103
|
```
|
|
180
104
|
|
|
181
|
-
|
|
105
|
+
> **Note:**
|
|
106
|
+
> If you use the builder, `.authorize()` will automatically set this property for you.
|
|
182
107
|
|
|
183
|
-
|
|
108
|
+
---
|
|
184
109
|
|
|
185
|
-
|
|
110
|
+
### **Field-Level Middleware**
|
|
186
111
|
|
|
187
|
-
|
|
112
|
+
- Use the `middlewares` field in `extensions` to add one or more middleware functions to a field.
|
|
113
|
+
- Middleware functions wrap the resolver and can perform logic before/after, modify args, or short-circuit the resolver.
|
|
188
114
|
|
|
189
|
-
|
|
115
|
+
**Example:**
|
|
190
116
|
|
|
191
117
|
```typescript
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
});
|
|
118
|
+
t.field('myField', {
|
|
119
|
+
type: 'String',
|
|
120
|
+
extensions: {
|
|
121
|
+
middlewares: [
|
|
122
|
+
next => async (root, args, ctx, info) => {
|
|
123
|
+
console.log('Before');
|
|
124
|
+
const result = await next(root, args, ctx, info);
|
|
125
|
+
console.log('After');
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
},
|
|
130
|
+
resolve: () => 'With middleware'
|
|
131
|
+
});
|
|
207
132
|
```
|
|
208
133
|
|
|
209
|
-
|
|
134
|
+
> **Note:**
|
|
135
|
+
> If you use the builder, `.middleware()` will automatically set this property for you.
|
|
210
136
|
|
|
211
|
-
|
|
212
|
-
field again. The field will be overridden with the new field definition. To make this easier we added a helper function
|
|
213
|
-
`overrideField` that will make this easier. The helper method also automatically generates a resolver for the field.
|
|
137
|
+
---
|
|
214
138
|
|
|
215
|
-
|
|
216
|
-
The authorize function is optional and can be used to authorize the field. The resolve function is also optional and can
|
|
217
|
-
be used to resolve the field.
|
|
139
|
+
## 🌍 Global Middleware and Authorization
|
|
218
140
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
import { extendType } from 'nexus';
|
|
141
|
+
You can register **global middleware** and **global authorization** that apply to multiple fields based on type, field
|
|
142
|
+
name, or tags.
|
|
222
143
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
extendType({
|
|
228
|
-
type: 'MyType',
|
|
229
|
-
definition(t) {
|
|
230
|
-
overrideField<MyContentType>(t, {
|
|
231
|
-
contentTypeName: 'api::my-type.my-type',
|
|
232
|
-
fieldName: 'myField',
|
|
233
|
-
description: 'My field description',
|
|
234
|
-
authorize: async (root, args, context) => {
|
|
235
|
-
// Authorization logic
|
|
236
|
-
return true;
|
|
237
|
-
},
|
|
238
|
-
resolve: async (root) => {
|
|
239
|
-
// Resolver is optional
|
|
240
|
-
return false;
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
})
|
|
144
|
+
**Important:**
|
|
145
|
+
> **Global middleware and authorization must be registered _before_ any fields that consume them are built or
|
|
146
|
+
registered.**
|
|
147
|
+
> This ensures that all relevant fields will pick up the global logic.
|
|
245
148
|
|
|
246
|
-
|
|
149
|
+
**Example:**
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
import { registerGlobalMiddleware, registerGlobalAuthorize } from 'buro26-strapi-graphql';
|
|
153
|
+
|
|
154
|
+
// Register global middleware for all admin-tagged Query fields
|
|
155
|
+
registerGlobalMiddleware(
|
|
156
|
+
({ type, tags }) => type === 'Query' && tags.includes('admin'),
|
|
157
|
+
next => async (root, args, ctx, info) => {
|
|
158
|
+
// ...admin logic...
|
|
159
|
+
return next(root, args, ctx, info);
|
|
247
160
|
}
|
|
248
|
-
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// Register global authorization for a specific field
|
|
164
|
+
registerGlobalAuthorize(
|
|
165
|
+
({ fieldName }) => fieldName === 'deleteUser',
|
|
166
|
+
async (parent, args, ctx) => ctx.state.user?.isAdmin
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// Now define fields that will use these global middlewares/authorizations
|
|
170
|
+
export default createGraphQLFieldBuilder('Query')
|
|
171
|
+
.fieldName('deleteUser')
|
|
172
|
+
.outputType('Boolean')
|
|
173
|
+
.tag('admin')
|
|
174
|
+
.resolver(() => true);
|
|
249
175
|
```
|
|
250
176
|
|
|
251
|
-
|
|
177
|
+
**Best Practice:**
|
|
178
|
+
|
|
179
|
+
- Always register global middleware and authorization at the top of your entry file (before any field or extension
|
|
180
|
+
exports).
|
|
181
|
+
- This guarantees that all fields will be able to consume the global logic.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## 📝 Best Practices
|
|
186
|
+
|
|
187
|
+
- Use the **builder’s `.authorize()` and `.middleware()`** methods for most use-cases—they automatically set the correct
|
|
188
|
+
`extensions` properties.
|
|
189
|
+
- Use the `extensions` property directly for advanced or manual Nexus usage.
|
|
190
|
+
- Register **global middleware and authorization before** any fields that should use them.
|
|
191
|
+
- Use **tags** to target groups of fields for global logic.
|
|
252
192
|
|
|
253
|
-
|
|
254
|
-
authorization. You can add authorization to an existing field by using the before mentioned `overrideField` function.
|
|
255
|
-
The authorize function will be called before the field is resolved. If the authorize function returns false the field
|
|
256
|
-
will not be resolved and an error will be thrown.
|
|
193
|
+
---
|
|
257
194
|
|
|
258
|
-
|
|
259
|
-
|
|
195
|
+
### **Validation, Tracing, and Testing**
|
|
196
|
+
|
|
197
|
+
**Argument validation with Zod:**
|
|
260
198
|
|
|
261
199
|
```typescript
|
|
262
|
-
import {
|
|
263
|
-
import { extendType } from 'nexus';
|
|
200
|
+
import { z } from 'zod';
|
|
264
201
|
|
|
265
|
-
|
|
266
|
-
types() {
|
|
267
|
-
return [
|
|
268
|
-
// Extend an existing type
|
|
269
|
-
extendType({
|
|
270
|
-
type: 'MyType',
|
|
271
|
-
definition(t) {
|
|
272
|
-
overrideField<MyContentType>(t, {
|
|
273
|
-
contentTypeName: 'api::my-type.my-type',
|
|
274
|
-
fieldName: 'myField',
|
|
275
|
-
authorize: async (root, args, context) => {
|
|
276
|
-
// Decide if the user is authorized to access the field
|
|
277
|
-
return true;
|
|
278
|
-
}
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
})
|
|
202
|
+
const nameSchema = z.object({ name: z.string().min(1) });
|
|
282
203
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
})
|
|
204
|
+
export default createGraphQLFieldBuilder('Query')
|
|
205
|
+
.fieldName('hello')
|
|
206
|
+
.args({ name: 'String!' })
|
|
207
|
+
.outputType('String')
|
|
208
|
+
.validateArgs(nameSchema)
|
|
209
|
+
.resolver((parent, args) => `Hello, ${args.name}!`);
|
|
286
210
|
```
|
|
287
211
|
|
|
288
|
-
|
|
212
|
+
**Tracing/profiling:**
|
|
289
213
|
|
|
290
|
-
|
|
214
|
+
```typescript
|
|
215
|
+
export default createGraphQLFieldBuilder('Query')
|
|
216
|
+
.fieldName('profiledHello')
|
|
217
|
+
.outputType('String')
|
|
218
|
+
.trace() // Pretty console log by default
|
|
219
|
+
.resolver(async () => {
|
|
220
|
+
await new Promise(r => setTimeout(r, 100));
|
|
221
|
+
return 'Hello, profiled!';
|
|
222
|
+
});
|
|
223
|
+
```
|
|
291
224
|
|
|
292
|
-
|
|
293
|
-
response parsing, etc.
|
|
225
|
+
**Testing in isolation:**
|
|
294
226
|
|
|
295
227
|
```typescript
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
type: 'MyFieldType',
|
|
308
|
-
extensions: {
|
|
309
|
-
middlewares: [
|
|
310
|
-
next => async (root, args, context, info) => {
|
|
311
|
-
// Do something before the field is resolved
|
|
228
|
+
const field = createGraphQLFieldBuilder('Query')
|
|
229
|
+
.fieldName('hello')
|
|
230
|
+
.outputType('String')
|
|
231
|
+
.resolver((parent, args, ctx) => `Hello, ${args.name}!`);
|
|
232
|
+
|
|
233
|
+
const result = await field.test({
|
|
234
|
+
args: { name: 'Henri' },
|
|
235
|
+
context: { user: { id: '123' } }
|
|
236
|
+
});
|
|
237
|
+
console.log(result); // "Hello, Henri!"
|
|
238
|
+
```
|
|
312
239
|
|
|
313
|
-
|
|
240
|
+
---
|
|
314
241
|
|
|
315
|
-
|
|
242
|
+
### **Batch Registration and Field Groups**
|
|
316
243
|
|
|
317
|
-
|
|
318
|
-
}
|
|
319
|
-
]
|
|
320
|
-
},
|
|
321
|
-
resolve: async (root) => {
|
|
322
|
-
// Resolver is optional
|
|
323
|
-
}
|
|
324
|
-
})
|
|
325
|
-
overrideField<MyContentType>(t, {
|
|
326
|
-
contentTypeName: 'api::my-type.my-type',
|
|
327
|
-
fieldName: 'myField',
|
|
328
|
-
extensions: {
|
|
329
|
-
middlewares: [
|
|
330
|
-
next => async (root, args, context, info) => {
|
|
331
|
-
// Do something before the field is resolved
|
|
244
|
+
**Register multiple fields at once:**
|
|
332
245
|
|
|
333
|
-
|
|
246
|
+
```typescript
|
|
247
|
+
import { createGraphQLFieldBuilder, registerFields } from 'buro26-strapi-graphql';
|
|
334
248
|
|
|
335
|
-
|
|
249
|
+
const fieldA = createGraphQLFieldBuilder('Query')
|
|
250
|
+
.fieldName('foo')
|
|
251
|
+
.outputType('String')
|
|
252
|
+
.resolver(() => 'Foo');
|
|
336
253
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
})
|
|
254
|
+
const fieldB = createGraphQLFieldBuilder('Query')
|
|
255
|
+
.fieldName('bar')
|
|
256
|
+
.outputType('String')
|
|
257
|
+
.resolver(() => 'Bar');
|
|
344
258
|
|
|
345
|
-
|
|
346
|
-
}
|
|
347
|
-
}));
|
|
259
|
+
export default registerFields(fieldA, fieldB);
|
|
348
260
|
```
|
|
349
261
|
|
|
350
|
-
|
|
262
|
+
**Apply shared logic to a group:**
|
|
351
263
|
|
|
352
|
-
|
|
353
|
-
|
|
264
|
+
```typescript
|
|
265
|
+
import { createFieldGroup } from 'buro26-strapi-graphql';
|
|
266
|
+
|
|
267
|
+
const requireUser = builder =>
|
|
268
|
+
builder.middleware(next => async (root, args, ctx, info) => {
|
|
269
|
+
if (!ctx.user) throw new Error('Not authenticated');
|
|
270
|
+
return next(root, args, ctx, info);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
export default createFieldGroup([fieldA, fieldB], requireUser);
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
354
277
|
|
|
355
|
-
|
|
278
|
+
## 🛠️ Advanced: The Extension Builder
|
|
356
279
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
EntityResponse.
|
|
280
|
+
For advanced use-cases, you can use `createGraphQLExtension()` to build a full extension with custom types, resolvers,
|
|
281
|
+
plugins, and more.
|
|
360
282
|
|
|
361
283
|
```typescript
|
|
362
|
-
import { createGraphQLExtension
|
|
284
|
+
import { createGraphQLExtension } from 'buro26-strapi-graphql';
|
|
363
285
|
import { extendType } from 'nexus';
|
|
364
286
|
|
|
365
|
-
export default createGraphQLExtension(
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
contentTypeName: 'api::my-type.my-type',
|
|
388
|
-
fieldName: 'myOtherField',
|
|
389
|
-
resolve: async (parent, args, context, info) => {
|
|
390
|
-
// Do something, and resolve the entit
|
|
391
|
-
return resolveEntity<MyContentType>('api::my-content-type.my-content-type', {
|
|
392
|
-
args: {
|
|
393
|
-
id: 'the id here',
|
|
394
|
-
},
|
|
395
|
-
parent,
|
|
396
|
-
context,
|
|
397
|
-
info,
|
|
398
|
-
})
|
|
399
|
-
}
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
})
|
|
403
|
-
|
|
404
|
-
];
|
|
405
|
-
}
|
|
406
|
-
}));
|
|
287
|
+
export default createGraphQLExtension()
|
|
288
|
+
.typeDefs(`
|
|
289
|
+
type HelloWorldResponse {
|
|
290
|
+
greeting: String!
|
|
291
|
+
}
|
|
292
|
+
`)
|
|
293
|
+
.types([
|
|
294
|
+
extendType({
|
|
295
|
+
type: 'Query',
|
|
296
|
+
definition(t) {
|
|
297
|
+
t.field('helloWorld', {
|
|
298
|
+
type: 'HelloWorldResponse',
|
|
299
|
+
resolve: () => ({ greeting: 'Hello, world!' })
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
})
|
|
303
|
+
])
|
|
304
|
+
.resolvers({
|
|
305
|
+
Query: {
|
|
306
|
+
helloWorld: () => ({ greeting: 'Hello, world!' })
|
|
307
|
+
}
|
|
308
|
+
});
|
|
407
309
|
```
|
|
408
310
|
|
|
409
|
-
|
|
311
|
+
> **Note:**
|
|
312
|
+
> For most use-cases, prefer the field builder.
|
|
313
|
+
> Use the extension builder for advanced scenarios (custom plugins, full type overrides, etc).
|
|
314
|
+
|
|
315
|
+
---
|
|
410
316
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
317
|
+
## 🧩 Overriding and Extending Fields
|
|
318
|
+
|
|
319
|
+
You can override or extend fields in existing types using the `overrideField` helper:
|
|
414
320
|
|
|
415
321
|
```typescript
|
|
416
|
-
import { createGraphQLExtension, overrideField
|
|
322
|
+
import { createGraphQLExtension, overrideField } from 'buro26-strapi-graphql';
|
|
417
323
|
import { extendType } from 'nexus';
|
|
418
324
|
|
|
419
325
|
export default createGraphQLExtension(strapi => ({
|
|
420
326
|
types() {
|
|
421
327
|
return [
|
|
422
|
-
// Extend an existing type
|
|
423
328
|
extendType({
|
|
424
329
|
type: 'MyType',
|
|
425
330
|
definition(t) {
|
|
426
|
-
t.field('myFields', {
|
|
427
|
-
type: 'MyFieldEntityResponse',
|
|
428
|
-
resolve: async (parent, args, context, info) => {
|
|
429
|
-
// Do something, and resolve the entit
|
|
430
|
-
return resolveEntityRelation<MyContentType>('api::my-content-type.my-content-type', {
|
|
431
|
-
args: {
|
|
432
|
-
id: 'the id here',
|
|
433
|
-
},
|
|
434
|
-
parent,
|
|
435
|
-
context,
|
|
436
|
-
info,
|
|
437
|
-
})
|
|
438
|
-
}
|
|
439
|
-
});
|
|
440
331
|
overrideField<MyContentType>(t, {
|
|
441
332
|
contentTypeName: 'api::my-type.my-type',
|
|
442
|
-
fieldName: '
|
|
443
|
-
|
|
444
|
-
//
|
|
445
|
-
return
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
})
|
|
333
|
+
fieldName: 'myField',
|
|
334
|
+
authorize: async (root, args, context) => {
|
|
335
|
+
// Authorization logic
|
|
336
|
+
return true;
|
|
337
|
+
},
|
|
338
|
+
resolve: async (root) => {
|
|
339
|
+
// Resolver is optional
|
|
340
|
+
return false;
|
|
451
341
|
}
|
|
452
342
|
});
|
|
453
343
|
}
|
|
454
344
|
})
|
|
455
|
-
|
|
456
345
|
];
|
|
457
346
|
}
|
|
458
347
|
}));
|
|
459
348
|
```
|
|
460
349
|
|
|
461
|
-
|
|
350
|
+
---
|
|
462
351
|
|
|
463
|
-
|
|
464
|
-
type name and an object with all the resolve args passed the resolver function. The function will return the resolved
|
|
465
|
-
collection as EntityCollectionResponse.
|
|
352
|
+
## 🧰 Utility Functions
|
|
466
353
|
|
|
467
|
-
|
|
468
|
-
import { createGraphQLExtension, overrideField, createEntityCollectionResolver } from 'buro26-strapi-graphql';
|
|
469
|
-
import { extendType } from 'nexus';
|
|
354
|
+
This package provides a set of utility functions to make working with Strapi GraphQL resolvers easier and more typesafe:
|
|
470
355
|
|
|
471
|
-
|
|
472
|
-
types() {
|
|
473
|
-
return [
|
|
474
|
-
// Extend an existing type
|
|
475
|
-
extendType({
|
|
476
|
-
type: 'MyType',
|
|
477
|
-
definition(t) {
|
|
478
|
-
t.field('myFields', {
|
|
479
|
-
type: 'MyFieldEntityResponseCollection',
|
|
480
|
-
resolve: async (parent, args, context, info) => {
|
|
481
|
-
// Do something, and resolve the entity
|
|
482
|
-
const resolver = createEntityCollectionResolver<MyContentType>('api::my-content-type.my-content-type');
|
|
483
|
-
return resolver({
|
|
484
|
-
args,
|
|
485
|
-
parent,
|
|
486
|
-
context,
|
|
487
|
-
info,
|
|
488
|
-
})
|
|
489
|
-
}
|
|
490
|
-
});
|
|
491
|
-
overrideField<MyContentType>(t, {
|
|
492
|
-
contentTypeName: 'api::my-type.my-type',
|
|
493
|
-
fieldName: 'myOtherField',
|
|
494
|
-
resolve: async (parent, args, context, info) => {
|
|
495
|
-
// Do something, and resolve the entity
|
|
496
|
-
const resolver = createEntityCollectionResolver<MyContentType>('api::my-content-type.my-content-type');
|
|
497
|
-
return resolver({
|
|
498
|
-
args,
|
|
499
|
-
parent,
|
|
500
|
-
context,
|
|
501
|
-
info,
|
|
502
|
-
})
|
|
503
|
-
}
|
|
504
|
-
});
|
|
505
|
-
}
|
|
506
|
-
})
|
|
356
|
+
### **Entity and Collection Resolvers**
|
|
507
357
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
}));
|
|
511
|
-
```
|
|
358
|
+
- **`resolveEntity`**
|
|
359
|
+
Resolve a single entity by ID.
|
|
512
360
|
|
|
513
|
-
|
|
361
|
+
```typescript
|
|
362
|
+
import { resolveEntity } from 'buro26-strapi-graphql';
|
|
514
363
|
|
|
515
|
-
|
|
516
|
-
|
|
364
|
+
// Inside a resolver:
|
|
365
|
+
return resolveEntity<MyContentType>('api::my-content-type.my-content-type', {
|
|
366
|
+
args: { id: 'the id here' },
|
|
367
|
+
parent,
|
|
368
|
+
context,
|
|
369
|
+
info,
|
|
370
|
+
});
|
|
371
|
+
```
|
|
517
372
|
|
|
518
|
-
|
|
373
|
+
- **`resolveEntityRelation`**
|
|
374
|
+
Resolve a related entity from a relation field.
|
|
519
375
|
|
|
520
|
-
|
|
521
|
-
|
|
376
|
+
```typescript
|
|
377
|
+
import { resolveEntityRelation } from 'buro26-strapi-graphql';
|
|
522
378
|
|
|
523
|
-
|
|
379
|
+
return resolveEntityRelation<MyContentType>(
|
|
380
|
+
'api::my-content-type.my-content-type',
|
|
381
|
+
'relationFieldName',
|
|
382
|
+
{ args, parent, context, info }
|
|
383
|
+
);
|
|
384
|
+
```
|
|
524
385
|
|
|
525
|
-
|
|
386
|
+
- **`resolveEntityCollection`**
|
|
387
|
+
Resolve a collection of entities.
|
|
526
388
|
|
|
527
|
-
|
|
528
|
-
|
|
389
|
+
```typescript
|
|
390
|
+
import { createEntityCollectionResolver } from 'buro26-strapi-graphql';
|
|
529
391
|
|
|
530
|
-
|
|
392
|
+
const resolver = createEntityCollectionResolver<MyContentType>('api::my-content-type.my-content-type');
|
|
393
|
+
return resolver({ args, parent, context, info });
|
|
394
|
+
```
|
|
531
395
|
|
|
532
|
-
|
|
396
|
+
- **`resolveEntityRelationCollection`**
|
|
397
|
+
Resolve a collection of related entities from a relation field.
|
|
533
398
|
|
|
534
|
-
|
|
535
|
-
graphql
|
|
536
|
-
args into args that can be passed to the entityManager.
|
|
399
|
+
```typescript
|
|
400
|
+
import { resolveEntityRelationCollection } from 'buro26-strapi-graphql';
|
|
537
401
|
|
|
538
|
-
|
|
402
|
+
return resolveEntityRelationCollection<MyContentType>(
|
|
403
|
+
'api::my-content-type.my-content-type',
|
|
404
|
+
'relationFieldName',
|
|
405
|
+
{ args, parent, context, info }
|
|
406
|
+
);
|
|
407
|
+
```
|
|
539
408
|
|
|
540
|
-
|
|
409
|
+
### **Response Builders**
|
|
541
410
|
|
|
542
|
-
|
|
411
|
+
- **`toEntityResponse`**
|
|
412
|
+
Typesafe builder for a single entity response.
|
|
543
413
|
|
|
544
|
-
```typescript
|
|
545
|
-
import {
|
|
414
|
+
```typescript
|
|
415
|
+
import { toEntityResponse } from 'buro26-strapi-graphql';
|
|
546
416
|
|
|
547
|
-
|
|
417
|
+
return toEntityResponse(entity, 'api::my-content-type.my-content-type');
|
|
418
|
+
```
|
|
548
419
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
.disableActions(['create', 'update', 'delete']);
|
|
420
|
+
- **`toEntityCollectionResponse`**
|
|
421
|
+
Typesafe builder for a collection response.
|
|
552
422
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
.disable();
|
|
556
|
-
}
|
|
423
|
+
```typescript
|
|
424
|
+
import { toEntityCollectionResponse } from 'buro26-strapi-graphql';
|
|
557
425
|
|
|
558
|
-
|
|
559
|
-
```
|
|
426
|
+
return toEntityCollectionResponse(entities, 'api::my-content-type.my-content-type');
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### **Argument and CRUD Utilities**
|
|
560
430
|
|
|
561
|
-
|
|
431
|
+
- **`resolveArgs`**
|
|
432
|
+
Typesafe utility to transform GraphQL args for use with Strapi’s entity manager.
|
|
562
433
|
|
|
563
|
-
|
|
434
|
+
```typescript
|
|
435
|
+
import { resolveArgs } from 'buro26-strapi-graphql';
|
|
564
436
|
|
|
565
|
-
|
|
437
|
+
const entityManagerArgs = resolveArgs(graphqlArgs);
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
- **`shadowCRUD`**
|
|
441
|
+
Typesafe utility to create shadow CRUD operations for a content type.
|
|
442
|
+
|
|
443
|
+
```typescript
|
|
444
|
+
import { shadowCRUD } from 'buro26-strapi-graphql';
|
|
445
|
+
|
|
446
|
+
shadowCRUD('api::article.article')
|
|
447
|
+
.disableActions(['create', 'update', 'delete']);
|
|
448
|
+
|
|
449
|
+
shadowCRUD('api::category.category')
|
|
450
|
+
.field('title')
|
|
451
|
+
.disable();
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
---
|
|
455
|
+
|
|
456
|
+
## 🧪 Testing
|
|
457
|
+
|
|
458
|
+
To run tests:
|
|
566
459
|
|
|
567
460
|
```bash
|
|
568
461
|
bun test
|
|
569
462
|
```
|
|
570
463
|
|
|
571
|
-
|
|
464
|
+
---
|
|
572
465
|
|
|
573
|
-
|
|
466
|
+
## 🤝 Contributing
|
|
574
467
|
|
|
575
|
-
|
|
468
|
+
Pull requests and contributions are welcome!
|
|
469
|
+
See the repo for details.
|
|
470
|
+
|
|
471
|
+
---
|
|
472
|
+
|
|
473
|
+
## 👥 Authors and Acknowledgment
|
|
474
|
+
|
|
475
|
+
**Buro26** – [https://buro26.digital](https://buro26.digital)
|
|
576
476
|
|
|
577
|
-
Buro26 - https://buro26.digital
|
|
578
477
|
Special thanks to all contributors and the open-source community for their support and contributions.
|
|
579
478
|
|
|
580
|
-
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
## 📄 License
|
|
581
482
|
|
|
582
483
|
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
583
484
|
|
|
584
|
-
|
|
485
|
+
---
|
|
486
|
+
|
|
487
|
+
## 🚦 Project Status
|
|
488
|
+
|
|
489
|
+
Active development.
|
|
490
|
+
Check issues for updates and planned features.
|
|
491
|
+
|
|
492
|
+
---
|
|
585
493
|
|
|
586
|
-
|
|
587
|
-
|
|
494
|
+
**Questions or suggestions?**
|
|
495
|
+
Open an issue or reach out to [Buro26](https://buro26.digital).
|
|
588
496
|
|
|
589
|
-
Feel free to reach out if you have any questions or suggestions!
|