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.
Files changed (44) hide show
  1. package/README.md +173 -16
  2. package/dist/server/index.d.ts +1 -1
  3. package/dist/server/nexus/plugins/field-authorize.plugin.d.ts +21 -0
  4. package/dist/server/nexus/plugins/field-authorize.plugin.js +68 -35
  5. package/dist/server/nexus/plugins/middleware.plugin.d.ts +10 -2
  6. package/dist/server/nexus/plugins/middleware.plugin.js +20 -33
  7. package/dist/server/services/file-service.d.ts +7 -6
  8. package/dist/server/services/file-service.js +36 -36
  9. package/dist/server/services/index.d.ts +1 -1
  10. package/dist/server/types.d.ts +18 -3
  11. package/dist/server/utils/collection-type.d.ts +13 -2
  12. package/dist/server/utils/collection-type.js +13 -2
  13. package/dist/server/utils/field-builder/builder.d.ts +13 -4
  14. package/dist/server/utils/field-builder/builder.js +214 -119
  15. package/dist/server/utils/field-builder/helpers.d.ts +21 -6
  16. package/dist/server/utils/field-builder/helpers.js +26 -4
  17. package/dist/server/utils/field-builder/types.d.ts +297 -76
  18. package/dist/server/utils/graphql-extension.d.ts +24 -3
  19. package/dist/server/utils/graphql-extension.js +73 -7
  20. package/dist/server/utils/index.d.ts +2 -0
  21. package/dist/server/utils/index.js +11 -0
  22. package/dist/server/utils/loaders.d.ts +110 -0
  23. package/dist/server/utils/loaders.js +222 -0
  24. package/dist/server/utils/misc.d.ts +6 -0
  25. package/dist/server/utils/misc.js +6 -0
  26. package/dist/server/utils/override.d.ts +32 -0
  27. package/dist/server/utils/override.js +54 -26
  28. package/dist/server/utils/registry/field-protection.d.ts +43 -0
  29. package/dist/server/utils/registry/field-protection.js +65 -0
  30. package/dist/server/utils/registry/global-registry.d.ts +69 -14
  31. package/dist/server/utils/registry/global-registry.js +53 -7
  32. package/dist/server/utils/registry/index.d.ts +2 -0
  33. package/dist/server/utils/registry/index.js +2 -0
  34. package/dist/server/utils/registry/resolve-field.d.ts +1 -0
  35. package/dist/server/utils/registry/resolve-field.js +67 -0
  36. package/dist/server/utils/shadow-crud.d.ts +28 -1
  37. package/dist/server/utils/shadow-crud.js +32 -1
  38. package/dist/server/utils/single-type.d.ts +30 -1
  39. package/dist/server/utils/single-type.js +25 -0
  40. package/dist/server/utils/strapi-compat.d.ts +90 -0
  41. package/dist/server/utils/strapi-compat.js +203 -0
  42. package/dist/server/utils/types.d.ts +25 -1
  43. package/dist/tsconfig.server.tsbuildinfo +1 -1
  44. 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 {};