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
|
@@ -3,7 +3,9 @@ export * from './single-type';
|
|
|
3
3
|
export * from './collection-type';
|
|
4
4
|
export * from './field-builder';
|
|
5
5
|
export * from './graphql-extension';
|
|
6
|
-
export * from './middleware';
|
|
7
6
|
export * from './misc';
|
|
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,12 +14,23 @@ 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);
|
|
20
21
|
__exportStar(require("./field-builder"), exports);
|
|
21
22
|
__exportStar(require("./graphql-extension"), exports);
|
|
22
|
-
__exportStar(require("./middleware"), exports);
|
|
23
23
|
__exportStar(require("./misc"), exports);
|
|
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,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-request batching and caching for GraphQL resolvers (DataLoader-style, without the dependency).
|
|
3
|
+
*
|
|
4
|
+
* Loads requested in the same tick are combined into one batch call; results are cached for the rest
|
|
5
|
+
* of the request. The cache lives on the GraphQL context object, which Strapi creates per request in
|
|
6
|
+
* both v4 and v5, so nothing leaks between requests or users.
|
|
7
|
+
*/
|
|
8
|
+
type AnyRecord = Record<string, any>;
|
|
9
|
+
/**
|
|
10
|
+
* Loads values for many keys at once. Must return one result per key, **in the same order**; return an
|
|
11
|
+
* `Error` at a position to reject only that key.
|
|
12
|
+
*/
|
|
13
|
+
export type BatchFunction<K, V> = (keys: readonly K[]) => Promise<ReadonlyArray<V | Error>>;
|
|
14
|
+
/**
|
|
15
|
+
* A batching, caching loader created by {@link createLoader} or {@link loaders}.
|
|
16
|
+
*/
|
|
17
|
+
export interface Loader<K, V> {
|
|
18
|
+
/** Load one key. Calls in the same tick are batched; repeated keys come from the cache. */
|
|
19
|
+
load(key: K): Promise<V>;
|
|
20
|
+
/** Load several keys; rejects if any key fails. */
|
|
21
|
+
loadMany(keys: readonly K[]): Promise<V[]>;
|
|
22
|
+
/** Remove a key from the cache, e.g. after a mutation changed it. */
|
|
23
|
+
clear(key: K): void;
|
|
24
|
+
/** Put a known value in the cache. Does nothing when the key is already cached. */
|
|
25
|
+
prime(key: K, value: V): void;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Create a batching loader. Keys are compared by `String(key)`, so `1` and `'1'` share a cache entry.
|
|
29
|
+
* Failed batches are not cached, so a later `load` retries.
|
|
30
|
+
*
|
|
31
|
+
* Usually you want {@link loaders}, which scopes loaders to the request; a loader created with this
|
|
32
|
+
* function caches for as long as you keep it.
|
|
33
|
+
*
|
|
34
|
+
* @param batch - See {@link BatchFunction}.
|
|
35
|
+
* @param options.maxBatchSize - Larger batches are split into several calls. @default 500
|
|
36
|
+
* @throws Error (per key) when `batch` returns a different number of results than keys.
|
|
37
|
+
*/
|
|
38
|
+
export declare function createLoader<K, V>(batch: BatchFunction<K, V>, { maxBatchSize }?: {
|
|
39
|
+
maxBatchSize?: number;
|
|
40
|
+
}): Loader<K, V>;
|
|
41
|
+
/**
|
|
42
|
+
* Options for {@link RequestLoaders.entity}. Loaders with different options are cached separately.
|
|
43
|
+
*/
|
|
44
|
+
export type EntityLoaderOptions = {
|
|
45
|
+
/** Columns to select (default: all) */
|
|
46
|
+
select?: string[];
|
|
47
|
+
/** Strapi query engine populate */
|
|
48
|
+
populate?: AnyRecord | string[] | true;
|
|
49
|
+
/**
|
|
50
|
+
* Only for draft & publish content types: which version to load. Ignored for other content types.
|
|
51
|
+
* @default 'published'
|
|
52
|
+
*/
|
|
53
|
+
status?: 'published' | 'draft';
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Loaders scoped to one GraphQL request, returned by {@link loaders}.
|
|
57
|
+
*/
|
|
58
|
+
export interface RequestLoaders {
|
|
59
|
+
/**
|
|
60
|
+
* Load entities of a content type by database id, batched into one `findMany` per tick.
|
|
61
|
+
* Resolves to `null` for ids that do not exist.
|
|
62
|
+
*
|
|
63
|
+
* Returns raw database rows (including private fields): sanitize before returning them to clients,
|
|
64
|
+
* see {@link sanitizeOutput}.
|
|
65
|
+
*/
|
|
66
|
+
entity<T = AnyRecord>(uid: string, options?: EntityLoaderOptions): Loader<number | string, T | null>;
|
|
67
|
+
/**
|
|
68
|
+
* Load a relation of many parents with one query on the parent content type.
|
|
69
|
+
* Resolves to the related entity or `null` (to-one), or an array (to-many), as raw database rows.
|
|
70
|
+
*
|
|
71
|
+
* @throws Error when `attribute` is not a relation of `uid`.
|
|
72
|
+
*/
|
|
73
|
+
relation<T = AnyRecord>(uid: string, attribute: string, options?: Omit<EntityLoaderOptions, 'select'>): Loader<number | string, T>;
|
|
74
|
+
/**
|
|
75
|
+
* A loader with your own batch function, e.g. for an external API. Cached per request under `name`:
|
|
76
|
+
* the first call's `batch` function is used for the rest of the request.
|
|
77
|
+
*/
|
|
78
|
+
custom<K, V>(name: string, batch: BatchFunction<K, V>): Loader<K, V>;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Loaders scoped to the current GraphQL request.
|
|
82
|
+
*
|
|
83
|
+
* Uses `strapi.db.query()` (same API in v4 and v5). That is the query engine, not the entity/document
|
|
84
|
+
* service: lifecycle-free and unsanitized, so it is meant for reading data inside resolvers.
|
|
85
|
+
*
|
|
86
|
+
* @param context - The resolver's third argument (`ctx`).
|
|
87
|
+
* @throws Error when `context` is not an object.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* const order = await loaders(ctx).entity(ORDER, { populate: { customer: true } }).load(parent.id)
|
|
91
|
+
* const customer = await loaders(ctx).relation(ORDER, 'customer').load(parent.id)
|
|
92
|
+
*/
|
|
93
|
+
export declare function loaders(context: object): RequestLoaders;
|
|
94
|
+
export {};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Per-request batching and caching for GraphQL resolvers (DataLoader-style, without the dependency).
|
|
4
|
+
*
|
|
5
|
+
* Loads requested in the same tick are combined into one batch call; results are cached for the rest
|
|
6
|
+
* of the request. The cache lives on the GraphQL context object, which Strapi creates per request in
|
|
7
|
+
* both v4 and v5, so nothing leaks between requests or users.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.loaders = exports.createLoader = void 0;
|
|
11
|
+
/**
|
|
12
|
+
* Create a batching loader. Keys are compared by `String(key)`, so `1` and `'1'` share a cache entry.
|
|
13
|
+
* Failed batches are not cached, so a later `load` retries.
|
|
14
|
+
*
|
|
15
|
+
* Usually you want {@link loaders}, which scopes loaders to the request; a loader created with this
|
|
16
|
+
* function caches for as long as you keep it.
|
|
17
|
+
*
|
|
18
|
+
* @param batch - See {@link BatchFunction}.
|
|
19
|
+
* @param options.maxBatchSize - Larger batches are split into several calls. @default 500
|
|
20
|
+
* @throws Error (per key) when `batch` returns a different number of results than keys.
|
|
21
|
+
*/
|
|
22
|
+
function createLoader(batch, { maxBatchSize = 500 } = {}) {
|
|
23
|
+
const cache = new Map();
|
|
24
|
+
let queue = [];
|
|
25
|
+
let scheduled = false;
|
|
26
|
+
const dispatch = async () => {
|
|
27
|
+
scheduled = false;
|
|
28
|
+
const current = queue;
|
|
29
|
+
queue = [];
|
|
30
|
+
for (let i = 0; i < current.length; i += maxBatchSize) {
|
|
31
|
+
const chunk = current.slice(i, i + maxBatchSize);
|
|
32
|
+
try {
|
|
33
|
+
const results = await batch(chunk.map((item) => item.key));
|
|
34
|
+
if (results.length !== chunk.length) {
|
|
35
|
+
throw new Error(`[buro26-strapi-graphql] Batch function returned ${results.length} results for ${chunk.length} keys`);
|
|
36
|
+
}
|
|
37
|
+
chunk.forEach((item, index) => {
|
|
38
|
+
const result = results[index];
|
|
39
|
+
if (result instanceof Error)
|
|
40
|
+
item.reject(result);
|
|
41
|
+
else
|
|
42
|
+
item.resolve(result);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
chunk.forEach((item) => {
|
|
47
|
+
cache.delete(String(item.key));
|
|
48
|
+
item.reject(error);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const loader = {
|
|
54
|
+
load(key) {
|
|
55
|
+
const cacheKey = String(key);
|
|
56
|
+
const cached = cache.get(cacheKey);
|
|
57
|
+
if (cached)
|
|
58
|
+
return cached;
|
|
59
|
+
const promise = new Promise((resolve, reject) => {
|
|
60
|
+
queue.push({ key, resolve, reject });
|
|
61
|
+
if (!scheduled) {
|
|
62
|
+
scheduled = true;
|
|
63
|
+
// Wait until all resolvers of the current tick have asked for their keys
|
|
64
|
+
process.nextTick(() => {
|
|
65
|
+
void dispatch();
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
cache.set(cacheKey, promise);
|
|
70
|
+
return promise;
|
|
71
|
+
},
|
|
72
|
+
loadMany(keys) {
|
|
73
|
+
return Promise.all(keys.map((key) => loader.load(key)));
|
|
74
|
+
},
|
|
75
|
+
clear(key) {
|
|
76
|
+
cache.delete(String(key));
|
|
77
|
+
},
|
|
78
|
+
prime(key, value) {
|
|
79
|
+
const cacheKey = String(key);
|
|
80
|
+
if (!cache.has(cacheKey))
|
|
81
|
+
cache.set(cacheKey, Promise.resolve(value));
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
return loader;
|
|
85
|
+
}
|
|
86
|
+
exports.createLoader = createLoader;
|
|
87
|
+
const requestLoaders = new WeakMap();
|
|
88
|
+
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); };
|
|
89
|
+
const statusWhere = (uid, status) => {
|
|
90
|
+
if (!hasDraftAndPublish(uid))
|
|
91
|
+
return {};
|
|
92
|
+
return { publishedAt: (status !== null && status !== void 0 ? status : 'published') === 'published' ? { $notNull: true } : { $null: true } };
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Loaders scoped to the current GraphQL request.
|
|
96
|
+
*
|
|
97
|
+
* Uses `strapi.db.query()` (same API in v4 and v5). That is the query engine, not the entity/document
|
|
98
|
+
* service: lifecycle-free and unsanitized, so it is meant for reading data inside resolvers.
|
|
99
|
+
*
|
|
100
|
+
* @param context - The resolver's third argument (`ctx`).
|
|
101
|
+
* @throws Error when `context` is not an object.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* const order = await loaders(ctx).entity(ORDER, { populate: { customer: true } }).load(parent.id)
|
|
105
|
+
* const customer = await loaders(ctx).relation(ORDER, 'customer').load(parent.id)
|
|
106
|
+
*/
|
|
107
|
+
function loaders(context) {
|
|
108
|
+
if (!context || typeof context !== 'object') {
|
|
109
|
+
throw new Error('[buro26-strapi-graphql] loaders(context) needs the GraphQL resolver context');
|
|
110
|
+
}
|
|
111
|
+
let registry = requestLoaders.get(context);
|
|
112
|
+
if (!registry) {
|
|
113
|
+
registry = new Map();
|
|
114
|
+
requestLoaders.set(context, registry);
|
|
115
|
+
}
|
|
116
|
+
const getOrCreate = (key, create) => {
|
|
117
|
+
let loader = registry.get(key);
|
|
118
|
+
if (!loader) {
|
|
119
|
+
loader = create();
|
|
120
|
+
registry.set(key, loader);
|
|
121
|
+
}
|
|
122
|
+
return loader;
|
|
123
|
+
};
|
|
124
|
+
return {
|
|
125
|
+
entity(uid, options = {}) {
|
|
126
|
+
return getOrCreate(`entity:${uid}:${JSON.stringify(options)}`, () => createLoader(async (ids) => {
|
|
127
|
+
const select = options.select ? Array.from(new Set(['id', ...options.select])) : undefined;
|
|
128
|
+
const rows = await strapi.db.query(uid).findMany({
|
|
129
|
+
where: { id: { $in: ids }, ...statusWhere(uid, options.status) },
|
|
130
|
+
...(select && { select }),
|
|
131
|
+
...(options.populate && { populate: options.populate }),
|
|
132
|
+
});
|
|
133
|
+
const byId = new Map(rows.map((row) => [String(row.id), row]));
|
|
134
|
+
return ids.map((id) => { var _a; return (_a = byId.get(String(id))) !== null && _a !== void 0 ? _a : null; });
|
|
135
|
+
}));
|
|
136
|
+
},
|
|
137
|
+
relation(uid, attribute, options = {}) {
|
|
138
|
+
return getOrCreate(`relation:${uid}:${attribute}:${JSON.stringify(options)}`, () => createLoader(async (ids) => {
|
|
139
|
+
var _a, _b;
|
|
140
|
+
const attr = (_b = (_a = strapi.getModel(uid)) === null || _a === void 0 ? void 0 : _a.attributes) === null || _b === void 0 ? void 0 : _b[attribute];
|
|
141
|
+
if (!attr || attr.type !== 'relation') {
|
|
142
|
+
throw new Error(`[buro26-strapi-graphql] "${uid}.${attribute}" is not a relation`);
|
|
143
|
+
}
|
|
144
|
+
const isToMany = String(attr.relation).endsWith('Many');
|
|
145
|
+
const rows = await strapi.db.query(uid).findMany({
|
|
146
|
+
where: { id: { $in: ids } },
|
|
147
|
+
select: ['id'],
|
|
148
|
+
populate: {
|
|
149
|
+
[attribute]: {
|
|
150
|
+
...(options.populate && typeof options.populate === 'object' && !Array.isArray(options.populate)
|
|
151
|
+
? { populate: options.populate }
|
|
152
|
+
: {}),
|
|
153
|
+
...(hasDraftAndPublish(attr.target) && { where: statusWhere(attr.target, options.status) }),
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
const byId = new Map(rows.map((row) => [String(row.id), row[attribute]]));
|
|
158
|
+
return ids.map((id) => { var _a; return (_a = byId.get(String(id))) !== null && _a !== void 0 ? _a : (isToMany ? [] : null); });
|
|
159
|
+
}));
|
|
160
|
+
},
|
|
161
|
+
custom(name, batch) {
|
|
162
|
+
return getOrCreate(`custom:${name}`, () => createLoader(batch));
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
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, {
|
|
@@ -5,16 +5,49 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { GraphQLResolveInfo } from 'graphql';
|
|
7
7
|
import type { ResolveContext } from './types';
|
|
8
|
-
import type { Middleware } from '
|
|
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[];
|
|
28
|
+
authorize?: (parent: Type, args: Args, context: Context, info: Info) => boolean | Promise<boolean>;
|
|
17
29
|
};
|
|
18
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
|
+
*/
|
|
19
52
|
export declare const overrideField: <T extends Object>(t: any, overrideOptions: OverrideFieldOptions<T, unknown, ResolveContext, GraphQLResolveInfo>) => void;
|
|
20
53
|
export {};
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
8
|
exports.overrideField = void 0;
|
|
9
|
+
const strapi_compat_1 = require("./strapi-compat");
|
|
9
10
|
const fp_1 = require("lodash/fp");
|
|
10
11
|
const utils_1 = require("@strapi/utils");
|
|
11
12
|
const getGraphQLService = (name) => strapi.plugin('graphql').service(name);
|
|
@@ -142,35 +143,27 @@ const addPolymorphicRelationalAttribute = (options) => {
|
|
|
142
143
|
}
|
|
143
144
|
};
|
|
144
145
|
/**
|
|
145
|
-
* Add a regular relational attribute to the type definition
|
|
146
|
+
* Add a regular relational attribute to the type definition.
|
|
147
|
+
*
|
|
148
|
+
* Uses the same field specs as Strapi itself (see `relationFieldSpecs`), so this works for both
|
|
149
|
+
* Strapi v4 (entity response types) and v5 (unwrapped types + `<field>_connection`).
|
|
146
150
|
*/
|
|
147
151
|
const addRegularRelationalAttribute = (options) => {
|
|
148
|
-
const { naming } = getGraphQLService('utils');
|
|
149
|
-
const { getContentTypeArgs } = getGraphQLService('builders').utils;
|
|
150
|
-
const { buildAssociationResolver } = getGraphQLService('builders').get('content-api');
|
|
151
152
|
const extension = getGraphQLService('extension');
|
|
152
|
-
const { builder } = options;
|
|
153
|
-
|
|
153
|
+
const { builder, attributeName, attribute, contentType, resolve, extensions } = options;
|
|
154
|
+
// @ts-ignore
|
|
154
155
|
if (extension.shadowCRUD(attribute.target).isDisabled()) {
|
|
155
156
|
return;
|
|
156
157
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const type = isToManyRelation
|
|
167
|
-
? naming.getRelationResponseCollectionName(targetContentType)
|
|
168
|
-
: naming.getEntityResponseName(targetContentType);
|
|
169
|
-
const args = isToManyRelation ? getContentTypeArgs(targetContentType) : undefined;
|
|
170
|
-
const resolverPath = `${naming.getTypeName(contentType)}.${attributeName}`;
|
|
171
|
-
const resolverScope = `${targetContentType.uid}.find`;
|
|
172
|
-
extension.use({ resolversConfig: { [resolverPath]: { auth: { scope: [resolverScope] } } } });
|
|
173
|
-
builder.field(attributeName, { type, resolve, args, ...options });
|
|
158
|
+
for (const spec of (0, strapi_compat_1.relationFieldSpecs)(contentType.uid, attributeName, resolve)) {
|
|
159
|
+
builder.field(spec.name, {
|
|
160
|
+
type: spec.type,
|
|
161
|
+
args: spec.args,
|
|
162
|
+
resolve: spec.resolve,
|
|
163
|
+
...(extensions && { extensions }),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
extension.use({ resolversConfig: (0, strapi_compat_1.relationResolversConfig)(contentType.uid, attributeName) });
|
|
174
167
|
};
|
|
175
168
|
const isPrivate = (contentType) => (attributeName) => {
|
|
176
169
|
return utils_1.contentTypes.isPrivateAttribute(contentType, attributeName);
|
|
@@ -179,7 +172,29 @@ const isDisabled = (contentType) => (attributeName) => {
|
|
|
179
172
|
const extension = getGraphQLService('extension');
|
|
180
173
|
return !extension.shadowCRUD(contentType.uid).field(attributeName).hasOutputEnabled();
|
|
181
174
|
};
|
|
175
|
+
/**
|
|
176
|
+
* Redefine a field Strapi generates for a content type attribute, inside a Nexus `extendType`, with the
|
|
177
|
+
* same type, arguments and resolver as Strapi uses (v4 and v5), plus your own `authorize`, `resolve` or
|
|
178
|
+
* middlewares.
|
|
179
|
+
*
|
|
180
|
+
* Private attributes and attributes disabled for output via shadow CRUD are skipped.
|
|
181
|
+
*
|
|
182
|
+
* @param t - The Nexus definition block (`definition(t)`).
|
|
183
|
+
* @throws Error when the attribute does not exist on the content type.
|
|
184
|
+
*
|
|
185
|
+
* @remarks To only restrict access use {@link protectFields}; for relations prefer
|
|
186
|
+
* {@link FieldExtensionBuilder.relation}. Both work without an extension and without redefining fields.
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* extendType({
|
|
190
|
+
* type: 'Article',
|
|
191
|
+
* definition(t) {
|
|
192
|
+
* overrideField(t, { contentTypeName: ARTICLE, fieldName: 'titel', resolve: (plan) => plan.titel ?? '' })
|
|
193
|
+
* },
|
|
194
|
+
* })
|
|
195
|
+
*/
|
|
182
196
|
const overrideField = (t, overrideOptions) => {
|
|
197
|
+
var _a;
|
|
183
198
|
const utils = getGraphQLService('utils');
|
|
184
199
|
const { contentTypeName, fieldName } = overrideOptions;
|
|
185
200
|
const { isStrapiScalar, isComponent, isDynamicZone, isEnumeration, isMedia, isMorphRelation, isRelation, } = utils.attributes;
|
|
@@ -187,10 +202,13 @@ const overrideField = (t, overrideOptions) => {
|
|
|
187
202
|
const { attributes } = contentType;
|
|
188
203
|
// @ts-ignore
|
|
189
204
|
const attribute = attributes[fieldName];
|
|
190
|
-
if (
|
|
205
|
+
if (!attribute) {
|
|
206
|
+
throw new Error(`[buro26-strapi-graphql] overrideField: "${contentTypeName}.${String(fieldName)}" does not exist`);
|
|
207
|
+
}
|
|
208
|
+
if (isPrivate(contentType)(fieldName)) {
|
|
191
209
|
return;
|
|
192
210
|
}
|
|
193
|
-
if (isDisabled(contentType)(
|
|
211
|
+
if (isDisabled(contentType)(fieldName)) {
|
|
194
212
|
return;
|
|
195
213
|
}
|
|
196
214
|
// We create a copy of the builder (t) to apply custom
|
|
@@ -199,12 +217,22 @@ const overrideField = (t, overrideOptions) => {
|
|
|
199
217
|
if (attribute.required) {
|
|
200
218
|
builder = builder.nonNull;
|
|
201
219
|
}
|
|
220
|
+
// `authorize` (deprecated, top-level) is only enforced when it ends up in `extensions.authorize`,
|
|
221
|
+
// which is what the package's Nexus authorize plugin reads.
|
|
222
|
+
const { authorize, extensions, resolve, type, contentTypeName: _ct, fieldName: _fn, ...rest } = overrideOptions;
|
|
223
|
+
const authorizeFn = (_a = extensions === null || extensions === void 0 ? void 0 : extensions.authorize) !== null && _a !== void 0 ? _a : authorize;
|
|
202
224
|
const options = {
|
|
225
|
+
...rest,
|
|
203
226
|
builder,
|
|
204
227
|
attributeName: fieldName,
|
|
205
228
|
attribute,
|
|
206
229
|
contentType,
|
|
207
|
-
...
|
|
230
|
+
...(type && { type }),
|
|
231
|
+
...(resolve && { resolve }),
|
|
232
|
+
extensions: {
|
|
233
|
+
...extensions,
|
|
234
|
+
...(authorizeFn && { authorize: authorizeFn }),
|
|
235
|
+
},
|
|
208
236
|
};
|
|
209
237
|
// Enums
|
|
210
238
|
if (isEnumeration(attribute)) {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ResolverFunction } from '../../types';
|
|
2
|
+
import type { Middleware } from '../../nexus';
|
|
3
|
+
/**
|
|
4
|
+
* Returned by {@link protectFields}; add checks and middlewares. Mutates the registration, so calls
|
|
5
|
+
* can be chained or made later on the same builder.
|
|
6
|
+
*/
|
|
7
|
+
export interface FieldProtectionBuilder<T = any> {
|
|
8
|
+
/**
|
|
9
|
+
* Add an authorization check. Runs after global and field checks; all must return `true`.
|
|
10
|
+
* Return `false` for "Not authorized" or an `Error` to send that error.
|
|
11
|
+
*/
|
|
12
|
+
authorize(fn: ResolverFunction<T>): FieldProtectionBuilder<T>;
|
|
13
|
+
/** Add a middleware that wraps the field resolver (innermost, after global and field middlewares). */
|
|
14
|
+
middleware(fn: Middleware<T>): FieldProtectionBuilder<T>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Protect fields of a content type, including the fields Strapi generates (scalars, enums, media,
|
|
18
|
+
* components and relations), without redefining them.
|
|
19
|
+
*
|
|
20
|
+
* The authorization and middleware are attached by the package's Nexus plugins when the schema is built,
|
|
21
|
+
* so Strapi's own types, arguments, filters, pagination and resolvers stay untouched. For to-many relations
|
|
22
|
+
* on Strapi v5 the matching `<field>_connection` field is protected as well.
|
|
23
|
+
*
|
|
24
|
+
* Call it at module level in a file inside a `graphql` folder; the file does not need to export anything.
|
|
25
|
+
*
|
|
26
|
+
* **Non-null fields:** when a check denies a non-null field, GraphQL nulls the nearest nullable parent
|
|
27
|
+
* instead of only the field (standard GraphQL error propagation). Strapi generates required attributes and,
|
|
28
|
+
* on v5, to-many relations (`[X]!`) as non-null; a warning is logged for those. Protect the
|
|
29
|
+
* `<field>_connection` field only, or check access in the parent query, when that is not acceptable.
|
|
30
|
+
* Several `protectFields` calls for the same field are combined.
|
|
31
|
+
*
|
|
32
|
+
* @typeParam T - Entity type, so `fields` is checked against its keys.
|
|
33
|
+
* @param contentType - Content type UID, e.g. `api::customer.customer`.
|
|
34
|
+
* @param fields - Attribute names to protect.
|
|
35
|
+
* @returns A {@link FieldProtectionBuilder} to add checks and middlewares.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* protectFields<Customer>('api::customer.customer', ['street', 'iban', 'orders'])
|
|
39
|
+
* .authorize((customer, _args, ctx) => isOwner(customer, ctx.state.user))
|
|
40
|
+
*
|
|
41
|
+
* @see {@link FieldExtensionBuilder.relation} to redefine a relation instead.
|
|
42
|
+
*/
|
|
43
|
+
export declare function protectFields<T = any>(contentType: string, fields: ReadonlyArray<keyof T & string>): FieldProtectionBuilder<T>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resetFieldProtections = exports.getFieldProtections = exports.protectFields = void 0;
|
|
4
|
+
const protections = [];
|
|
5
|
+
/**
|
|
6
|
+
* Protect fields of a content type, including the fields Strapi generates (scalars, enums, media,
|
|
7
|
+
* components and relations), without redefining them.
|
|
8
|
+
*
|
|
9
|
+
* The authorization and middleware are attached by the package's Nexus plugins when the schema is built,
|
|
10
|
+
* so Strapi's own types, arguments, filters, pagination and resolvers stay untouched. For to-many relations
|
|
11
|
+
* on Strapi v5 the matching `<field>_connection` field is protected as well.
|
|
12
|
+
*
|
|
13
|
+
* Call it at module level in a file inside a `graphql` folder; the file does not need to export anything.
|
|
14
|
+
*
|
|
15
|
+
* **Non-null fields:** when a check denies a non-null field, GraphQL nulls the nearest nullable parent
|
|
16
|
+
* instead of only the field (standard GraphQL error propagation). Strapi generates required attributes and,
|
|
17
|
+
* on v5, to-many relations (`[X]!`) as non-null; a warning is logged for those. Protect the
|
|
18
|
+
* `<field>_connection` field only, or check access in the parent query, when that is not acceptable.
|
|
19
|
+
* Several `protectFields` calls for the same field are combined.
|
|
20
|
+
*
|
|
21
|
+
* @typeParam T - Entity type, so `fields` is checked against its keys.
|
|
22
|
+
* @param contentType - Content type UID, e.g. `api::customer.customer`.
|
|
23
|
+
* @param fields - Attribute names to protect.
|
|
24
|
+
* @returns A {@link FieldProtectionBuilder} to add checks and middlewares.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* protectFields<Customer>('api::customer.customer', ['street', 'iban', 'orders'])
|
|
28
|
+
* .authorize((customer, _args, ctx) => isOwner(customer, ctx.state.user))
|
|
29
|
+
*
|
|
30
|
+
* @see {@link FieldExtensionBuilder.relation} to redefine a relation instead.
|
|
31
|
+
*/
|
|
32
|
+
function protectFields(contentType, fields) {
|
|
33
|
+
const protection = { contentType, fields: [...fields], authorizes: [], middlewares: [] };
|
|
34
|
+
protections.push(protection);
|
|
35
|
+
const builder = {
|
|
36
|
+
authorize(fn) {
|
|
37
|
+
protection.authorizes.push(fn);
|
|
38
|
+
return builder;
|
|
39
|
+
},
|
|
40
|
+
middleware(fn) {
|
|
41
|
+
protection.middlewares.push(fn);
|
|
42
|
+
return builder;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
return builder;
|
|
46
|
+
}
|
|
47
|
+
exports.protectFields = protectFields;
|
|
48
|
+
const stripConnectionSuffix = (fieldName) => fieldName.replace(/_connection$/, '');
|
|
49
|
+
/**
|
|
50
|
+
* All protections that apply to `contentType.fieldName` (also matches `<field>_connection`).
|
|
51
|
+
*
|
|
52
|
+
* @internal Used by the Nexus plugins.
|
|
53
|
+
*/
|
|
54
|
+
function getFieldProtections(contentType, fieldName) {
|
|
55
|
+
if (!contentType)
|
|
56
|
+
return [];
|
|
57
|
+
const baseName = stripConnectionSuffix(fieldName);
|
|
58
|
+
return protections.filter((p) => p.contentType === contentType && p.fields.includes(baseName));
|
|
59
|
+
}
|
|
60
|
+
exports.getFieldProtections = getFieldProtections;
|
|
61
|
+
/** @internal Only for tests. */
|
|
62
|
+
function resetFieldProtections() {
|
|
63
|
+
protections.length = 0;
|
|
64
|
+
}
|
|
65
|
+
exports.resetFieldProtections = resetFieldProtections;
|