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.
Files changed (36) hide show
  1. package/README.md +353 -446
  2. package/dist/server/index.d.ts +13 -7
  3. package/dist/server/index.js +1 -0
  4. package/dist/server/nexus/index.d.ts +1 -0
  5. package/dist/server/nexus/index.js +17 -0
  6. package/dist/server/nexus/plugins/field-authorize.plugin.d.ts +15 -0
  7. package/dist/server/nexus/plugins/field-authorize.plugin.js +89 -0
  8. package/dist/server/nexus/plugins/index.d.ts +2 -0
  9. package/dist/server/nexus/plugins/index.js +18 -0
  10. package/dist/server/nexus/plugins/middleware.plugin.d.ts +17 -0
  11. package/dist/server/{utils/middleware.js → nexus/plugins/middleware.plugin.js} +2 -3
  12. package/dist/server/register.js +6 -7
  13. package/dist/server/services/file-service.js +7 -1
  14. package/dist/server/types.d.ts +10 -10
  15. package/dist/server/utils/field-builder/builder.d.ts +24 -0
  16. package/dist/server/utils/field-builder/builder.js +510 -0
  17. package/dist/server/utils/field-builder/helpers.d.ts +19 -0
  18. package/dist/server/utils/field-builder/helpers.js +56 -0
  19. package/dist/server/utils/field-builder/index.d.ts +3 -0
  20. package/dist/server/utils/field-builder/index.js +19 -0
  21. package/dist/server/utils/field-builder/types.d.ts +160 -0
  22. package/dist/server/utils/field-builder/types.js +2 -0
  23. package/dist/server/utils/graphql-extension.d.ts +5 -0
  24. package/dist/server/utils/graphql-extension.js +26 -24
  25. package/dist/server/utils/index.d.ts +1 -1
  26. package/dist/server/utils/index.js +1 -1
  27. package/dist/server/utils/override.d.ts +2 -1
  28. package/dist/server/utils/registry/global-registry.d.ts +19 -0
  29. package/dist/server/utils/registry/global-registry.js +21 -0
  30. package/dist/server/utils/registry/index.d.ts +1 -0
  31. package/dist/server/utils/registry/index.js +17 -0
  32. package/dist/tsconfig.server.tsbuildinfo +1 -1
  33. package/package.json +5 -3
  34. package/dist/server/utils/field-builder.d.ts +0 -31
  35. package/dist/server/utils/field-builder.js +0 -113
  36. 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
- Extension and utilities for Strapi GraphQL API. This plugin adds additional features to the Strapi GraphQL plugin such
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
- This let's you define your GraphQL schema and resolvers in the content-type directory, making it easier to manage and
7
- maintain your GraphQL API.
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
- This plugin is compatible with both Strapi v4 and Strapi v5.
10
+ > **Supports Strapi v4 and v5**
10
11
 
11
- ## Getting started
12
+ ---
13
+
14
+ ## 🚀 Getting Started
12
15
 
13
16
  ```bash
14
17
  npm i buro26-strapi-graphql
15
18
  ```
16
19
 
17
- ## Usage
18
-
19
- ### Configure Strapi GraphQL plugin
20
+ ---
20
21
 
21
- To be able to use this plugin, you will first need to configure
22
- the [Strapi GraphQL plugin](https://docs.strapi.io/dev-docs/plugins/graphql).
22
+ ## 🏗️ Recommended: The Field Builder Pattern
23
23
 
24
- ### Enable plugin
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
- Add plugin in `./config/plugins.[js/ts]` in your Strapi project.
27
+ ### **Example: A Custom Query Field**
27
28
 
28
29
  ```typescript
29
- export default () => ({
30
- // ...
31
- 'burotwosix-graphql-plugin': {
32
- enabled: true
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
- ### Create a new GraphQL Extension
42
+ ---
39
43
 
40
- To create a new GraphQL extension, create a folder named `graphql` in the content-type directory or within the
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
- From each file create a default export that uses th `createGraphQLExtension` factory function. This function takes a
46
- function as an argument that will receive the Strapi instance and should return an object with the following methods:
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
- | Method | Description |
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
- The functions you can implement follow the pattern of the Strapi customization
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
- ```typescript
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
- export default createGraphQLExtension(strapi => ({
64
+ ```typescript
65
+ const adminPreset = createGraphQLFieldPreset(builder =>
66
+ builder
67
+ .middleware(requireAdmin)
68
+ .authorize(adminAuth)
69
+ );
65
70
 
66
- init() {
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
- },
71
+ export default createGraphQLFieldBuilder('Query')
72
+ .use(adminPreset)
73
+ .fieldName('adminHello')
74
+ .outputType('String')
75
+ .resolver(() => 'Hello, admin!');
76
+ ```
74
77
 
75
- typeDefs() {
76
- return `
77
- type HelloWorldResponse {
78
- greeting: String!
79
- }
80
- `;
81
- },
78
+ ---
82
79
 
83
- types() {
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
- // Create a new query
105
- extendType({
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
- resolvers() {
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
- resolversConfig() {
141
- return {
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
- ### Fluent Builder Pattern
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
- export default createGraphQLExtension()
166
- .init(() => {
167
- // Initialization logic here...
168
- })
169
- .typeDefs(`
170
- extend type Query {
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
- .resolvers({
175
- Query: {
176
- hello: () => 'Hello from builder!'
177
- }
178
- });
100
+ },
101
+ resolve: () => 'Secret data'
102
+ });
179
103
  ```
180
104
 
181
- In this pattern, `createGraphQLExtension()` detects that no argument was passed and returns a builder. The builder exposes methods like `.init()`, `.typeDefs()`, `.types()`, `.plugins()`, `.resolvers()`, and `.resolversConfig()` so you can chain calls and define your extension inline.
105
+ > **Note:**
106
+ > If you use the builder, `.authorize()` will automatically set this property for you.
182
107
 
183
- ### Shorter way to define a new field
108
+ ---
184
109
 
185
- The `createGraphQLFieldBuilder()` function provides a fluent builder API to create a single GraphQL field extension using Nexus. It simplifies the process of extending the GraphQL schema by letting you define the field's name, arguments, output type, resolver, and resolver configuration in a chainable way.
110
+ ### **Field-Level Middleware**
186
111
 
187
- The builder pattern ensures that you only need to specify the necessary parts, and the extension is automatically registered using the provided settings. This approach minimizes boilerplate code while ensuring consistency across your GraphQL extensions.
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
- Below is an example of how to define a new field using the builder pattern. This example creates a new query field called `helloWorld` that takes a non-null `name` argument and returns a non-null `String`.
115
+ **Example:**
190
116
 
191
117
  ```typescript
192
- import { createGraphQLFieldBuilder } from "buro26-strapi-graphql";
193
- import { nonNull, stringArg } from 'nexus';
194
-
195
- export default createGraphQLFieldBuilder('Query')
196
- .fieldName('helloWorld')
197
- .args({
198
- name: nonNull(stringArg()),
199
- })
200
- .outputType(nonNull('String'))
201
- .resolver(async (parent, args, ctx) => {
202
- return `Hello World ${args.name}`;
203
- })
204
- .resolverConfig({
205
- auth: false,
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
- ### Overriding a field
134
+ > **Note:**
135
+ > If you use the builder, `.middleware()` will automatically set this property for you.
210
136
 
211
- Sometimes you will need to override a field in an existing type. You can do this by extending the type and adding the
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
- The overrideField function needs the content type and fieldName to be able to resolve the field type and the resolver.
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
- ```typescript
220
- import { createGraphQLExtension, overrideField } from 'buro26-strapi-graphql';
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
- export default createGraphQLExtension(strapi => ({
224
- types() {
225
- return [
226
- // Extend an existing type
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
- ### Add authorization to an existing field
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
- By default Strapi GraphQL does not support field level authorization. This plugin adds support for field level
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
- If you decide not to pass the resolve function and only the authorize function, the field will be resolved by the
259
- default resolver generated by the plugin.
195
+ ### **Validation, Tracing, and Testing**
196
+
197
+ **Argument validation with Zod:**
260
198
 
261
199
  ```typescript
262
- import { createGraphQLExtension, overrideField } from 'buro26-strapi-graphql';
263
- import { extendType } from 'nexus';
200
+ import { z } from 'zod';
264
201
 
265
- export default createGraphQLExtension(strapi => ({
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
- ### Field middleware
212
+ **Tracing/profiling:**
289
213
 
290
- You can add middleware to a specific field, similarly to adding authorization. Each field can have multiple middlewares.
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
- This can be useful when you want to add some logic before or after the field is resolved. E.g. logging, caching,
293
- response parsing, etc.
225
+ **Testing in isolation:**
294
226
 
295
227
  ```typescript
296
- import { createGraphQLExtension, overrideField } from 'buro26-strapi-graphql';
297
- import { extendType } from 'nexus';
298
-
299
- export default createGraphQLExtension(strapi => ({
300
- types() {
301
- return [
302
- // Extend an existing type
303
- extendType({
304
- type: 'MyType',
305
- definition(t) {
306
- t.field('myField', {
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
- const res = await next(root, args, context, info);
240
+ ---
314
241
 
315
- // Do something after the field is resolved
242
+ ### **Batch Registration and Field Groups**
316
243
 
317
- return res;
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
- const res = await next(root, args, context, info);
246
+ ```typescript
247
+ import { createGraphQLFieldBuilder, registerFields } from 'buro26-strapi-graphql';
334
248
 
335
- // Do something after the field is resolved
249
+ const fieldA = createGraphQLFieldBuilder('Query')
250
+ .fieldName('foo')
251
+ .outputType('String')
252
+ .resolver(() => 'Foo');
336
253
 
337
- return res;
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
- ### Utility functions
262
+ **Apply shared logic to a group:**
351
263
 
352
- The plugin also provides utility functions to help you resolve entities and collections. These functions are useful when
353
- you need to resolve entities and collections in your resolvers.
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
- #### resolveEntity
278
+ ## 🛠️ Advanced: The Extension Builder
356
279
 
357
- The `resolveEntity` function can be used to resolve an entity by id. The function takes the content type name and an
358
- object with all the resolve args passed the resolver function. The function will return the resolved entity as
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, overrideField, resolveEntity } from 'buro26-strapi-graphql';
284
+ import { createGraphQLExtension } from 'buro26-strapi-graphql';
363
285
  import { extendType } from 'nexus';
364
286
 
365
- export default createGraphQLExtension(strapi => ({
366
- types() {
367
- return [
368
- // Extend an existing type
369
- extendType({
370
- type: 'MyType',
371
- definition(t) {
372
- t.field('myFields', {
373
- type: 'MyFieldEntityResponse',
374
- resolve: async (parent, args, context, info) => {
375
- // Do something, and resolve the entit
376
- return resolveEntity<MyContentType>('api::my-content-type.my-content-type', {
377
- args: {
378
- id: 'the id here',
379
- },
380
- parent,
381
- context,
382
- info,
383
- })
384
- }
385
- });
386
- overrideField<MyContentType>(t, {
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
- #### resolveEntityRelation
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
- The `resolveEntityRelation` function can be used to resolve an entity by id. The function takes the content type name and an
412
- object with all the resolve args passed the resolver function. The function will return the resolved entity as
413
- EntityResponse.
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, resolveEntityRelation } from 'buro26-strapi-graphql';
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: 'myOtherField',
443
- resolve: async (parent, args, context, info) => {
444
- // Do something, and resolve the entit
445
- return resolveEntityRelation<MyContentType>('api::my-content-type.my-content-type', 'myRelationField', {
446
- args,
447
- parent, // needs to have the id field populated
448
- context,
449
- info,
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
- #### resolveEntityCollection
350
+ ---
462
351
 
463
- The `resolveEntityCollection` function can be used to resolve a collection of entities. The function takes the content
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
- ```typescript
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
- export default createGraphQLExtension(strapi => ({
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
- #### resolveEntityRelationCollection
361
+ ```typescript
362
+ import { resolveEntity } from 'buro26-strapi-graphql';
514
363
 
515
- This function has a similar API as `createEntityRelationCollectionResolver` but is used to resolve a collection of entities from a
516
- relational field on the provided content type. The only difference is that the resolvers first argument is the relation field name that should be resolved.
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
- #### toEntityResponse
373
+ - **`resolveEntityRelation`**
374
+ Resolve a related entity from a relation field.
519
375
 
520
- This function is a typesafe response builder for a single entity. It takes the entity and the content type name and will
521
- return an EntityResponse.
376
+ ```typescript
377
+ import { resolveEntityRelation } from 'buro26-strapi-graphql';
522
378
 
523
- This function is re-exported from the Strapi GraphQL plugin.
379
+ return resolveEntityRelation<MyContentType>(
380
+ 'api::my-content-type.my-content-type',
381
+ 'relationFieldName',
382
+ { args, parent, context, info }
383
+ );
384
+ ```
524
385
 
525
- #### toEntityCollectionResponse
386
+ - **`resolveEntityCollection`**
387
+ Resolve a collection of entities.
526
388
 
527
- This function is a typesafe response builder for a collection of entities. It takes the entities and the content type
528
- name and will return an EntityCollectionResponse.
389
+ ```typescript
390
+ import { createEntityCollectionResolver } from 'buro26-strapi-graphql';
529
391
 
530
- This function is re-exported from the Strapi GraphQL plugin.
392
+ const resolver = createEntityCollectionResolver<MyContentType>('api::my-content-type.my-content-type');
393
+ return resolver({ args, parent, context, info });
394
+ ```
531
395
 
532
- #### resolveArgs
396
+ - **`resolveEntityRelationCollection`**
397
+ Resolve a collection of related entities from a relation field.
533
398
 
534
- This function is a typesafe utility function to resolve the args passed to the resolver function. It will transform the
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
- This function is re-exported from the Strapi GraphQL plugin.
402
+ return resolveEntityRelationCollection<MyContentType>(
403
+ 'api::my-content-type.my-content-type',
404
+ 'relationFieldName',
405
+ { args, parent, context, info }
406
+ );
407
+ ```
539
408
 
540
- #### shadowCRUD
409
+ ### **Response Builders**
541
410
 
542
- This function is a typesafe utility function to create shadow CRUD operations for a content type. It will create the CRUD operations for the content type and return the shadow CRUD object.
411
+ - **`toEntityResponse`**
412
+ Typesafe builder for a single entity response.
543
413
 
544
- ```typescript
545
- import { createGraphQLExtension, shadowCRUD } from 'buro26-strapi-graphql';
414
+ ```typescript
415
+ import { toEntityResponse } from 'buro26-strapi-graphql';
546
416
 
547
- export default createGraphQLExtension((strapi) => ({
417
+ return toEntityResponse(entity, 'api::my-content-type.my-content-type');
418
+ ```
548
419
 
549
- init() {
550
- shadowCRUD('api::article.article')
551
- .disableActions(['create', 'update', 'delete']);
420
+ - **`toEntityCollectionResponse`**
421
+ Typesafe builder for a collection response.
552
422
 
553
- shadowCRUD('api::category.category')
554
- .field('title')
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
- ## Test and Deploy
431
+ - **`resolveArgs`**
432
+ Typesafe utility to transform GraphQL args for use with Strapi’s entity manager.
562
433
 
563
- ### Running tests
434
+ ```typescript
435
+ import { resolveArgs } from 'buro26-strapi-graphql';
564
436
 
565
- To run tests, run the following command:
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
- ## Contributing
464
+ ---
572
465
 
573
- Wish to contribute to this project? Pull the project from the repository and create a merge request.
466
+ ## 🤝 Contributing
574
467
 
575
- ## Authors and acknowledgment
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
- ## License
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
- ## Project status
485
+ ---
486
+
487
+ ## 🚦 Project Status
488
+
489
+ Active development.
490
+ Check issues for updates and planned features.
491
+
492
+ ---
585
493
 
586
- The project is currently in active development. We are continuously working on adding new features and improving the
587
- existing ones. Check the issues section for the latest updates and planned features.
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!