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.
- package/README.md +458 -439
- package/dist/server/index.d.ts +14 -8
- package/dist/server/index.js +1 -0
- package/dist/server/nexus/index.d.ts +1 -0
- package/dist/server/nexus/index.js +17 -0
- package/dist/server/nexus/plugins/field-authorize.plugin.d.ts +36 -0
- package/dist/server/nexus/plugins/field-authorize.plugin.js +122 -0
- package/dist/server/nexus/plugins/index.d.ts +2 -0
- package/dist/server/nexus/plugins/index.js +18 -0
- package/dist/server/nexus/plugins/middleware.plugin.d.ts +25 -0
- package/dist/server/nexus/plugins/middleware.plugin.js +63 -0
- package/dist/server/register.js +6 -7
- package/dist/server/services/file-service.d.ts +7 -6
- package/dist/server/services/file-service.js +36 -30
- package/dist/server/services/index.d.ts +1 -1
- package/dist/server/types.d.ts +28 -13
- 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 +33 -0
- package/dist/server/utils/field-builder/builder.js +552 -0
- package/dist/server/utils/field-builder/helpers.d.ts +34 -0
- package/dist/server/utils/field-builder/helpers.js +78 -0
- package/dist/server/utils/field-builder/index.d.ts +3 -0
- package/dist/server/utils/field-builder/index.js +19 -0
- package/dist/server/utils/field-builder/types.d.ts +371 -0
- package/dist/server/utils/field-builder/types.js +2 -0
- package/dist/server/utils/graphql-extension.d.ts +29 -3
- package/dist/server/utils/graphql-extension.js +94 -26
- package/dist/server/utils/index.d.ts +3 -1
- package/dist/server/utils/index.js +12 -1
- 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 +34 -1
- 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 +63 -0
- package/dist/server/utils/registry/global-registry.js +56 -0
- package/dist/server/utils/registry/index.d.ts +3 -0
- package/dist/server/utils/registry/index.js +19 -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 +7 -4
- package/dist/server/utils/field-builder.d.ts +0 -31
- package/dist/server/utils/field-builder.js +0 -113
- package/dist/server/utils/middleware.d.ts +0 -17
- package/dist/server/utils/middleware.js +0 -77
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { FieldExtensionBuilder, FieldExtensionOptions, GraphQLFieldHook } from './types';
|
|
2
|
+
import type { ResolverFunction } from '../../types';
|
|
3
|
+
import type { Middleware } from '../../nexus';
|
|
4
|
+
/**
|
|
5
|
+
* Internal state for the builder.
|
|
6
|
+
*/
|
|
7
|
+
type BuilderState<Type = any, TArgs = any, TOutput = any> = {
|
|
8
|
+
options: FieldExtensionOptions<Type, TArgs, TOutput>;
|
|
9
|
+
initFunctions: Array<() => void>;
|
|
10
|
+
typeFunctions: Array<() => unknown[]>;
|
|
11
|
+
middlewareFunctions: Middleware[];
|
|
12
|
+
authorizeFunctions: ResolverFunction<Partial<Type> | null, TArgs, boolean>[];
|
|
13
|
+
hooks: GraphQLFieldHook[];
|
|
14
|
+
built: boolean;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Create a field builder: the recommended way to add or replace a GraphQL field.
|
|
18
|
+
*
|
|
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(' '))
|
|
31
|
+
*/
|
|
32
|
+
export declare const createGraphQLFieldBuilder: <Type = any, TArgs = any, TOutput = Type>(rootType?: string, _state?: BuilderState<Type, TArgs, TOutput>) => FieldExtensionBuilder<Type, TArgs, TOutput>;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resetBuiltFields = exports.createGraphQLFieldBuilder = void 0;
|
|
4
|
+
const nexus_1 = require("nexus");
|
|
5
|
+
const utils_1 = require("@strapi/utils");
|
|
6
|
+
const immer_1 = require("immer");
|
|
7
|
+
const zod_1 = require("zod");
|
|
8
|
+
const graphql_extension_1 = require("../graphql-extension");
|
|
9
|
+
const helpers_1 = require("./helpers");
|
|
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");
|
|
14
|
+
/**
|
|
15
|
+
* Helper to update builder state immutably using Immer.
|
|
16
|
+
*/
|
|
17
|
+
function updateState(state, updater) {
|
|
18
|
+
return (0, immer_1.produce)(state, updater);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Compose all middleware functions into a single resolver.
|
|
22
|
+
*/
|
|
23
|
+
function composeMiddlewares(resolver, middlewares) {
|
|
24
|
+
return middlewares.reduceRight((next, mw) => mw(next), resolver);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Wrap resolver with Zod validation if schema is provided.
|
|
28
|
+
*/
|
|
29
|
+
function withArgValidation(resolver, schema) {
|
|
30
|
+
if (!schema)
|
|
31
|
+
return resolver;
|
|
32
|
+
return async (parent, args, ctx, info) => {
|
|
33
|
+
try {
|
|
34
|
+
const parsedArgs = schema.parse(args);
|
|
35
|
+
return resolver(parent, parsedArgs, ctx, info);
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
if (err instanceof zod_1.ZodError) {
|
|
39
|
+
throw new utils_1.errors.ValidationError('Invalid arguments', { cause: err });
|
|
40
|
+
}
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Wrap resolver with tracing handlers.
|
|
47
|
+
*/
|
|
48
|
+
function withTracing(resolver, traceHandlers) {
|
|
49
|
+
if (!traceHandlers.length)
|
|
50
|
+
return resolver;
|
|
51
|
+
return async (parent, args, ctx, info) => {
|
|
52
|
+
const start = Date.now();
|
|
53
|
+
let result, error;
|
|
54
|
+
try {
|
|
55
|
+
result = await resolver(parent, args, ctx, info);
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
error = err;
|
|
60
|
+
throw err;
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
const durationMs = Date.now() - start;
|
|
64
|
+
for (const handler of traceHandlers) {
|
|
65
|
+
Promise.resolve()
|
|
66
|
+
.then(() => {
|
|
67
|
+
var _a;
|
|
68
|
+
return handler({
|
|
69
|
+
type: (_a = info === null || info === void 0 ? void 0 : info.parentType) === null || _a === void 0 ? void 0 : _a.name,
|
|
70
|
+
fieldName: info === null || info === void 0 ? void 0 : info.fieldName,
|
|
71
|
+
durationMs,
|
|
72
|
+
args,
|
|
73
|
+
context: ctx,
|
|
74
|
+
result,
|
|
75
|
+
error
|
|
76
|
+
});
|
|
77
|
+
})
|
|
78
|
+
.catch((err) => {
|
|
79
|
+
// Swallow errors in trace handlers
|
|
80
|
+
console.error('[TRACE HANDLER ERROR]', err);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Run before/after resolve hooks.
|
|
88
|
+
*/
|
|
89
|
+
async function runHooks(hooks, when, ...args) {
|
|
90
|
+
for (const hook of hooks) {
|
|
91
|
+
if (hook[when]) {
|
|
92
|
+
// @ts-ignore
|
|
93
|
+
await hook[when](...args);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
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.
|
|
101
|
+
*
|
|
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(' '))
|
|
112
|
+
*/
|
|
113
|
+
const createGraphQLFieldBuilder = (rootType, _state) => {
|
|
114
|
+
const initialState = _state !== null && _state !== void 0 ? _state : {
|
|
115
|
+
options: {
|
|
116
|
+
rootType: rootType !== null && rootType !== void 0 ? rootType : '',
|
|
117
|
+
fieldName: undefined,
|
|
118
|
+
resolver: undefined,
|
|
119
|
+
resolverConfig: { policies: [], middlewares: [] },
|
|
120
|
+
typeDefs: '',
|
|
121
|
+
types: () => [],
|
|
122
|
+
args: {},
|
|
123
|
+
outputType: undefined,
|
|
124
|
+
tags: [],
|
|
125
|
+
traceHandlers: []
|
|
126
|
+
},
|
|
127
|
+
initFunctions: [],
|
|
128
|
+
typeFunctions: [],
|
|
129
|
+
middlewareFunctions: [],
|
|
130
|
+
authorizeFunctions: [],
|
|
131
|
+
hooks: [],
|
|
132
|
+
built: false
|
|
133
|
+
};
|
|
134
|
+
function ensureNotBuilt(state) {
|
|
135
|
+
if (state.built) {
|
|
136
|
+
throw new utils_1.errors.ApplicationError('Builder has already been built.');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const builder = {
|
|
140
|
+
rootType(type) {
|
|
141
|
+
ensureNotBuilt(initialState);
|
|
142
|
+
if (initialState.options.rootType) {
|
|
143
|
+
throw new Error('Root type has already been set.');
|
|
144
|
+
}
|
|
145
|
+
return (0, exports.createGraphQLFieldBuilder)(undefined, updateState(initialState, (draft) => {
|
|
146
|
+
draft.options.rootType = type;
|
|
147
|
+
}));
|
|
148
|
+
},
|
|
149
|
+
fieldName(name) {
|
|
150
|
+
ensureNotBuilt(initialState);
|
|
151
|
+
if (initialState.options.fieldName) {
|
|
152
|
+
throw new Error('Field name has already been set.');
|
|
153
|
+
}
|
|
154
|
+
return (0, exports.createGraphQLFieldBuilder)(undefined, updateState(initialState, (draft) => {
|
|
155
|
+
draft.options.fieldName = name;
|
|
156
|
+
}));
|
|
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
|
+
},
|
|
171
|
+
description(text) {
|
|
172
|
+
ensureNotBuilt(initialState);
|
|
173
|
+
return (0, exports.createGraphQLFieldBuilder)(undefined, updateState(initialState, (draft) => {
|
|
174
|
+
draft.options.description = text;
|
|
175
|
+
}));
|
|
176
|
+
},
|
|
177
|
+
deprecate(reason) {
|
|
178
|
+
ensureNotBuilt(initialState);
|
|
179
|
+
return (0, exports.createGraphQLFieldBuilder)(undefined, updateState(initialState, (draft) => {
|
|
180
|
+
draft.options.deprecationReason = reason;
|
|
181
|
+
}));
|
|
182
|
+
},
|
|
183
|
+
init(fn) {
|
|
184
|
+
ensureNotBuilt(initialState);
|
|
185
|
+
return (0, exports.createGraphQLFieldBuilder)(undefined, updateState(initialState, (draft) => {
|
|
186
|
+
draft.initFunctions.push(fn);
|
|
187
|
+
}));
|
|
188
|
+
},
|
|
189
|
+
resolver(fn) {
|
|
190
|
+
ensureNotBuilt(initialState);
|
|
191
|
+
if (initialState.options.resolver) {
|
|
192
|
+
throw new Error('Resolver function has already been set.');
|
|
193
|
+
}
|
|
194
|
+
return (0, exports.createGraphQLFieldBuilder)(undefined, updateState(initialState, (draft) => {
|
|
195
|
+
draft.options.resolver = fn;
|
|
196
|
+
}));
|
|
197
|
+
},
|
|
198
|
+
middleware(middleware) {
|
|
199
|
+
ensureNotBuilt(initialState);
|
|
200
|
+
return (0, exports.createGraphQLFieldBuilder)(undefined, updateState(initialState, (draft) => {
|
|
201
|
+
draft.middlewareFunctions.push(middleware);
|
|
202
|
+
}));
|
|
203
|
+
},
|
|
204
|
+
resolverConfig(config) {
|
|
205
|
+
ensureNotBuilt(initialState);
|
|
206
|
+
const resolverConfig = typeof config === 'function' ? config() : config;
|
|
207
|
+
return (0, exports.createGraphQLFieldBuilder)(undefined, updateState(initialState, (draft) => {
|
|
208
|
+
var _a, _b;
|
|
209
|
+
draft.options.resolverConfig = {
|
|
210
|
+
policies: [
|
|
211
|
+
...(((_a = draft.options.resolverConfig) === null || _a === void 0 ? void 0 : _a.policies) || []),
|
|
212
|
+
...(resolverConfig.policies || [])
|
|
213
|
+
],
|
|
214
|
+
middlewares: [
|
|
215
|
+
...(((_b = draft.options.resolverConfig) === null || _b === void 0 ? void 0 : _b.middlewares) || []),
|
|
216
|
+
...(resolverConfig.middlewares || [])
|
|
217
|
+
],
|
|
218
|
+
...(resolverConfig.auth !== undefined && {
|
|
219
|
+
auth: resolverConfig.auth
|
|
220
|
+
})
|
|
221
|
+
};
|
|
222
|
+
if (resolverConfig.auth !== undefined) {
|
|
223
|
+
draft.options.auth = resolverConfig.auth !== false;
|
|
224
|
+
}
|
|
225
|
+
}));
|
|
226
|
+
},
|
|
227
|
+
typeDefs(sdl) {
|
|
228
|
+
ensureNotBuilt(initialState);
|
|
229
|
+
return (0, exports.createGraphQLFieldBuilder)(undefined, updateState(initialState, (draft) => {
|
|
230
|
+
draft.options.typeDefs = `${draft.options.typeDefs}\n${sdl}`;
|
|
231
|
+
}));
|
|
232
|
+
},
|
|
233
|
+
types(typesFn) {
|
|
234
|
+
ensureNotBuilt(initialState);
|
|
235
|
+
const fn = typeof typesFn === 'function' ? typesFn : () => typesFn;
|
|
236
|
+
return (0, exports.createGraphQLFieldBuilder)(undefined, updateState(initialState, (draft) => {
|
|
237
|
+
draft.typeFunctions.push(fn);
|
|
238
|
+
}));
|
|
239
|
+
},
|
|
240
|
+
args(a) {
|
|
241
|
+
ensureNotBuilt(initialState);
|
|
242
|
+
return (0, exports.createGraphQLFieldBuilder)(undefined, updateState(initialState, (draft) => {
|
|
243
|
+
draft.options.args = a;
|
|
244
|
+
}));
|
|
245
|
+
},
|
|
246
|
+
validateArgs(schema) {
|
|
247
|
+
ensureNotBuilt(initialState);
|
|
248
|
+
return (0, exports.createGraphQLFieldBuilder)(undefined, updateState(initialState, (draft) => {
|
|
249
|
+
draft.options.validateArgsSchema = schema;
|
|
250
|
+
}));
|
|
251
|
+
},
|
|
252
|
+
outputType(type) {
|
|
253
|
+
ensureNotBuilt(initialState);
|
|
254
|
+
return (0, exports.createGraphQLFieldBuilder)(undefined, updateState(initialState, (draft) => {
|
|
255
|
+
draft.options.outputType = type;
|
|
256
|
+
}));
|
|
257
|
+
},
|
|
258
|
+
withAuth(auth) {
|
|
259
|
+
ensureNotBuilt(initialState);
|
|
260
|
+
return (0, exports.createGraphQLFieldBuilder)(undefined, updateState(initialState, (draft) => {
|
|
261
|
+
draft.options.auth = auth;
|
|
262
|
+
draft.options.resolverConfig = {
|
|
263
|
+
...draft.options.resolverConfig,
|
|
264
|
+
auth
|
|
265
|
+
};
|
|
266
|
+
}));
|
|
267
|
+
},
|
|
268
|
+
authorize(authorizeFn) {
|
|
269
|
+
ensureNotBuilt(initialState);
|
|
270
|
+
return (0, exports.createGraphQLFieldBuilder)(undefined, updateState(initialState, (draft) => {
|
|
271
|
+
draft.authorizeFunctions.push(authorizeFn);
|
|
272
|
+
}));
|
|
273
|
+
},
|
|
274
|
+
tag(tag) {
|
|
275
|
+
ensureNotBuilt(initialState);
|
|
276
|
+
return (0, exports.createGraphQLFieldBuilder)(undefined, updateState(initialState, (draft) => {
|
|
277
|
+
draft.options.tags = [...(draft.options.tags || []), tag];
|
|
278
|
+
}));
|
|
279
|
+
},
|
|
280
|
+
tags(tags) {
|
|
281
|
+
ensureNotBuilt(initialState);
|
|
282
|
+
return (0, exports.createGraphQLFieldBuilder)(undefined, updateState(initialState, (draft) => {
|
|
283
|
+
draft.options.tags = [...(draft.options.tags || []), ...tags];
|
|
284
|
+
}));
|
|
285
|
+
},
|
|
286
|
+
use(preset) {
|
|
287
|
+
return preset(this);
|
|
288
|
+
},
|
|
289
|
+
hook(hook) {
|
|
290
|
+
ensureNotBuilt(initialState);
|
|
291
|
+
return (0, exports.createGraphQLFieldBuilder)(undefined, updateState(initialState, (draft) => {
|
|
292
|
+
draft.hooks.push(hook);
|
|
293
|
+
}));
|
|
294
|
+
},
|
|
295
|
+
validate() {
|
|
296
|
+
var _a, _b, _c, _d;
|
|
297
|
+
const { options } = initialState;
|
|
298
|
+
if (!options.rootType) {
|
|
299
|
+
throw new utils_1.errors.ApplicationError('Root type must be provided via rootType()');
|
|
300
|
+
}
|
|
301
|
+
if (!options.fieldName) {
|
|
302
|
+
throw new utils_1.errors.ApplicationError('Field name must be provided via fieldName()');
|
|
303
|
+
}
|
|
304
|
+
if (!options.outputType && !options.relation) {
|
|
305
|
+
throw new utils_1.errors.ApplicationError(`Output type must be provided for field "${options.fieldName}" via outputType()`);
|
|
306
|
+
}
|
|
307
|
+
if (!options.resolver && !options.relation) {
|
|
308
|
+
throw new utils_1.errors.ApplicationError(`Resolver function must be provided for field "${options.fieldName}" via resolver()`);
|
|
309
|
+
}
|
|
310
|
+
const isRootType = ['Query', 'Mutation'].includes(options.rootType);
|
|
311
|
+
if (!isRootType &&
|
|
312
|
+
(!!((_b = (_a = options.resolverConfig) === null || _a === void 0 ? void 0 : _a.middlewares) === null || _b === void 0 ? void 0 : _b.length) ||
|
|
313
|
+
!!((_d = (_c = options.resolverConfig) === null || _c === void 0 ? void 0 : _c.policies) === null || _d === void 0 ? void 0 : _d.length))) {
|
|
314
|
+
throw new utils_1.errors.ApplicationError(`Resolver config is only supported for Query and Mutation root types.`);
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
describe() {
|
|
318
|
+
// Return a snapshot of the current config for debugging/docs
|
|
319
|
+
const { options, initFunctions, typeFunctions, middlewareFunctions, authorizeFunctions, hooks } = initialState;
|
|
320
|
+
return {
|
|
321
|
+
options,
|
|
322
|
+
initFunctions: initFunctions.map(fn => fn.name || 'anonymous'),
|
|
323
|
+
typeFunctions: typeFunctions.length,
|
|
324
|
+
middlewareFunctions: middlewareFunctions.length,
|
|
325
|
+
authorizeFunctions: authorizeFunctions.length,
|
|
326
|
+
hooks: hooks.length
|
|
327
|
+
};
|
|
328
|
+
},
|
|
329
|
+
test({ parent, args, context, info, skipMiddleware = false, skipAuth = false } = {}) {
|
|
330
|
+
this.validate();
|
|
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`);
|
|
334
|
+
}
|
|
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);
|
|
343
|
+
return (async () => {
|
|
344
|
+
if (!skipAuth) {
|
|
345
|
+
await (0, field_authorize_plugin_1.runAuthorization)(pipeline, parent, args, context, testInfo);
|
|
346
|
+
}
|
|
347
|
+
return withMiddlewares(parent, args, context, testInfo);
|
|
348
|
+
})();
|
|
349
|
+
},
|
|
350
|
+
trace(handler) {
|
|
351
|
+
ensureNotBuilt(initialState);
|
|
352
|
+
return (0, exports.createGraphQLFieldBuilder)(undefined, updateState(initialState, (draft) => {
|
|
353
|
+
if (!draft.options.traceHandlers) {
|
|
354
|
+
draft.options.traceHandlers = [];
|
|
355
|
+
}
|
|
356
|
+
// If no handler is provided, use the default
|
|
357
|
+
draft.options.traceHandlers.push(handler !== null && handler !== void 0 ? handler : defaultTraceHandler);
|
|
358
|
+
}));
|
|
359
|
+
},
|
|
360
|
+
build() {
|
|
361
|
+
ensureNotBuilt(initialState);
|
|
362
|
+
this.validate();
|
|
363
|
+
const { options, initFunctions, typeFunctions } = initialState;
|
|
364
|
+
registerBuiltField(options.rootType, options.fieldName);
|
|
365
|
+
const isRootType = ['Query', 'Mutation'].includes(options.rootType);
|
|
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);
|
|
369
|
+
const nexusExtension = (0, nexus_1.extendType)({
|
|
370
|
+
type: options.rootType,
|
|
371
|
+
definition(t) {
|
|
372
|
+
var _a, _b;
|
|
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
|
+
}
|
|
389
|
+
t.field(options.fieldName, {
|
|
390
|
+
type: options.outputType,
|
|
391
|
+
args: nexusArgs,
|
|
392
|
+
description: options.description,
|
|
393
|
+
deprecation: options.deprecationReason,
|
|
394
|
+
extensions: extensions,
|
|
395
|
+
resolve: composeResolver(initialState, options.resolver)
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
return (0, graphql_extension_1.createGraphQLExtension)((_) => ({
|
|
400
|
+
init: () => {
|
|
401
|
+
for (const initFn of initFunctions) {
|
|
402
|
+
initFn();
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
typeDefs: () => options.typeDefs || '',
|
|
406
|
+
types: () => [
|
|
407
|
+
nexusExtension,
|
|
408
|
+
...typeFunctions.flatMap((fn) => fn())
|
|
409
|
+
],
|
|
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
|
+
})
|
|
419
|
+
}));
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
// The implementation is untyped internally; the public generics are enforced by the interface
|
|
423
|
+
return Object.freeze(builder);
|
|
424
|
+
};
|
|
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
|
+
}
|
|
506
|
+
function defaultTraceHandler({ type, fieldName, durationMs, args, error }) {
|
|
507
|
+
// Colors
|
|
508
|
+
const green = '\x1b[32m';
|
|
509
|
+
const yellow = '\x1b[33m';
|
|
510
|
+
const red = '\x1b[31m';
|
|
511
|
+
const cyan = '\x1b[36m';
|
|
512
|
+
const reset = '\x1b[0m';
|
|
513
|
+
// Status
|
|
514
|
+
const status = error ? `${red}❌ ERROR${reset}` : `${green}✅ OK${reset}`;
|
|
515
|
+
// Timing color
|
|
516
|
+
let timingColor = green;
|
|
517
|
+
if (durationMs > 1000)
|
|
518
|
+
timingColor = red;
|
|
519
|
+
else if (durationMs > 300)
|
|
520
|
+
timingColor = yellow;
|
|
521
|
+
// Args preview, with proper table formatting for multi-line
|
|
522
|
+
let argsPreview = '';
|
|
523
|
+
try {
|
|
524
|
+
argsPreview = JSON.stringify(args, null, 2);
|
|
525
|
+
if (argsPreview.length > 120)
|
|
526
|
+
argsPreview = argsPreview.slice(0, 117) + '...';
|
|
527
|
+
}
|
|
528
|
+
catch {
|
|
529
|
+
argsPreview = '[unserializable]';
|
|
530
|
+
}
|
|
531
|
+
const argsLines = argsPreview.split('\n');
|
|
532
|
+
const argsTableLines = argsLines.map((line, idx) => idx === 0
|
|
533
|
+
? `${cyan}│${reset} ${'Args:'.padEnd(12)}${line}`
|
|
534
|
+
: `${cyan}│${reset} ${' '.repeat(12)}${line}`);
|
|
535
|
+
// Table-style output
|
|
536
|
+
const lines = [
|
|
537
|
+
`${cyan}┌─────────────────────────────[GraphQL Field Trace]─────────────────────────────┐${reset}`,
|
|
538
|
+
`${cyan}│${reset} ${'Type:'.padEnd(12)}${type}`,
|
|
539
|
+
`${cyan}│${reset} ${'Field:'.padEnd(12)}${fieldName}`,
|
|
540
|
+
`${cyan}│${reset} ${'Status:'.padEnd(12)}${status}`,
|
|
541
|
+
`${cyan}│${reset} ${'Duration:'.padEnd(12)}${timingColor}${durationMs}ms${reset}`,
|
|
542
|
+
...argsTableLines,
|
|
543
|
+
`${cyan}└───────────────────────────────────────────────────────────────────────────────┘${reset}`
|
|
544
|
+
];
|
|
545
|
+
if (error) {
|
|
546
|
+
lines.push(`${red}[Trace Error]${reset} ${error instanceof Error ? error.stack : error}`);
|
|
547
|
+
console.error(lines.join('\n'));
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
console.log(lines.join('\n'));
|
|
551
|
+
}
|
|
552
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { FieldExtensionBuilder, FieldGroupPreset, GraphQLFieldPreset } from './types';
|
|
2
|
+
import type { Middleware } from '../../nexus';
|
|
3
|
+
/**
|
|
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.
|
|
7
|
+
*/
|
|
8
|
+
export declare function makeAuthMiddleware(): Middleware;
|
|
9
|
+
/**
|
|
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') // ...
|
|
18
|
+
*/
|
|
19
|
+
export declare function createGraphQLFieldPreset(preset: GraphQLFieldPreset): GraphQLFieldPreset;
|
|
20
|
+
/**
|
|
21
|
+
* Export several fields from one file. Builds each builder and returns the extensions.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* export default registerFields(fieldA, fieldB)
|
|
25
|
+
*/
|
|
26
|
+
export declare function registerFields(...fields: FieldExtensionBuilder[]): import("../..").GraphQLExtension[];
|
|
27
|
+
/**
|
|
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))
|
|
33
|
+
*/
|
|
34
|
+
export declare function createFieldGroup(fields: FieldExtensionBuilder[], ...presets: FieldGroupPreset[]): FieldExtensionBuilder[];
|