buro26-strapi-graphql 0.0.25 → 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 +362 -407
- package/dist/server/index.d.ts +14 -9
- package/dist/server/index.js +1 -3
- 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 +13 -4
- package/dist/server/types.d.ts +11 -12
- 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 +30 -0
- package/dist/server/utils/graphql-extension.js +57 -0
- package/dist/server/utils/index.d.ts +3 -1
- package/dist/server/utils/index.js +3 -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/middleware.d.ts +0 -17
package/README.md
CHANGED
|
@@ -1,541 +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
|
+
---
|
|
43
|
+
|
|
44
|
+
### **Why use the Field Builder?**
|
|
39
45
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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()`)
|
|
44
57
|
|
|
45
|
-
|
|
46
|
-
function as an argument that will receive the Strapi instance and should return an object with the following methods:
|
|
58
|
+
---
|
|
47
59
|
|
|
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 |
|
|
60
|
+
### **Composable Presets**
|
|
55
61
|
|
|
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.
|
|
62
|
+
Create reusable sets of middleware/auth logic and apply them to any field:
|
|
59
63
|
|
|
60
64
|
```typescript
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
const adminPreset = createGraphQLFieldPreset(builder =>
|
|
66
|
+
builder
|
|
67
|
+
.middleware(requireAdmin)
|
|
68
|
+
.authorize(adminAuth)
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
export default createGraphQLFieldBuilder('Query')
|
|
72
|
+
.use(adminPreset)
|
|
73
|
+
.fieldName('adminHello')
|
|
74
|
+
.outputType('String')
|
|
75
|
+
.resolver(() => 'Hello, admin!');
|
|
76
|
+
```
|
|
63
77
|
|
|
64
|
-
|
|
78
|
+
---
|
|
65
79
|
|
|
66
|
-
|
|
67
|
-
// e.g. disable actions
|
|
68
|
-
strapi
|
|
69
|
-
.plugin('graphql')
|
|
70
|
-
.service('extension')
|
|
71
|
-
.shadowCRUD('api::my-type.my-type')
|
|
72
|
-
.disableActions(['find', 'findOne', 'create', 'update', 'delete']);
|
|
73
|
-
},
|
|
80
|
+
## 🔒 Field-Level Authorization and Middleware
|
|
74
81
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
type HelloWorldResponse {
|
|
78
|
-
greeting: String!
|
|
79
|
-
}
|
|
80
|
-
`;
|
|
81
|
-
},
|
|
82
|
+
Every field defined with `createGraphQLFieldBuilder()` or via Nexus’s `t.field` can use the `extensions` property to
|
|
83
|
+
attach **authorization** and **middleware** logic.
|
|
82
84
|
|
|
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
|
-
}),
|
|
85
|
+
### **Field-Level Authorization**
|
|
103
86
|
|
|
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
|
-
},
|
|
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.
|
|
122
89
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
updateMyType: async (root, args) => {
|
|
134
|
-
// resolve logic
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
};
|
|
90
|
+
**Example:**
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
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';
|
|
99
|
+
}
|
|
138
100
|
},
|
|
101
|
+
resolve: () => 'Secret data'
|
|
102
|
+
});
|
|
103
|
+
```
|
|
139
104
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
105
|
+
> **Note:**
|
|
106
|
+
> If you use the builder, `.authorize()` will automatically set this property for you.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
### **Field-Level Middleware**
|
|
111
|
+
|
|
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.
|
|
114
|
+
|
|
115
|
+
**Example:**
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
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;
|
|
155
127
|
}
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
128
|
+
]
|
|
129
|
+
},
|
|
130
|
+
resolve: () => 'With middleware'
|
|
131
|
+
});
|
|
159
132
|
```
|
|
160
133
|
|
|
161
|
-
|
|
134
|
+
> **Note:**
|
|
135
|
+
> If you use the builder, `.middleware()` will automatically set this property for you.
|
|
162
136
|
|
|
163
|
-
|
|
164
|
-
field again. The field will be overridden with the new field definition. To make this easier we added a helper function
|
|
165
|
-
`overrideField` that will make this easier. The helper method also automatically generates a resolver for the field.
|
|
137
|
+
---
|
|
166
138
|
|
|
167
|
-
|
|
168
|
-
The authorize function is optional and can be used to authorize the field. The resolve function is also optional and can
|
|
169
|
-
be used to resolve the field.
|
|
139
|
+
## 🌍 Global Middleware and Authorization
|
|
170
140
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
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.
|
|
174
143
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
extendType({
|
|
180
|
-
type: 'MyType',
|
|
181
|
-
definition(t) {
|
|
182
|
-
overrideField<MyContentType>(t, {
|
|
183
|
-
contentTypeName: 'api::my-type.my-type',
|
|
184
|
-
fieldName: 'myField',
|
|
185
|
-
description: 'My field description',
|
|
186
|
-
authorize: async (root, args, context) => {
|
|
187
|
-
// Authorization logic
|
|
188
|
-
return true;
|
|
189
|
-
},
|
|
190
|
-
resolve: async (root) => {
|
|
191
|
-
// Resolver is optional
|
|
192
|
-
return false;
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
})
|
|
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.
|
|
197
148
|
|
|
198
|
-
|
|
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);
|
|
199
160
|
}
|
|
200
|
-
|
|
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);
|
|
201
175
|
```
|
|
202
176
|
|
|
203
|
-
|
|
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
|
|
204
186
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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.
|
|
209
192
|
|
|
210
|
-
|
|
211
|
-
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
### **Validation, Tracing, and Testing**
|
|
196
|
+
|
|
197
|
+
**Argument validation with Zod:**
|
|
212
198
|
|
|
213
199
|
```typescript
|
|
214
|
-
import {
|
|
215
|
-
import { extendType } from 'nexus';
|
|
200
|
+
import { z } from 'zod';
|
|
216
201
|
|
|
217
|
-
|
|
218
|
-
types() {
|
|
219
|
-
return [
|
|
220
|
-
// Extend an existing type
|
|
221
|
-
extendType({
|
|
222
|
-
type: 'MyType',
|
|
223
|
-
definition(t) {
|
|
224
|
-
overrideField<MyContentType>(t, {
|
|
225
|
-
contentTypeName: 'api::my-type.my-type',
|
|
226
|
-
fieldName: 'myField',
|
|
227
|
-
authorize: async (root, args, context) => {
|
|
228
|
-
// Decide if the user is authorized to access the field
|
|
229
|
-
return true;
|
|
230
|
-
}
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
})
|
|
202
|
+
const nameSchema = z.object({ name: z.string().min(1) });
|
|
234
203
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
})
|
|
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}!`);
|
|
238
210
|
```
|
|
239
211
|
|
|
240
|
-
|
|
212
|
+
**Tracing/profiling:**
|
|
241
213
|
|
|
242
|
-
|
|
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
|
+
```
|
|
243
224
|
|
|
244
|
-
|
|
245
|
-
response parsing, etc.
|
|
225
|
+
**Testing in isolation:**
|
|
246
226
|
|
|
247
227
|
```typescript
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
type: 'MyFieldType',
|
|
260
|
-
extensions: {
|
|
261
|
-
middlewares: [
|
|
262
|
-
next => async (root, args, context, info) => {
|
|
263
|
-
// 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
|
+
```
|
|
264
239
|
|
|
265
|
-
|
|
240
|
+
---
|
|
266
241
|
|
|
267
|
-
|
|
242
|
+
### **Batch Registration and Field Groups**
|
|
268
243
|
|
|
269
|
-
|
|
270
|
-
}
|
|
271
|
-
]
|
|
272
|
-
},
|
|
273
|
-
resolve: async (root) => {
|
|
274
|
-
// Resolver is optional
|
|
275
|
-
}
|
|
276
|
-
})
|
|
277
|
-
overrideField<MyContentType>(t, {
|
|
278
|
-
contentTypeName: 'api::my-type.my-type',
|
|
279
|
-
fieldName: 'myField',
|
|
280
|
-
extensions: {
|
|
281
|
-
middlewares: [
|
|
282
|
-
next => async (root, args, context, info) => {
|
|
283
|
-
// Do something before the field is resolved
|
|
244
|
+
**Register multiple fields at once:**
|
|
284
245
|
|
|
285
|
-
|
|
246
|
+
```typescript
|
|
247
|
+
import { createGraphQLFieldBuilder, registerFields } from 'buro26-strapi-graphql';
|
|
286
248
|
|
|
287
|
-
|
|
249
|
+
const fieldA = createGraphQLFieldBuilder('Query')
|
|
250
|
+
.fieldName('foo')
|
|
251
|
+
.outputType('String')
|
|
252
|
+
.resolver(() => 'Foo');
|
|
288
253
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
})
|
|
254
|
+
const fieldB = createGraphQLFieldBuilder('Query')
|
|
255
|
+
.fieldName('bar')
|
|
256
|
+
.outputType('String')
|
|
257
|
+
.resolver(() => 'Bar');
|
|
296
258
|
|
|
297
|
-
|
|
298
|
-
}
|
|
299
|
-
}));
|
|
259
|
+
export default registerFields(fieldA, fieldB);
|
|
300
260
|
```
|
|
301
261
|
|
|
302
|
-
|
|
262
|
+
**Apply shared logic to a group:**
|
|
303
263
|
|
|
304
|
-
|
|
305
|
-
|
|
264
|
+
```typescript
|
|
265
|
+
import { createFieldGroup } from 'buro26-strapi-graphql';
|
|
306
266
|
|
|
307
|
-
|
|
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
|
+
});
|
|
308
272
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
273
|
+
export default createFieldGroup([fieldA, fieldB], requireUser);
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## 🛠️ Advanced: The Extension Builder
|
|
279
|
+
|
|
280
|
+
For advanced use-cases, you can use `createGraphQLExtension()` to build a full extension with custom types, resolvers,
|
|
281
|
+
plugins, and more.
|
|
312
282
|
|
|
313
283
|
```typescript
|
|
314
|
-
import { createGraphQLExtension
|
|
284
|
+
import { createGraphQLExtension } from 'buro26-strapi-graphql';
|
|
315
285
|
import { extendType } from 'nexus';
|
|
316
286
|
|
|
317
|
-
export default createGraphQLExtension(
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
contentTypeName: 'api::my-type.my-type',
|
|
340
|
-
fieldName: 'myOtherField',
|
|
341
|
-
resolve: async (parent, args, context, info) => {
|
|
342
|
-
// Do something, and resolve the entit
|
|
343
|
-
return resolveEntity<MyContentType>('api::my-content-type.my-content-type', {
|
|
344
|
-
args: {
|
|
345
|
-
id: 'the id here',
|
|
346
|
-
},
|
|
347
|
-
parent,
|
|
348
|
-
context,
|
|
349
|
-
info,
|
|
350
|
-
})
|
|
351
|
-
}
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
})
|
|
355
|
-
|
|
356
|
-
];
|
|
357
|
-
}
|
|
358
|
-
}));
|
|
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
|
+
});
|
|
359
309
|
```
|
|
360
310
|
|
|
361
|
-
|
|
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
|
+
---
|
|
316
|
+
|
|
317
|
+
## 🧩 Overriding and Extending Fields
|
|
362
318
|
|
|
363
|
-
|
|
364
|
-
object with all the resolve args passed the resolver function. The function will return the resolved entity as
|
|
365
|
-
EntityResponse.
|
|
319
|
+
You can override or extend fields in existing types using the `overrideField` helper:
|
|
366
320
|
|
|
367
321
|
```typescript
|
|
368
|
-
import { createGraphQLExtension, overrideField
|
|
322
|
+
import { createGraphQLExtension, overrideField } from 'buro26-strapi-graphql';
|
|
369
323
|
import { extendType } from 'nexus';
|
|
370
324
|
|
|
371
325
|
export default createGraphQLExtension(strapi => ({
|
|
372
326
|
types() {
|
|
373
327
|
return [
|
|
374
|
-
// Extend an existing type
|
|
375
328
|
extendType({
|
|
376
329
|
type: 'MyType',
|
|
377
330
|
definition(t) {
|
|
378
|
-
t.field('myFields', {
|
|
379
|
-
type: 'MyFieldEntityResponse',
|
|
380
|
-
resolve: async (parent, args, context, info) => {
|
|
381
|
-
// Do something, and resolve the entit
|
|
382
|
-
return resolveEntityRelation<MyContentType>('api::my-content-type.my-content-type', {
|
|
383
|
-
args: {
|
|
384
|
-
id: 'the id here',
|
|
385
|
-
},
|
|
386
|
-
parent,
|
|
387
|
-
context,
|
|
388
|
-
info,
|
|
389
|
-
})
|
|
390
|
-
}
|
|
391
|
-
});
|
|
392
331
|
overrideField<MyContentType>(t, {
|
|
393
332
|
contentTypeName: 'api::my-type.my-type',
|
|
394
|
-
fieldName: '
|
|
395
|
-
|
|
396
|
-
//
|
|
397
|
-
return
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
})
|
|
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;
|
|
403
341
|
}
|
|
404
342
|
});
|
|
405
343
|
}
|
|
406
344
|
})
|
|
407
|
-
|
|
408
345
|
];
|
|
409
346
|
}
|
|
410
347
|
}));
|
|
411
348
|
```
|
|
412
349
|
|
|
413
|
-
|
|
350
|
+
---
|
|
414
351
|
|
|
415
|
-
|
|
416
|
-
type name and an object with all the resolve args passed the resolver function. The function will return the resolved
|
|
417
|
-
collection as EntityCollectionResponse.
|
|
352
|
+
## 🧰 Utility Functions
|
|
418
353
|
|
|
419
|
-
|
|
420
|
-
import { createGraphQLExtension, overrideField, createEntityCollectionResolver } from 'buro26-strapi-graphql';
|
|
421
|
-
import { extendType } from 'nexus';
|
|
354
|
+
This package provides a set of utility functions to make working with Strapi GraphQL resolvers easier and more typesafe:
|
|
422
355
|
|
|
423
|
-
|
|
424
|
-
types() {
|
|
425
|
-
return [
|
|
426
|
-
// Extend an existing type
|
|
427
|
-
extendType({
|
|
428
|
-
type: 'MyType',
|
|
429
|
-
definition(t) {
|
|
430
|
-
t.field('myFields', {
|
|
431
|
-
type: 'MyFieldEntityResponseCollection',
|
|
432
|
-
resolve: async (parent, args, context, info) => {
|
|
433
|
-
// Do something, and resolve the entity
|
|
434
|
-
const resolver = createEntityCollectionResolver<MyContentType>('api::my-content-type.my-content-type');
|
|
435
|
-
return resolver({
|
|
436
|
-
args,
|
|
437
|
-
parent,
|
|
438
|
-
context,
|
|
439
|
-
info,
|
|
440
|
-
})
|
|
441
|
-
}
|
|
442
|
-
});
|
|
443
|
-
overrideField<MyContentType>(t, {
|
|
444
|
-
contentTypeName: 'api::my-type.my-type',
|
|
445
|
-
fieldName: 'myOtherField',
|
|
446
|
-
resolve: async (parent, args, context, info) => {
|
|
447
|
-
// Do something, and resolve the entity
|
|
448
|
-
const resolver = createEntityCollectionResolver<MyContentType>('api::my-content-type.my-content-type');
|
|
449
|
-
return resolver({
|
|
450
|
-
args,
|
|
451
|
-
parent,
|
|
452
|
-
context,
|
|
453
|
-
info,
|
|
454
|
-
})
|
|
455
|
-
}
|
|
456
|
-
});
|
|
457
|
-
}
|
|
458
|
-
})
|
|
356
|
+
### **Entity and Collection Resolvers**
|
|
459
357
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
}));
|
|
463
|
-
```
|
|
358
|
+
- **`resolveEntity`**
|
|
359
|
+
Resolve a single entity by ID.
|
|
464
360
|
|
|
465
|
-
|
|
361
|
+
```typescript
|
|
362
|
+
import { resolveEntity } from 'buro26-strapi-graphql';
|
|
466
363
|
|
|
467
|
-
|
|
468
|
-
|
|
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
|
+
```
|
|
469
372
|
|
|
470
|
-
|
|
373
|
+
- **`resolveEntityRelation`**
|
|
374
|
+
Resolve a related entity from a relation field.
|
|
471
375
|
|
|
472
|
-
|
|
473
|
-
|
|
376
|
+
```typescript
|
|
377
|
+
import { resolveEntityRelation } from 'buro26-strapi-graphql';
|
|
474
378
|
|
|
475
|
-
|
|
379
|
+
return resolveEntityRelation<MyContentType>(
|
|
380
|
+
'api::my-content-type.my-content-type',
|
|
381
|
+
'relationFieldName',
|
|
382
|
+
{ args, parent, context, info }
|
|
383
|
+
);
|
|
384
|
+
```
|
|
476
385
|
|
|
477
|
-
|
|
386
|
+
- **`resolveEntityCollection`**
|
|
387
|
+
Resolve a collection of entities.
|
|
478
388
|
|
|
479
|
-
|
|
480
|
-
|
|
389
|
+
```typescript
|
|
390
|
+
import { createEntityCollectionResolver } from 'buro26-strapi-graphql';
|
|
481
391
|
|
|
482
|
-
|
|
392
|
+
const resolver = createEntityCollectionResolver<MyContentType>('api::my-content-type.my-content-type');
|
|
393
|
+
return resolver({ args, parent, context, info });
|
|
394
|
+
```
|
|
483
395
|
|
|
484
|
-
|
|
396
|
+
- **`resolveEntityRelationCollection`**
|
|
397
|
+
Resolve a collection of related entities from a relation field.
|
|
485
398
|
|
|
486
|
-
|
|
487
|
-
graphql
|
|
488
|
-
args into args that can be passed to the entityManager.
|
|
399
|
+
```typescript
|
|
400
|
+
import { resolveEntityRelationCollection } from 'buro26-strapi-graphql';
|
|
489
401
|
|
|
490
|
-
|
|
402
|
+
return resolveEntityRelationCollection<MyContentType>(
|
|
403
|
+
'api::my-content-type.my-content-type',
|
|
404
|
+
'relationFieldName',
|
|
405
|
+
{ args, parent, context, info }
|
|
406
|
+
);
|
|
407
|
+
```
|
|
491
408
|
|
|
492
|
-
|
|
409
|
+
### **Response Builders**
|
|
493
410
|
|
|
494
|
-
|
|
411
|
+
- **`toEntityResponse`**
|
|
412
|
+
Typesafe builder for a single entity response.
|
|
495
413
|
|
|
496
|
-
```typescript
|
|
497
|
-
import {
|
|
414
|
+
```typescript
|
|
415
|
+
import { toEntityResponse } from 'buro26-strapi-graphql';
|
|
498
416
|
|
|
499
|
-
|
|
417
|
+
return toEntityResponse(entity, 'api::my-content-type.my-content-type');
|
|
418
|
+
```
|
|
500
419
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
.disableActions(['create', 'update', 'delete']);
|
|
420
|
+
- **`toEntityCollectionResponse`**
|
|
421
|
+
Typesafe builder for a collection response.
|
|
504
422
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
.disable();
|
|
508
|
-
}
|
|
423
|
+
```typescript
|
|
424
|
+
import { toEntityCollectionResponse } from 'buro26-strapi-graphql';
|
|
509
425
|
|
|
510
|
-
|
|
511
|
-
```
|
|
426
|
+
return toEntityCollectionResponse(entities, 'api::my-content-type.my-content-type');
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### **Argument and CRUD Utilities**
|
|
430
|
+
|
|
431
|
+
- **`resolveArgs`**
|
|
432
|
+
Typesafe utility to transform GraphQL args for use with Strapi’s entity manager.
|
|
433
|
+
|
|
434
|
+
```typescript
|
|
435
|
+
import { resolveArgs } from 'buro26-strapi-graphql';
|
|
512
436
|
|
|
513
|
-
|
|
437
|
+
const entityManagerArgs = resolveArgs(graphqlArgs);
|
|
438
|
+
```
|
|
514
439
|
|
|
515
|
-
|
|
440
|
+
- **`shadowCRUD`**
|
|
441
|
+
Typesafe utility to create shadow CRUD operations for a content type.
|
|
516
442
|
|
|
517
|
-
|
|
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:
|
|
518
459
|
|
|
519
460
|
```bash
|
|
520
461
|
bun test
|
|
521
462
|
```
|
|
522
463
|
|
|
523
|
-
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
## 🤝 Contributing
|
|
524
467
|
|
|
525
|
-
|
|
468
|
+
Pull requests and contributions are welcome!
|
|
469
|
+
See the repo for details.
|
|
526
470
|
|
|
527
|
-
|
|
471
|
+
---
|
|
472
|
+
|
|
473
|
+
## 👥 Authors and Acknowledgment
|
|
474
|
+
|
|
475
|
+
**Buro26** – [https://buro26.digital](https://buro26.digital)
|
|
528
476
|
|
|
529
|
-
Buro26 - https://buro26.digital
|
|
530
477
|
Special thanks to all contributors and the open-source community for their support and contributions.
|
|
531
478
|
|
|
532
|
-
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
## 📄 License
|
|
533
482
|
|
|
534
483
|
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
535
484
|
|
|
536
|
-
|
|
485
|
+
---
|
|
486
|
+
|
|
487
|
+
## 🚦 Project Status
|
|
488
|
+
|
|
489
|
+
Active development.
|
|
490
|
+
Check issues for updates and planned features.
|
|
491
|
+
|
|
492
|
+
---
|
|
537
493
|
|
|
538
|
-
|
|
539
|
-
|
|
494
|
+
**Questions or suggestions?**
|
|
495
|
+
Open an issue or reach out to [Buro26](https://buro26.digital).
|
|
540
496
|
|
|
541
|
-
Feel free to reach out if you have any questions or suggestions!
|