buro26-strapi-graphql 0.0.26 → 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 (57) hide show
  1. package/README.md +458 -439
  2. package/dist/server/index.d.ts +14 -8
  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 +36 -0
  7. package/dist/server/nexus/plugins/field-authorize.plugin.js +122 -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 +25 -0
  11. package/dist/server/nexus/plugins/middleware.plugin.js +63 -0
  12. package/dist/server/register.js +6 -7
  13. package/dist/server/services/file-service.d.ts +7 -6
  14. package/dist/server/services/file-service.js +36 -30
  15. package/dist/server/services/index.d.ts +1 -1
  16. package/dist/server/types.d.ts +28 -13
  17. package/dist/server/utils/collection-type.d.ts +13 -2
  18. package/dist/server/utils/collection-type.js +13 -2
  19. package/dist/server/utils/field-builder/builder.d.ts +33 -0
  20. package/dist/server/utils/field-builder/builder.js +552 -0
  21. package/dist/server/utils/field-builder/helpers.d.ts +34 -0
  22. package/dist/server/utils/field-builder/helpers.js +78 -0
  23. package/dist/server/utils/field-builder/index.d.ts +3 -0
  24. package/dist/server/utils/field-builder/index.js +19 -0
  25. package/dist/server/utils/field-builder/types.d.ts +371 -0
  26. package/dist/server/utils/field-builder/types.js +2 -0
  27. package/dist/server/utils/graphql-extension.d.ts +29 -3
  28. package/dist/server/utils/graphql-extension.js +94 -26
  29. package/dist/server/utils/index.d.ts +3 -1
  30. package/dist/server/utils/index.js +12 -1
  31. package/dist/server/utils/loaders.d.ts +94 -0
  32. package/dist/server/utils/loaders.js +166 -0
  33. package/dist/server/utils/misc.d.ts +6 -0
  34. package/dist/server/utils/misc.js +6 -0
  35. package/dist/server/utils/override.d.ts +34 -1
  36. package/dist/server/utils/override.js +54 -26
  37. package/dist/server/utils/registry/field-protection.d.ts +43 -0
  38. package/dist/server/utils/registry/field-protection.js +65 -0
  39. package/dist/server/utils/registry/global-registry.d.ts +63 -0
  40. package/dist/server/utils/registry/global-registry.js +56 -0
  41. package/dist/server/utils/registry/index.d.ts +3 -0
  42. package/dist/server/utils/registry/index.js +19 -0
  43. package/dist/server/utils/registry/resolve-field.d.ts +1 -0
  44. package/dist/server/utils/registry/resolve-field.js +67 -0
  45. package/dist/server/utils/shadow-crud.d.ts +28 -1
  46. package/dist/server/utils/shadow-crud.js +32 -1
  47. package/dist/server/utils/single-type.d.ts +30 -1
  48. package/dist/server/utils/single-type.js +25 -0
  49. package/dist/server/utils/strapi-compat.d.ts +85 -0
  50. package/dist/server/utils/strapi-compat.js +194 -0
  51. package/dist/server/utils/types.d.ts +25 -1
  52. package/dist/tsconfig.server.tsbuildinfo +1 -1
  53. package/package.json +7 -4
  54. package/dist/server/utils/field-builder.d.ts +0 -31
  55. package/dist/server/utils/field-builder.js +0 -113
  56. package/dist/server/utils/middleware.d.ts +0 -17
  57. package/dist/server/utils/middleware.js +0 -77
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createFieldGroup = exports.registerFields = exports.createGraphQLFieldPreset = exports.makeAuthMiddleware = exports.parseArgs = void 0;
4
+ const nexus_1 = require("nexus");
5
+ const utils_1 = require("@strapi/utils");
6
+ /**
7
+ * Convert an args map with type strings (`'String!'`, `'[ID]'`) into Nexus args.
8
+ * Non-string values (e.g. `arg({ type: 'X' })`) are passed through unchanged.
9
+ *
10
+ * @internal Used by `.args()`.
11
+ */
12
+ function parseArgs(args) {
13
+ const nexusArgs = {};
14
+ for (const [argName, argType] of Object.entries(args)) {
15
+ if (typeof argType !== 'string') {
16
+ nexusArgs[argName] = argType;
17
+ continue;
18
+ }
19
+ const isNonNull = argType.endsWith('!');
20
+ const baseType = isNonNull ? argType.slice(0, -1) : argType;
21
+ nexusArgs[argName] = isNonNull
22
+ ? (0, nexus_1.nonNull)((0, nexus_1.arg)({ type: baseType }))
23
+ : (0, nexus_1.arg)({ type: baseType });
24
+ }
25
+ return nexusArgs;
26
+ }
27
+ exports.parseArgs = parseArgs;
28
+ /**
29
+ * Middleware that throws a `ForbiddenError` ("Not authenticated") when there is no `ctx.state.user`.
30
+ *
31
+ * @deprecated Use `.withAuth(true)` on the field builder; it is checked before authorization runs.
32
+ */
33
+ function makeAuthMiddleware() {
34
+ return (next) => async (root, args, context, info) => {
35
+ var _a;
36
+ // @ts-ignore
37
+ if (!((_a = context.state) === null || _a === void 0 ? void 0 : _a.user)) {
38
+ throw new utils_1.errors.ForbiddenError('Not authenticated');
39
+ }
40
+ return next(root, args, context, info);
41
+ };
42
+ }
43
+ exports.makeAuthMiddleware = makeAuthMiddleware;
44
+ /**
45
+ * Define a reusable preset for `.use()`. Identity function that exists for typing and readability.
46
+ *
47
+ * @example
48
+ * export const adminOnly = createGraphQLFieldPreset((builder) =>
49
+ * builder.withAuth(true).authorize((_p, _a, ctx) => ctx.state.user?.role?.type === 'admin')
50
+ * )
51
+ *
52
+ * createGraphQLFieldBuilder().use(adminOnly).rootType('Query') // ...
53
+ */
54
+ function createGraphQLFieldPreset(preset) {
55
+ return preset;
56
+ }
57
+ exports.createGraphQLFieldPreset = createGraphQLFieldPreset;
58
+ /**
59
+ * Export several fields from one file. Builds each builder and returns the extensions.
60
+ *
61
+ * @example
62
+ * export default registerFields(fieldA, fieldB)
63
+ */
64
+ function registerFields(...fields) {
65
+ return fields.map(builder => builder.build());
66
+ }
67
+ exports.registerFields = registerFields;
68
+ /**
69
+ * Apply presets to every field in a group (e.g. one auth rule for all fields of a feature).
70
+ * Presets are applied in order. Returns unbuilt builders; export them with {@link registerFields}.
71
+ *
72
+ * @example
73
+ * export default registerFields(...createFieldGroup([fieldA, fieldB], requireUser))
74
+ */
75
+ function createFieldGroup(fields, ...presets) {
76
+ return fields.map(field => presets.reduce((acc, preset) => preset(acc), field));
77
+ }
78
+ exports.createFieldGroup = createFieldGroup;
@@ -0,0 +1,3 @@
1
+ export * from './types';
2
+ export * from './helpers';
3
+ export * from './builder';
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./types"), exports);
18
+ __exportStar(require("./helpers"), exports);
19
+ __exportStar(require("./builder"), exports);
@@ -0,0 +1,371 @@
1
+ import type { GraphQLResolveInfo } from 'graphql';
2
+ import type { output, ZodTypeAny } from 'zod';
3
+ import type { ResolverConfigOption, ResolverFunction } from '../../types';
4
+ import type { Middleware } from '../../nexus';
5
+ import type { createGraphQLExtension } from '../graphql-extension';
6
+ import type { ParentOf, ResolveContext } from '../types';
7
+ /**
8
+ * The `parent` value resolvers, authorize functions and middlewares receive.
9
+ *
10
+ * - Untyped builders (`Type` is `any`, e.g. `Query`/`Mutation` fields): `any`.
11
+ * - Entity types with an `id` (e.g. registered via {@link Register} `types`): `Partial<Type>` with `id`
12
+ * required. Strapi always passes the entity, but relations are only present when populated.
13
+ * - Other types: `Partial<Type> | null`.
14
+ */
15
+ export type FieldParent<Type> = 0 extends (1 & Type) ? any : Type extends {
16
+ id: infer Id;
17
+ } ? Partial<Type> & {
18
+ id: Id;
19
+ } : Partial<Type> | null;
20
+ /**
21
+ * TypeScript types for GraphQL scalar names used in `.args()` type strings.
22
+ * Unknown names (custom scalars, input types) map to `unknown`.
23
+ */
24
+ export type GraphQLScalarTypes = {
25
+ String: string;
26
+ ID: string | number;
27
+ Int: number;
28
+ Float: number;
29
+ Long: number;
30
+ Boolean: boolean;
31
+ Date: string;
32
+ DateTime: string;
33
+ Time: string;
34
+ JSON: unknown;
35
+ };
36
+ type ParseNonNullArg<S extends string> = S extends `[${infer Item}]` ? Array<ParseArgType<Item>> : S extends keyof GraphQLScalarTypes ? GraphQLScalarTypes[S] : unknown;
37
+ /** TypeScript type for a GraphQL type string such as `'String!'`, `'[ID!]'` or `'Int'`. */
38
+ export type ParseArgType<S extends string> = S extends `${infer Inner}!` ? ParseNonNullArg<Inner> : ParseNonNullArg<S> | null;
39
+ type RequiredArgKeys<A> = {
40
+ [K in keyof A]: A[K] extends `${string}!` ? K : never;
41
+ }[keyof A];
42
+ /**
43
+ * Argument types for an `.args()` map, for typing a builder explicitly. Non-null (`!`) arguments are
44
+ * required; others optional. Non-string definitions (e.g. Nexus `arg()`) are `unknown`.
45
+ *
46
+ * @example
47
+ * type A = InferArgs<{ id: 'ID!'; limit: 'Int' }> // { id: string | number; limit?: number | null }
48
+ */
49
+ export type InferArgs<A> = {
50
+ [K in Extract<keyof A, RequiredArgKeys<A>>]: A[K] extends string ? ParseArgType<A[K]> : unknown;
51
+ } & {
52
+ [K in Exclude<keyof A, RequiredArgKeys<A>>]?: A[K] extends string ? ParseArgType<A[K]> : unknown;
53
+ };
54
+ /**
55
+ * Information passed to a trace handler after a field resolved (or failed).
56
+ *
57
+ * @see {@link FieldExtensionBuilder.trace}
58
+ */
59
+ export type FieldTraceInfo = {
60
+ /** GraphQL parent type name, e.g. `Query` */
61
+ type: string;
62
+ fieldName: string;
63
+ /** Wall-clock duration of the resolver, in milliseconds */
64
+ durationMs: number;
65
+ args: any;
66
+ context: any;
67
+ /** The resolved value; `undefined` when the resolver threw */
68
+ result: any;
69
+ /** The thrown error, if any */
70
+ error?: any;
71
+ };
72
+ /**
73
+ * Handler that receives {@link FieldTraceInfo}. Runs asynchronously after the resolver and never affects
74
+ * the response; errors thrown by a handler are logged and swallowed.
75
+ */
76
+ export type FieldTraceHandler = (info: FieldTraceInfo) => void;
77
+ /**
78
+ * Relation configuration set by {@link FieldExtensionBuilder.relation}.
79
+ */
80
+ export type RelationOptions = {
81
+ /** Content type UID that owns the relation, e.g. `api::order.order` */
82
+ contentType: string;
83
+ /** Relation attribute name on that content type, e.g. `customer` */
84
+ attribute: string;
85
+ /**
86
+ * Resolve a to-one relation with the per-request relation loader: one query for all parents in the
87
+ * request instead of one query per parent. Output is sanitized for the content API, like Strapi's own
88
+ * resolver does.
89
+ *
90
+ * Not supported for to-many relations, because those take filter/pagination arguments per parent.
91
+ *
92
+ * @default false
93
+ * @see {@link loaders}
94
+ */
95
+ batch?: boolean;
96
+ };
97
+ /**
98
+ * Fluent, immutable builder for one GraphQL field. Every method returns a **new** builder, so a
99
+ * partially configured builder can be shared as a base for several fields.
100
+ *
101
+ * A field needs at least {@link FieldExtensionBuilder.rootType | rootType},
102
+ * {@link FieldExtensionBuilder.fieldName | fieldName}, {@link FieldExtensionBuilder.outputType | outputType}
103
+ * and {@link FieldExtensionBuilder.resolver | resolver} — or `rootType` plus
104
+ * {@link FieldExtensionBuilder.relation | relation}.
105
+ *
106
+ * Export the builder (or an array via `registerFields`) as the default export of a file in a `graphql`
107
+ * folder; the plugin calls {@link FieldExtensionBuilder.build | build} for you.
108
+ *
109
+ * @typeParam Type - Parent value the resolver receives. Inferred from {@link Register} `types` when
110
+ * `rootType` is a registered type name.
111
+ * @typeParam TArgs - Field arguments.
112
+ * @typeParam TOutput - Resolved value.
113
+ *
114
+ * @example
115
+ * export default createGraphQLFieldBuilder()
116
+ * .rootType('Query')
117
+ * .fieldName('hello')
118
+ * .args({ name: 'String!' })
119
+ * .outputType('String')
120
+ * .withAuth(true)
121
+ * .resolver((_parent, { name }) => `Hello ${name}`)
122
+ *
123
+ * @see https://gitlab.com/buro26/public/strapi/strapi-graphql-extension#-recommended-the-field-builder-pattern
124
+ */
125
+ export interface FieldExtensionBuilder<Type = any, TArgs = any, TOutput = Type> {
126
+ /**
127
+ * Set the GraphQL type the field is added to: `Query`, `Mutation` or any object type such as a
128
+ * content type (`Customer`).
129
+ *
130
+ * When `type` is registered in {@link Register} `types`, the resolver's `parent` is typed accordingly.
131
+ *
132
+ * @param type - GraphQL type name.
133
+ * @throws Error when the root type was already set (e.g. by `createGraphQLFieldBuilder('Query')`).
134
+ */
135
+ rootType<N extends string>(type: N): FieldExtensionBuilder<ParentOf<N, Type>, TArgs, TOutput>;
136
+ /**
137
+ * Define the field as a relation of a Strapi content type. It gets exactly the type, filter/pagination
138
+ * arguments, resolver (query validation, output sanitizing, draft & publish handling) and `<target>.find`
139
+ * permission scope of the field Strapi generates, on Strapi v4 and v5.
140
+ *
141
+ * Sets {@link FieldExtensionBuilder.fieldName | fieldName} to `attribute`; `outputType` and `resolver`
142
+ * are optional (a custom `resolver` replaces Strapi's association resolver). Combine with
143
+ * `.authorize()`, `.middleware()` or presets to protect or extend the relation.
144
+ *
145
+ * | | v4 | v5 |
146
+ * |---|---|---|
147
+ * | to-one | `XEntityResponse` | `X` |
148
+ * | to-many | `XRelationResponseCollection` | `[X]!` and `<attribute>_connection` |
149
+ *
150
+ * On v5 both to-many fields get the same authorization and middleware. Note that the v5 list field is
151
+ * non-null: when `.authorize()` denies it, GraphQL nulls the parent object (standard error propagation).
152
+ *
153
+ * @param contentType - UID of the content type that owns the relation.
154
+ * @param attribute - Name of a regular (non-polymorphic) relation attribute.
155
+ * @param options - See {@link RelationOptions}.
156
+ * @throws Error when a relation was already set, or `fieldName` was set to a different name.
157
+ * @throws ApplicationError at schema build when the attribute is not a relation, or `batch` is used on
158
+ * a to-many relation.
159
+ *
160
+ * @example
161
+ * createGraphQLFieldBuilder()
162
+ * .rootType('Order')
163
+ * .relation('api::order.order', 'customer', { batch: true })
164
+ * .authorize(isOrderCustomer)
165
+ */
166
+ relation(contentType: string, attribute: string, options?: Pick<RelationOptions, 'batch'>): FieldExtensionBuilder<Type, TArgs, TOutput>;
167
+ /**
168
+ * Set the field name. Use the name of an existing field to replace it.
169
+ *
170
+ * @throws Error when the field name was already set (also by `.relation()`).
171
+ */
172
+ fieldName(name: string): FieldExtensionBuilder<Type, TArgs, TOutput>;
173
+ /**
174
+ * Description shown in the GraphQL schema, GraphiQL and IDE hints.
175
+ */
176
+ description(text: string): FieldExtensionBuilder<Type, TArgs, TOutput>;
177
+ /**
178
+ * Mark the field as deprecated in the schema.
179
+ *
180
+ * @param reason - Shown to API consumers; mention the replacement.
181
+ */
182
+ deprecate(reason: string): FieldExtensionBuilder<Type, TArgs, TOutput>;
183
+ /**
184
+ * Run a function once when the plugin registers the field (Strapi's `register` phase), e.g. to
185
+ * configure shadow CRUD. Can be called multiple times; functions run in order.
186
+ */
187
+ init(fn: () => void): FieldExtensionBuilder<Type, TArgs, TOutput>;
188
+ /**
189
+ * Set the resolver. It runs inside the authorization and middlewares, after argument validation and
190
+ * `beforeResolve` hooks.
191
+ *
192
+ * @throws Error when a resolver was already set.
193
+ */
194
+ resolver(fn: ResolverFunction<FieldParent<Type>, TArgs, Partial<TOutput> | null | any>): FieldExtensionBuilder<Type, TArgs, TOutput>;
195
+ /**
196
+ * Strapi resolver config (policies, Strapi middlewares, `auth`). Merged with earlier calls.
197
+ * Only applies to `Query` and `Mutation` fields; for other types use `.authorize()` / `.middleware()`.
198
+ *
199
+ * @deprecated Use {@link FieldExtensionBuilder.withAuth | withAuth} for authentication and
200
+ * {@link FieldExtensionBuilder.authorize | authorize} / {@link FieldExtensionBuilder.middleware | middleware}
201
+ * for the rest; they work on every type and run in a defined order.
202
+ * @see https://docs.strapi.io/dev-docs/plugins/graphql#customization
203
+ */
204
+ resolverConfig(config: (() => ResolverConfigOption<Type, TArgs, TOutput>) | ResolverConfigOption<Type, TArgs, TOutput>): FieldExtensionBuilder<Type, TArgs, TOutput>;
205
+ /**
206
+ * Wrap the resolver. Middlewares receive `next` and return a resolver; call `next` to continue.
207
+ * Field middlewares run after global middlewares and before `protectFields` middlewares.
208
+ *
209
+ * @example
210
+ * .middleware((next) => async (parent, args, ctx, info) => {
211
+ * const result = await next(parent, args, ctx, info)
212
+ * return result ?? fallback
213
+ * })
214
+ */
215
+ middleware(middleware: Middleware<FieldParent<Type>, TArgs>): FieldExtensionBuilder<Type, TArgs, TOutput>;
216
+ /**
217
+ * Add SDL type definitions needed by this field (e.g. its output type). Appended to earlier calls.
218
+ * Prefer {@link FieldExtensionBuilder.types | types} with Nexus for type-safe definitions.
219
+ */
220
+ typeDefs(sdl: string): FieldExtensionBuilder<Type, TArgs, TOutput>;
221
+ /**
222
+ * Add Nexus types needed by this field (object types, input types, enums). Appended to earlier calls.
223
+ */
224
+ types(typesFn: (() => unknown[]) | unknown[]): FieldExtensionBuilder<Type, TArgs, TOutput>;
225
+ /**
226
+ * Set the field arguments. Replaces earlier calls.
227
+ *
228
+ * This does not change the resolver's `args` type. Type them with {@link FieldExtensionBuilder.validateArgs}
229
+ * (inferred from the schema), or explicitly with {@link InferArgs}:
230
+ * `createGraphQLFieldBuilder<unknown, InferArgs<typeof args>>()`.
231
+ *
232
+ * @param a - Map of argument name to a type string (`'String!'`, `'ID'`) or a Nexus `arg()`.
233
+ */
234
+ args(a: Record<string, string | unknown>): FieldExtensionBuilder<Type, TArgs, TOutput>;
235
+ /**
236
+ * Validate (and parse) arguments with Zod before the resolver runs. The resolver receives the parsed
237
+ * value. Invalid arguments throw a Strapi `ValidationError` ("Invalid arguments") with the `ZodError`
238
+ * as `cause`.
239
+ *
240
+ * When the builder has no explicit `TArgs`, the resolver's `args` are typed as the schema's output.
241
+ *
242
+ * @example
243
+ * .args({ id: 'ID!', limit: 'Int' })
244
+ * .validateArgs(z.object({ id: z.coerce.number(), limit: z.number().default(10) }))
245
+ * .resolver((_parent, { id, limit }) => ...) // id: number, limit: number
246
+ */
247
+ validateArgs<S extends ZodTypeAny>(schema: S): FieldExtensionBuilder<Type, 0 extends (1 & TArgs) ? output<S> : TArgs, TOutput>;
248
+ /**
249
+ * Set the GraphQL output type: a type name (`'String'`, `'CustomerEntityResponse'`) or a Nexus wrapper
250
+ * (`nonNull('Boolean')`, `list('Customer')`). Not needed with {@link FieldExtensionBuilder.relation | relation}.
251
+ */
252
+ outputType(type: string | unknown): FieldExtensionBuilder<Type, TArgs, TOutput>;
253
+ /**
254
+ * Require an authenticated user. Checked before any authorization runs; unauthenticated requests get a
255
+ * `ForbiddenError` ("Not authenticated"). For `Query`/`Mutation` fields this also sets Strapi's
256
+ * resolver `auth` option.
257
+ *
258
+ * @default false
259
+ */
260
+ withAuth(auth: boolean): FieldExtensionBuilder<Type, TArgs, TOutput>;
261
+ /**
262
+ * Add an authorization check. All checks (global, field and `protectFields`) must return `true`;
263
+ * return `false` for a generic "Not authorized" or an `Error` to send that error to the client.
264
+ * Checks run in order and stop at the first failure.
265
+ *
266
+ * @example
267
+ * .authorize((parent, _args, ctx) => parent?.user?.id === ctx.state.user?.id)
268
+ */
269
+ authorize(authorizeFn: ResolverFunction<FieldParent<Type>, TArgs, Partial<TOutput> | null | any>): FieldExtensionBuilder<Type, TArgs, TOutput>;
270
+ /**
271
+ * Add a tag. Tags let global middleware/authorization target groups of fields.
272
+ *
273
+ * @see {@link registerGlobalAuthorize}
274
+ * @see {@link registerGlobalMiddleware}
275
+ */
276
+ tag(tag: string): FieldExtensionBuilder<Type, TArgs, TOutput>;
277
+ /**
278
+ * Add several tags at once.
279
+ *
280
+ * @see {@link FieldExtensionBuilder.tag}
281
+ */
282
+ tags(tags: string[]): FieldExtensionBuilder<Type, TArgs, TOutput>;
283
+ /**
284
+ * Apply a preset: a function that takes a builder and returns a configured builder. Use presets for
285
+ * reusable auth, feature flags or tags.
286
+ *
287
+ * @see {@link createGraphQLFieldPreset}
288
+ */
289
+ use(preset: (builder: FieldExtensionBuilder) => FieldExtensionBuilder<Type, TArgs, TOutput>): FieldExtensionBuilder<Type, TArgs, TOutput>;
290
+ /**
291
+ * Add `beforeResolve` / `afterResolve` hooks. They run inside the middlewares, around the resolver;
292
+ * hooks are awaited and can throw to abort the request.
293
+ */
294
+ hook(hook: GraphQLFieldHook<FieldParent<Type>, TArgs>): FieldExtensionBuilder<Type, TArgs, TOutput>;
295
+ /**
296
+ * Check that the configuration is complete. Called by `build()` and `test()`.
297
+ *
298
+ * @throws ApplicationError when `rootType` or `fieldName` is missing, `outputType`/`resolver` is missing
299
+ * on a non-relation field, or resolver config policies/middlewares are used outside `Query`/`Mutation`.
300
+ */
301
+ validate(): void;
302
+ /**
303
+ * Snapshot of the configuration, for debugging and documentation tooling. The shape is not stable.
304
+ */
305
+ describe(): unknown;
306
+ /**
307
+ * Run the field in isolation with the same pipeline as in production: `withAuth`, global and field
308
+ * authorization, `protectFields` checks, middlewares, argument validation, hooks and tracing.
309
+ *
310
+ * @returns The resolved value.
311
+ * @throws ForbiddenError when authentication or authorization fails (unless `skipAuth`).
312
+ * @throws ApplicationError for relation fields without a custom resolver (they need a running Strapi).
313
+ *
314
+ * @example
315
+ * await expect(field.test({ context: { state: {} } })).rejects.toThrow('Not authenticated')
316
+ */
317
+ test(params: {
318
+ parent?: FieldParent<Type>;
319
+ /** Raw arguments, as a client sends them (before `validateArgs` parsing) */
320
+ args?: TArgs | Record<string, unknown>;
321
+ /** Resolver context; set `state.user` to simulate a logged-in user */
322
+ context?: Partial<ResolveContext> | Record<string, unknown>;
323
+ /** Defaults to a minimal object with `fieldName` and `parentType.name` */
324
+ info?: GraphQLResolveInfo;
325
+ /**
326
+ * Skip all middlewares (global and field-level).
327
+ * @default false
328
+ */
329
+ skipMiddleware?: boolean;
330
+ /**
331
+ * Skip authentication and all authorization (global and field-level).
332
+ * @default false
333
+ */
334
+ skipAuth?: boolean;
335
+ }): Promise<any>;
336
+ /**
337
+ * Log or collect timing information for each resolve. Can be called multiple times.
338
+ *
339
+ * @param handler - Defaults to a coloured console table.
340
+ */
341
+ trace(handler?: FieldTraceHandler): FieldExtensionBuilder<Type, TArgs, TOutput>;
342
+ /**
343
+ * Build the GraphQL extension. Called by the plugin for default exports in `graphql` folders, so you
344
+ * normally don't call this yourself. A builder can only be built once.
345
+ *
346
+ * @throws ApplicationError when the builder was already built or {@link FieldExtensionBuilder.validate | validate} fails.
347
+ */
348
+ build(): ReturnType<typeof createGraphQLExtension>;
349
+ }
350
+ /**
351
+ * A reusable function that configures a field builder.
352
+ *
353
+ * @see {@link createGraphQLFieldPreset}
354
+ */
355
+ export type GraphQLFieldPreset = (builder: FieldExtensionBuilder) => FieldExtensionBuilder;
356
+ /**
357
+ * Hooks around a field resolver. Both are awaited; throwing aborts the resolve.
358
+ */
359
+ export type GraphQLFieldHook<TParent = any, TArgs = any> = {
360
+ /** Runs after argument validation, before the resolver */
361
+ beforeResolve?: (parent: TParent, args: TArgs, context: ResolveContext, info: GraphQLResolveInfo) => void | Promise<void>;
362
+ /** Runs after the resolver succeeded, with its result */
363
+ afterResolve?: (result: any, parent: TParent, args: TArgs, context: ResolveContext, info: GraphQLResolveInfo) => void | Promise<void>;
364
+ };
365
+ /**
366
+ * A preset applied to every field of a group.
367
+ *
368
+ * @see {@link createFieldGroup}
369
+ */
370
+ export type FieldGroupPreset<TContext = any, TArgs = any> = (builder: FieldExtensionBuilder<TContext, TArgs>) => FieldExtensionBuilder<TContext, TArgs>;
371
+ export {};
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,4 +1,5 @@
1
1
  import type { GraphQLExtension, ResolverConfigOption, ResolverFunction, Strapi } from '../types';
2
+ import { GraphQLFieldPreset } from './field-builder';
2
3
  type ResolversObject = {
3
4
  Query?: Record<string, ResolverFunction>;
4
5
  Mutation?: Record<string, ResolverFunction>;
@@ -13,13 +14,38 @@ interface GraphQLExtensionBuilder {
13
14
  plugins(pluginsFn: (() => any[]) | any[]): GraphQLExtensionBuilder;
14
15
  resolvers(resolversObj: (() => ResolversObject) | ResolversObject): GraphQLExtensionBuilder;
15
16
  resolversConfig(config: (() => Record<string, ResolverConfigOption>) | Record<string, ResolverConfigOption>): GraphQLExtensionBuilder;
17
+ /**
18
+ * Apply a preset or extension to this builder.
19
+ * A preset is a function that takes a builder and returns a builder.
20
+ */
21
+ use(preset: (builder: GraphQLExtensionBuilder) => GraphQLExtensionBuilder): GraphQLExtensionBuilder;
16
22
  build(): GraphQLExtension;
17
23
  }
18
24
  /**
19
- * Overloaded createGraphQLExtension:
20
- * - When called with no arguments, returns a new builder.
21
- * - When called with a factory function returns a GraphQLExtension.
25
+ * Create a full GraphQL extension: custom types, SDL, resolvers, Nexus plugins and resolver config.
26
+ *
27
+ * Use it when you need control over the whole extension, e.g. shared type bundles, full type overrides,
28
+ * Nexus plugins or shadow CRUD changes. For the common cases there are more focused APIs: fields with
29
+ * {@link createGraphQLFieldBuilder}, access to generated fields with {@link protectFields} and relations
30
+ * with {@link FieldExtensionBuilder.relation}.
31
+ *
32
+ * - Without arguments: returns a fluent builder (`.types()`, `.resolvers()`, ... then `.build()`).
33
+ * - With a factory: calls it with `strapi` and returns the {@link GraphQLExtension}.
34
+ *
35
+ * @example
36
+ * export default createGraphQLExtension((strapi) => ({
37
+ * types: () => [objectType({ name: 'Stats', definition: (t) => t.int('total') })],
38
+ * }))
22
39
  */
23
40
  export declare function createGraphQLExtension(): GraphQLExtensionBuilder;
24
41
  export declare function createGraphQLExtension(extension: ((strapi: Strapi) => GraphQLExtension)): GraphQLExtension;
42
+ /**
43
+ * Maps a field-level preset to all (or matching) fields in a GraphQLExtension.
44
+ * Throws if a field is not a function (i.e., not a resolver).
45
+ *
46
+ * @param fieldPreset - a GraphQLFieldPreset (FieldExtensionBuilder => FieldExtensionBuilder)
47
+ * @param matcher - a function (type, fieldName) => boolean or an array of field names
48
+ * @returns a function that takes a GraphQLExtension and returns a new one with wrapped resolvers
49
+ */
50
+ export declare function mapFieldPresetToExtension(fieldPreset: GraphQLFieldPreset, matcher?: ((type: string, fieldName: string) => boolean) | string[]): (extension: GraphQLExtension) => GraphQLExtension;
25
51
  export {};
@@ -1,48 +1,51 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createGraphQLExtension = void 0;
3
+ exports.mapFieldPresetToExtension = exports.createGraphQLExtension = void 0;
4
+ const field_builder_1 = require("./field-builder");
4
5
  /**
5
6
  * Internal function that creates a new builder instance.
6
7
  */
7
- function graphQLExtensionBuilder() {
8
- // Options to store configuration.
9
- const options = {};
8
+ function graphQLExtensionBuilder(options = {}) {
10
9
  return {
11
10
  init(fn) {
12
- options.init = fn;
13
- return this;
11
+ return graphQLExtensionBuilder({ ...options, init: fn });
14
12
  },
15
13
  typeDefs(sdl) {
16
- options.typeDefs = typeof sdl === 'string' ? () => sdl : sdl;
17
- return this;
14
+ return graphQLExtensionBuilder({
15
+ ...options,
16
+ typeDefs: typeof sdl === 'string' ? () => sdl : sdl
17
+ });
18
18
  },
19
19
  types(typesFn) {
20
- options.types = typeof typesFn === 'function' ? typesFn : () => typesFn;
21
- return this;
20
+ return graphQLExtensionBuilder({
21
+ ...options,
22
+ types: typeof typesFn === 'function' ? typesFn : () => typesFn
23
+ });
22
24
  },
23
25
  plugins(pluginsFn) {
24
- options.plugins = typeof pluginsFn === 'function' ? pluginsFn : () => pluginsFn;
25
- return this;
26
+ return graphQLExtensionBuilder({
27
+ ...options,
28
+ plugins: typeof pluginsFn === 'function' ? pluginsFn : () => pluginsFn
29
+ });
26
30
  },
27
31
  resolvers(resolversObj) {
28
- options.resolvers = typeof resolversObj === 'function' ? resolversObj : () => resolversObj;
29
- return this;
32
+ return graphQLExtensionBuilder({
33
+ ...options,
34
+ resolvers: typeof resolversObj === 'function' ? resolversObj : () => resolversObj
35
+ });
30
36
  },
31
37
  resolversConfig(configObj) {
32
- options.resolversConfig = typeof configObj === 'function' ? configObj : () => configObj;
33
- return this;
38
+ return graphQLExtensionBuilder({
39
+ ...options,
40
+ resolversConfig: typeof configObj === 'function' ? configObj : () => configObj
41
+ });
34
42
  },
35
- build() {
36
- // Create the extension using the options collected.
37
- return {
38
- init: options.init,
39
- typeDefs: options.typeDefs,
40
- types: options.types,
41
- plugins: options.plugins,
42
- resolvers: options.resolvers,
43
- resolversConfig: options.resolversConfig,
44
- };
43
+ use(preset) {
44
+ return preset(this);
45
45
  },
46
+ build() {
47
+ return { ...options };
48
+ }
46
49
  };
47
50
  }
48
51
  function createGraphQLExtension(extension) {
@@ -53,3 +56,68 @@ function createGraphQLExtension(extension) {
53
56
  return extension(strapi);
54
57
  }
55
58
  exports.createGraphQLExtension = createGraphQLExtension;
59
+ /**
60
+ * Maps a field-level preset to all (or matching) fields in a GraphQLExtension.
61
+ * Throws if a field is not a function (i.e., not a resolver).
62
+ *
63
+ * @param fieldPreset - a GraphQLFieldPreset (FieldExtensionBuilder => FieldExtensionBuilder)
64
+ * @param matcher - a function (type, fieldName) => boolean or an array of field names
65
+ * @returns a function that takes a GraphQLExtension and returns a new one with wrapped resolvers
66
+ */
67
+ function mapFieldPresetToExtension(fieldPreset, matcher = () => true) {
68
+ return (extension) => {
69
+ var _a, _b, _c;
70
+ const originalResolvers = ((_a = extension.resolvers) === null || _a === void 0 ? void 0 : _a.call(extension)) || {};
71
+ const newResolvers = {};
72
+ for (const type of Object.keys(originalResolvers)) {
73
+ newResolvers[type] = {};
74
+ // @ts-ignore
75
+ for (const fieldName of Object.keys(originalResolvers[type] || {})) {
76
+ const shouldApply = typeof matcher === 'function'
77
+ ? matcher(type, fieldName)
78
+ : matcher.includes(fieldName);
79
+ // @ts-ignore
80
+ const originalResolver = originalResolvers[type][fieldName];
81
+ if (typeof originalResolver !== 'function') {
82
+ throw new Error(`Cannot apply field preset: ${type}.${fieldName} is not a resolver function`);
83
+ }
84
+ if (shouldApply) {
85
+ // Build a field builder for this resolver
86
+ let fieldBuilder = (0, field_builder_1.createGraphQLFieldBuilder)(type)
87
+ .fieldName(fieldName)
88
+ .resolver(originalResolver);
89
+ // Apply the preset
90
+ fieldBuilder = fieldPreset(fieldBuilder);
91
+ // Build the field and extract the resolver
92
+ const builtExtension = fieldBuilder.build();
93
+ const builtType = (_c = (_b = builtExtension === null || builtExtension === void 0 ? void 0 : builtExtension.types) === null || _b === void 0 ? void 0 : _b.call(builtExtension)) === null || _c === void 0 ? void 0 : _c.find((t) => t.name === type);
94
+ if (!builtType || !builtType.definition) {
95
+ throw new Error(`Cannot extract wrapped resolver for ${type}.${fieldName}`);
96
+ }
97
+ // Extract the resolver from the field definition
98
+ let wrappedResolver = null;
99
+ builtType.definition({
100
+ field: (name, config) => {
101
+ if (name === fieldName) {
102
+ wrappedResolver = config.resolve;
103
+ }
104
+ },
105
+ });
106
+ if (typeof wrappedResolver !== 'function') {
107
+ throw new Error(`Preset did not produce a valid resolver for ${type}.${fieldName}`);
108
+ }
109
+ newResolvers[type][fieldName] = wrappedResolver;
110
+ }
111
+ else {
112
+ newResolvers[type][fieldName] = originalResolver;
113
+ }
114
+ }
115
+ }
116
+ // Return a new extension with the updated resolvers
117
+ return {
118
+ ...extension,
119
+ resolvers: () => newResolvers,
120
+ };
121
+ };
122
+ }
123
+ exports.mapFieldPresetToExtension = mapFieldPresetToExtension;