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.
Files changed (34) hide show
  1. package/README.md +362 -407
  2. package/dist/server/index.d.ts +14 -9
  3. package/dist/server/index.js +1 -3
  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 +13 -4
  14. package/dist/server/types.d.ts +11 -12
  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 +30 -0
  24. package/dist/server/utils/graphql-extension.js +57 -0
  25. package/dist/server/utils/index.d.ts +3 -1
  26. package/dist/server/utils/index.js +3 -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/middleware.d.ts +0 -17
package/README.md CHANGED
@@ -1,541 +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
+ ---
43
+
44
+ ### **Why use the Field Builder?**
39
45
 
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.
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
- 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:
58
+ ---
47
59
 
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 |
60
+ ### **Composable Presets**
55
61
 
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.
62
+ Create reusable sets of middleware/auth logic and apply them to any field:
59
63
 
60
64
  ```typescript
61
- import { createGraphQLExtension } from 'buro26-strapi-graphql';
62
- import { extendType, nonNull, stringArg } from 'nexus';
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
- export default createGraphQLExtension(strapi => ({
78
+ ---
65
79
 
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
- },
80
+ ## 🔒 Field-Level Authorization and Middleware
74
81
 
75
- typeDefs() {
76
- return `
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
- 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
- }),
85
+ ### **Field-Level Authorization**
103
86
 
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
- },
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
- 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
- };
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
- 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
- ]
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
- ### Overriding a field
134
+ > **Note:**
135
+ > If you use the builder, `.middleware()` will automatically set this property for you.
162
136
 
163
- Sometimes you will need to override a field in an existing type. You can do this by extending the type and adding the
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
- The overrideField function needs the content type and fieldName to be able to resolve the field type and the resolver.
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
- ```typescript
172
- import { createGraphQLExtension, overrideField } from 'buro26-strapi-graphql';
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
- export default createGraphQLExtension(strapi => ({
176
- types() {
177
- return [
178
- // Extend an existing type
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
- ### 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
204
186
 
205
- By default Strapi GraphQL does not support field level authorization. This plugin adds support for field level
206
- authorization. You can add authorization to an existing field by using the before mentioned `overrideField` function.
207
- The authorize function will be called before the field is resolved. If the authorize function returns false the field
208
- will not be resolved and an error will be thrown.
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
- If you decide not to pass the resolve function and only the authorize function, the field will be resolved by the
211
- default resolver generated by the plugin.
193
+ ---
194
+
195
+ ### **Validation, Tracing, and Testing**
196
+
197
+ **Argument validation with Zod:**
212
198
 
213
199
  ```typescript
214
- import { createGraphQLExtension, overrideField } from 'buro26-strapi-graphql';
215
- import { extendType } from 'nexus';
200
+ import { z } from 'zod';
216
201
 
217
- export default createGraphQLExtension(strapi => ({
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
- ### Field middleware
212
+ **Tracing/profiling:**
241
213
 
242
- 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
+ ```
243
224
 
244
- This can be useful when you want to add some logic before or after the field is resolved. E.g. logging, caching,
245
- response parsing, etc.
225
+ **Testing in isolation:**
246
226
 
247
227
  ```typescript
248
- import { createGraphQLExtension, overrideField } from 'buro26-strapi-graphql';
249
- import { extendType } from 'nexus';
250
-
251
- export default createGraphQLExtension(strapi => ({
252
- types() {
253
- return [
254
- // Extend an existing type
255
- extendType({
256
- type: 'MyType',
257
- definition(t) {
258
- t.field('myField', {
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
- const res = await next(root, args, context, info);
240
+ ---
266
241
 
267
- // Do something after the field is resolved
242
+ ### **Batch Registration and Field Groups**
268
243
 
269
- return res;
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
- const res = await next(root, args, context, info);
246
+ ```typescript
247
+ import { createGraphQLFieldBuilder, registerFields } from 'buro26-strapi-graphql';
286
248
 
287
- // Do something after the field is resolved
249
+ const fieldA = createGraphQLFieldBuilder('Query')
250
+ .fieldName('foo')
251
+ .outputType('String')
252
+ .resolver(() => 'Foo');
288
253
 
289
- return res;
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
- ### Utility functions
262
+ **Apply shared logic to a group:**
303
263
 
304
- The plugin also provides utility functions to help you resolve entities and collections. These functions are useful when
305
- you need to resolve entities and collections in your resolvers.
264
+ ```typescript
265
+ import { createFieldGroup } from 'buro26-strapi-graphql';
306
266
 
307
- #### resolveEntity
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
- The `resolveEntity` function can be used to resolve an entity by id. The function takes the content type name and an
310
- object with all the resolve args passed the resolver function. The function will return the resolved entity as
311
- EntityResponse.
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, overrideField, resolveEntity } from 'buro26-strapi-graphql';
284
+ import { createGraphQLExtension } from 'buro26-strapi-graphql';
315
285
  import { extendType } from 'nexus';
316
286
 
317
- export default createGraphQLExtension(strapi => ({
318
- types() {
319
- return [
320
- // Extend an existing type
321
- extendType({
322
- type: 'MyType',
323
- definition(t) {
324
- t.field('myFields', {
325
- type: 'MyFieldEntityResponse',
326
- resolve: async (parent, args, context, info) => {
327
- // Do something, and resolve the entit
328
- return resolveEntity<MyContentType>('api::my-content-type.my-content-type', {
329
- args: {
330
- id: 'the id here',
331
- },
332
- parent,
333
- context,
334
- info,
335
- })
336
- }
337
- });
338
- overrideField<MyContentType>(t, {
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
- #### 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
+ ---
316
+
317
+ ## 🧩 Overriding and Extending Fields
362
318
 
363
- The `resolveEntityRelation` function can be used to resolve an entity by id. The function takes the content type name and an
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, resolveEntityRelation } from 'buro26-strapi-graphql';
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: 'myOtherField',
395
- resolve: async (parent, args, context, info) => {
396
- // Do something, and resolve the entit
397
- return resolveEntityRelation<MyContentType>('api::my-content-type.my-content-type', 'myRelationField', {
398
- args,
399
- parent, // needs to have the id field populated
400
- context,
401
- info,
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
- #### resolveEntityCollection
350
+ ---
414
351
 
415
- The `resolveEntityCollection` function can be used to resolve a collection of entities. The function takes the content
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
- ```typescript
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
- export default createGraphQLExtension(strapi => ({
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
- #### resolveEntityRelationCollection
361
+ ```typescript
362
+ import { resolveEntity } from 'buro26-strapi-graphql';
466
363
 
467
- This function has a similar API as `createEntityRelationCollectionResolver` but is used to resolve a collection of entities from a
468
- 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
+ ```
469
372
 
470
- #### toEntityResponse
373
+ - **`resolveEntityRelation`**
374
+ Resolve a related entity from a relation field.
471
375
 
472
- This function is a typesafe response builder for a single entity. It takes the entity and the content type name and will
473
- return an EntityResponse.
376
+ ```typescript
377
+ import { resolveEntityRelation } from 'buro26-strapi-graphql';
474
378
 
475
- 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
+ ```
476
385
 
477
- #### toEntityCollectionResponse
386
+ - **`resolveEntityCollection`**
387
+ Resolve a collection of entities.
478
388
 
479
- This function is a typesafe response builder for a collection of entities. It takes the entities and the content type
480
- name and will return an EntityCollectionResponse.
389
+ ```typescript
390
+ import { createEntityCollectionResolver } from 'buro26-strapi-graphql';
481
391
 
482
- 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
+ ```
483
395
 
484
- #### resolveArgs
396
+ - **`resolveEntityRelationCollection`**
397
+ Resolve a collection of related entities from a relation field.
485
398
 
486
- This function is a typesafe utility function to resolve the args passed to the resolver function. It will transform the
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
- 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
+ ```
491
408
 
492
- #### shadowCRUD
409
+ ### **Response Builders**
493
410
 
494
- 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.
495
413
 
496
- ```typescript
497
- import { createGraphQLExtension, shadowCRUD } from 'buro26-strapi-graphql';
414
+ ```typescript
415
+ import { toEntityResponse } from 'buro26-strapi-graphql';
498
416
 
499
- export default createGraphQLExtension((strapi) => ({
417
+ return toEntityResponse(entity, 'api::my-content-type.my-content-type');
418
+ ```
500
419
 
501
- init() {
502
- shadowCRUD('api::article.article')
503
- .disableActions(['create', 'update', 'delete']);
420
+ - **`toEntityCollectionResponse`**
421
+ Typesafe builder for a collection response.
504
422
 
505
- shadowCRUD('api::category.category')
506
- .field('title')
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
- ## Test and Deploy
437
+ const entityManagerArgs = resolveArgs(graphqlArgs);
438
+ ```
514
439
 
515
- ### Running tests
440
+ - **`shadowCRUD`**
441
+ Typesafe utility to create shadow CRUD operations for a content type.
516
442
 
517
- To run tests, run the following command:
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
- ## Contributing
464
+ ---
465
+
466
+ ## 🤝 Contributing
524
467
 
525
- Wish to contribute to this project? Pull the project from the repository and create a merge request.
468
+ Pull requests and contributions are welcome!
469
+ See the repo for details.
526
470
 
527
- ## Authors and acknowledgment
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
- ## License
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
- ## Project status
485
+ ---
486
+
487
+ ## 🚦 Project Status
488
+
489
+ Active development.
490
+ Check issues for updates and planned features.
491
+
492
+ ---
537
493
 
538
- The project is currently in active development. We are continuously working on adding new features and improving the
539
- 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).
540
496
 
541
- Feel free to reach out if you have any questions or suggestions!