@directus/api 24.0.1 → 25.0.1

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 (92) hide show
  1. package/dist/app.js +10 -4
  2. package/dist/auth/drivers/oauth2.js +2 -3
  3. package/dist/auth/drivers/openid.js +2 -3
  4. package/dist/cache.d.ts +2 -2
  5. package/dist/cache.js +20 -7
  6. package/dist/controllers/assets.js +2 -2
  7. package/dist/controllers/metrics.d.ts +2 -0
  8. package/dist/controllers/metrics.js +33 -0
  9. package/dist/controllers/server.js +1 -1
  10. package/dist/database/helpers/number/dialects/mssql.d.ts +2 -2
  11. package/dist/database/helpers/number/dialects/mssql.js +3 -3
  12. package/dist/database/helpers/number/dialects/oracle.d.ts +2 -2
  13. package/dist/database/helpers/number/dialects/oracle.js +2 -2
  14. package/dist/database/helpers/number/dialects/sqlite.d.ts +2 -2
  15. package/dist/database/helpers/number/dialects/sqlite.js +2 -2
  16. package/dist/database/helpers/number/types.d.ts +2 -2
  17. package/dist/database/helpers/number/types.js +2 -2
  18. package/dist/database/index.js +3 -0
  19. package/dist/metrics/index.d.ts +1 -0
  20. package/dist/metrics/index.js +1 -0
  21. package/dist/metrics/lib/create-metrics.d.ts +15 -0
  22. package/dist/metrics/lib/create-metrics.js +239 -0
  23. package/dist/metrics/lib/use-metrics.d.ts +17 -0
  24. package/dist/metrics/lib/use-metrics.js +15 -0
  25. package/dist/metrics/types/metric.d.ts +1 -0
  26. package/dist/metrics/types/metric.js +1 -0
  27. package/dist/middleware/respond.js +7 -1
  28. package/dist/operations/condition/index.js +7 -2
  29. package/dist/schedules/metrics.d.ts +7 -0
  30. package/dist/schedules/metrics.js +44 -0
  31. package/dist/services/assets.d.ts +6 -1
  32. package/dist/services/assets.js +8 -6
  33. package/dist/services/collections.js +31 -1
  34. package/dist/services/fields/build-collection-and-field-relations.d.ts +21 -0
  35. package/dist/services/fields/build-collection-and-field-relations.js +55 -0
  36. package/dist/services/fields/get-collection-meta-updates.d.ts +11 -0
  37. package/dist/services/fields/get-collection-meta-updates.js +72 -0
  38. package/dist/services/fields/get-collection-relation-list.d.ts +5 -0
  39. package/dist/services/fields/get-collection-relation-list.js +28 -0
  40. package/dist/services/fields.js +18 -13
  41. package/dist/services/graphql/errors/format.d.ts +6 -0
  42. package/dist/services/graphql/errors/format.js +14 -0
  43. package/dist/services/graphql/index.d.ts +5 -53
  44. package/dist/services/graphql/index.js +5 -2720
  45. package/dist/services/graphql/resolvers/mutation.d.ts +4 -0
  46. package/dist/services/graphql/resolvers/mutation.js +74 -0
  47. package/dist/services/graphql/resolvers/query.d.ts +8 -0
  48. package/dist/services/graphql/resolvers/query.js +87 -0
  49. package/dist/services/graphql/resolvers/system-admin.d.ts +5 -0
  50. package/dist/services/graphql/resolvers/system-admin.js +236 -0
  51. package/dist/services/graphql/resolvers/system-global.d.ts +7 -0
  52. package/dist/services/graphql/resolvers/system-global.js +435 -0
  53. package/dist/services/graphql/resolvers/system.d.ts +11 -0
  54. package/dist/services/graphql/resolvers/system.js +554 -0
  55. package/dist/services/graphql/schema/get-types.d.ts +12 -0
  56. package/dist/services/graphql/schema/get-types.js +223 -0
  57. package/dist/services/graphql/schema/index.d.ts +32 -0
  58. package/dist/services/graphql/schema/index.js +190 -0
  59. package/dist/services/graphql/schema/parse-args.d.ts +9 -0
  60. package/dist/services/graphql/schema/parse-args.js +35 -0
  61. package/dist/services/graphql/schema/parse-query.d.ts +7 -0
  62. package/dist/services/graphql/schema/parse-query.js +98 -0
  63. package/dist/services/graphql/schema/read.d.ts +12 -0
  64. package/dist/services/graphql/schema/read.js +653 -0
  65. package/dist/services/graphql/schema/write.d.ts +9 -0
  66. package/dist/services/graphql/schema/write.js +142 -0
  67. package/dist/services/graphql/subscription.d.ts +1 -1
  68. package/dist/services/graphql/subscription.js +7 -6
  69. package/dist/services/graphql/utils/aggrgate-query.d.ts +6 -0
  70. package/dist/services/graphql/utils/aggrgate-query.js +32 -0
  71. package/dist/services/graphql/utils/replace-fragments.d.ts +6 -0
  72. package/dist/services/graphql/utils/replace-fragments.js +21 -0
  73. package/dist/services/graphql/utils/replace-funcs.d.ts +5 -0
  74. package/dist/services/graphql/utils/replace-funcs.js +21 -0
  75. package/dist/services/graphql/utils/sanitize-gql-schema.d.ts +1 -1
  76. package/dist/services/graphql/utils/sanitize-gql-schema.js +5 -5
  77. package/dist/services/items.js +0 -2
  78. package/dist/services/meta.js +27 -84
  79. package/dist/services/users.d.ts +4 -0
  80. package/dist/services/users.js +23 -1
  81. package/dist/utils/apply-query.d.ts +1 -1
  82. package/dist/utils/apply-query.js +58 -21
  83. package/dist/utils/freeze-schema.d.ts +3 -0
  84. package/dist/utils/freeze-schema.js +31 -0
  85. package/dist/utils/get-accountability-for-token.js +1 -0
  86. package/dist/utils/get-milliseconds.js +1 -1
  87. package/dist/utils/get-schema.js +10 -5
  88. package/dist/utils/permissions-cachable.d.ts +8 -0
  89. package/dist/utils/permissions-cachable.js +38 -0
  90. package/dist/utils/sanitize-schema.d.ts +1 -1
  91. package/dist/websocket/messages.d.ts +6 -6
  92. package/package.json +23 -20
@@ -1,6 +1,7 @@
1
1
  import { useEnv } from '@directus/env';
2
2
  import { parse as parseBytesConfiguration } from 'bytes';
3
3
  import { getCache, setCacheValue } from '../cache.js';
4
+ import getDatabase from '../database/index.js';
4
5
  import { useLogger } from '../logger/index.js';
5
6
  import { ExportService } from '../services/import-export.js';
6
7
  import asyncHandler from '../utils/async-handler.js';
@@ -9,6 +10,7 @@ import { getCacheKey } from '../utils/get-cache-key.js';
9
10
  import { getDateFormatted } from '../utils/get-date-formatted.js';
10
11
  import { getMilliseconds } from '../utils/get-milliseconds.js';
11
12
  import { stringByteSize } from '../utils/get-string-byte-size.js';
13
+ import { permissionsCachable } from '../utils/permissions-cachable.js';
12
14
  export const respond = asyncHandler(async (req, res) => {
13
15
  const env = useEnv();
14
16
  const logger = useLogger();
@@ -26,7 +28,11 @@ export const respond = asyncHandler(async (req, res) => {
26
28
  cache &&
27
29
  !req.sanitizedQuery.export &&
28
30
  res.locals['cache'] !== false &&
29
- exceedsMaxSize === false) {
31
+ exceedsMaxSize === false &&
32
+ (await permissionsCachable(req.collection, {
33
+ knex: getDatabase(),
34
+ schema: req.schema,
35
+ }, req.accountability))) {
30
36
  const key = await getCacheKey(req);
31
37
  try {
32
38
  await setCacheValue(cache, key, res.locals['payload'], getMilliseconds(env['CACHE_TTL']));
@@ -1,11 +1,16 @@
1
- import { validatePayload } from '@directus/utils';
2
1
  import { defineOperationApi } from '@directus/extensions';
2
+ import { validatePayload } from '@directus/utils';
3
+ import { FailedValidationError, joiValidationErrorItemToErrorExtensions } from '@directus/validation';
3
4
  export default defineOperationApi({
4
5
  id: 'condition',
5
6
  handler: ({ filter }, { data }) => {
6
7
  const errors = validatePayload(filter, data, { requireAll: true });
7
8
  if (errors.length > 0) {
8
- throw errors;
9
+ // sanitize and format errors
10
+ const validationErrors = errors
11
+ .map((error) => error.details.map((details) => new FailedValidationError(joiValidationErrorItemToErrorExtensions(details))))
12
+ .flat();
13
+ throw validationErrors;
9
14
  }
10
15
  else {
11
16
  return null;
@@ -0,0 +1,7 @@
1
+ export declare function handleMetricsJob(): Promise<void>;
2
+ /**
3
+ * Schedule the metric generation
4
+ *
5
+ * @returns Whether or not metrics has been initialized
6
+ */
7
+ export default function schedule(): Promise<boolean>;
@@ -0,0 +1,44 @@
1
+ import { useEnv } from '@directus/env';
2
+ import { toBoolean } from '@directus/utils';
3
+ import { scheduleJob } from 'node-schedule';
4
+ import { useLogger } from '../logger/index.js';
5
+ import { useMetrics } from '../metrics/index.js';
6
+ import { validateCron } from '../utils/schedule.js';
7
+ const METRICS_LOCK_TIMEOUT = 10 * 60 * 1000; // 10 mins
8
+ let lockedAt = 0;
9
+ const logger = useLogger();
10
+ const metrics = useMetrics();
11
+ export async function handleMetricsJob() {
12
+ const now = Date.now();
13
+ if (lockedAt !== 0 && lockedAt > now - METRICS_LOCK_TIMEOUT) {
14
+ // ensure only generating metrics once per node
15
+ return;
16
+ }
17
+ lockedAt = Date.now();
18
+ try {
19
+ await metrics?.generate();
20
+ }
21
+ catch (err) {
22
+ logger.warn(`An error was thrown while attempting metric generation`);
23
+ logger.warn(err);
24
+ }
25
+ finally {
26
+ lockedAt = 0;
27
+ }
28
+ }
29
+ /**
30
+ * Schedule the metric generation
31
+ *
32
+ * @returns Whether or not metrics has been initialized
33
+ */
34
+ export default async function schedule() {
35
+ const env = useEnv();
36
+ if (!toBoolean(env['METRICS_ENABLED'])) {
37
+ return false;
38
+ }
39
+ if (!validateCron(String(env['METRICS_SCHEDULE']))) {
40
+ return false;
41
+ }
42
+ scheduleJob('metrics', String(env['METRICS_SCHEDULE']), handleMetricsJob);
43
+ return true;
44
+ }
@@ -10,9 +10,14 @@ export declare class AssetsService {
10
10
  schema: SchemaOverview;
11
11
  filesService: FilesService;
12
12
  constructor(options: AbstractServiceOptions);
13
- getAsset(id: string, transformation?: TransformationSet, range?: Range): Promise<{
13
+ getAsset(id: string, transformation?: TransformationSet, range?: Range, deferStream?: false): Promise<{
14
14
  stream: Readable;
15
15
  file: any;
16
16
  stat: Stat;
17
17
  }>;
18
+ getAsset(id: string, transformation?: TransformationSet, range?: Range, deferStream?: true): Promise<{
19
+ stream: () => Promise<Readable>;
20
+ file: any;
21
+ stat: Stat;
22
+ }>;
18
23
  }
@@ -28,7 +28,7 @@ export class AssetsService {
28
28
  this.schema = options.schema;
29
29
  this.filesService = new FilesService({ ...options, accountability: null });
30
30
  }
31
- async getAsset(id, transformation, range) {
31
+ async getAsset(id, transformation, range, deferStream = false) {
32
32
  const storage = await getStorage();
33
33
  const publicSettings = await this.knex
34
34
  .select('project_logo', 'public_background', 'public_foreground', 'public_favicon')
@@ -99,8 +99,9 @@ export class AssetsService {
99
99
  file.type = contentType(assetFilename) || null;
100
100
  }
101
101
  if (exists) {
102
+ const assetStream = () => storage.location(file.storage).read(assetFilename, { range });
102
103
  return {
103
- stream: await storage.location(file.storage).read(assetFilename, { range }),
104
+ stream: deferStream ? assetStream : await assetStream(),
104
105
  file,
105
106
  stat: await storage.location(file.storage).stat(assetFilename),
106
107
  };
@@ -123,7 +124,6 @@ export class AssetsService {
123
124
  reason: 'Server too busy',
124
125
  });
125
126
  }
126
- const readStream = await storage.location(file.storage).read(file.filename_disk, { range, version });
127
127
  const transformer = getSharpInstance();
128
128
  transformer.timeout({
129
129
  seconds: clamp(Math.round(getMilliseconds(env['ASSETS_TRANSFORM_TIMEOUT'], 0) / 1000), 1, 3600),
@@ -131,6 +131,7 @@ export class AssetsService {
131
131
  if (transforms.find((transform) => transform[0] === 'rotate') === undefined)
132
132
  transformer.rotate();
133
133
  transforms.forEach(([method, ...args]) => transformer[method].apply(transformer, args));
134
+ const readStream = await storage.location(file.storage).read(file.filename_disk, { range, version });
134
135
  readStream.on('error', (e) => {
135
136
  logger.error(e, `Couldn't transform file ${file.id}`);
136
137
  readStream.unpipe(transformer);
@@ -152,16 +153,17 @@ export class AssetsService {
152
153
  throw error;
153
154
  }
154
155
  }
156
+ const assetStream = () => storage.location(file.storage).read(assetFilename, { range, version });
155
157
  return {
156
- stream: await storage.location(file.storage).read(assetFilename, { range, version }),
158
+ stream: deferStream ? assetStream : await assetStream(),
157
159
  stat: await storage.location(file.storage).stat(assetFilename),
158
160
  file,
159
161
  };
160
162
  }
161
163
  else {
162
- const readStream = await storage.location(file.storage).read(file.filename_disk, { range, version });
164
+ const assetStream = () => storage.location(file.storage).read(file.filename_disk, { range, version });
163
165
  const stat = await storage.location(file.storage).stat(file.filename_disk);
164
- return { stream: readStream, file, stat };
166
+ return { stream: deferStream ? assetStream : await assetStream(), file, stat };
165
167
  }
166
168
  }
167
169
  }
@@ -15,6 +15,9 @@ import { getSchema } from '../utils/get-schema.js';
15
15
  import { shouldClearCache } from '../utils/should-clear-cache.js';
16
16
  import { transaction } from '../utils/transaction.js';
17
17
  import { FieldsService } from './fields.js';
18
+ import { buildCollectionAndFieldRelations } from './fields/build-collection-and-field-relations.js';
19
+ import { getCollectionMetaUpdates } from './fields/get-collection-meta-updates.js';
20
+ import { getCollectionRelationList } from './fields/get-collection-relation-list.js';
18
21
  import { ItemsService } from './items.js';
19
22
  export class CollectionsService {
20
23
  knex;
@@ -478,7 +481,18 @@ export class CollectionsService {
478
481
  accountability: this.accountability,
479
482
  schema: this.schema,
480
483
  });
481
- await trx('directus_fields').delete().where('collection', '=', collectionKey);
484
+ const fieldItemsService = new ItemsService('directus_fields', {
485
+ knex: trx,
486
+ accountability: this.accountability,
487
+ schema: this.schema,
488
+ });
489
+ await fieldItemsService.deleteByQuery({
490
+ filter: {
491
+ collection: { _eq: collectionKey },
492
+ },
493
+ }, {
494
+ bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params),
495
+ });
482
496
  await trx('directus_presets').delete().where('collection', '=', collectionKey);
483
497
  const revisionsToDelete = await trx
484
498
  .select('id')
@@ -494,6 +508,22 @@ export class CollectionsService {
494
508
  await trx('directus_activity').delete().where('collection', '=', collectionKey);
495
509
  await trx('directus_permissions').delete().where('collection', '=', collectionKey);
496
510
  await trx('directus_relations').delete().where({ many_collection: collectionKey });
511
+ const { collectionRelationTree, fieldToCollectionList } = await buildCollectionAndFieldRelations(this.schema.relations);
512
+ const collectionRelationList = getCollectionRelationList(collectionKey, collectionRelationTree);
513
+ // only process duplication fields if related collections have them
514
+ if (collectionRelationList.size !== 0) {
515
+ const collectionMetas = await trx
516
+ .select('collection', 'archive_field', 'sort_field', 'item_duplication_fields')
517
+ .from('directus_collections')
518
+ .whereIn('collection', Array.from(collectionRelationList))
519
+ .whereNotNull('item_duplication_fields');
520
+ await Promise.all(Object.keys(this.schema.collections[collectionKey]?.fields ?? {}).map(async (fieldKey) => {
521
+ const collectionMetaUpdates = getCollectionMetaUpdates(collectionKey, fieldKey, collectionMetas, this.schema.collections, fieldToCollectionList);
522
+ for (const meta of collectionMetaUpdates) {
523
+ await trx('directus_collections').update(meta.updates).where({ collection: meta.collection });
524
+ }
525
+ }));
526
+ }
497
527
  const relations = this.schema.relations.filter((relation) => {
498
528
  return relation.collection === collectionKey || relation.related_collection === collectionKey;
499
529
  });
@@ -0,0 +1,21 @@
1
+ import type { Relation } from '@directus/types';
2
+ /**
3
+ * Builds two maps where collectionRelationTree is a map of collection to related collections
4
+ * and fieldToCollectionList is a map of field:collection to related collection.
5
+ *
6
+ * @example
7
+ * returns {
8
+ * collectionRelationTree: new Map([
9
+ * ['B', new Set(['A'])],
10
+ * ['A', new Set(['B'])],
11
+ * ]),
12
+ * fieldToCollectionList: new Map([
13
+ * ['B:b', 'A'],
14
+ * ['A:a', 'B'],
15
+ * ]),
16
+ * }
17
+ */
18
+ export declare function buildCollectionAndFieldRelations(relations: Relation[]): {
19
+ collectionRelationTree: Map<string, Set<string>>;
20
+ fieldToCollectionList: Map<string, string>;
21
+ };
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Builds two maps where collectionRelationTree is a map of collection to related collections
3
+ * and fieldToCollectionList is a map of field:collection to related collection.
4
+ *
5
+ * @example
6
+ * returns {
7
+ * collectionRelationTree: new Map([
8
+ * ['B', new Set(['A'])],
9
+ * ['A', new Set(['B'])],
10
+ * ]),
11
+ * fieldToCollectionList: new Map([
12
+ * ['B:b', 'A'],
13
+ * ['A:a', 'B'],
14
+ * ]),
15
+ * }
16
+ */
17
+ export function buildCollectionAndFieldRelations(relations) {
18
+ // Map<Collection, Set<RelatedCollection>>
19
+ const collectionRelationTree = new Map();
20
+ // Map<Field:Collection, RelatedCollection>
21
+ const fieldToCollectionList = new Map();
22
+ for (const relation of relations) {
23
+ let relatedCollections = [];
24
+ if (relation.related_collection) {
25
+ relatedCollections.push(relation.related_collection);
26
+ }
27
+ else if (relation.meta?.one_collection_field && relation.meta?.one_allowed_collections) {
28
+ relatedCollections = relation.meta?.one_allowed_collections;
29
+ }
30
+ else {
31
+ // safe guard
32
+ continue;
33
+ }
34
+ for (const relatedCollection of relatedCollections) {
35
+ let fieldToCollectionListKey = relation.collection + ':' + relation.field;
36
+ const collectionList = collectionRelationTree.get(relatedCollection) ?? new Set();
37
+ collectionList.add(relation.collection);
38
+ // O2M can have outward duplication path
39
+ if (relation.meta?.one_field) {
40
+ const relatedfieldToCollectionListKey = relatedCollection + ':' + relation.meta.one_field;
41
+ const realatedCollectionList = collectionRelationTree.get(relation.collection) ?? new Set();
42
+ realatedCollectionList.add(relatedCollection);
43
+ fieldToCollectionList.set(relatedfieldToCollectionListKey, relation.collection);
44
+ collectionRelationTree.set(relation.collection, realatedCollectionList);
45
+ }
46
+ // m2a fields show as field:collection in duplication path
47
+ if (relation.meta?.one_allowed_collections) {
48
+ fieldToCollectionListKey += ':' + relatedCollection;
49
+ }
50
+ fieldToCollectionList.set(fieldToCollectionListKey, relatedCollection);
51
+ collectionRelationTree.set(relatedCollection, collectionList);
52
+ }
53
+ }
54
+ return { collectionRelationTree, fieldToCollectionList };
55
+ }
@@ -0,0 +1,11 @@
1
+ import type { CollectionsOverview } from '@directus/types';
2
+ import type { Knex } from 'knex';
3
+ export declare function getCollectionMetaUpdates(collection: string, field: string, collectionMetas: {
4
+ archive_field?: null | string;
5
+ sort_field?: null | string;
6
+ item_duplication_fields?: null | string | string[];
7
+ collection: string;
8
+ }[], collections: CollectionsOverview, fieldToCollectionList: Map<string, string>): {
9
+ collection: string;
10
+ updates: Record<string, Knex.Value>;
11
+ }[];
@@ -0,0 +1,72 @@
1
+ import { parseJSON } from '@directus/utils';
2
+ export function getCollectionMetaUpdates(collection, field, collectionMetas, collections, fieldToCollectionList) {
3
+ const collectionMetaUpdates = [];
4
+ for (const collectionMeta of collectionMetas) {
5
+ let hasUpdates = false;
6
+ const meta = {
7
+ collection: collectionMeta.collection,
8
+ updates: {},
9
+ };
10
+ if (collectionMeta.collection === collection) {
11
+ if (collectionMeta?.archive_field === field) {
12
+ meta.updates['archive_field'] = null;
13
+ hasUpdates = true;
14
+ }
15
+ if (collectionMeta?.sort_field === field) {
16
+ meta.updates['sort_field'] = null;
17
+ hasUpdates = true;
18
+ }
19
+ }
20
+ if (collectionMeta?.item_duplication_fields) {
21
+ const itemDuplicationPaths = typeof collectionMeta.item_duplication_fields === 'string'
22
+ ? parseJSON(collectionMeta.item_duplication_fields)
23
+ : collectionMeta.item_duplication_fields;
24
+ const updatedPaths = [];
25
+ for (const path of itemDuplicationPaths) {
26
+ const updatedPath = updateItemDuplicationPath(path, collectionMeta.collection, field, collection, collections, fieldToCollectionList);
27
+ if (updatedPath && updatedPath.length !== 0) {
28
+ updatedPaths.push(updatedPath.join('.'));
29
+ }
30
+ }
31
+ // only add updates on change
32
+ if (updatedPaths.length !== itemDuplicationPaths.length) {
33
+ meta.updates['item_duplication_fields'] = updatedPaths.length !== 0 ? JSON.stringify(updatedPaths) : null;
34
+ hasUpdates = true;
35
+ }
36
+ }
37
+ if (hasUpdates) {
38
+ collectionMetaUpdates.push(meta);
39
+ }
40
+ }
41
+ return collectionMetaUpdates;
42
+ }
43
+ function updateItemDuplicationPath(path, root, field, collection, collections, fieldToCollectionList) {
44
+ let currentCollection = root;
45
+ const parts = path.split('.');
46
+ // if the field name is not present in the path we can skip processing as no possible match
47
+ if ([field, `.${field}`, `.${field}.`, `${field}.`].some((fieldPart) => path.includes(fieldPart)) === false) {
48
+ return parts;
49
+ }
50
+ const updatedParts = [];
51
+ for (let index = 0; index < parts.length; index++) {
52
+ const part = parts[index];
53
+ // Invalid path for the field that is currently being removed
54
+ if (currentCollection === collection && part === field)
55
+ return;
56
+ const isLastPart = index === parts.length - 1;
57
+ const isLocalField = typeof collections[currentCollection]?.['fields'][part] !== 'undefined';
58
+ const nextCollectionNode = fieldToCollectionList.get(`${currentCollection}:${part}`);
59
+ // Invalid path for old deleted collections
60
+ if (!nextCollectionNode && !isLastPart)
61
+ return;
62
+ // Invalid path for old deleted fields
63
+ if (!nextCollectionNode && isLastPart && !isLocalField)
64
+ return;
65
+ // next/last path is relational
66
+ if (nextCollectionNode) {
67
+ currentCollection = nextCollectionNode;
68
+ }
69
+ updatedParts.push(part);
70
+ }
71
+ return updatedParts;
72
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Returns a list of all related collections for a given collection.
3
+ * Or in math terms, returns the [strongly connected component](https://en.wikipedia.org/wiki/Strongly_connected_component) that a given node belongs to.
4
+ */
5
+ export declare function getCollectionRelationList(collection: string, collectionRelationTree: Map<string, Set<string>>): Set<string>;
@@ -0,0 +1,28 @@
1
+ import { isSystemCollection } from '@directus/system-data';
2
+ /**
3
+ * Returns a list of all related collections for a given collection.
4
+ * Or in math terms, returns the [strongly connected component](https://en.wikipedia.org/wiki/Strongly_connected_component) that a given node belongs to.
5
+ */
6
+ export function getCollectionRelationList(collection, collectionRelationTree) {
7
+ const collectionRelationList = new Set();
8
+ traverseCollectionRelationTree(collection);
9
+ return collectionRelationList;
10
+ function traverseCollectionRelationTree(root) {
11
+ const relationTree = collectionRelationTree.get(root);
12
+ if (!relationTree)
13
+ return;
14
+ for (const relationNode of relationTree) {
15
+ addRelationNode(relationNode);
16
+ }
17
+ }
18
+ function addRelationNode(node) {
19
+ // system collections cannot have duplication fields and therfore can be skipped
20
+ if (isSystemCollection(node))
21
+ return;
22
+ // skip circular reference and existing linked nodes
23
+ if (node === collection || collectionRelationList.has(node))
24
+ return;
25
+ collectionRelationList.add(node);
26
+ traverseCollectionRelationTree(node);
27
+ }
28
+ }
@@ -20,6 +20,9 @@ import { getSchema } from '../utils/get-schema.js';
20
20
  import { sanitizeColumn } from '../utils/sanitize-schema.js';
21
21
  import { shouldClearCache } from '../utils/should-clear-cache.js';
22
22
  import { transaction } from '../utils/transaction.js';
23
+ import { buildCollectionAndFieldRelations } from './fields/build-collection-and-field-relations.js';
24
+ import { getCollectionMetaUpdates } from './fields/get-collection-meta-updates.js';
25
+ import { getCollectionRelationList } from './fields/get-collection-relation-list.js';
23
26
  import { ItemsService } from './items.js';
24
27
  import { PayloadService } from './payload.js';
25
28
  import { RelationsService } from './relations.js';
@@ -58,7 +61,7 @@ export class FieldsService {
58
61
  if (!columnInfo) {
59
62
  columnInfo = await this.schemaInspector.columnInfo();
60
63
  if (schemaCacheIsEnabled) {
61
- setCacheValue(this.schemaCache, 'columnInfo', columnInfo);
64
+ await setCacheValue(this.schemaCache, 'columnInfo', columnInfo);
62
65
  }
63
66
  }
64
67
  if (collection) {
@@ -556,20 +559,22 @@ export class FieldsService {
556
559
  table.dropColumn(field);
557
560
  });
558
561
  }
559
- const collectionMeta = await trx
560
- .select('archive_field', 'sort_field')
562
+ const { collectionRelationTree, fieldToCollectionList } = await buildCollectionAndFieldRelations(this.schema.relations);
563
+ const collectionRelationList = getCollectionRelationList(collection, collectionRelationTree);
564
+ const collectionMetaQuery = trx
565
+ .queryBuilder()
566
+ .select('collection', 'archive_field', 'sort_field', 'item_duplication_fields')
561
567
  .from('directus_collections')
562
- .where({ collection })
563
- .first();
564
- const collectionMetaUpdates = {};
565
- if (collectionMeta?.archive_field === field) {
566
- collectionMetaUpdates['archive_field'] = null;
567
- }
568
- if (collectionMeta?.sort_field === field) {
569
- collectionMetaUpdates['sort_field'] = null;
568
+ .where({ collection });
569
+ if (collectionRelationList.size !== 0) {
570
+ collectionMetaQuery.orWhere(function () {
571
+ this.whereIn('collection', Array.from(collectionRelationList)).whereNotNull('item_duplication_fields');
572
+ });
570
573
  }
571
- if (Object.keys(collectionMetaUpdates).length > 0) {
572
- await trx('directus_collections').update(collectionMetaUpdates).where({ collection });
574
+ const collectionMetas = await collectionMetaQuery;
575
+ const collectionMetaUpdates = getCollectionMetaUpdates(collection, field, collectionMetas, this.schema.collections, fieldToCollectionList);
576
+ for (const meta of collectionMetaUpdates) {
577
+ await trx('directus_collections').update(meta.updates).where({ collection: meta.collection });
573
578
  }
574
579
  // Cleanup directus_fields
575
580
  const metaRow = await trx
@@ -0,0 +1,6 @@
1
+ import { type DirectusError } from '@directus/errors';
2
+ import { GraphQLError } from 'graphql';
3
+ /**
4
+ * Convert Directus-Exception into a GraphQL format, so it can be returned by GraphQL properly.
5
+ */
6
+ export declare function formatError(error: DirectusError | DirectusError[]): GraphQLError;
@@ -0,0 +1,14 @@
1
+ import {} from '@directus/errors';
2
+ import { GraphQLError } from 'graphql';
3
+ import { set } from 'lodash-es';
4
+ /**
5
+ * Convert Directus-Exception into a GraphQL format, so it can be returned by GraphQL properly.
6
+ */
7
+ export function formatError(error) {
8
+ if (Array.isArray(error)) {
9
+ set(error[0], 'extensions.code', error[0].code);
10
+ return new GraphQLError(error[0].message, undefined, undefined, undefined, undefined, error[0]);
11
+ }
12
+ set(error, 'extensions.code', error.code);
13
+ return new GraphQLError(error.message, undefined, undefined, undefined, undefined, error);
14
+ }
@@ -1,17 +1,15 @@
1
- import { type DirectusError } from '@directus/errors';
2
- import type { Accountability, Filter, Item, Query, SchemaOverview } from '@directus/types';
3
- import type { ArgumentNode, FormattedExecutionResult, FragmentDefinitionNode, GraphQLResolveInfo, SelectionNode } from 'graphql';
4
- import { GraphQLError, GraphQLSchema } from 'graphql';
5
- import { ObjectTypeComposer, SchemaComposer } from 'graphql-compose';
1
+ import type { Accountability, Item, Query, SchemaOverview } from '@directus/types';
2
+ import type { FormattedExecutionResult, GraphQLSchema } from 'graphql';
6
3
  import type { Knex } from 'knex';
7
4
  import type { AbstractServiceOptions, GraphQLParams } from '../../types/index.js';
5
+ export type GQLScope = 'items' | 'system';
8
6
  export declare class GraphQLService {
9
7
  accountability: Accountability | null;
10
8
  knex: Knex;
11
9
  schema: SchemaOverview;
12
- scope: 'items' | 'system';
10
+ scope: GQLScope;
13
11
  constructor(options: AbstractServiceOptions & {
14
- scope: 'items' | 'system';
12
+ scope: GQLScope;
15
13
  });
16
14
  /**
17
15
  * Execute a GraphQL structure
@@ -23,12 +21,6 @@ export declare class GraphQLService {
23
21
  getSchema(): Promise<GraphQLSchema>;
24
22
  getSchema(type: 'schema'): Promise<GraphQLSchema>;
25
23
  getSchema(type: 'sdl'): Promise<GraphQLSchema | string>;
26
- /**
27
- * Generic resolver that's used for every "regular" items/system query. Converts the incoming GraphQL AST / fragments into
28
- * Directus' query structure which is then executed by the services.
29
- */
30
- resolveQuery(info: GraphQLResolveInfo): Promise<Partial<Item> | null>;
31
- resolveMutation(args: Record<string, any>, info: GraphQLResolveInfo): Promise<Partial<Item> | boolean | undefined>;
32
24
  /**
33
25
  * Execute the read action on the correct service. Checks for singleton as well.
34
26
  */
@@ -37,44 +29,4 @@ export declare class GraphQLService {
37
29
  * Upsert and read singleton item
38
30
  */
39
31
  upsertSingleton(collection: string, body: Record<string, any> | Record<string, any>[], query: Query): Promise<Partial<Item> | boolean>;
40
- /**
41
- * GraphQL's regular resolver `args` variable only contains the "top-level" arguments. Seeing that we convert the
42
- * whole nested tree into one big query using Directus' own query resolver, we want to have a nested structure of
43
- * arguments for the whole resolving tree, which can later be transformed into Directus' AST using `deep`.
44
- * In order to do that, we'll parse over all ArgumentNodes and ObjectFieldNodes to manually recreate an object structure
45
- * of arguments
46
- */
47
- parseArgs(args: readonly ArgumentNode[], variableValues: GraphQLResolveInfo['variableValues']): Record<string, any>;
48
- /**
49
- * Get a Directus Query object from the parsed arguments (rawQuery) and GraphQL AST selectionSet. Converts SelectionSet into
50
- * Directus' `fields` query for use in the resolver. Also applies variables where appropriate.
51
- */
52
- getQuery(rawQuery: Query, selections: readonly SelectionNode[], variableValues: GraphQLResolveInfo['variableValues']): Query;
53
- /**
54
- * Resolve the aggregation query based on the requested aggregated fields
55
- */
56
- getAggregateQuery(rawQuery: Query, selections: readonly SelectionNode[]): Query;
57
- /**
58
- * Replace functions from GraphQL format to Directus-Filter format
59
- */
60
- replaceFuncs(filter: Filter): Filter;
61
- /**
62
- * Convert Directus-Exception into a GraphQL format, so it can be returned by GraphQL properly.
63
- */
64
- formatError(error: DirectusError | DirectusError[]): GraphQLError;
65
- /**
66
- * Replace all fragments in a selectionset for the actual selection set as defined in the fragment
67
- * Effectively merges the selections with the fragments used in those selections
68
- */
69
- replaceFragmentsInSelections(selections: readonly SelectionNode[] | undefined, fragments: Record<string, FragmentDefinitionNode>): readonly SelectionNode[] | null;
70
- injectSystemResolvers(schemaComposer: SchemaComposer<GraphQLParams['contextValue']>, { CreateCollectionTypes, ReadCollectionTypes, UpdateCollectionTypes, }: {
71
- CreateCollectionTypes: Record<string, ObjectTypeComposer<any, any>>;
72
- ReadCollectionTypes: Record<string, ObjectTypeComposer<any, any>>;
73
- UpdateCollectionTypes: Record<string, ObjectTypeComposer<any, any>>;
74
- }, schema: {
75
- create: SchemaOverview;
76
- read: SchemaOverview;
77
- update: SchemaOverview;
78
- delete: SchemaOverview;
79
- }): SchemaComposer<any>;
80
32
  }