buro26-strapi-graphql 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +173 -16
- package/dist/server/index.d.ts +1 -1
- package/dist/server/nexus/plugins/field-authorize.plugin.d.ts +21 -0
- package/dist/server/nexus/plugins/field-authorize.plugin.js +68 -35
- package/dist/server/nexus/plugins/middleware.plugin.d.ts +10 -2
- package/dist/server/nexus/plugins/middleware.plugin.js +20 -33
- package/dist/server/services/file-service.d.ts +7 -6
- package/dist/server/services/file-service.js +36 -36
- package/dist/server/services/index.d.ts +1 -1
- package/dist/server/types.d.ts +18 -3
- package/dist/server/utils/collection-type.d.ts +13 -2
- package/dist/server/utils/collection-type.js +13 -2
- package/dist/server/utils/field-builder/builder.d.ts +13 -4
- package/dist/server/utils/field-builder/builder.js +214 -119
- package/dist/server/utils/field-builder/helpers.d.ts +21 -6
- package/dist/server/utils/field-builder/helpers.js +26 -4
- package/dist/server/utils/field-builder/types.d.ts +297 -76
- package/dist/server/utils/graphql-extension.d.ts +24 -3
- package/dist/server/utils/graphql-extension.js +73 -7
- package/dist/server/utils/index.d.ts +2 -0
- package/dist/server/utils/index.js +11 -0
- package/dist/server/utils/loaders.d.ts +110 -0
- package/dist/server/utils/loaders.js +222 -0
- package/dist/server/utils/misc.d.ts +6 -0
- package/dist/server/utils/misc.js +6 -0
- package/dist/server/utils/override.d.ts +32 -0
- package/dist/server/utils/override.js +54 -26
- package/dist/server/utils/registry/field-protection.d.ts +43 -0
- package/dist/server/utils/registry/field-protection.js +65 -0
- package/dist/server/utils/registry/global-registry.d.ts +69 -14
- package/dist/server/utils/registry/global-registry.js +53 -7
- package/dist/server/utils/registry/index.d.ts +2 -0
- package/dist/server/utils/registry/index.js +2 -0
- package/dist/server/utils/registry/resolve-field.d.ts +1 -0
- package/dist/server/utils/registry/resolve-field.js +67 -0
- package/dist/server/utils/shadow-crud.d.ts +28 -1
- package/dist/server/utils/shadow-crud.js +32 -1
- package/dist/server/utils/single-type.d.ts +30 -1
- package/dist/server/utils/single-type.js +25 -0
- package/dist/server/utils/strapi-compat.d.ts +90 -0
- package/dist/server/utils/strapi-compat.js +203 -0
- package/dist/server/utils/types.d.ts +25 -1
- package/dist/tsconfig.server.tsbuildinfo +1 -1
- package/package.json +3 -2
|
@@ -1,19 +1,34 @@
|
|
|
1
1
|
import type { FieldExtensionBuilder, FieldGroupPreset, GraphQLFieldPreset } from './types';
|
|
2
2
|
import type { Middleware } from '../../nexus';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
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
|
|
8
|
+
export declare function makeAuthMiddleware(): Middleware;
|
|
7
9
|
/**
|
|
8
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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[];
|
|
@@ -4,7 +4,10 @@ exports.createFieldGroup = exports.registerFields = exports.createGraphQLFieldPr
|
|
|
4
4
|
const nexus_1 = require("nexus");
|
|
5
5
|
const utils_1 = require("@strapi/utils");
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
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()`.
|
|
8
11
|
*/
|
|
9
12
|
function parseArgs(args) {
|
|
10
13
|
const nexusArgs = {};
|
|
@@ -23,7 +26,9 @@ function parseArgs(args) {
|
|
|
23
26
|
}
|
|
24
27
|
exports.parseArgs = parseArgs;
|
|
25
28
|
/**
|
|
26
|
-
* Middleware that throws
|
|
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.
|
|
27
32
|
*/
|
|
28
33
|
function makeAuthMiddleware() {
|
|
29
34
|
return (next) => async (root, args, context, info) => {
|
|
@@ -36,19 +41,36 @@ function makeAuthMiddleware() {
|
|
|
36
41
|
};
|
|
37
42
|
}
|
|
38
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
|
+
*/
|
|
39
54
|
function createGraphQLFieldPreset(preset) {
|
|
40
55
|
return preset;
|
|
41
56
|
}
|
|
42
57
|
exports.createGraphQLFieldPreset = createGraphQLFieldPreset;
|
|
43
58
|
/**
|
|
44
|
-
*
|
|
59
|
+
* Export several fields from one file. Builds each builder and returns the extensions.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* export default registerFields(fieldA, fieldB)
|
|
45
63
|
*/
|
|
46
64
|
function registerFields(...fields) {
|
|
47
65
|
return fields.map(builder => builder.build());
|
|
48
66
|
}
|
|
49
67
|
exports.registerFields = registerFields;
|
|
50
68
|
/**
|
|
51
|
-
* Apply
|
|
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))
|
|
52
74
|
*/
|
|
53
75
|
function createFieldGroup(fields, ...presets) {
|
|
54
76
|
return fields.map(field => presets.reduce((acc, preset) => preset(acc), field));
|
|
@@ -1,160 +1,381 @@
|
|
|
1
1
|
import type { GraphQLResolveInfo } from 'graphql';
|
|
2
|
-
import type {
|
|
2
|
+
import type { output, ZodTypeAny } from 'zod';
|
|
3
3
|
import type { ResolverConfigOption, ResolverFunction } from '../../types';
|
|
4
4
|
import type { Middleware } from '../../nexus';
|
|
5
5
|
import type { createGraphQLExtension } from '../graphql-extension';
|
|
6
|
-
import type { ResolveContext } from '../types';
|
|
6
|
+
import type { ParentOf, ResolveContext } from '../types';
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
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`.
|
|
9
14
|
*/
|
|
10
|
-
export type
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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;
|
|
34
71
|
};
|
|
35
72
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
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 the 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. Works for to-one and to-many relations; a to-many field returns the same relation
|
|
89
|
+
* response collection as Strapi.
|
|
90
|
+
*
|
|
91
|
+
* Batched loads cannot apply per-parent query arguments: when a client passes `filters`,
|
|
92
|
+
* `pagination`, `sort` or (v4) `publicationState`, that call falls back to Strapi's normal
|
|
93
|
+
* per-parent association resolver.
|
|
94
|
+
*
|
|
95
|
+
* @default false
|
|
96
|
+
* @see {@link loaders}
|
|
97
|
+
*/
|
|
98
|
+
batch?: boolean;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Fluent, immutable builder for one GraphQL field. Every method returns a **new** builder, so a
|
|
102
|
+
* partially configured builder can be shared as a base for several fields.
|
|
103
|
+
*
|
|
104
|
+
* A field needs at least {@link FieldExtensionBuilder.rootType | rootType},
|
|
105
|
+
* {@link FieldExtensionBuilder.fieldName | fieldName}, {@link FieldExtensionBuilder.outputType | outputType}
|
|
106
|
+
* and {@link FieldExtensionBuilder.resolver | resolver} — or `rootType` plus
|
|
107
|
+
* {@link FieldExtensionBuilder.relation | relation}.
|
|
108
|
+
*
|
|
109
|
+
* Export the builder (or an array via `registerFields`) as the default export of a file in a `graphql`
|
|
110
|
+
* folder; the plugin calls {@link FieldExtensionBuilder.build | build} for you.
|
|
111
|
+
*
|
|
112
|
+
* @typeParam Type - Parent value the resolver receives. Inferred from {@link Register} `types` when
|
|
113
|
+
* `rootType` is a registered type name.
|
|
114
|
+
* @typeParam TArgs - Field arguments.
|
|
115
|
+
* @typeParam TOutput - Resolved value.
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* export default createGraphQLFieldBuilder()
|
|
119
|
+
* .rootType('Query')
|
|
120
|
+
* .fieldName('hello')
|
|
121
|
+
* .args({ name: 'String!' })
|
|
122
|
+
* .outputType('String')
|
|
123
|
+
* .withAuth(true)
|
|
124
|
+
* .resolver((_parent, { name }) => `Hello ${name}`)
|
|
125
|
+
*
|
|
126
|
+
* @see https://gitlab.com/buro26/public/strapi/strapi-graphql-extension#-recommended-the-field-builder-pattern
|
|
38
127
|
*/
|
|
39
128
|
export interface FieldExtensionBuilder<Type = any, TArgs = any, TOutput = Type> {
|
|
40
129
|
/**
|
|
41
|
-
* Set the
|
|
130
|
+
* Set the GraphQL type the field is added to: `Query`, `Mutation` or any object type such as a
|
|
131
|
+
* content type (`Customer`).
|
|
132
|
+
*
|
|
133
|
+
* When `type` is registered in {@link Register} `types`, the resolver's `parent` is typed accordingly.
|
|
134
|
+
*
|
|
135
|
+
* @param type - GraphQL type name.
|
|
136
|
+
* @throws Error when the root type was already set (e.g. by `createGraphQLFieldBuilder('Query')`).
|
|
42
137
|
*/
|
|
43
|
-
rootType(type:
|
|
138
|
+
rootType<N extends string>(type: N): FieldExtensionBuilder<ParentOf<N, Type>, TArgs, TOutput>;
|
|
44
139
|
/**
|
|
45
|
-
*
|
|
140
|
+
* Define the field as a relation or media attribute of a Strapi content type. It gets exactly the
|
|
141
|
+
* type, filter/pagination arguments, resolver (query validation, output sanitizing, draft & publish
|
|
142
|
+
* handling) and permission scope of the field Strapi generates, on Strapi v4 and v5.
|
|
143
|
+
*
|
|
144
|
+
* Media fields resolve against `plugin::upload.file`. Strapi attaches no permission scope to the
|
|
145
|
+
* media fields it generates, so neither does this builder.
|
|
146
|
+
*
|
|
147
|
+
* Sets {@link FieldExtensionBuilder.fieldName | fieldName} to `attribute`; `outputType` and `resolver`
|
|
148
|
+
* are optional (a custom `resolver` replaces Strapi's association resolver). Combine with
|
|
149
|
+
* `.authorize()`, `.middleware()` or presets to protect or extend the relation.
|
|
150
|
+
*
|
|
151
|
+
* | | v4 | v5 |
|
|
152
|
+
* |---|---|---|
|
|
153
|
+
* | to-one | `XEntityResponse` | `X` |
|
|
154
|
+
* | to-many | `XRelationResponseCollection` | `[X]!` and `<attribute>_connection` |
|
|
155
|
+
*
|
|
156
|
+
* On v5 both to-many fields get the same authorization and middleware. Note that the v5 list field is
|
|
157
|
+
* non-null: when `.authorize()` denies it, GraphQL nulls the parent object (standard error propagation).
|
|
158
|
+
*
|
|
159
|
+
* @param contentType - UID of the content type that owns the relation or media attribute.
|
|
160
|
+
* @param attribute - Name of a regular (non-polymorphic) relation or media attribute.
|
|
161
|
+
* @param options - See {@link RelationOptions}.
|
|
162
|
+
* @throws Error when a relation was already set, or `fieldName` was set to a different name.
|
|
163
|
+
* @throws ApplicationError at schema build when the attribute is not a relation or media attribute.
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* createGraphQLFieldBuilder()
|
|
167
|
+
* .rootType('Order')
|
|
168
|
+
* .relation('api::order.order', 'customer', { batch: true })
|
|
169
|
+
* .authorize(isOrderCustomer)
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* createGraphQLFieldBuilder()
|
|
173
|
+
* .rootType('Customer')
|
|
174
|
+
* .relation('api::customer.customer', 'avatar', { batch: true })
|
|
175
|
+
*/
|
|
176
|
+
relation(contentType: string, attribute: string, options?: Pick<RelationOptions, 'batch'>): FieldExtensionBuilder<Type, TArgs, TOutput>;
|
|
177
|
+
/**
|
|
178
|
+
* Set the field name. Use the name of an existing field to replace it.
|
|
179
|
+
*
|
|
180
|
+
* @throws Error when the field name was already set (also by `.relation()`).
|
|
46
181
|
*/
|
|
47
182
|
fieldName(name: string): FieldExtensionBuilder<Type, TArgs, TOutput>;
|
|
48
183
|
/**
|
|
49
|
-
*
|
|
184
|
+
* Description shown in the GraphQL schema, GraphiQL and IDE hints.
|
|
50
185
|
*/
|
|
51
186
|
description(text: string): FieldExtensionBuilder<Type, TArgs, TOutput>;
|
|
52
187
|
/**
|
|
53
|
-
* Mark
|
|
188
|
+
* Mark the field as deprecated in the schema.
|
|
189
|
+
*
|
|
190
|
+
* @param reason - Shown to API consumers; mention the replacement.
|
|
54
191
|
*/
|
|
55
192
|
deprecate(reason: string): FieldExtensionBuilder<Type, TArgs, TOutput>;
|
|
56
193
|
/**
|
|
57
|
-
*
|
|
194
|
+
* Run a function once when the plugin registers the field (Strapi's `register` phase), e.g. to
|
|
195
|
+
* configure shadow CRUD. Can be called multiple times; functions run in order.
|
|
58
196
|
*/
|
|
59
197
|
init(fn: () => void): FieldExtensionBuilder<Type, TArgs, TOutput>;
|
|
60
198
|
/**
|
|
61
|
-
* Set the resolver
|
|
199
|
+
* Set the resolver. It runs inside the authorization and middlewares, after argument validation and
|
|
200
|
+
* `beforeResolve` hooks.
|
|
201
|
+
*
|
|
202
|
+
* @throws Error when a resolver was already set.
|
|
62
203
|
*/
|
|
63
|
-
resolver(fn: ResolverFunction<
|
|
204
|
+
resolver(fn: ResolverFunction<FieldParent<Type>, TArgs, Partial<TOutput> | null | any>): FieldExtensionBuilder<Type, TArgs, TOutput>;
|
|
64
205
|
/**
|
|
65
|
-
*
|
|
66
|
-
* Only
|
|
206
|
+
* Strapi resolver config (policies, Strapi middlewares, `auth`). Merged with earlier calls.
|
|
207
|
+
* Only applies to `Query` and `Mutation` fields; for other types use `.authorize()` / `.middleware()`.
|
|
208
|
+
*
|
|
209
|
+
* @deprecated Use {@link FieldExtensionBuilder.withAuth | withAuth} for authentication and
|
|
210
|
+
* {@link FieldExtensionBuilder.authorize | authorize} / {@link FieldExtensionBuilder.middleware | middleware}
|
|
211
|
+
* for the rest; they work on every type and run in a defined order.
|
|
212
|
+
* @see https://docs.strapi.io/dev-docs/plugins/graphql#customization
|
|
67
213
|
*/
|
|
68
|
-
resolverConfig(config: (() => ResolverConfigOption<Type, TArgs, TOutput>) | ResolverConfigOption<Type, TArgs, TOutput>): FieldExtensionBuilder
|
|
214
|
+
resolverConfig(config: (() => ResolverConfigOption<Type, TArgs, TOutput>) | ResolverConfigOption<Type, TArgs, TOutput>): FieldExtensionBuilder<Type, TArgs, TOutput>;
|
|
69
215
|
/**
|
|
70
|
-
*
|
|
216
|
+
* Wrap the resolver. Middlewares receive `next` and return a resolver; call `next` to continue.
|
|
217
|
+
* Field middlewares run after global middlewares and before `protectFields` middlewares.
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* .middleware((next) => async (parent, args, ctx, info) => {
|
|
221
|
+
* const result = await next(parent, args, ctx, info)
|
|
222
|
+
* return result ?? fallback
|
|
223
|
+
* })
|
|
71
224
|
*/
|
|
72
|
-
middleware(middleware: Middleware<
|
|
225
|
+
middleware(middleware: Middleware<FieldParent<Type>, TArgs>): FieldExtensionBuilder<Type, TArgs, TOutput>;
|
|
73
226
|
/**
|
|
74
|
-
* Add SDL type definitions
|
|
227
|
+
* Add SDL type definitions needed by this field (e.g. its output type). Appended to earlier calls.
|
|
228
|
+
* Prefer {@link FieldExtensionBuilder.types | types} with Nexus for type-safe definitions.
|
|
75
229
|
*/
|
|
76
230
|
typeDefs(sdl: string): FieldExtensionBuilder<Type, TArgs, TOutput>;
|
|
77
231
|
/**
|
|
78
|
-
* Add
|
|
232
|
+
* Add Nexus types needed by this field (object types, input types, enums). Appended to earlier calls.
|
|
79
233
|
*/
|
|
80
234
|
types(typesFn: (() => unknown[]) | unknown[]): FieldExtensionBuilder<Type, TArgs, TOutput>;
|
|
81
235
|
/**
|
|
82
|
-
* Set the arguments
|
|
236
|
+
* Set the field arguments. Replaces earlier calls.
|
|
237
|
+
*
|
|
238
|
+
* This does not change the resolver's `args` type. Type them with {@link FieldExtensionBuilder.validateArgs}
|
|
239
|
+
* (inferred from the schema), or explicitly with {@link InferArgs}:
|
|
240
|
+
* `createGraphQLFieldBuilder<unknown, InferArgs<typeof args>>()`.
|
|
241
|
+
*
|
|
242
|
+
* @param a - Map of argument name to a type string (`'String!'`, `'ID'`) or a Nexus `arg()`.
|
|
83
243
|
*/
|
|
84
244
|
args(a: Record<string, string | unknown>): FieldExtensionBuilder<Type, TArgs, TOutput>;
|
|
85
245
|
/**
|
|
86
|
-
* Validate arguments
|
|
246
|
+
* Validate (and parse) arguments with Zod before the resolver runs. The resolver receives the parsed
|
|
247
|
+
* value. Invalid arguments throw a Strapi `ValidationError` ("Invalid arguments") with the `ZodError`
|
|
248
|
+
* as `cause`.
|
|
249
|
+
*
|
|
250
|
+
* When the builder has no explicit `TArgs`, the resolver's `args` are typed as the schema's output.
|
|
251
|
+
*
|
|
252
|
+
* @example
|
|
253
|
+
* .args({ id: 'ID!', limit: 'Int' })
|
|
254
|
+
* .validateArgs(z.object({ id: z.coerce.number(), limit: z.number().default(10) }))
|
|
255
|
+
* .resolver((_parent, { id, limit }) => ...) // id: number, limit: number
|
|
87
256
|
*/
|
|
88
|
-
validateArgs(schema:
|
|
257
|
+
validateArgs<S extends ZodTypeAny>(schema: S): FieldExtensionBuilder<Type, 0 extends (1 & TArgs) ? output<S> : TArgs, TOutput>;
|
|
89
258
|
/**
|
|
90
|
-
* Set the output type
|
|
259
|
+
* Set the GraphQL output type: a type name (`'String'`, `'CustomerEntityResponse'`) or a Nexus wrapper
|
|
260
|
+
* (`nonNull('Boolean')`, `list('Customer')`). Not needed with {@link FieldExtensionBuilder.relation | relation}.
|
|
91
261
|
*/
|
|
92
262
|
outputType(type: string | unknown): FieldExtensionBuilder<Type, TArgs, TOutput>;
|
|
93
263
|
/**
|
|
94
|
-
*
|
|
264
|
+
* Require an authenticated user. Checked before any authorization runs; unauthenticated requests get a
|
|
265
|
+
* `ForbiddenError` ("Not authenticated"). For `Query`/`Mutation` fields this also sets Strapi's
|
|
266
|
+
* resolver `auth` option.
|
|
267
|
+
*
|
|
268
|
+
* @default false
|
|
95
269
|
*/
|
|
96
270
|
withAuth(auth: boolean): FieldExtensionBuilder<Type, TArgs, TOutput>;
|
|
97
271
|
/**
|
|
98
|
-
* Add
|
|
272
|
+
* Add an authorization check. All checks (global, field and `protectFields`) must return `true`;
|
|
273
|
+
* return `false` for a generic "Not authorized" or an `Error` to send that error to the client.
|
|
274
|
+
* Checks run in order and stop at the first failure.
|
|
275
|
+
*
|
|
276
|
+
* @example
|
|
277
|
+
* .authorize((parent, _args, ctx) => parent?.user?.id === ctx.state.user?.id)
|
|
99
278
|
*/
|
|
100
|
-
authorize(authorizeFn: ResolverFunction<
|
|
279
|
+
authorize(authorizeFn: ResolverFunction<FieldParent<Type>, TArgs, Partial<TOutput> | null | any>): FieldExtensionBuilder<Type, TArgs, TOutput>;
|
|
101
280
|
/**
|
|
102
|
-
* Add a tag
|
|
281
|
+
* Add a tag. Tags let global middleware/authorization target groups of fields.
|
|
282
|
+
*
|
|
283
|
+
* @see {@link registerGlobalAuthorize}
|
|
284
|
+
* @see {@link registerGlobalMiddleware}
|
|
103
285
|
*/
|
|
104
286
|
tag(tag: string): FieldExtensionBuilder<Type, TArgs, TOutput>;
|
|
105
287
|
/**
|
|
106
|
-
* Add
|
|
288
|
+
* Add several tags at once.
|
|
289
|
+
*
|
|
290
|
+
* @see {@link FieldExtensionBuilder.tag}
|
|
107
291
|
*/
|
|
108
292
|
tags(tags: string[]): FieldExtensionBuilder<Type, TArgs, TOutput>;
|
|
109
293
|
/**
|
|
110
|
-
* Apply a preset
|
|
111
|
-
*
|
|
294
|
+
* Apply a preset: a function that takes a builder and returns a configured builder. Use presets for
|
|
295
|
+
* reusable auth, feature flags or tags.
|
|
296
|
+
*
|
|
297
|
+
* @see {@link createGraphQLFieldPreset}
|
|
112
298
|
*/
|
|
113
299
|
use(preset: (builder: FieldExtensionBuilder) => FieldExtensionBuilder<Type, TArgs, TOutput>): FieldExtensionBuilder<Type, TArgs, TOutput>;
|
|
114
300
|
/**
|
|
115
|
-
*
|
|
301
|
+
* Add `beforeResolve` / `afterResolve` hooks. They run inside the middlewares, around the resolver;
|
|
302
|
+
* hooks are awaited and can throw to abort the request.
|
|
116
303
|
*/
|
|
117
|
-
hook(hook: GraphQLFieldHook<Type
|
|
304
|
+
hook(hook: GraphQLFieldHook<FieldParent<Type>, TArgs>): FieldExtensionBuilder<Type, TArgs, TOutput>;
|
|
118
305
|
/**
|
|
119
|
-
*
|
|
306
|
+
* Check that the configuration is complete. Called by `build()` and `test()`.
|
|
307
|
+
*
|
|
308
|
+
* @throws ApplicationError when `rootType` or `fieldName` is missing, `outputType`/`resolver` is missing
|
|
309
|
+
* on a non-relation field, or resolver config policies/middlewares are used outside `Query`/`Mutation`.
|
|
120
310
|
*/
|
|
121
311
|
validate(): void;
|
|
122
312
|
/**
|
|
123
|
-
*
|
|
313
|
+
* Snapshot of the configuration, for debugging and documentation tooling. The shape is not stable.
|
|
124
314
|
*/
|
|
125
315
|
describe(): unknown;
|
|
126
316
|
/**
|
|
127
|
-
*
|
|
317
|
+
* Run the field in isolation with the same pipeline as in production: `withAuth`, global and field
|
|
318
|
+
* authorization, `protectFields` checks, middlewares, argument validation, hooks and tracing.
|
|
319
|
+
*
|
|
320
|
+
* @returns The resolved value.
|
|
321
|
+
* @throws ForbiddenError when authentication or authorization fails (unless `skipAuth`).
|
|
322
|
+
* @throws ApplicationError for relation fields without a custom resolver (they need a running Strapi).
|
|
323
|
+
*
|
|
324
|
+
* @example
|
|
325
|
+
* await expect(field.test({ context: { state: {} } })).rejects.toThrow('Not authenticated')
|
|
128
326
|
*/
|
|
129
327
|
test(params: {
|
|
130
|
-
parent?:
|
|
131
|
-
|
|
132
|
-
|
|
328
|
+
parent?: FieldParent<Type>;
|
|
329
|
+
/** Raw arguments, as a client sends them (before `validateArgs` parsing) */
|
|
330
|
+
args?: TArgs | Record<string, unknown>;
|
|
331
|
+
/** Resolver context; set `state.user` to simulate a logged-in user */
|
|
332
|
+
context?: Partial<ResolveContext> | Record<string, unknown>;
|
|
333
|
+
/** Defaults to a minimal object with `fieldName` and `parentType.name` */
|
|
133
334
|
info?: GraphQLResolveInfo;
|
|
335
|
+
/**
|
|
336
|
+
* Skip all middlewares (global and field-level).
|
|
337
|
+
* @default false
|
|
338
|
+
*/
|
|
134
339
|
skipMiddleware?: boolean;
|
|
340
|
+
/**
|
|
341
|
+
* Skip authentication and all authorization (global and field-level).
|
|
342
|
+
* @default false
|
|
343
|
+
*/
|
|
135
344
|
skipAuth?: boolean;
|
|
136
345
|
}): Promise<any>;
|
|
137
346
|
/**
|
|
138
|
-
*
|
|
139
|
-
*
|
|
347
|
+
* Log or collect timing information for each resolve. Can be called multiple times.
|
|
348
|
+
*
|
|
349
|
+
* @param handler - Defaults to a coloured console table.
|
|
140
350
|
*/
|
|
141
|
-
trace(handler?:
|
|
142
|
-
type: string;
|
|
143
|
-
fieldName: string;
|
|
144
|
-
durationMs: number;
|
|
145
|
-
args: any;
|
|
146
|
-
context: any;
|
|
147
|
-
result: any;
|
|
148
|
-
error?: any;
|
|
149
|
-
}) => void): FieldExtensionBuilder<Type, TArgs, TOutput>;
|
|
351
|
+
trace(handler?: FieldTraceHandler): FieldExtensionBuilder<Type, TArgs, TOutput>;
|
|
150
352
|
/**
|
|
151
|
-
* Build the extension for
|
|
353
|
+
* Build the GraphQL extension. Called by the plugin for default exports in `graphql` folders, so you
|
|
354
|
+
* normally don't call this yourself. A builder can only be built once.
|
|
355
|
+
*
|
|
356
|
+
* @throws ApplicationError when the builder was already built or {@link FieldExtensionBuilder.validate | validate} fails.
|
|
152
357
|
*/
|
|
153
358
|
build(): ReturnType<typeof createGraphQLExtension>;
|
|
154
359
|
}
|
|
360
|
+
/**
|
|
361
|
+
* A reusable function that configures a field builder.
|
|
362
|
+
*
|
|
363
|
+
* @see {@link createGraphQLFieldPreset}
|
|
364
|
+
*/
|
|
155
365
|
export type GraphQLFieldPreset = (builder: FieldExtensionBuilder) => FieldExtensionBuilder;
|
|
366
|
+
/**
|
|
367
|
+
* Hooks around a field resolver. Both are awaited; throwing aborts the resolve.
|
|
368
|
+
*/
|
|
156
369
|
export type GraphQLFieldHook<TParent = any, TArgs = any> = {
|
|
370
|
+
/** Runs after argument validation, before the resolver */
|
|
157
371
|
beforeResolve?: (parent: TParent, args: TArgs, context: ResolveContext, info: GraphQLResolveInfo) => void | Promise<void>;
|
|
158
|
-
|
|
372
|
+
/** Runs after the resolver succeeded, with its result */
|
|
373
|
+
afterResolve?: (result: any, parent: TParent, args: TArgs, context: ResolveContext, info: GraphQLResolveInfo) => void | Promise<void>;
|
|
159
374
|
};
|
|
375
|
+
/**
|
|
376
|
+
* A preset applied to every field of a group.
|
|
377
|
+
*
|
|
378
|
+
* @see {@link createFieldGroup}
|
|
379
|
+
*/
|
|
160
380
|
export type FieldGroupPreset<TContext = any, TArgs = any> = (builder: FieldExtensionBuilder<TContext, TArgs>) => FieldExtensionBuilder<TContext, TArgs>;
|
|
381
|
+
export {};
|
|
@@ -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>;
|
|
@@ -21,10 +22,30 @@ interface GraphQLExtensionBuilder {
|
|
|
21
22
|
build(): GraphQLExtension;
|
|
22
23
|
}
|
|
23
24
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
+
* }))
|
|
27
39
|
*/
|
|
28
40
|
export declare function createGraphQLExtension(): GraphQLExtensionBuilder;
|
|
29
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;
|
|
30
51
|
export {};
|