buro26-strapi-graphql 0.1.0 → 0.2.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 (44) hide show
  1. package/README.md +128 -16
  2. package/dist/server/index.d.ts +1 -1
  3. package/dist/server/nexus/plugins/field-authorize.plugin.d.ts +21 -0
  4. package/dist/server/nexus/plugins/field-authorize.plugin.js +68 -35
  5. package/dist/server/nexus/plugins/middleware.plugin.d.ts +10 -2
  6. package/dist/server/nexus/plugins/middleware.plugin.js +20 -33
  7. package/dist/server/services/file-service.d.ts +7 -6
  8. package/dist/server/services/file-service.js +36 -36
  9. package/dist/server/services/index.d.ts +1 -1
  10. package/dist/server/types.d.ts +18 -3
  11. package/dist/server/utils/collection-type.d.ts +13 -2
  12. package/dist/server/utils/collection-type.js +13 -2
  13. package/dist/server/utils/field-builder/builder.d.ts +13 -4
  14. package/dist/server/utils/field-builder/builder.js +161 -119
  15. package/dist/server/utils/field-builder/helpers.d.ts +21 -6
  16. package/dist/server/utils/field-builder/helpers.js +26 -4
  17. package/dist/server/utils/field-builder/types.d.ts +287 -76
  18. package/dist/server/utils/graphql-extension.d.ts +24 -3
  19. package/dist/server/utils/graphql-extension.js +73 -7
  20. package/dist/server/utils/index.d.ts +2 -0
  21. package/dist/server/utils/index.js +11 -0
  22. package/dist/server/utils/loaders.d.ts +94 -0
  23. package/dist/server/utils/loaders.js +166 -0
  24. package/dist/server/utils/misc.d.ts +6 -0
  25. package/dist/server/utils/misc.js +6 -0
  26. package/dist/server/utils/override.d.ts +32 -0
  27. package/dist/server/utils/override.js +54 -26
  28. package/dist/server/utils/registry/field-protection.d.ts +43 -0
  29. package/dist/server/utils/registry/field-protection.js +65 -0
  30. package/dist/server/utils/registry/global-registry.d.ts +58 -14
  31. package/dist/server/utils/registry/global-registry.js +42 -7
  32. package/dist/server/utils/registry/index.d.ts +2 -0
  33. package/dist/server/utils/registry/index.js +2 -0
  34. package/dist/server/utils/registry/resolve-field.d.ts +1 -0
  35. package/dist/server/utils/registry/resolve-field.js +67 -0
  36. package/dist/server/utils/shadow-crud.d.ts +28 -1
  37. package/dist/server/utils/shadow-crud.js +32 -1
  38. package/dist/server/utils/single-type.d.ts +30 -1
  39. package/dist/server/utils/single-type.js +25 -0
  40. package/dist/server/utils/strapi-compat.d.ts +85 -0
  41. package/dist/server/utils/strapi-compat.js +194 -0
  42. package/dist/server/utils/types.d.ts +25 -1
  43. package/dist/tsconfig.server.tsbuildinfo +1 -1
  44. package/package.json +3 -2
@@ -1,6 +1,12 @@
1
1
  import type { ResolveContext } from './utils';
2
2
  import type { GraphQLResolveInfo } from 'graphql';
3
3
  export type Strapi = typeof strapi;
4
+ /**
5
+ * A GraphQL extension as registered with Strapi's GraphQL plugin. Created by
6
+ * {@link createGraphQLExtension} or by building a field builder.
7
+ *
8
+ * @see https://docs.strapi.io/dev-docs/plugins/graphql#customization
9
+ */
4
10
  export interface GraphQLExtension {
5
11
  /**
6
12
  * Called when the extension is initialized, use this to perform any initialization logic
@@ -16,7 +22,7 @@ export interface GraphQLExtension {
16
22
  * Return array the types for the extension
17
23
  * Use Nexus to define types
18
24
  *
19
- * @deprecated Use types() instead
25
+ * @deprecated Renamed to `types()`; same signature. Will be removed in the next major version.
20
26
  */
21
27
  getTypes?: () => any[];
22
28
  /**
@@ -43,7 +49,7 @@ export interface GraphQLExtension {
43
49
  * This is used to configure middlewares and other resolver options
44
50
  * @see https://docs.strapi.io/developer-docs/latest/plugins/graphql.html#extending-the-schema
45
51
  *
46
- * @deprecated Use resolversConfig() instead
52
+ * @deprecated Renamed to `resolversConfig()`; same signature. Will be removed in the next major version.
47
53
  */
48
54
  getResolverConfig?: () => any;
49
55
  /**
@@ -53,8 +59,14 @@ export interface GraphQLExtension {
53
59
  */
54
60
  resolversConfig?: () => Record<string, ResolverConfigOption>;
55
61
  }
62
+ /**
63
+ * Strapi resolver configuration for a `Type.field` path.
64
+ */
56
65
  export type ResolverConfigOption<TParent = any, TArgs = any, TOutput = any> = {
57
- auth?: boolean | undefined;
66
+ /** `false` makes the field public; `{ scope }` requires these users-permissions actions */
67
+ auth?: boolean | {
68
+ scope: string[];
69
+ } | undefined;
58
70
  policies?: (string | PolicyFunction<TParent, TArgs>)[];
59
71
  middlewares?: MiddlewareFunction<TParent, TArgs, TOutput>[];
60
72
  };
@@ -72,4 +84,7 @@ export type PolicyFunctionOptions = {
72
84
  export type PolicyFunction<TParent = any, TArgs = any> = (context: PolicyFunctionContext<TParent, TArgs>, config: any, opts: PolicyFunctionOptions) => boolean | Promise<boolean>;
73
85
  export type MiddlewareFunction<TParent = any, TArgs = any, TOutput = any> = (next: MiddlewareNextFunction<TParent, TArgs, TOutput>, parent: TParent, args: TArgs, context: ResolveContext, info: GraphQLResolveInfo) => TOutput | Promise<TOutput>;
74
86
  export type MiddlewareNextFunction<TParent = any, TArgs = any, TOutput = any> = (parent: TParent, args: TArgs, context: ResolveContext, info: GraphQLResolveInfo) => Promise<TOutput>;
87
+ /**
88
+ * A GraphQL resolver with Strapi's resolver context.
89
+ */
75
90
  export type ResolverFunction<TParent = any, TArgs = any, TOutput = any> = (parent: TParent, args: TArgs, context: ResolveContext, info: GraphQLResolveInfo) => TOutput | Promise<TOutput>;
@@ -8,14 +8,18 @@ import { type ResolveArgs } from './types';
8
8
  * Create a resolver for a collection of entities
9
9
  * Returns a resolver that resolves the entities based on the content type UID
10
10
  *
11
- * @param contentType The content type UID of the entities
11
+ * @param contentType - Content type UID of the entities.
12
+ * @returns A resolver taking `{ parent, args, context, info }` that applies Strapi's filters, sort and
13
+ * pagination and returns an entity response collection.
12
14
  */
13
15
  export declare const createEntityCollectionResolver: <ContentType extends Object>(contentType: string) => (resolveArgs: ResolveArgs<ContentType>) => Promise<EntityResponseCollection<ContentType>>;
14
16
  /**
15
17
  * Create a resolver for a collection of entities related to a parent entity
16
18
  * Returns a resolver that resolves the related entities based on the relation field name
17
19
  *
18
- * @param contentType The content type UID of the related entities
20
+ * @param contentType - Content type UID of the **parent** (owner of the relation).
21
+ * @deprecated Prefer {@link FieldExtensionBuilder.relation}, which also keeps the `find` permission scope
22
+ * and works on Strapi v5.
19
23
  */
20
24
  export declare function createEntityRelationCollectionResolver<Parent extends object>(contentType: string): <K extends keyof Parent>(relationFieldName: K, resolveArgs: ResolveArgs<Parent>) => Promise<EntityResponseCollection<ExtractRelation<Parent, K>>>;
21
25
  type EntityResponseCollection<T extends Object> = {
@@ -26,6 +30,13 @@ type EntityResponseCollection<T extends Object> = {
26
30
  };
27
31
  };
28
32
  type ExtractRelation<Parent, K extends keyof Parent> = Parent[K] extends Array<infer U> ? U extends object ? U : never : Parent[K] extends object ? Parent[K] : never;
33
+ /**
34
+ * Wrap entities in Strapi's response collection (`{ nodes, info }`), the return shape of
35
+ * `XEntityResponseCollection` fields.
36
+ *
37
+ * @param args - The (transformed) field arguments, used for pagination info.
38
+ * @param resourceUID - Content type UID of the items.
39
+ */
29
40
  export declare function toEntityResponseCollection<T extends Object>(items: T[], { args, resourceUID }: {
30
41
  args: Record<string, any>;
31
42
  resourceUID: string;
@@ -10,7 +10,9 @@ exports.toEntityResponseCollection = exports.createEntityRelationCollectionResol
10
10
  * Create a resolver for a collection of entities
11
11
  * Returns a resolver that resolves the entities based on the content type UID
12
12
  *
13
- * @param contentType The content type UID of the entities
13
+ * @param contentType - Content type UID of the entities.
14
+ * @returns A resolver taking `{ parent, args, context, info }` that applies Strapi's filters, sort and
15
+ * pagination and returns an entity response collection.
14
16
  */
15
17
  const createEntityCollectionResolver = (contentType) => {
16
18
  const { transformArgs } = strapi.plugin('graphql').service('builders').utils;
@@ -36,7 +38,9 @@ exports.createEntityCollectionResolver = createEntityCollectionResolver;
36
38
  * Create a resolver for a collection of entities related to a parent entity
37
39
  * Returns a resolver that resolves the related entities based on the relation field name
38
40
  *
39
- * @param contentType The content type UID of the related entities
41
+ * @param contentType - Content type UID of the **parent** (owner of the relation).
42
+ * @deprecated Prefer {@link FieldExtensionBuilder.relation}, which also keeps the `find` permission scope
43
+ * and works on Strapi v5.
40
44
  */
41
45
  function createEntityRelationCollectionResolver(contentType) {
42
46
  return async (relationFieldName, resolveArgs) => {
@@ -52,6 +56,13 @@ function createEntityRelationCollectionResolver(contentType) {
52
56
  };
53
57
  }
54
58
  exports.createEntityRelationCollectionResolver = createEntityRelationCollectionResolver;
59
+ /**
60
+ * Wrap entities in Strapi's response collection (`{ nodes, info }`), the return shape of
61
+ * `XEntityResponseCollection` fields.
62
+ *
63
+ * @param args - The (transformed) field arguments, used for pagination info.
64
+ * @param resourceUID - Content type UID of the items.
65
+ */
55
66
  async function toEntityResponseCollection(items, { args, resourceUID }) {
56
67
  const { toEntityResponseCollection } = strapi.plugin('graphql').service('format').returnTypes;
57
68
  return toEntityResponseCollection(items, {
@@ -14,11 +14,20 @@ type BuilderState<Type = any, TArgs = any, TOutput = any> = {
14
14
  built: boolean;
15
15
  };
16
16
  /**
17
- * Creates a fluent, deeply immutable builder to extend a root field in the GraphQL schema.
18
- * Each method returns a new builder instance with its own state.
17
+ * Create a field builder: the recommended way to add or replace a GraphQL field.
19
18
  *
20
- * @param rootType - Either 'Query' or 'Mutation' or any other Type to extend.
21
- * @param _state
19
+ * See {@link FieldExtensionBuilder} for all methods and the execution order.
20
+ *
21
+ * @param rootType - Shorthand for `.rootType(...)`. Prefer calling `.rootType()` so presets can be applied
22
+ * before the type is known.
23
+ * @param _state - Internal; do not pass.
24
+ *
25
+ * @example
26
+ * export default createGraphQLFieldBuilder()
27
+ * .rootType('Customer')
28
+ * .fieldName('fullName')
29
+ * .outputType('String')
30
+ * .resolver((customer) => [customer?.firstName, customer?.lastName].filter(Boolean).join(' '))
22
31
  */
23
32
  export declare const createGraphQLFieldBuilder: <Type = any, TArgs = any, TOutput = Type>(rootType?: string, _state?: BuilderState<Type, TArgs, TOutput>) => FieldExtensionBuilder<Type, TArgs, TOutput>;
24
33
  export {};
@@ -1,13 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createGraphQLFieldBuilder = void 0;
3
+ exports.resetBuiltFields = exports.createGraphQLFieldBuilder = void 0;
4
4
  const nexus_1 = require("nexus");
5
5
  const utils_1 = require("@strapi/utils");
6
6
  const immer_1 = require("immer");
7
7
  const zod_1 = require("zod");
8
8
  const graphql_extension_1 = require("../graphql-extension");
9
9
  const helpers_1 = require("./helpers");
10
- const registry_1 = require("../registry");
10
+ const resolve_field_1 = require("../registry/resolve-field");
11
+ const field_authorize_plugin_1 = require("../../nexus/plugins/field-authorize.plugin");
12
+ const strapi_compat_1 = require("../strapi-compat");
13
+ const loaders_1 = require("../loaders");
11
14
  /**
12
15
  * Helper to update builder state immutably using Immer.
13
16
  */
@@ -20,15 +23,6 @@ function updateState(state, updater) {
20
23
  function composeMiddlewares(resolver, middlewares) {
21
24
  return middlewares.reduceRight((next, mw) => mw(next), resolver);
22
25
  }
23
- /**
24
- * Compose all authorize functions into a single function.
25
- */
26
- async function runAuthorizes(authorizes, parent, args, ctx, info) {
27
- if (!authorizes.length)
28
- return true;
29
- const results = await Promise.all(authorizes.map(fn => fn(parent, args, ctx, info)));
30
- return results.every(Boolean);
31
- }
32
26
  /**
33
27
  * Wrap resolver with Zod validation if schema is provided.
34
28
  */
@@ -101,11 +95,20 @@ async function runHooks(hooks, when, ...args) {
101
95
  }
102
96
  }
103
97
  /**
104
- * Creates a fluent, deeply immutable builder to extend a root field in the GraphQL schema.
105
- * Each method returns a new builder instance with its own state.
98
+ * Create a field builder: the recommended way to add or replace a GraphQL field.
99
+ *
100
+ * See {@link FieldExtensionBuilder} for all methods and the execution order.
106
101
  *
107
- * @param rootType - Either 'Query' or 'Mutation' or any other Type to extend.
108
- * @param _state
102
+ * @param rootType - Shorthand for `.rootType(...)`. Prefer calling `.rootType()` so presets can be applied
103
+ * before the type is known.
104
+ * @param _state - Internal; do not pass.
105
+ *
106
+ * @example
107
+ * export default createGraphQLFieldBuilder()
108
+ * .rootType('Customer')
109
+ * .fieldName('fullName')
110
+ * .outputType('String')
111
+ * .resolver((customer) => [customer?.firstName, customer?.lastName].filter(Boolean).join(' '))
109
112
  */
110
113
  const createGraphQLFieldBuilder = (rootType, _state) => {
111
114
  const initialState = _state !== null && _state !== void 0 ? _state : {
@@ -134,9 +137,6 @@ const createGraphQLFieldBuilder = (rootType, _state) => {
134
137
  }
135
138
  }
136
139
  const builder = {
137
- /**
138
- * Set the root type to extend (e.g., 'Query', 'Mutation').
139
- */
140
140
  rootType(type) {
141
141
  ensureNotBuilt(initialState);
142
142
  if (initialState.options.rootType) {
@@ -146,9 +146,6 @@ const createGraphQLFieldBuilder = (rootType, _state) => {
146
146
  draft.options.rootType = type;
147
147
  }));
148
148
  },
149
- /**
150
- * Set the name of the field to add/extend.
151
- */
152
149
  fieldName(name) {
153
150
  ensureNotBuilt(initialState);
154
151
  if (initialState.options.fieldName) {
@@ -158,6 +155,19 @@ const createGraphQLFieldBuilder = (rootType, _state) => {
158
155
  draft.options.fieldName = name;
159
156
  }));
160
157
  },
158
+ relation(contentType, attribute, relationOptions = {}) {
159
+ ensureNotBuilt(initialState);
160
+ if (initialState.options.relation) {
161
+ throw new Error('Relation has already been set.');
162
+ }
163
+ if (initialState.options.fieldName && initialState.options.fieldName !== attribute) {
164
+ throw new Error(`Field name "${initialState.options.fieldName}" does not match relation attribute "${attribute}".`);
165
+ }
166
+ return (0, exports.createGraphQLFieldBuilder)(undefined, updateState(initialState, (draft) => {
167
+ draft.options.fieldName = attribute;
168
+ draft.options.relation = { contentType, attribute, batch: Boolean(relationOptions.batch) };
169
+ }));
170
+ },
161
171
  description(text) {
162
172
  ensureNotBuilt(initialState);
163
173
  return (0, exports.createGraphQLFieldBuilder)(undefined, updateState(initialState, (draft) => {
@@ -170,18 +180,12 @@ const createGraphQLFieldBuilder = (rootType, _state) => {
170
180
  draft.options.deprecationReason = reason;
171
181
  }));
172
182
  },
173
- /**
174
- * Register a function to be called when the extension is initialized.
175
- */
176
183
  init(fn) {
177
184
  ensureNotBuilt(initialState);
178
185
  return (0, exports.createGraphQLFieldBuilder)(undefined, updateState(initialState, (draft) => {
179
186
  draft.initFunctions.push(fn);
180
187
  }));
181
188
  },
182
- /**
183
- * Set the resolver function for the field.
184
- */
185
189
  resolver(fn) {
186
190
  ensureNotBuilt(initialState);
187
191
  if (initialState.options.resolver) {
@@ -191,19 +195,12 @@ const createGraphQLFieldBuilder = (rootType, _state) => {
191
195
  draft.options.resolver = fn;
192
196
  }));
193
197
  },
194
- /**
195
- * Add a middleware function for the field.
196
- */
197
198
  middleware(middleware) {
198
199
  ensureNotBuilt(initialState);
199
200
  return (0, exports.createGraphQLFieldBuilder)(undefined, updateState(initialState, (draft) => {
200
201
  draft.middlewareFunctions.push(middleware);
201
202
  }));
202
203
  },
203
- /**
204
- * Set resolver config (policies, middlewares, etc).
205
- * Only supported for Query and Mutation root types.
206
- */
207
204
  resolverConfig(config) {
208
205
  ensureNotBuilt(initialState);
209
206
  const resolverConfig = typeof config === 'function' ? config() : config;
@@ -223,22 +220,16 @@ const createGraphQLFieldBuilder = (rootType, _state) => {
223
220
  })
224
221
  };
225
222
  if (resolverConfig.auth !== undefined) {
226
- draft.options.auth = resolverConfig.auth;
223
+ draft.options.auth = resolverConfig.auth !== false;
227
224
  }
228
225
  }));
229
226
  },
230
- /**
231
- * Add SDL type definitions for the field.
232
- */
233
227
  typeDefs(sdl) {
234
228
  ensureNotBuilt(initialState);
235
229
  return (0, exports.createGraphQLFieldBuilder)(undefined, updateState(initialState, (draft) => {
236
230
  draft.options.typeDefs = `${draft.options.typeDefs}\n${sdl}`;
237
231
  }));
238
232
  },
239
- /**
240
- * Add additional Nexus types if needed.
241
- */
242
233
  types(typesFn) {
243
234
  ensureNotBuilt(initialState);
244
235
  const fn = typeof typesFn === 'function' ? typesFn : () => typesFn;
@@ -246,9 +237,6 @@ const createGraphQLFieldBuilder = (rootType, _state) => {
246
237
  draft.typeFunctions.push(fn);
247
238
  }));
248
239
  },
249
- /**
250
- * Set the arguments for the field.
251
- */
252
240
  args(a) {
253
241
  ensureNotBuilt(initialState);
254
242
  return (0, exports.createGraphQLFieldBuilder)(undefined, updateState(initialState, (draft) => {
@@ -261,18 +249,12 @@ const createGraphQLFieldBuilder = (rootType, _state) => {
261
249
  draft.options.validateArgsSchema = schema;
262
250
  }));
263
251
  },
264
- /**
265
- * Set the output type for the field.
266
- */
267
252
  outputType(type) {
268
253
  ensureNotBuilt(initialState);
269
254
  return (0, exports.createGraphQLFieldBuilder)(undefined, updateState(initialState, (draft) => {
270
255
  draft.options.outputType = type;
271
256
  }));
272
257
  },
273
- /**
274
- * Make authentication mandatory/optional for the field.
275
- */
276
258
  withAuth(auth) {
277
259
  ensureNotBuilt(initialState);
278
260
  return (0, exports.createGraphQLFieldBuilder)(undefined, updateState(initialState, (draft) => {
@@ -283,9 +265,6 @@ const createGraphQLFieldBuilder = (rootType, _state) => {
283
265
  };
284
266
  }));
285
267
  },
286
- /**
287
- * Add additional authorization logic to the field.
288
- */
289
268
  authorize(authorizeFn) {
290
269
  ensureNotBuilt(initialState);
291
270
  return (0, exports.createGraphQLFieldBuilder)(undefined, updateState(initialState, (draft) => {
@@ -305,7 +284,6 @@ const createGraphQLFieldBuilder = (rootType, _state) => {
305
284
  }));
306
285
  },
307
286
  use(preset) {
308
- // Just call the preset with this builder and return the result
309
287
  return preset(this);
310
288
  },
311
289
  hook(hook) {
@@ -323,10 +301,10 @@ const createGraphQLFieldBuilder = (rootType, _state) => {
323
301
  if (!options.fieldName) {
324
302
  throw new utils_1.errors.ApplicationError('Field name must be provided via fieldName()');
325
303
  }
326
- if (!options.outputType) {
304
+ if (!options.outputType && !options.relation) {
327
305
  throw new utils_1.errors.ApplicationError(`Output type must be provided for field "${options.fieldName}" via outputType()`);
328
306
  }
329
- if (!options.resolver) {
307
+ if (!options.resolver && !options.relation) {
330
308
  throw new utils_1.errors.ApplicationError(`Resolver function must be provided for field "${options.fieldName}" via resolver()`);
331
309
  }
332
310
  const isRootType = ['Query', 'Mutation'].includes(options.rootType);
@@ -350,25 +328,23 @@ const createGraphQLFieldBuilder = (rootType, _state) => {
350
328
  },
351
329
  test({ parent, args, context, info, skipMiddleware = false, skipAuth = false } = {}) {
352
330
  this.validate();
353
- const { options, middlewareFunctions, authorizeFunctions } = initialState;
354
- // Compose middleware chain
355
- let resolver = options.resolver;
356
- if (!skipMiddleware && middlewareFunctions.length > 0) {
357
- resolver = middlewareFunctions.reduceRight((next, mw) => mw(next), resolver);
331
+ const { options } = initialState;
332
+ if (!options.resolver) {
333
+ throw new utils_1.errors.ApplicationError(`test() needs a resolver; relation fields without a custom resolver need a running Strapi instance`);
358
334
  }
359
- // Compose auth chain
360
- const runAuth = async () => {
361
- if (skipAuth || authorizeFunctions.length === 0)
362
- return true;
363
- const results = await Promise.all(authorizeFunctions.map(fn => fn(parent, args, context, info)));
364
- return results.every(Boolean);
365
- };
335
+ // Same pipeline as in production: globals, withAuth, authorize, middlewares, validation, tracing, hooks
336
+ const pipeline = (0, resolve_field_1.resolveFieldPipeline)(options.rootType, options.fieldName, fieldExtensions(initialState));
337
+ const testInfo = (info !== null && info !== void 0 ? info : {
338
+ fieldName: options.fieldName,
339
+ parentType: { name: options.rootType },
340
+ });
341
+ const resolver = composeResolver(initialState, options.resolver);
342
+ const withMiddlewares = skipMiddleware ? resolver : composeMiddlewares(resolver, pipeline.middlewares);
366
343
  return (async () => {
367
- const isAuthorized = await runAuth();
368
- if (!isAuthorized) {
369
- throw new Error('Not authorized');
344
+ if (!skipAuth) {
345
+ await (0, field_authorize_plugin_1.runAuthorization)(pipeline, parent, args, context, testInfo);
370
346
  }
371
- return resolver(parent, args, context, info);
347
+ return withMiddlewares(parent, args, context, testInfo);
372
348
  })();
373
349
  },
374
350
  trace(handler) {
@@ -381,59 +357,42 @@ const createGraphQLFieldBuilder = (rootType, _state) => {
381
357
  draft.options.traceHandlers.push(handler !== null && handler !== void 0 ? handler : defaultTraceHandler);
382
358
  }));
383
359
  },
384
- /**
385
- * Build the extension for registration.
386
- */
387
360
  build() {
388
- var _a;
389
361
  ensureNotBuilt(initialState);
390
362
  this.validate();
391
- const { options, initFunctions, typeFunctions, middlewareFunctions, authorizeFunctions, hooks } = initialState;
363
+ const { options, initFunctions, typeFunctions } = initialState;
364
+ registerBuiltField(options.rootType, options.fieldName);
392
365
  const isRootType = ['Query', 'Mutation'].includes(options.rootType);
393
- const fieldInfo = {
394
- type: options.rootType,
395
- fieldName: options.fieldName,
396
- tags: options.tags || []
397
- };
398
- // Compose all middleware and authorize functions
399
- const allMiddlewares = [
400
- ...(0, registry_1.getGlobalMiddlewares)(fieldInfo),
401
- ...middlewareFunctions
402
- ];
403
- const allAuthorizes = [
404
- ...(0, registry_1.getGlobalAuthorizes)(fieldInfo),
405
- ...authorizeFunctions
406
- ];
407
- // Compose resolver pipeline
408
- let resolver = options.resolver;
409
- resolver = withArgValidation(resolver, options.validateArgsSchema);
410
- resolver = composeMiddlewares(resolver, allMiddlewares);
411
- resolver = withTracing(resolver, (_a = options.traceHandlers) !== null && _a !== void 0 ? _a : []);
366
+ // Authorization and middlewares (own + global + protectFields) are applied by the package's Nexus
367
+ // plugins when the schema is built, so they are not composed into the resolver here.
368
+ const extensions = fieldExtensions(initialState);
412
369
  const nexusExtension = (0, nexus_1.extendType)({
413
370
  type: options.rootType,
414
371
  definition(t) {
372
+ var _a, _b;
415
373
  const nexusArgs = options.args ? (0, helpers_1.parseArgs)(options.args) : {};
374
+ if (options.relation) {
375
+ const { contentType, attribute } = options.relation;
376
+ const customResolver = (_a = options.resolver) !== null && _a !== void 0 ? _a : (options.relation.batch ? batchedRelationResolver(options.relation) : undefined);
377
+ for (const spec of (0, strapi_compat_1.relationFieldSpecs)(contentType, attribute, customResolver)) {
378
+ t.field(spec.name, {
379
+ type: spec.type,
380
+ args: { ...((_b = spec.args) !== null && _b !== void 0 ? _b : {}), ...nexusArgs },
381
+ description: options.description,
382
+ deprecation: options.deprecationReason,
383
+ extensions: extensions,
384
+ resolve: composeResolver(initialState, spec.resolve)
385
+ });
386
+ }
387
+ return;
388
+ }
416
389
  t.field(options.fieldName, {
417
390
  type: options.outputType,
418
391
  args: nexusArgs,
419
392
  description: options.description,
420
393
  deprecation: options.deprecationReason,
421
- extensions: {
422
- middlewares: [
423
- ...(options.auth ? [(0, helpers_1.makeAuthMiddleware)()] : []),
424
- ...allMiddlewares
425
- ],
426
- // @ts-ignore
427
- authorize: async (parent, args, ctx, info) => {
428
- return runAuthorizes(allAuthorizes, parent, args, ctx, info);
429
- },
430
- },
431
- async resolve(parent, args, context, info) {
432
- await runHooks(hooks, 'beforeResolve', parent, args, context, info);
433
- const result = await resolver(parent, args, context, info);
434
- await runHooks(hooks, 'afterResolve', result, parent, args, context, info);
435
- return result;
436
- }
394
+ extensions: extensions,
395
+ resolve: composeResolver(initialState, options.resolver)
437
396
  });
438
397
  }
439
398
  });
@@ -448,19 +407,102 @@ const createGraphQLFieldBuilder = (rootType, _state) => {
448
407
  nexusExtension,
449
408
  ...typeFunctions.flatMap((fn) => fn())
450
409
  ],
451
- resolversConfig: isRootType
452
- ? () => ({
453
- [options.rootType]: {
454
- [options.fieldName]: options.resolverConfig || {}
455
- }
456
- })
457
- : undefined
410
+ resolversConfig: () => ({
411
+ // Keep Strapi's `<target>.find` permission scope on (re)defined relation fields
412
+ ...(options.relation
413
+ ? (0, strapi_compat_1.relationResolversConfig)(options.relation.contentType, options.relation.attribute)
414
+ : {}),
415
+ ...(isRootType
416
+ ? { [options.rootType]: { [options.fieldName]: options.resolverConfig || {} } }
417
+ : {})
418
+ })
458
419
  }));
459
420
  }
460
421
  };
422
+ // The implementation is untyped internally; the public generics are enforced by the interface
461
423
  return Object.freeze(builder);
462
424
  };
463
425
  exports.createGraphQLFieldBuilder = createGraphQLFieldBuilder;
426
+ const builtFields = new Set();
427
+ /**
428
+ * Warn when two builders define the same `Type.field`: only one of them ends up in the schema, which is
429
+ * almost always a mistake (e.g. a copied file). Replacing a field Strapi generates is not reported.
430
+ */
431
+ function registerBuiltField(type, fieldName) {
432
+ var _a, _b, _c;
433
+ const key = `${type}.${fieldName}`;
434
+ if (builtFields.has(key)) {
435
+ const warn = (_c = (_b = (_a = globalThis.strapi) === null || _a === void 0 ? void 0 : _a.log) === null || _b === void 0 ? void 0 : _b.warn) !== null && _c !== void 0 ? _c : console.warn;
436
+ warn(`[buro26-strapi-graphql] Field "${key}" is defined by more than one field builder; only one definition is used.`);
437
+ }
438
+ builtFields.add(key);
439
+ }
440
+ /** @internal Only for tests. */
441
+ function resetBuiltFields() {
442
+ builtFields.clear();
443
+ }
444
+ exports.resetBuiltFields = resetBuiltFields;
445
+ /**
446
+ * Field extensions read by the Nexus authorize/middleware plugins.
447
+ */
448
+ function fieldExtensions(state) {
449
+ var _a;
450
+ const { options, authorizeFunctions, middlewareFunctions } = state;
451
+ return {
452
+ ...(authorizeFunctions.length > 0 && {
453
+ authorize: async (parent, args, ctx, info) => {
454
+ for (const authorize of authorizeFunctions) {
455
+ const result = await authorize(parent, args, ctx, info);
456
+ if (result !== true)
457
+ return result instanceof Error ? result : false;
458
+ }
459
+ return true;
460
+ }
461
+ }),
462
+ middlewares: [...middlewareFunctions],
463
+ builder: {
464
+ tags: [...((_a = options.tags) !== null && _a !== void 0 ? _a : [])],
465
+ requireAuth: Boolean(options.auth)
466
+ }
467
+ };
468
+ }
469
+ /**
470
+ * The resolver as executed inside the middlewares: argument validation, hooks and tracing.
471
+ */
472
+ function composeResolver(state, baseResolver) {
473
+ var _a;
474
+ const { options, hooks } = state;
475
+ const validated = withArgValidation(baseResolver, options.validateArgsSchema);
476
+ const withHooks = async (parent, args, context, info) => {
477
+ await runHooks(hooks, 'beforeResolve', parent, args, context, info);
478
+ const result = await validated(parent, args, context, info);
479
+ await runHooks(hooks, 'afterResolve', result, parent, args, context, info);
480
+ return result;
481
+ };
482
+ return withTracing(withHooks, (_a = options.traceHandlers) !== null && _a !== void 0 ? _a : []);
483
+ }
484
+ /**
485
+ * Resolver for `.relation(..., { batch: true })`: loads the related entity of all parents in the request
486
+ * with one query, sanitizes it like Strapi does and returns the shape the relation field expects.
487
+ */
488
+ function batchedRelationResolver({ contentType, attribute }) {
489
+ var _a, _b;
490
+ const attr = (_b = (_a = strapi.getModel(contentType)) === null || _a === void 0 ? void 0 : _a.attributes) === null || _b === void 0 ? void 0 : _b[attribute];
491
+ if (!attr || attr.type !== 'relation') {
492
+ throw new utils_1.errors.ApplicationError(`"${contentType}.${attribute}" is not a relation`);
493
+ }
494
+ if (String(attr.relation).endsWith('Many')) {
495
+ throw new utils_1.errors.ApplicationError(`relation(..., { batch: true }) is only supported for to-one relations ("${contentType}.${attribute}" is to-many)`);
496
+ }
497
+ // Returns Strapi's response wrapper (`{ value, info }`), like `buildAssociationResolver`; the relation
498
+ // field spec unwraps it on Strapi v5.
499
+ const { toEntityResponse } = strapi.plugin('graphql').service('format').returnTypes;
500
+ return async (parent, _args, context) => {
501
+ const related = (parent === null || parent === void 0 ? void 0 : parent.id) ? await (0, loaders_1.loaders)(context).relation(contentType, attribute).load(parent.id) : null;
502
+ const sanitized = related ? await (0, strapi_compat_1.sanitizeOutput)(related, attr.target, context) : null;
503
+ return toEntityResponse(sanitized, { args: {}, resourceUID: attr.target });
504
+ };
505
+ }
464
506
  function defaultTraceHandler({ type, fieldName, durationMs, args, error }) {
465
507
  // Colors
466
508
  const green = '\x1b[32m';
@@ -1,19 +1,34 @@
1
1
  import type { FieldExtensionBuilder, FieldGroupPreset, GraphQLFieldPreset } from './types';
2
2
  import type { Middleware } from '../../nexus';
3
3
  /**
4
- * Parse argument definitions into Nexus-compatible args.
4
+ * Middleware that throws a `ForbiddenError` ("Not authenticated") when there is no `ctx.state.user`.
5
+ *
6
+ * @deprecated Use `.withAuth(true)` on the field builder; it is checked before authorization runs.
5
7
  */
6
- export declare function parseArgs(args: Record<string, string | unknown>): Record<string, unknown>;
8
+ export declare function makeAuthMiddleware(): Middleware;
7
9
  /**
8
- * Middleware that throws if the user is not authenticated.
10
+ * Define a reusable preset for `.use()`. Identity function that exists for typing and readability.
11
+ *
12
+ * @example
13
+ * export const adminOnly = createGraphQLFieldPreset((builder) =>
14
+ * builder.withAuth(true).authorize((_p, _a, ctx) => ctx.state.user?.role?.type === 'admin')
15
+ * )
16
+ *
17
+ * createGraphQLFieldBuilder().use(adminOnly).rootType('Query') // ...
9
18
  */
10
- export declare function makeAuthMiddleware(): Middleware;
11
19
  export declare function createGraphQLFieldPreset(preset: GraphQLFieldPreset): GraphQLFieldPreset;
12
20
  /**
13
- * Combine multiple field builders into a single GraphQL extension.
21
+ * Export several fields from one file. Builds each builder and returns the extensions.
22
+ *
23
+ * @example
24
+ * export default registerFields(fieldA, fieldB)
14
25
  */
15
26
  export declare function registerFields(...fields: FieldExtensionBuilder[]): import("../..").GraphQLExtension[];
16
27
  /**
17
- * Apply one or more presets to a group of fields.
28
+ * Apply presets to every field in a group (e.g. one auth rule for all fields of a feature).
29
+ * Presets are applied in order. Returns unbuilt builders; export them with {@link registerFields}.
30
+ *
31
+ * @example
32
+ * export default registerFields(...createFieldGroup([fieldA, fieldB], requireUser))
18
33
  */
19
34
  export declare function createFieldGroup(fields: FieldExtensionBuilder[], ...presets: FieldGroupPreset[]): FieldExtensionBuilder[];