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.
Files changed (57) hide show
  1. package/README.md +458 -439
  2. package/dist/server/index.d.ts +14 -8
  3. package/dist/server/index.js +1 -0
  4. package/dist/server/nexus/index.d.ts +1 -0
  5. package/dist/server/nexus/index.js +17 -0
  6. package/dist/server/nexus/plugins/field-authorize.plugin.d.ts +36 -0
  7. package/dist/server/nexus/plugins/field-authorize.plugin.js +122 -0
  8. package/dist/server/nexus/plugins/index.d.ts +2 -0
  9. package/dist/server/nexus/plugins/index.js +18 -0
  10. package/dist/server/nexus/plugins/middleware.plugin.d.ts +25 -0
  11. package/dist/server/nexus/plugins/middleware.plugin.js +63 -0
  12. package/dist/server/register.js +6 -7
  13. package/dist/server/services/file-service.d.ts +7 -6
  14. package/dist/server/services/file-service.js +36 -30
  15. package/dist/server/services/index.d.ts +1 -1
  16. package/dist/server/types.d.ts +28 -13
  17. package/dist/server/utils/collection-type.d.ts +13 -2
  18. package/dist/server/utils/collection-type.js +13 -2
  19. package/dist/server/utils/field-builder/builder.d.ts +33 -0
  20. package/dist/server/utils/field-builder/builder.js +552 -0
  21. package/dist/server/utils/field-builder/helpers.d.ts +34 -0
  22. package/dist/server/utils/field-builder/helpers.js +78 -0
  23. package/dist/server/utils/field-builder/index.d.ts +3 -0
  24. package/dist/server/utils/field-builder/index.js +19 -0
  25. package/dist/server/utils/field-builder/types.d.ts +371 -0
  26. package/dist/server/utils/field-builder/types.js +2 -0
  27. package/dist/server/utils/graphql-extension.d.ts +29 -3
  28. package/dist/server/utils/graphql-extension.js +94 -26
  29. package/dist/server/utils/index.d.ts +3 -1
  30. package/dist/server/utils/index.js +12 -1
  31. package/dist/server/utils/loaders.d.ts +94 -0
  32. package/dist/server/utils/loaders.js +166 -0
  33. package/dist/server/utils/misc.d.ts +6 -0
  34. package/dist/server/utils/misc.js +6 -0
  35. package/dist/server/utils/override.d.ts +34 -1
  36. package/dist/server/utils/override.js +54 -26
  37. package/dist/server/utils/registry/field-protection.d.ts +43 -0
  38. package/dist/server/utils/registry/field-protection.js +65 -0
  39. package/dist/server/utils/registry/global-registry.d.ts +63 -0
  40. package/dist/server/utils/registry/global-registry.js +56 -0
  41. package/dist/server/utils/registry/index.d.ts +3 -0
  42. package/dist/server/utils/registry/index.js +19 -0
  43. package/dist/server/utils/registry/resolve-field.d.ts +1 -0
  44. package/dist/server/utils/registry/resolve-field.js +67 -0
  45. package/dist/server/utils/shadow-crud.d.ts +28 -1
  46. package/dist/server/utils/shadow-crud.js +32 -1
  47. package/dist/server/utils/single-type.d.ts +30 -1
  48. package/dist/server/utils/single-type.js +25 -0
  49. package/dist/server/utils/strapi-compat.d.ts +85 -0
  50. package/dist/server/utils/strapi-compat.js +194 -0
  51. package/dist/server/utils/types.d.ts +25 -1
  52. package/dist/tsconfig.server.tsbuildinfo +1 -1
  53. package/package.json +7 -4
  54. package/dist/server/utils/field-builder.d.ts +0 -31
  55. package/dist/server/utils/field-builder.js +0 -113
  56. package/dist/server/utils/middleware.d.ts +0 -17
  57. 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 './middleware';
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
- const { attributeName, attribute, contentType } = options;
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
- // @ts-ignore
158
- const isToManyRelation = attribute.relation.endsWith('Many');
159
- const resolve = buildAssociationResolver({
160
- contentTypeUID: contentType.uid,
161
- attributeName,
162
- strapi,
163
- });
164
- // @ts-ignore
165
- const targetContentType = strapi.getModel(attribute.target);
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 (isPrivate(contentType)(attribute)) {
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)(attribute)) {
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
- ...overrideOptions,
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;