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/README.md
CHANGED
|
@@ -30,13 +30,115 @@ This fluent, type-safe builder gives you the best DX, composability, and testabi
|
|
|
30
30
|
import { createGraphQLFieldBuilder } from 'buro26-strapi-graphql';
|
|
31
31
|
import { nonNull, stringArg } from 'nexus';
|
|
32
32
|
|
|
33
|
-
export default createGraphQLFieldBuilder(
|
|
33
|
+
export default createGraphQLFieldBuilder()
|
|
34
|
+
.rootType('Query')
|
|
34
35
|
.fieldName('helloWorld')
|
|
35
36
|
.args({ name: nonNull(stringArg()) })
|
|
36
37
|
.outputType(nonNull('String'))
|
|
37
38
|
.description('Returns a personalized greeting')
|
|
38
|
-
.
|
|
39
|
-
.
|
|
39
|
+
.withAuth(false)
|
|
40
|
+
.resolver(async (parent, args) => `Hello World ${args.name}`);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
> **Canonical style:** `createGraphQLFieldBuilder().rootType(...)`, `.withAuth(...)` for authentication and
|
|
44
|
+
> `.authorize(...)` for authorization. `createGraphQLFieldBuilder('Query')` and `.resolverConfig({ auth })`
|
|
45
|
+
> still work but are not used in new code.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 🔗 Relations
|
|
50
|
+
|
|
51
|
+
Use `.relation()` to define (or protect) a relation field. It produces exactly the field Strapi generates:
|
|
52
|
+
the same output type, filter/pagination arguments, resolver (with query validation and output sanitizing)
|
|
53
|
+
and `<target>.find` permission scope, on **Strapi v4 and v5**.
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
export default createGraphQLFieldBuilder()
|
|
57
|
+
.rootType('Order')
|
|
58
|
+
.relation('api::order.order', 'customer')
|
|
59
|
+
.authorize(isOrderCustomer);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
| | Strapi v4 | Strapi v5 |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| to-one | `XEntityResponse` | `X` |
|
|
65
|
+
| to-many | `XRelationResponseCollection` | `[X]!` **and** `<field>_connection` (both get the same auth/middleware) |
|
|
66
|
+
|
|
67
|
+
For to-one relations you can pass `{ batch: true }` to load the relation of all parents in the request with a
|
|
68
|
+
single query (see [Data loading](#-data-loading)).
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 🛡️ Protecting Strapi's Generated Fields
|
|
73
|
+
|
|
74
|
+
`protectFields()` adds authorization and/or middleware to fields Strapi generates for a content type —
|
|
75
|
+
scalars, enums, media, components and relations — **without redefining them**. Strapi's types, arguments and
|
|
76
|
+
resolvers stay untouched, so it behaves the same on v4 and v5 (for v5 to-many relations the
|
|
77
|
+
`<field>_connection` field is protected too).
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
// e.g. src/api/customer/graphql/_protect.ts
|
|
81
|
+
import { protectFields } from 'buro26-strapi-graphql';
|
|
82
|
+
|
|
83
|
+
protectFields<Customer>('api::customer.customer', ['street', 'postalCode', 'iban', 'orders'])
|
|
84
|
+
.authorize((customer, _args, ctx) => isSelf(customer, ctx.state.user))
|
|
85
|
+
.middleware(auditLog);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The checks are attached by the package's Nexus plugins when the schema is built, so the file only needs to be
|
|
89
|
+
loaded from a `graphql` folder; it does not have to export anything.
|
|
90
|
+
|
|
91
|
+
> **Non-null fields:** a denied non-null field makes GraphQL null its parent object (standard error propagation).
|
|
92
|
+
> Strapi generates required attributes and v5 to-many relations (`[X]!`) as non-null, so protecting those hides the
|
|
93
|
+
> whole parent for users without access. A warning is logged when the schema is built. Prefer this over `overrideField` when you
|
|
94
|
+
only want to restrict access.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## ⚡ Data Loading
|
|
99
|
+
|
|
100
|
+
`loaders(ctx)` gives every request its own batching cache (DataLoader-style, no extra dependency). Loads from the
|
|
101
|
+
same tick are combined into one query; results are cached until the request ends.
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import { loaders, sanitizeOutput } from 'buro26-strapi-graphql';
|
|
105
|
+
|
|
106
|
+
// One findMany ... where id IN (...) for all parents in the request
|
|
107
|
+
const order = await loaders(ctx).entity(ORDER, { populate: { customer: true } }).load(parent.id);
|
|
108
|
+
|
|
109
|
+
// The relation of many parents in one query (raw rows, sanitize before returning them)
|
|
110
|
+
const customer = await loaders(ctx).relation(ORDER, 'customer').load(parent.id);
|
|
111
|
+
|
|
112
|
+
// Your own batch function, e.g. for an external API
|
|
113
|
+
const profile = await loaders(ctx).custom('profile', (ids: readonly number[]) => fetchProfiles(ids)).load(parent.id);
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Loaders use `strapi.db.query()` (identical in v4 and v5) and return **unsanitized** database rows. For draft &
|
|
117
|
+
publish content types they load the published version unless `status: 'draft'` is passed.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## 🏷️ Typing Your Project
|
|
122
|
+
|
|
123
|
+
Register your user type and entity types once, so `context.state.user` and `parent` are typed everywhere:
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
declare module 'buro26-strapi-graphql' {
|
|
127
|
+
interface Register {
|
|
128
|
+
user: User;
|
|
129
|
+
types: { Customer: Customer; Order: Order };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
createGraphQLFieldBuilder()
|
|
134
|
+
.rootType('Customer') // parent is now typed as Customer
|
|
135
|
+
.fieldName('fullName')
|
|
136
|
+
.outputType('String')
|
|
137
|
+
.resolver((customer) => `${customer?.firstName} ${customer?.lastName}`);
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
For registered entity types, `parent` is `Partial<Entity>` with `id` required: Strapi always passes the entity,
|
|
141
|
+
but relations are only present when populated. Unregistered `Query`/`Mutation` fields keep `parent: any`.
|
|
40
142
|
```
|
|
41
143
|
|
|
42
144
|
---
|
|
@@ -141,10 +243,15 @@ t.field('myField', {
|
|
|
141
243
|
You can register **global middleware** and **global authorization** that apply to multiple fields based on type, field
|
|
142
244
|
name, or tags.
|
|
143
245
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
246
|
+
Global middleware and authorization are matched when the GraphQL schema is built, so they apply regardless of
|
|
247
|
+
the order in which files are loaded. The matcher receives `{ type, fieldName, tags, contentType, isBuilderField }`.
|
|
248
|
+
|
|
249
|
+
By default globals only apply to fields created with the field builder. Pass `{ includeNonBuilderFields: true }`
|
|
250
|
+
to also apply them to Strapi's generated fields and plain Nexus fields:
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
registerGlobalAuthorize(() => true, rateLimit, { includeNonBuilderFields: true });
|
|
254
|
+
```
|
|
148
255
|
|
|
149
256
|
**Example:**
|
|
150
257
|
|
|
@@ -174,11 +281,9 @@ export default createGraphQLFieldBuilder('Query')
|
|
|
174
281
|
.resolver(() => true);
|
|
175
282
|
```
|
|
176
283
|
|
|
177
|
-
**
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
exports).
|
|
181
|
-
- This guarantees that all fields will be able to consume the global logic.
|
|
284
|
+
**Order of execution per field:** authentication (`withAuth(true)`) → global authorizes → the field's own
|
|
285
|
+
`authorize` → `protectFields` authorizes → global middlewares → the field's own middlewares → `protectFields`
|
|
286
|
+
middlewares → resolver (argument validation, hooks, tracing).
|
|
182
287
|
|
|
183
288
|
---
|
|
184
289
|
|
|
@@ -187,14 +292,14 @@ export default createGraphQLFieldBuilder('Query')
|
|
|
187
292
|
- Use the **builder’s `.authorize()` and `.middleware()`** methods for most use-cases—they automatically set the correct
|
|
188
293
|
`extensions` properties.
|
|
189
294
|
- Use the `extensions` property directly for advanced or manual Nexus usage.
|
|
190
|
-
-
|
|
295
|
+
- Use **`protectFields()`** to restrict Strapi's generated fields and **`.relation()`** for relation fields.
|
|
191
296
|
- Use **tags** to target groups of fields for global logic.
|
|
192
297
|
|
|
193
298
|
---
|
|
194
299
|
|
|
195
300
|
### **Validation, Tracing, and Testing**
|
|
196
301
|
|
|
197
|
-
**Argument validation with Zod
|
|
302
|
+
**Argument validation with Zod** (also types the resolver's `args` as the parsed schema output):
|
|
198
303
|
|
|
199
304
|
```typescript
|
|
200
305
|
import { z } from 'zod';
|
|
@@ -222,7 +327,8 @@ export default createGraphQLFieldBuilder('Query')
|
|
|
222
327
|
});
|
|
223
328
|
```
|
|
224
329
|
|
|
225
|
-
**Testing in isolation
|
|
330
|
+
**Testing in isolation** (runs the same pipeline as in production: global and field authorization, `withAuth`,
|
|
331
|
+
middlewares, argument validation and hooks; pass `skipAuth` / `skipMiddleware` to leave parts out):
|
|
226
332
|
|
|
227
333
|
```typescript
|
|
228
334
|
const field = createGraphQLFieldBuilder('Query')
|
|
@@ -316,7 +422,11 @@ export default createGraphQLExtension()
|
|
|
316
422
|
|
|
317
423
|
## 🧩 Overriding and Extending Fields
|
|
318
424
|
|
|
319
|
-
|
|
425
|
+
> To only restrict access to generated fields, use [`protectFields()`](#️-protecting-strapis-generated-fields);
|
|
426
|
+
> for relations, use [`.relation()`](#-relations). Use `overrideField` when you need to replace a field's resolver.
|
|
427
|
+
|
|
428
|
+
You can override or extend fields in existing types using the `overrideField` helper. Pass authorization via
|
|
429
|
+
`authorize` (or `extensions.authorize`); relation fields are generated for both Strapi v4 and v5:
|
|
320
430
|
|
|
321
431
|
```typescript
|
|
322
432
|
import { createGraphQLExtension, overrideField } from 'buro26-strapi-graphql';
|
|
@@ -461,6 +571,8 @@ To run tests:
|
|
|
461
571
|
bun test
|
|
462
572
|
```
|
|
463
573
|
|
|
574
|
+
Tests live in `test/` and run against real Nexus/GraphQL schemas with a minimal `strapi` mock.
|
|
575
|
+
|
|
464
576
|
---
|
|
465
577
|
|
|
466
578
|
## 🤝 Contributing
|
package/dist/server/index.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ declare const _default: {
|
|
|
27
27
|
fileService: ({ strapi }: {
|
|
28
28
|
strapi: Required<import("@strapi/types").Strapi>;
|
|
29
29
|
}) => {
|
|
30
|
-
getAllFilesInGraphQlDirectory(directoryPath: string
|
|
30
|
+
getAllFilesInGraphQlDirectory(directoryPath: string): Promise<GraphQLExtension[]>;
|
|
31
31
|
};
|
|
32
32
|
graphQlService: ({ strapi }: {
|
|
33
33
|
strapi: Required<import("@strapi/types").Strapi>;
|
|
@@ -8,8 +8,29 @@ export interface FieldAuthorizePluginErrorConfig {
|
|
|
8
8
|
ctx: GetGen<'context'>;
|
|
9
9
|
info: GraphQLResolveInfo;
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Options for {@link nexusFieldAuthorizePlugin}.
|
|
13
|
+
*/
|
|
11
14
|
export interface FieldAuthorizePluginConfig {
|
|
15
|
+
/**
|
|
16
|
+
* Turn a denied authorization into the error sent to the client.
|
|
17
|
+
* @default {@link defaultFormatError}
|
|
18
|
+
*/
|
|
12
19
|
formatError?: (authConfig: FieldAuthorizePluginErrorConfig) => Error;
|
|
13
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Default error for denied authorization: a Strapi `ForbiddenError` with the original message and
|
|
23
|
+
* trace details (type, field, user id, args, timestamp) in `details.trace`.
|
|
24
|
+
*/
|
|
14
25
|
export declare const defaultFormatError: ({ error, root, args, ctx, info }: FieldAuthorizePluginErrorConfig) => Error;
|
|
26
|
+
/**
|
|
27
|
+
* Field-level authentication and authorization.
|
|
28
|
+
*
|
|
29
|
+
* Applies to every field in the schema (including the ones Strapi generates) and combines:
|
|
30
|
+
* `withAuth(true)` of builder fields, global authorizes, the field's own `authorize` (in `extensions` or
|
|
31
|
+
* as top-level field option) and `protectFields()` registrations.
|
|
32
|
+
*
|
|
33
|
+
* Registered automatically by this Strapi plugin; only add it yourself when building a schema outside
|
|
34
|
+
* Strapi (e.g. in tests).
|
|
35
|
+
*/
|
|
15
36
|
export declare const nexusFieldAuthorizePlugin: (authConfig?: FieldAuthorizePluginConfig) => import("nexus/dist/plugin").NexusPlugin;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.nexusFieldAuthorizePlugin = exports.defaultFormatError = void 0;
|
|
3
|
+
exports.nexusFieldAuthorizePlugin = exports.runAuthorization = exports.defaultFormatError = void 0;
|
|
4
4
|
const nexus_1 = require("nexus");
|
|
5
5
|
const utils_1 = require("nexus/dist/utils");
|
|
6
6
|
const utils_2 = require("@strapi/utils");
|
|
7
|
+
const graphql_1 = require("graphql");
|
|
8
|
+
const resolve_field_1 = require("../../utils/registry/resolve-field");
|
|
7
9
|
const fieldDefTypes = (0, utils_1.printedGenTyping)({
|
|
8
10
|
optional: true,
|
|
9
11
|
name: 'authorize',
|
|
@@ -17,6 +19,10 @@ const fieldDefTypes = (0, utils_1.printedGenTyping)({
|
|
|
17
19
|
`,
|
|
18
20
|
type: 'FieldAuthorizeResolver<TypeName, FieldName>',
|
|
19
21
|
});
|
|
22
|
+
/**
|
|
23
|
+
* Default error for denied authorization: a Strapi `ForbiddenError` with the original message and
|
|
24
|
+
* trace details (type, field, user id, args, timestamp) in `details.trace`.
|
|
25
|
+
*/
|
|
20
26
|
const defaultFormatError = ({ error, root, args, ctx, info }) => {
|
|
21
27
|
var _a, _b;
|
|
22
28
|
// Add rich tracing info
|
|
@@ -32,9 +38,15 @@ const defaultFormatError = ({ error, root, args, ctx, info }) => {
|
|
|
32
38
|
return new utils_2.errors.ForbiddenError((error === null || error === void 0 ? void 0 : error.message) || 'Not authorized', { trace });
|
|
33
39
|
};
|
|
34
40
|
exports.defaultFormatError = defaultFormatError;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Run the authentication + authorization part of a field pipeline.
|
|
43
|
+
* Resolves when allowed, rejects with the (formatted) error otherwise.
|
|
44
|
+
*
|
|
45
|
+
* @internal Used by the authorize plugin and `.test()`.
|
|
46
|
+
*/
|
|
47
|
+
const runAuthorization = async (pipeline, root, args, ctx, info, formatError = exports.defaultFormatError) => {
|
|
48
|
+
var _a, _b;
|
|
49
|
+
const fail = (error) => {
|
|
38
50
|
const finalErr = formatError({ error, root, args, ctx, info });
|
|
39
51
|
if (finalErr instanceof Error) {
|
|
40
52
|
throw finalErr;
|
|
@@ -42,46 +54,67 @@ const nexusFieldAuthorizePlugin = (authConfig = {}) => {
|
|
|
42
54
|
console.error(`Non-Error value ${finalErr} returned from custom formatError in authorize plugin`);
|
|
43
55
|
throw new utils_2.errors.ForbiddenError('Not authorized');
|
|
44
56
|
};
|
|
57
|
+
if (pipeline.requireAuth && !((_a = ctx === null || ctx === void 0 ? void 0 : ctx.state) === null || _a === void 0 ? void 0 : _a.user)) {
|
|
58
|
+
// Same error as before, but now evaluated before any authorize function runs
|
|
59
|
+
throw new utils_2.errors.ForbiddenError('Not authenticated');
|
|
60
|
+
}
|
|
61
|
+
for (const authorize of pipeline.authorizes) {
|
|
62
|
+
let authResult;
|
|
63
|
+
try {
|
|
64
|
+
authResult = await authorize(root, args, ctx, info);
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
fail(e instanceof Error ? e : new utils_2.errors.ForbiddenError(String(e)));
|
|
68
|
+
}
|
|
69
|
+
if (authResult === true)
|
|
70
|
+
continue;
|
|
71
|
+
if (authResult instanceof Error)
|
|
72
|
+
fail(authResult);
|
|
73
|
+
if (authResult === false)
|
|
74
|
+
fail(new utils_2.errors.ForbiddenError('Not authorized'));
|
|
75
|
+
fail(new utils_2.errors.ForbiddenError(`Authorize for ${(_b = info === null || info === void 0 ? void 0 : info.parentType) === null || _b === void 0 ? void 0 : _b.name}.${info === null || info === void 0 ? void 0 : info.fieldName} Expected a boolean or Error, saw ${authResult}`));
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
exports.runAuthorization = runAuthorization;
|
|
79
|
+
/**
|
|
80
|
+
* Field-level authentication and authorization.
|
|
81
|
+
*
|
|
82
|
+
* Applies to every field in the schema (including the ones Strapi generates) and combines:
|
|
83
|
+
* `withAuth(true)` of builder fields, global authorizes, the field's own `authorize` (in `extensions` or
|
|
84
|
+
* as top-level field option) and `protectFields()` registrations.
|
|
85
|
+
*
|
|
86
|
+
* Registered automatically by this Strapi plugin; only add it yourself when building a schema outside
|
|
87
|
+
* Strapi (e.g. in tests).
|
|
88
|
+
*/
|
|
89
|
+
const nexusFieldAuthorizePlugin = (authConfig = {}) => {
|
|
90
|
+
const { formatError = exports.defaultFormatError } = authConfig;
|
|
45
91
|
return (0, nexus_1.plugin)({
|
|
46
92
|
name: 'CustomFieldAuthorize',
|
|
47
93
|
description: 'Custom field-level authorization for Strapi GraphQL, with tracing.',
|
|
48
94
|
fieldDefTypes: fieldDefTypes,
|
|
49
95
|
onCreateFieldResolver(config) {
|
|
50
|
-
var _a;
|
|
96
|
+
var _a, _b, _c;
|
|
51
97
|
// @ts-ignore
|
|
52
|
-
const
|
|
53
|
-
if (authorize
|
|
54
|
-
|
|
98
|
+
const extensions = config.fieldConfig.extensions;
|
|
99
|
+
if ((extensions === null || extensions === void 0 ? void 0 : extensions.authorize) != null && typeof extensions.authorize !== 'function') {
|
|
100
|
+
console.error(new Error(`The authorize property provided to ${config.fieldConfig.name} with type ${config.fieldConfig.type} should be a function, saw ${typeof extensions.authorize}`));
|
|
101
|
+
// Fail closed: a broken authorize must not silently disable the other checks on this field
|
|
102
|
+
return async function () {
|
|
103
|
+
throw new utils_2.errors.ForbiddenError('Not authorized');
|
|
104
|
+
};
|
|
55
105
|
}
|
|
56
|
-
|
|
57
|
-
|
|
106
|
+
const pipeline = (0, resolve_field_1.resolveFieldPipeline)(config.parentTypeConfig.name, config.fieldConfig.name, extensions);
|
|
107
|
+
if (!pipeline.requireAuth && pipeline.authorizes.length === 0) {
|
|
58
108
|
return;
|
|
59
109
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
return nexus_1.plugin.completeValue(toComplete, (authResult) => {
|
|
70
|
-
if (authResult === true) {
|
|
71
|
-
return next(root, args, ctx, info);
|
|
72
|
-
}
|
|
73
|
-
const finalFormatError = ensureError(root, args, ctx, info);
|
|
74
|
-
if (authResult instanceof Error) {
|
|
75
|
-
finalFormatError(authResult);
|
|
76
|
-
}
|
|
77
|
-
if (authResult === false) {
|
|
78
|
-
finalFormatError(new utils_2.errors.ForbiddenError('Not authorized'));
|
|
79
|
-
}
|
|
80
|
-
const { fieldName, parentType: { name: parentTypeName } } = info;
|
|
81
|
-
finalFormatError(new utils_2.errors.ForbiddenError(`Authorize for ${parentTypeName}.${fieldName} Expected a boolean or Error, saw ${authResult}`));
|
|
82
|
-
}, (err) => {
|
|
83
|
-
ensureError(root, args, ctx, info)(err);
|
|
84
|
-
});
|
|
110
|
+
if (pipeline.isProtected && (0, graphql_1.isNonNullType)(config.fieldConfig.type)) {
|
|
111
|
+
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;
|
|
112
|
+
warn(`[buro26-strapi-graphql] protectFields: "${config.parentTypeConfig.name}.${config.fieldConfig.name}" is non-null; ` +
|
|
113
|
+
`when access is denied GraphQL nulls the parent object instead of only this field.`);
|
|
114
|
+
}
|
|
115
|
+
return async function (root, args, ctx, info, next) {
|
|
116
|
+
await (0, exports.runAuthorization)(pipeline, root, args, ctx, info, formatError);
|
|
117
|
+
return next(root, args, ctx, info);
|
|
85
118
|
};
|
|
86
119
|
}
|
|
87
120
|
});
|
|
@@ -9,9 +9,17 @@ export type Resolver = <Parent extends any, Args extends any, Context = ResolveC
|
|
|
9
9
|
* Middleware is a function that takes a resolver and returns a new resolver.
|
|
10
10
|
*/
|
|
11
11
|
export type Middleware<TParent = any, TArgs = any> = (resolver: ResolverFunction<TParent, TArgs>) => ResolverFunction<TParent, TArgs>;
|
|
12
|
+
/**
|
|
13
|
+
* Field-level middlewares: global middlewares, the field's own `middlewares` (in `extensions` or as
|
|
14
|
+
* top-level field option) and `protectFields()` middlewares, outermost first.
|
|
15
|
+
*
|
|
16
|
+
* Registered automatically by this Strapi plugin; only add it yourself when building a schema outside
|
|
17
|
+
* Strapi (e.g. in tests).
|
|
18
|
+
*/
|
|
12
19
|
export declare const nexusMiddlewarePlugin: () => import("nexus/dist/plugin").NexusPlugin;
|
|
13
20
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
21
|
+
* Example middleware that logs every resolve, including the result.
|
|
22
|
+
*
|
|
23
|
+
* Development only: it logs resolved values, which may contain personal data.
|
|
16
24
|
*/
|
|
17
25
|
export declare const loggingMiddleware: Middleware;
|
|
@@ -3,19 +3,23 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.loggingMiddleware = exports.nexusMiddlewarePlugin = void 0;
|
|
4
4
|
const nexus_1 = require("nexus");
|
|
5
5
|
const utils_1 = require("nexus/dist/utils");
|
|
6
|
+
const resolve_field_1 = require("../../utils/registry/resolve-field");
|
|
6
7
|
const fieldDefTypes = (0, utils_1.printedGenTyping)({
|
|
7
8
|
optional: true,
|
|
8
9
|
name: 'middlewares',
|
|
9
10
|
description: `
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
Returning "false" or "Promise<false>" will respond
|
|
13
|
-
with a "Not Authorized" error for the field.
|
|
14
|
-
Returning or throwing an error will also prevent the
|
|
15
|
-
resolver from executing.
|
|
11
|
+
Middlewares for an individual field. Each middleware receives the
|
|
12
|
+
next resolver and returns a new resolver.
|
|
16
13
|
`,
|
|
17
14
|
type: 'any'
|
|
18
15
|
});
|
|
16
|
+
/**
|
|
17
|
+
* Field-level middlewares: global middlewares, the field's own `middlewares` (in `extensions` or as
|
|
18
|
+
* top-level field option) and `protectFields()` middlewares, outermost first.
|
|
19
|
+
*
|
|
20
|
+
* Registered automatically by this Strapi plugin; only add it yourself when building a schema outside
|
|
21
|
+
* Strapi (e.g. in tests).
|
|
22
|
+
*/
|
|
19
23
|
const nexusMiddlewarePlugin = () => (0, nexus_1.plugin)({
|
|
20
24
|
name: 'NexusMiddlewarePlugin',
|
|
21
25
|
description: 'The middleware plugin allows attaching middleware functions to individual fields.',
|
|
@@ -27,43 +31,26 @@ const nexusMiddlewarePlugin = () => (0, nexus_1.plugin)({
|
|
|
27
31
|
* Hook into field resolver creation to apply middleware.
|
|
28
32
|
*/
|
|
29
33
|
onCreateFieldResolver(config) {
|
|
30
|
-
var _a;
|
|
31
34
|
const { fieldConfig } = config;
|
|
35
|
+
const { middlewares } = (0, resolve_field_1.resolveFieldPipeline)(config.parentTypeConfig.name, fieldConfig.name,
|
|
32
36
|
// @ts-ignore
|
|
33
|
-
|
|
37
|
+
fieldConfig.extensions);
|
|
34
38
|
if (middlewares.length === 0) {
|
|
35
39
|
return undefined; // No middleware to apply
|
|
36
40
|
}
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
};
|
|
43
|
-
});
|
|
44
|
-
// Compose the middleware functions into a single MiddlewareFn
|
|
45
|
-
return async (source, args, context, info, next) => {
|
|
46
|
-
let index = -1;
|
|
47
|
-
const dispatch = async (i) => {
|
|
48
|
-
if (i <= index)
|
|
49
|
-
throw new Error('next() called multiple times');
|
|
50
|
-
index = i;
|
|
51
|
-
const middlewareFn = middlewareFns[i];
|
|
52
|
-
if (middlewareFn) {
|
|
53
|
-
return middlewareFn(source, args, context, info, () => dispatch(i + 1));
|
|
54
|
-
}
|
|
55
|
-
else {
|
|
56
|
-
return next(source, args, context, info);
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
return dispatch(0);
|
|
41
|
+
// Outermost first. Each middleware may call the next resolver with changed parent/args/context,
|
|
42
|
+
// which are passed on (same composition as `.test()`).
|
|
43
|
+
return (source, args, context, info, next) => {
|
|
44
|
+
const resolver = middlewares.reduceRight((inner, middleware) => middleware(inner), (s, a, c, i) => next(s, a, c, i));
|
|
45
|
+
return resolver(source, args, context, info);
|
|
60
46
|
};
|
|
61
47
|
}
|
|
62
48
|
});
|
|
63
49
|
exports.nexusMiddlewarePlugin = nexusMiddlewarePlugin;
|
|
64
50
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
51
|
+
* Example middleware that logs every resolve, including the result.
|
|
52
|
+
*
|
|
53
|
+
* Development only: it logs resolved values, which may contain personal data.
|
|
67
54
|
*/
|
|
68
55
|
const loggingMiddleware = (resolver) => {
|
|
69
56
|
return async (parent, args, context, info) => {
|
|
@@ -3,13 +3,14 @@ declare const _default: ({ strapi }: {
|
|
|
3
3
|
strapi: Strapi;
|
|
4
4
|
}) => {
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
* Recursively searches for files in the graphql directory
|
|
8
|
-
* Returns an array of GraphQLExtension objects
|
|
6
|
+
* Load all extensions from `graphql` folders below `directoryPath` (recursively).
|
|
9
7
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* Files are loaded in a stable order (sorted by name, depth-first), so the generated schema and the
|
|
9
|
+
* order of side effects such as `protectFields()` / `registerGlobalAuthorize()` calls are the same on
|
|
10
|
+
* every start.
|
|
11
|
+
*
|
|
12
|
+
* @param directoryPath - Usually `strapi.dirs.dist.src`.
|
|
12
13
|
*/
|
|
13
|
-
getAllFilesInGraphQlDirectory(directoryPath: string
|
|
14
|
+
getAllFilesInGraphQlDirectory(directoryPath: string): Promise<GraphQLExtension[]>;
|
|
14
15
|
};
|
|
15
16
|
export default _default;
|
|
@@ -25,50 +25,50 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
26
|
const fs_1 = require("fs");
|
|
27
27
|
const path = __importStar(require("path"));
|
|
28
|
+
const GRAPHQL_FOLDER = 'graphql';
|
|
29
|
+
const isInGraphQLFolder = (directoryPath) => directoryPath.endsWith(path.sep + GRAPHQL_FOLDER) || directoryPath.includes(path.sep + GRAPHQL_FOLDER + path.sep);
|
|
30
|
+
const isLoadableFile = (file) => file.endsWith('.js');
|
|
31
|
+
/**
|
|
32
|
+
* Turn a module's export into extensions. Builders (anything with `build()`) are built; arrays are flattened.
|
|
33
|
+
*/
|
|
34
|
+
const toExtensions = (graphqlModule) => {
|
|
35
|
+
var _a;
|
|
36
|
+
const exported = (_a = graphqlModule === null || graphqlModule === void 0 ? void 0 : graphqlModule.default) !== null && _a !== void 0 ? _a : graphqlModule;
|
|
37
|
+
if (!exported)
|
|
38
|
+
return [];
|
|
39
|
+
const build = (item) => (item && typeof item.build === 'function' ? item.build() : item);
|
|
40
|
+
if (Array.isArray(exported)) {
|
|
41
|
+
return exported.map(build).filter(Boolean);
|
|
42
|
+
}
|
|
43
|
+
// Modules without a default export (e.g. one that only calls `protectFields`) yield an empty extension,
|
|
44
|
+
// which registers nothing.
|
|
45
|
+
return [build(exported)];
|
|
46
|
+
};
|
|
28
47
|
exports.default = ({ strapi }) => ({
|
|
29
48
|
/**
|
|
30
|
-
*
|
|
31
|
-
* Recursively searches for files in the graphql directory
|
|
32
|
-
* Returns an array of GraphQLExtension objects
|
|
49
|
+
* Load all extensions from `graphql` folders below `directoryPath` (recursively).
|
|
33
50
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
51
|
+
* Files are loaded in a stable order (sorted by name, depth-first), so the generated schema and the
|
|
52
|
+
* order of side effects such as `protectFields()` / `registerGlobalAuthorize()` calls are the same on
|
|
53
|
+
* every start.
|
|
54
|
+
*
|
|
55
|
+
* @param directoryPath - Usually `strapi.dirs.dist.src`.
|
|
36
56
|
*/
|
|
37
|
-
async getAllFilesInGraphQlDirectory(directoryPath
|
|
38
|
-
const fileEntries = await fs_1.promises.readdir(directoryPath);
|
|
39
|
-
|
|
40
|
-
const
|
|
57
|
+
async getAllFilesInGraphQlDirectory(directoryPath) {
|
|
58
|
+
const fileEntries = [...await fs_1.promises.readdir(directoryPath)].sort();
|
|
59
|
+
const collected = [];
|
|
60
|
+
for (const file of fileEntries) {
|
|
41
61
|
const filePath = path.join(directoryPath, file);
|
|
42
62
|
const fileStats = await fs_1.promises.stat(filePath);
|
|
43
63
|
if (fileStats.isDirectory()) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
const folderName = 'graphql';
|
|
48
|
-
if (!directoryPath.endsWith(path.sep + folderName) &&
|
|
49
|
-
!directoryPath.includes(path.sep + folderName + path.sep)) {
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
const graphqlModule = require(filePath.replace('.js', ''));
|
|
53
|
-
if (!graphqlModule) {
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
// Support both ES module default export and CommonJS exports
|
|
57
|
-
let extension = graphqlModule.default || graphqlModule;
|
|
58
|
-
// If the extension is a builder (has a build function), call build() automatically
|
|
59
|
-
if (extension && typeof extension.build === 'function') {
|
|
60
|
-
extension = extension.build();
|
|
61
|
-
}
|
|
62
|
-
// If it's an array, flatten and push all
|
|
63
|
-
if (Array.isArray(extension)) {
|
|
64
|
-
collectedExtensions.push(...extension.map(ext => ext.build ? ext.build() : ext));
|
|
64
|
+
collected.push(...await this.getAllFilesInGraphQlDirectory(filePath));
|
|
65
|
+
continue;
|
|
65
66
|
}
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
if (!isInGraphQLFolder(directoryPath) || !isLoadableFile(file)) {
|
|
68
|
+
continue;
|
|
68
69
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return collectedExtensions;
|
|
70
|
+
collected.push(...toExtensions(require(filePath)));
|
|
71
|
+
}
|
|
72
|
+
return collected;
|
|
73
73
|
}
|
|
74
74
|
});
|
|
@@ -2,7 +2,7 @@ declare const _default: {
|
|
|
2
2
|
fileService: ({ strapi }: {
|
|
3
3
|
strapi: Required<import("@strapi/types").Strapi>;
|
|
4
4
|
}) => {
|
|
5
|
-
getAllFilesInGraphQlDirectory(directoryPath: string
|
|
5
|
+
getAllFilesInGraphQlDirectory(directoryPath: string): Promise<import("..").GraphQLExtension[]>;
|
|
6
6
|
};
|
|
7
7
|
graphQlService: ({ strapi }: {
|
|
8
8
|
strapi: Required<import("@strapi/types").Strapi>;
|