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.
- package/README.md +128 -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 +161 -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 +287 -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 +94 -0
- package/dist/server/utils/loaders.js +166 -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 +58 -14
- package/dist/server/utils/registry/global-registry.js +42 -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 +85 -0
- package/dist/server/utils/strapi-compat.js +194 -0
- package/dist/server/utils/types.d.ts +25 -1
- package/dist/tsconfig.server.tsbuildinfo +1 -1
- package/package.json +3 -2
package/dist/server/types.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
* @
|
|
21
|
-
*
|
|
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
|
|
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
|
-
*
|
|
105
|
-
*
|
|
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 -
|
|
108
|
-
*
|
|
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
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
//
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
368
|
-
|
|
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
|
|
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
|
|
363
|
+
const { options, initFunctions, typeFunctions } = initialState;
|
|
364
|
+
registerBuiltField(options.rootType, options.fieldName);
|
|
392
365
|
const isRootType = ['Query', 'Mutation'].includes(options.rootType);
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
|
|
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:
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
*
|
|
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[];
|