buro26-strapi-graphql 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +173 -16
- package/dist/server/index.d.ts +1 -1
- package/dist/server/nexus/plugins/field-authorize.plugin.d.ts +21 -0
- package/dist/server/nexus/plugins/field-authorize.plugin.js +68 -35
- package/dist/server/nexus/plugins/middleware.plugin.d.ts +10 -2
- package/dist/server/nexus/plugins/middleware.plugin.js +20 -33
- package/dist/server/services/file-service.d.ts +7 -6
- package/dist/server/services/file-service.js +36 -36
- package/dist/server/services/index.d.ts +1 -1
- package/dist/server/types.d.ts +18 -3
- package/dist/server/utils/collection-type.d.ts +13 -2
- package/dist/server/utils/collection-type.js +13 -2
- package/dist/server/utils/field-builder/builder.d.ts +13 -4
- package/dist/server/utils/field-builder/builder.js +214 -119
- package/dist/server/utils/field-builder/helpers.d.ts +21 -6
- package/dist/server/utils/field-builder/helpers.js +26 -4
- package/dist/server/utils/field-builder/types.d.ts +297 -76
- package/dist/server/utils/graphql-extension.d.ts +24 -3
- package/dist/server/utils/graphql-extension.js +73 -7
- package/dist/server/utils/index.d.ts +2 -0
- package/dist/server/utils/index.js +11 -0
- package/dist/server/utils/loaders.d.ts +110 -0
- package/dist/server/utils/loaders.js +222 -0
- package/dist/server/utils/misc.d.ts +6 -0
- package/dist/server/utils/misc.js +6 -0
- package/dist/server/utils/override.d.ts +32 -0
- package/dist/server/utils/override.js +54 -26
- package/dist/server/utils/registry/field-protection.d.ts +43 -0
- package/dist/server/utils/registry/field-protection.js +65 -0
- package/dist/server/utils/registry/global-registry.d.ts +69 -14
- package/dist/server/utils/registry/global-registry.js +53 -7
- package/dist/server/utils/registry/index.d.ts +2 -0
- package/dist/server/utils/registry/index.js +2 -0
- package/dist/server/utils/registry/resolve-field.d.ts +1 -0
- package/dist/server/utils/registry/resolve-field.js +67 -0
- package/dist/server/utils/shadow-crud.d.ts +28 -1
- package/dist/server/utils/shadow-crud.js +32 -1
- package/dist/server/utils/single-type.d.ts +30 -1
- package/dist/server/utils/single-type.js +25 -0
- package/dist/server/utils/strapi-compat.d.ts +90 -0
- package/dist/server/utils/strapi-compat.js +203 -0
- package/dist/server/utils/types.d.ts +25 -1
- package/dist/tsconfig.server.tsbuildinfo +1 -1
- package/package.json +3 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createGraphQLExtension = void 0;
|
|
3
|
+
exports.mapFieldPresetToExtension = exports.createGraphQLExtension = void 0;
|
|
4
|
+
const field_builder_1 = require("./field-builder");
|
|
4
5
|
/**
|
|
5
6
|
* Internal function that creates a new builder instance.
|
|
6
7
|
*/
|
|
@@ -12,31 +13,31 @@ function graphQLExtensionBuilder(options = {}) {
|
|
|
12
13
|
typeDefs(sdl) {
|
|
13
14
|
return graphQLExtensionBuilder({
|
|
14
15
|
...options,
|
|
15
|
-
typeDefs: typeof sdl === 'string' ? () => sdl : sdl
|
|
16
|
+
typeDefs: typeof sdl === 'string' ? () => sdl : sdl
|
|
16
17
|
});
|
|
17
18
|
},
|
|
18
19
|
types(typesFn) {
|
|
19
20
|
return graphQLExtensionBuilder({
|
|
20
21
|
...options,
|
|
21
|
-
types: typeof typesFn === 'function' ? typesFn : () => typesFn
|
|
22
|
+
types: typeof typesFn === 'function' ? typesFn : () => typesFn
|
|
22
23
|
});
|
|
23
24
|
},
|
|
24
25
|
plugins(pluginsFn) {
|
|
25
26
|
return graphQLExtensionBuilder({
|
|
26
27
|
...options,
|
|
27
|
-
plugins: typeof pluginsFn === 'function' ? pluginsFn : () => pluginsFn
|
|
28
|
+
plugins: typeof pluginsFn === 'function' ? pluginsFn : () => pluginsFn
|
|
28
29
|
});
|
|
29
30
|
},
|
|
30
31
|
resolvers(resolversObj) {
|
|
31
32
|
return graphQLExtensionBuilder({
|
|
32
33
|
...options,
|
|
33
|
-
resolvers: typeof resolversObj === 'function' ? resolversObj : () => resolversObj
|
|
34
|
+
resolvers: typeof resolversObj === 'function' ? resolversObj : () => resolversObj
|
|
34
35
|
});
|
|
35
36
|
},
|
|
36
37
|
resolversConfig(configObj) {
|
|
37
38
|
return graphQLExtensionBuilder({
|
|
38
39
|
...options,
|
|
39
|
-
resolversConfig: typeof configObj === 'function' ? configObj : () => configObj
|
|
40
|
+
resolversConfig: typeof configObj === 'function' ? configObj : () => configObj
|
|
40
41
|
});
|
|
41
42
|
},
|
|
42
43
|
use(preset) {
|
|
@@ -44,7 +45,7 @@ function graphQLExtensionBuilder(options = {}) {
|
|
|
44
45
|
},
|
|
45
46
|
build() {
|
|
46
47
|
return { ...options };
|
|
47
|
-
}
|
|
48
|
+
}
|
|
48
49
|
};
|
|
49
50
|
}
|
|
50
51
|
function createGraphQLExtension(extension) {
|
|
@@ -55,3 +56,68 @@ function createGraphQLExtension(extension) {
|
|
|
55
56
|
return extension(strapi);
|
|
56
57
|
}
|
|
57
58
|
exports.createGraphQLExtension = createGraphQLExtension;
|
|
59
|
+
/**
|
|
60
|
+
* Maps a field-level preset to all (or matching) fields in a GraphQLExtension.
|
|
61
|
+
* Throws if a field is not a function (i.e., not a resolver).
|
|
62
|
+
*
|
|
63
|
+
* @param fieldPreset - a GraphQLFieldPreset (FieldExtensionBuilder => FieldExtensionBuilder)
|
|
64
|
+
* @param matcher - a function (type, fieldName) => boolean or an array of field names
|
|
65
|
+
* @returns a function that takes a GraphQLExtension and returns a new one with wrapped resolvers
|
|
66
|
+
*/
|
|
67
|
+
function mapFieldPresetToExtension(fieldPreset, matcher = () => true) {
|
|
68
|
+
return (extension) => {
|
|
69
|
+
var _a, _b, _c;
|
|
70
|
+
const originalResolvers = ((_a = extension.resolvers) === null || _a === void 0 ? void 0 : _a.call(extension)) || {};
|
|
71
|
+
const newResolvers = {};
|
|
72
|
+
for (const type of Object.keys(originalResolvers)) {
|
|
73
|
+
newResolvers[type] = {};
|
|
74
|
+
// @ts-ignore
|
|
75
|
+
for (const fieldName of Object.keys(originalResolvers[type] || {})) {
|
|
76
|
+
const shouldApply = typeof matcher === 'function'
|
|
77
|
+
? matcher(type, fieldName)
|
|
78
|
+
: matcher.includes(fieldName);
|
|
79
|
+
// @ts-ignore
|
|
80
|
+
const originalResolver = originalResolvers[type][fieldName];
|
|
81
|
+
if (typeof originalResolver !== 'function') {
|
|
82
|
+
throw new Error(`Cannot apply field preset: ${type}.${fieldName} is not a resolver function`);
|
|
83
|
+
}
|
|
84
|
+
if (shouldApply) {
|
|
85
|
+
// Build a field builder for this resolver
|
|
86
|
+
let fieldBuilder = (0, field_builder_1.createGraphQLFieldBuilder)(type)
|
|
87
|
+
.fieldName(fieldName)
|
|
88
|
+
.resolver(originalResolver);
|
|
89
|
+
// Apply the preset
|
|
90
|
+
fieldBuilder = fieldPreset(fieldBuilder);
|
|
91
|
+
// Build the field and extract the resolver
|
|
92
|
+
const builtExtension = fieldBuilder.build();
|
|
93
|
+
const builtType = (_c = (_b = builtExtension === null || builtExtension === void 0 ? void 0 : builtExtension.types) === null || _b === void 0 ? void 0 : _b.call(builtExtension)) === null || _c === void 0 ? void 0 : _c.find((t) => t.name === type);
|
|
94
|
+
if (!builtType || !builtType.definition) {
|
|
95
|
+
throw new Error(`Cannot extract wrapped resolver for ${type}.${fieldName}`);
|
|
96
|
+
}
|
|
97
|
+
// Extract the resolver from the field definition
|
|
98
|
+
let wrappedResolver = null;
|
|
99
|
+
builtType.definition({
|
|
100
|
+
field: (name, config) => {
|
|
101
|
+
if (name === fieldName) {
|
|
102
|
+
wrappedResolver = config.resolve;
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
if (typeof wrappedResolver !== 'function') {
|
|
107
|
+
throw new Error(`Preset did not produce a valid resolver for ${type}.${fieldName}`);
|
|
108
|
+
}
|
|
109
|
+
newResolvers[type][fieldName] = wrappedResolver;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
newResolvers[type][fieldName] = originalResolver;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Return a new extension with the updated resolvers
|
|
117
|
+
return {
|
|
118
|
+
...extension,
|
|
119
|
+
resolvers: () => newResolvers,
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
exports.mapFieldPresetToExtension = mapFieldPresetToExtension;
|
|
@@ -7,3 +7,5 @@ export * from './misc';
|
|
|
7
7
|
export * from './registry';
|
|
8
8
|
export * from './override';
|
|
9
9
|
export * from './shadow-crud';
|
|
10
|
+
export * from './loaders';
|
|
11
|
+
export { isStrapiV5, isV4CompatibilityMode, typeNameOf, uidOfTypeName, relationFieldSpecs, relationResolversConfig, sanitizeOutput, toRelationValue, } from './strapi-compat';
|
|
@@ -14,6 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.toRelationValue = exports.sanitizeOutput = exports.relationResolversConfig = exports.relationFieldSpecs = exports.uidOfTypeName = exports.typeNameOf = exports.isV4CompatibilityMode = exports.isStrapiV5 = void 0;
|
|
17
18
|
__exportStar(require("./types"), exports);
|
|
18
19
|
__exportStar(require("./single-type"), exports);
|
|
19
20
|
__exportStar(require("./collection-type"), exports);
|
|
@@ -23,3 +24,13 @@ __exportStar(require("./misc"), exports);
|
|
|
23
24
|
__exportStar(require("./registry"), exports);
|
|
24
25
|
__exportStar(require("./override"), exports);
|
|
25
26
|
__exportStar(require("./shadow-crud"), exports);
|
|
27
|
+
__exportStar(require("./loaders"), exports);
|
|
28
|
+
var strapi_compat_1 = require("./strapi-compat");
|
|
29
|
+
Object.defineProperty(exports, "isStrapiV5", { enumerable: true, get: function () { return strapi_compat_1.isStrapiV5; } });
|
|
30
|
+
Object.defineProperty(exports, "isV4CompatibilityMode", { enumerable: true, get: function () { return strapi_compat_1.isV4CompatibilityMode; } });
|
|
31
|
+
Object.defineProperty(exports, "typeNameOf", { enumerable: true, get: function () { return strapi_compat_1.typeNameOf; } });
|
|
32
|
+
Object.defineProperty(exports, "uidOfTypeName", { enumerable: true, get: function () { return strapi_compat_1.uidOfTypeName; } });
|
|
33
|
+
Object.defineProperty(exports, "relationFieldSpecs", { enumerable: true, get: function () { return strapi_compat_1.relationFieldSpecs; } });
|
|
34
|
+
Object.defineProperty(exports, "relationResolversConfig", { enumerable: true, get: function () { return strapi_compat_1.relationResolversConfig; } });
|
|
35
|
+
Object.defineProperty(exports, "sanitizeOutput", { enumerable: true, get: function () { return strapi_compat_1.sanitizeOutput; } });
|
|
36
|
+
Object.defineProperty(exports, "toRelationValue", { enumerable: true, get: function () { return strapi_compat_1.toRelationValue; } });
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
type AnyRecord = Record<string, any>;
|
|
2
|
+
/**
|
|
3
|
+
* Loads values for many keys at once. Must return one result per key, **in the same order**; return an
|
|
4
|
+
* `Error` at a position to reject only that key.
|
|
5
|
+
*/
|
|
6
|
+
export type BatchFunction<K, V> = (keys: readonly K[]) => Promise<ReadonlyArray<V | Error>>;
|
|
7
|
+
/**
|
|
8
|
+
* A batching, caching loader created by {@link createLoader} or {@link loaders}.
|
|
9
|
+
*/
|
|
10
|
+
export interface Loader<K, V> {
|
|
11
|
+
/** Load one key. Calls in the same tick are batched; repeated keys come from the cache. */
|
|
12
|
+
load(key: K): Promise<V>;
|
|
13
|
+
/** Load several keys; rejects if any key fails. */
|
|
14
|
+
loadMany(keys: readonly K[]): Promise<V[]>;
|
|
15
|
+
/** Remove a key from the cache, e.g. after a mutation changed it. */
|
|
16
|
+
clear(key: K): void;
|
|
17
|
+
/** Put a known value in the cache. Does nothing when the key is already cached. */
|
|
18
|
+
prime(key: K, value: V): void;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Create a batching loader. Keys are compared by `String(key)`, so `1` and `'1'` share a cache entry.
|
|
22
|
+
* Failed batches are not cached, so a later `load` retries.
|
|
23
|
+
*
|
|
24
|
+
* Usually you want {@link loaders}, which scopes loaders to the request; a loader created with this
|
|
25
|
+
* function caches for as long as you keep it.
|
|
26
|
+
*
|
|
27
|
+
* @param batch - See {@link BatchFunction}.
|
|
28
|
+
* @param options.maxBatchSize - Larger batches are split into several calls. @default 500
|
|
29
|
+
* @throws Error (per key) when `batch` returns a different number of results than keys.
|
|
30
|
+
*/
|
|
31
|
+
export declare function createLoader<K, V>(batch: BatchFunction<K, V>, { maxBatchSize }?: {
|
|
32
|
+
maxBatchSize?: number;
|
|
33
|
+
}): Loader<K, V>;
|
|
34
|
+
/**
|
|
35
|
+
* Options for {@link RequestLoaders.entity}. Loaders with different options are cached separately.
|
|
36
|
+
*
|
|
37
|
+
* Options are compared by value, not by reference: key order does not matter and `select` is treated
|
|
38
|
+
* as a set. Loaders with a different `populate` never share a batch, because the rows they load differ.
|
|
39
|
+
*/
|
|
40
|
+
export type EntityLoaderOptions = {
|
|
41
|
+
/** Columns to select (default: all) */
|
|
42
|
+
select?: string[];
|
|
43
|
+
/** Strapi query engine populate */
|
|
44
|
+
populate?: AnyRecord | string[] | true;
|
|
45
|
+
/**
|
|
46
|
+
* Only for draft & publish content types: which version to load. Ignored for other content types.
|
|
47
|
+
* @default 'published'
|
|
48
|
+
*/
|
|
49
|
+
status?: 'published' | 'draft';
|
|
50
|
+
/**
|
|
51
|
+
* Pass every loaded row through {@link sanitizeOutput} before resolving it, so private fields and
|
|
52
|
+
* fields the current auth may not read are removed.
|
|
53
|
+
*
|
|
54
|
+
* Sanitizing needs the content type schema, so this throws for an unknown `uid`.
|
|
55
|
+
*
|
|
56
|
+
* @default false
|
|
57
|
+
*/
|
|
58
|
+
sanitize?: boolean;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Loaders scoped to one GraphQL request, returned by {@link loaders}.
|
|
62
|
+
*/
|
|
63
|
+
export interface RequestLoaders {
|
|
64
|
+
/**
|
|
65
|
+
* Load entities of a content type by database id, batched into one `findMany` per tick.
|
|
66
|
+
* Resolves to `null` for ids that do not exist.
|
|
67
|
+
*
|
|
68
|
+
* **Returns raw database rows, including private fields and fields the current auth may not read.**
|
|
69
|
+
* Pass `{ sanitize: true }` to run every row through {@link sanitizeOutput} first, or sanitize the
|
|
70
|
+
* rows yourself before returning them to clients.
|
|
71
|
+
*/
|
|
72
|
+
entity<T = AnyRecord>(uid: string, options?: EntityLoaderOptions): Loader<number | string, T | null>;
|
|
73
|
+
/**
|
|
74
|
+
* Load a relation or media attribute of many parents with one query on the parent content type.
|
|
75
|
+
* Resolves to the related entity or `null` (to-one, single media), or an array (to-many, multiple
|
|
76
|
+
* media; empty when there is none).
|
|
77
|
+
*
|
|
78
|
+
* **Returns raw database rows, including private fields and fields the current auth may not read.**
|
|
79
|
+
* Pass `{ sanitize: true }` to run every related row through {@link sanitizeOutput} first, or
|
|
80
|
+
* sanitize the rows yourself before returning them to clients.
|
|
81
|
+
*
|
|
82
|
+
* @throws Error when `attribute` is not a relation or media attribute of `uid`.
|
|
83
|
+
*/
|
|
84
|
+
relation<T = AnyRecord>(uid: string, attribute: string, options?: Omit<EntityLoaderOptions, 'select'>): Loader<number | string, T>;
|
|
85
|
+
/**
|
|
86
|
+
* A loader with your own batch function, e.g. for an external API. Cached per request under `name`:
|
|
87
|
+
* the first call's `batch` function is used for the rest of the request.
|
|
88
|
+
*/
|
|
89
|
+
custom<K, V>(name: string, batch: BatchFunction<K, V>): Loader<K, V>;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Loaders scoped to the current GraphQL request.
|
|
93
|
+
*
|
|
94
|
+
* Uses `strapi.db.query()` (same API in v4 and v5). That is the query engine, not the entity/document
|
|
95
|
+
* service: lifecycle-free and unsanitized, so it is meant for reading data inside resolvers. Add
|
|
96
|
+
* `{ sanitize: true }` to return rows that are safe for the content API.
|
|
97
|
+
*
|
|
98
|
+
* Loaders are cached per request and per options value: options are compared by value (key order and
|
|
99
|
+
* `select` order do not matter), and loaders with a different `populate` do not share batches.
|
|
100
|
+
*
|
|
101
|
+
* @param context - The resolver's third argument (`ctx`).
|
|
102
|
+
* @throws Error when `context` is not an object.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* const order = await loaders(ctx).entity(ORDER, { populate: { customer: true } }).load(parent.id)
|
|
106
|
+
* const customer = await loaders(ctx).relation(ORDER, 'customer').load(parent.id)
|
|
107
|
+
* const safeOrder = await loaders(ctx).entity(ORDER, { sanitize: true }).load(parent.id)
|
|
108
|
+
*/
|
|
109
|
+
export declare function loaders(context: object): RequestLoaders;
|
|
110
|
+
export {};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loaders = exports.createLoader = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Per-request batching and caching for GraphQL resolvers (DataLoader-style, without the dependency).
|
|
6
|
+
*
|
|
7
|
+
* Loads requested in the same tick are combined into one batch call; results are cached for the rest
|
|
8
|
+
* of the request. The cache lives on the GraphQL context object, which Strapi creates per request in
|
|
9
|
+
* both v4 and v5, so nothing leaks between requests or users.
|
|
10
|
+
*/
|
|
11
|
+
const strapi_compat_1 = require("./strapi-compat");
|
|
12
|
+
/**
|
|
13
|
+
* Create a batching loader. Keys are compared by `String(key)`, so `1` and `'1'` share a cache entry.
|
|
14
|
+
* Failed batches are not cached, so a later `load` retries.
|
|
15
|
+
*
|
|
16
|
+
* Usually you want {@link loaders}, which scopes loaders to the request; a loader created with this
|
|
17
|
+
* function caches for as long as you keep it.
|
|
18
|
+
*
|
|
19
|
+
* @param batch - See {@link BatchFunction}.
|
|
20
|
+
* @param options.maxBatchSize - Larger batches are split into several calls. @default 500
|
|
21
|
+
* @throws Error (per key) when `batch` returns a different number of results than keys.
|
|
22
|
+
*/
|
|
23
|
+
function createLoader(batch, { maxBatchSize = 500 } = {}) {
|
|
24
|
+
const cache = new Map();
|
|
25
|
+
let queue = [];
|
|
26
|
+
let scheduled = false;
|
|
27
|
+
const dispatch = async () => {
|
|
28
|
+
scheduled = false;
|
|
29
|
+
const current = queue;
|
|
30
|
+
queue = [];
|
|
31
|
+
for (let i = 0; i < current.length; i += maxBatchSize) {
|
|
32
|
+
const chunk = current.slice(i, i + maxBatchSize);
|
|
33
|
+
try {
|
|
34
|
+
const results = await batch(chunk.map((item) => item.key));
|
|
35
|
+
if (results.length !== chunk.length) {
|
|
36
|
+
throw new Error(`[buro26-strapi-graphql] Batch function returned ${results.length} results for ${chunk.length} keys`);
|
|
37
|
+
}
|
|
38
|
+
chunk.forEach((item, index) => {
|
|
39
|
+
const result = results[index];
|
|
40
|
+
if (result instanceof Error)
|
|
41
|
+
item.reject(result);
|
|
42
|
+
else
|
|
43
|
+
item.resolve(result);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
chunk.forEach((item) => {
|
|
48
|
+
cache.delete(String(item.key));
|
|
49
|
+
item.reject(error);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const loader = {
|
|
55
|
+
load(key) {
|
|
56
|
+
const cacheKey = String(key);
|
|
57
|
+
const cached = cache.get(cacheKey);
|
|
58
|
+
if (cached)
|
|
59
|
+
return cached;
|
|
60
|
+
const promise = new Promise((resolve, reject) => {
|
|
61
|
+
queue.push({ key, resolve, reject });
|
|
62
|
+
if (!scheduled) {
|
|
63
|
+
scheduled = true;
|
|
64
|
+
// Wait until all resolvers of the current tick have asked for their keys
|
|
65
|
+
process.nextTick(() => {
|
|
66
|
+
void dispatch();
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
cache.set(cacheKey, promise);
|
|
71
|
+
return promise;
|
|
72
|
+
},
|
|
73
|
+
loadMany(keys) {
|
|
74
|
+
return Promise.all(keys.map((key) => loader.load(key)));
|
|
75
|
+
},
|
|
76
|
+
clear(key) {
|
|
77
|
+
cache.delete(String(key));
|
|
78
|
+
},
|
|
79
|
+
prime(key, value) {
|
|
80
|
+
const cacheKey = String(key);
|
|
81
|
+
if (!cache.has(cacheKey))
|
|
82
|
+
cache.set(cacheKey, Promise.resolve(value));
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
return loader;
|
|
86
|
+
}
|
|
87
|
+
exports.createLoader = createLoader;
|
|
88
|
+
const requestLoaders = new WeakMap();
|
|
89
|
+
/**
|
|
90
|
+
* Normalize a value for a cache key: object keys are sorted recursively, `select` arrays are sorted,
|
|
91
|
+
* and `undefined` properties are dropped.
|
|
92
|
+
*/
|
|
93
|
+
const normalizeCacheValue = (value) => {
|
|
94
|
+
if (Array.isArray(value))
|
|
95
|
+
return value.map(normalizeCacheValue);
|
|
96
|
+
if (!value || typeof value !== 'object')
|
|
97
|
+
return value;
|
|
98
|
+
const entries = Object.entries(value)
|
|
99
|
+
.filter(([, entryValue]) => entryValue !== undefined)
|
|
100
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
101
|
+
.map(([key, entryValue]) => [
|
|
102
|
+
key,
|
|
103
|
+
key === 'select' && Array.isArray(entryValue)
|
|
104
|
+
? [...entryValue].sort()
|
|
105
|
+
: normalizeCacheValue(entryValue)
|
|
106
|
+
]);
|
|
107
|
+
return Object.fromEntries(entries);
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Stable cache key for loader options, so equivalent options share one loader regardless of property
|
|
111
|
+
* order (`{ status: 'draft', populate: {...} }` and `{ populate: {...}, status: 'draft' }`) or column
|
|
112
|
+
* order in `select`.
|
|
113
|
+
*/
|
|
114
|
+
const optionsCacheKey = (options) => JSON.stringify(normalizeCacheValue(options));
|
|
115
|
+
/** Sanitize one related value: an array for to-many, a single row for to-one, or `null`. */
|
|
116
|
+
const sanitizeRelationValue = async (value, targetUID, context) => {
|
|
117
|
+
if (value == null)
|
|
118
|
+
return value;
|
|
119
|
+
if (Array.isArray(value)) {
|
|
120
|
+
return Promise.all(value.map((row) => (0, strapi_compat_1.sanitizeOutput)(row, targetUID, context)));
|
|
121
|
+
}
|
|
122
|
+
return (0, strapi_compat_1.sanitizeOutput)(value, targetUID, context);
|
|
123
|
+
};
|
|
124
|
+
const hasDraftAndPublish = (uid) => { var _a, _b; return Boolean((_b = (_a = strapi.getModel(uid)) === null || _a === void 0 ? void 0 : _a.options) === null || _b === void 0 ? void 0 : _b.draftAndPublish); };
|
|
125
|
+
const statusWhere = (uid, status) => {
|
|
126
|
+
if (!hasDraftAndPublish(uid))
|
|
127
|
+
return {};
|
|
128
|
+
return { publishedAt: (status !== null && status !== void 0 ? status : 'published') === 'published' ? { $notNull: true } : { $null: true } };
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Loaders scoped to the current GraphQL request.
|
|
132
|
+
*
|
|
133
|
+
* Uses `strapi.db.query()` (same API in v4 and v5). That is the query engine, not the entity/document
|
|
134
|
+
* service: lifecycle-free and unsanitized, so it is meant for reading data inside resolvers. Add
|
|
135
|
+
* `{ sanitize: true }` to return rows that are safe for the content API.
|
|
136
|
+
*
|
|
137
|
+
* Loaders are cached per request and per options value: options are compared by value (key order and
|
|
138
|
+
* `select` order do not matter), and loaders with a different `populate` do not share batches.
|
|
139
|
+
*
|
|
140
|
+
* @param context - The resolver's third argument (`ctx`).
|
|
141
|
+
* @throws Error when `context` is not an object.
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* const order = await loaders(ctx).entity(ORDER, { populate: { customer: true } }).load(parent.id)
|
|
145
|
+
* const customer = await loaders(ctx).relation(ORDER, 'customer').load(parent.id)
|
|
146
|
+
* const safeOrder = await loaders(ctx).entity(ORDER, { sanitize: true }).load(parent.id)
|
|
147
|
+
*/
|
|
148
|
+
function loaders(context) {
|
|
149
|
+
if (!context || typeof context !== 'object') {
|
|
150
|
+
throw new Error('[buro26-strapi-graphql] loaders(context) needs the GraphQL resolver context');
|
|
151
|
+
}
|
|
152
|
+
let registry = requestLoaders.get(context);
|
|
153
|
+
if (!registry) {
|
|
154
|
+
registry = new Map();
|
|
155
|
+
requestLoaders.set(context, registry);
|
|
156
|
+
}
|
|
157
|
+
const getOrCreate = (key, create) => {
|
|
158
|
+
let loader = registry.get(key);
|
|
159
|
+
if (!loader) {
|
|
160
|
+
loader = create();
|
|
161
|
+
registry.set(key, loader);
|
|
162
|
+
}
|
|
163
|
+
return loader;
|
|
164
|
+
};
|
|
165
|
+
return {
|
|
166
|
+
entity(uid, options = {}) {
|
|
167
|
+
return getOrCreate(`entity:${uid}:${optionsCacheKey(options)}`, () => createLoader(async (ids) => {
|
|
168
|
+
const select = options.select ? Array.from(new Set(['id', ...options.select])) : undefined;
|
|
169
|
+
const rows = await strapi.db.query(uid).findMany({
|
|
170
|
+
where: { id: { $in: ids }, ...statusWhere(uid, options.status) },
|
|
171
|
+
...(select && { select }),
|
|
172
|
+
...(options.populate && { populate: options.populate }),
|
|
173
|
+
});
|
|
174
|
+
const byId = new Map(rows.map((row) => [String(row.id), row]));
|
|
175
|
+
const resolved = ids.map((id) => { var _a; return (_a = byId.get(String(id))) !== null && _a !== void 0 ? _a : null; });
|
|
176
|
+
if (!options.sanitize)
|
|
177
|
+
return resolved;
|
|
178
|
+
return Promise.all(resolved.map((row) => (row ? (0, strapi_compat_1.sanitizeOutput)(row, uid, context) : row)));
|
|
179
|
+
}));
|
|
180
|
+
},
|
|
181
|
+
relation(uid, attribute, options = {}) {
|
|
182
|
+
return getOrCreate(`relation:${uid}:${attribute}:${optionsCacheKey(options)}`, () => createLoader(async (ids) => {
|
|
183
|
+
var _a, _b;
|
|
184
|
+
const attr = (_b = (_a = strapi.getModel(uid)) === null || _a === void 0 ? void 0 : _a.attributes) === null || _b === void 0 ? void 0 : _b[attribute];
|
|
185
|
+
const isMedia = (attr === null || attr === void 0 ? void 0 : attr.type) === 'media';
|
|
186
|
+
if (!attr || (!isMedia && attr.type !== 'relation')) {
|
|
187
|
+
throw new Error(`[buro26-strapi-graphql] "${uid}.${attribute}" is not a relation or media attribute`);
|
|
188
|
+
}
|
|
189
|
+
// Media are stored as a morph relation to the upload plugin's file content type. Their
|
|
190
|
+
// target lives there, not on the attribute, and draft & publish never applies to files.
|
|
191
|
+
const targetUID = isMedia ? 'plugin::upload.file' : attr.target;
|
|
192
|
+
const isToMany = isMedia ? Boolean(attr.multiple) : String(attr.relation).endsWith('Many');
|
|
193
|
+
const nestedPopulate = options.populate && typeof options.populate === 'object' && !Array.isArray(options.populate)
|
|
194
|
+
? { populate: options.populate }
|
|
195
|
+
: {};
|
|
196
|
+
// Media always need `true`/a nested populate; their file rows have no draft & publish status.
|
|
197
|
+
const populate = isMedia
|
|
198
|
+
? Object.keys(nestedPopulate).length > 0
|
|
199
|
+
? nestedPopulate
|
|
200
|
+
: true
|
|
201
|
+
: {
|
|
202
|
+
...nestedPopulate,
|
|
203
|
+
...(hasDraftAndPublish(targetUID) && { where: statusWhere(targetUID, options.status) }),
|
|
204
|
+
};
|
|
205
|
+
const rows = await strapi.db.query(uid).findMany({
|
|
206
|
+
where: { id: { $in: ids } },
|
|
207
|
+
select: ['id'],
|
|
208
|
+
populate: { [attribute]: populate },
|
|
209
|
+
});
|
|
210
|
+
const byId = new Map(rows.map((row) => [String(row.id), row[attribute]]));
|
|
211
|
+
const resolved = ids.map((id) => { var _a; return (_a = byId.get(String(id))) !== null && _a !== void 0 ? _a : (isToMany ? [] : null); });
|
|
212
|
+
if (!options.sanitize)
|
|
213
|
+
return resolved;
|
|
214
|
+
return Promise.all(resolved.map((value) => sanitizeRelationValue(value, targetUID, context)));
|
|
215
|
+
}));
|
|
216
|
+
},
|
|
217
|
+
custom(name, batch) {
|
|
218
|
+
return getOrCreate(`custom:${name}`, () => createLoader(batch));
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
exports.loaders = loaders;
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert GraphQL arguments (filters, pagination, sort) into Strapi query parameters, using the GraphQL
|
|
3
|
+
* plugin's own transformer.
|
|
4
|
+
*
|
|
5
|
+
* @param options.usePagination - Translate `pagination` into `start`/`limit`. @default true
|
|
6
|
+
*/
|
|
1
7
|
export declare function transformArgs(args: Record<string, any>, { contentType, usePagination }: {
|
|
2
8
|
contentType: string;
|
|
3
9
|
usePagination?: boolean;
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.transformArgs = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Convert GraphQL arguments (filters, pagination, sort) into Strapi query parameters, using the GraphQL
|
|
6
|
+
* plugin's own transformer.
|
|
7
|
+
*
|
|
8
|
+
* @param options.usePagination - Translate `pagination` into `start`/`limit`. @default true
|
|
9
|
+
*/
|
|
4
10
|
function transformArgs(args, { contentType, usePagination }) {
|
|
5
11
|
const { transformArgs } = strapi.plugin('graphql').service('builders').utils;
|
|
6
12
|
return transformArgs(args, {
|
|
@@ -6,16 +6,48 @@
|
|
|
6
6
|
import type { GraphQLResolveInfo } from 'graphql';
|
|
7
7
|
import type { ResolveContext } from './types';
|
|
8
8
|
import type { Middleware } from '../nexus';
|
|
9
|
+
/**
|
|
10
|
+
* Options for {@link overrideField}.
|
|
11
|
+
*/
|
|
9
12
|
type OverrideFieldOptions<Type extends Object, Args = unknown, Context = ResolveContext, Info = GraphQLResolveInfo> = {
|
|
13
|
+
/** Content type UID the field belongs to */
|
|
10
14
|
contentTypeName: string;
|
|
15
|
+
/** Attribute name to (re)define */
|
|
11
16
|
fieldName: keyof Type;
|
|
17
|
+
/** Override the GraphQL output type. Defaults to the type Strapi generates. */
|
|
12
18
|
type?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Authorization check; return `true` to allow.
|
|
21
|
+
* Same as `extensions.authorize` (which wins when both are set).
|
|
22
|
+
*/
|
|
13
23
|
authorize?: (parent: Type, args: Args, context: Context, info: Info) => boolean | Promise<boolean>;
|
|
24
|
+
/** Replace the resolver. Defaults to Strapi's resolver for the attribute. */
|
|
14
25
|
resolve?: (parent: Type, args: Args, context: Context, info: Info) => unknown;
|
|
15
26
|
extensions?: {
|
|
16
27
|
middlewares?: Middleware[];
|
|
17
28
|
authorize?: (parent: Type, args: Args, context: Context, info: Info) => boolean | Promise<boolean>;
|
|
18
29
|
};
|
|
19
30
|
};
|
|
31
|
+
/**
|
|
32
|
+
* Redefine a field Strapi generates for a content type attribute, inside a Nexus `extendType`, with the
|
|
33
|
+
* same type, arguments and resolver as Strapi uses (v4 and v5), plus your own `authorize`, `resolve` or
|
|
34
|
+
* middlewares.
|
|
35
|
+
*
|
|
36
|
+
* Private attributes and attributes disabled for output via shadow CRUD are skipped.
|
|
37
|
+
*
|
|
38
|
+
* @param t - The Nexus definition block (`definition(t)`).
|
|
39
|
+
* @throws Error when the attribute does not exist on the content type.
|
|
40
|
+
*
|
|
41
|
+
* @remarks To only restrict access use {@link protectFields}; for relations prefer
|
|
42
|
+
* {@link FieldExtensionBuilder.relation}. Both work without an extension and without redefining fields.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* extendType({
|
|
46
|
+
* type: 'Article',
|
|
47
|
+
* definition(t) {
|
|
48
|
+
* overrideField(t, { contentTypeName: ARTICLE, fieldName: 'titel', resolve: (plan) => plan.titel ?? '' })
|
|
49
|
+
* },
|
|
50
|
+
* })
|
|
51
|
+
*/
|
|
20
52
|
export declare const overrideField: <T extends Object>(t: any, overrideOptions: OverrideFieldOptions<T, unknown, ResolveContext, GraphQLResolveInfo>) => void;
|
|
21
53
|
export {};
|