@directus/api 25.0.0 → 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.
@@ -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';
@@ -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
@@ -33,9 +33,10 @@ export class MetaService {
33
33
  return this.filterCount(collection, {});
34
34
  }
35
35
  async filterCount(collection, query) {
36
+ const primaryKeyName = this.schema.collections[collection].primary;
36
37
  const aggregateQuery = {
37
38
  aggregate: {
38
- count: ['*'],
39
+ countDistinct: [primaryKeyName],
39
40
  },
40
41
  search: query.search ?? null,
41
42
  filter: query.filter ?? null,
@@ -52,6 +53,7 @@ export class MetaService {
52
53
  const records = await runAst(ast, this.schema, this.accountability, {
53
54
  knex: this.knex,
54
55
  });
55
- return Number((isArray(records) ? records[0]?.['count'] : records?.['count']) ?? 0);
56
+ return Number((isArray(records) ? records[0]?.['countDistinct'][primaryKeyName] : records?.['countDistinct'][primaryKeyName]) ??
57
+ 0);
56
58
  }
57
59
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@directus/api",
3
- "version": "25.0.0",
3
+ "version": "25.0.1",
4
4
  "description": "Directus is a real-time API and App dashboard for managing SQL database content",
5
5
  "keywords": [
6
6
  "directus",
@@ -150,29 +150,29 @@
150
150
  "ws": "8.18.0",
151
151
  "zod": "3.24.1",
152
152
  "zod-validation-error": "3.4.0",
153
+ "@directus/env": "5.0.2",
153
154
  "@directus/constants": "13.0.0",
154
- "@directus/app": "13.7.0",
155
- "@directus/env": "5.0.1",
156
155
  "@directus/errors": "2.0.0",
157
- "@directus/extensions": "3.0.2",
158
- "@directus/extensions-sdk": "13.0.2",
156
+ "@directus/app": "13.7.1",
157
+ "@directus/extensions-sdk": "13.0.3",
158
+ "@directus/extensions": "3.0.3",
159
159
  "@directus/format-title": "12.0.0",
160
- "@directus/memory": "3.0.1",
161
- "@directus/pressure": "3.0.1",
162
- "@directus/extensions-registry": "3.0.2",
163
- "@directus/storage": "12.0.0",
160
+ "@directus/extensions-registry": "3.0.3",
161
+ "@directus/pressure": "3.0.2",
162
+ "@directus/memory": "3.0.2",
164
163
  "@directus/schema": "13.0.0",
165
164
  "@directus/specs": "11.1.0",
166
- "@directus/storage-driver-azure": "12.0.1",
167
- "@directus/storage-driver-gcs": "12.0.1",
168
- "@directus/storage-driver-cloudinary": "12.0.1",
169
- "@directus/storage-driver-local": "12.0.0",
170
- "@directus/storage-driver-supabase": "3.0.1",
171
- "@directus/storage-driver-s3": "12.0.1",
172
- "@directus/system-data": "3.0.0",
173
- "@directus/utils": "13.0.1",
174
- "@directus/validation": "2.0.1",
175
- "directus": "11.5.0"
165
+ "@directus/storage": "12.0.0",
166
+ "@directus/storage-driver-azure": "12.0.2",
167
+ "@directus/storage-driver-cloudinary": "12.0.2",
168
+ "@directus/storage-driver-gcs": "12.0.2",
169
+ "@directus/storage-driver-s3": "12.0.2",
170
+ "@directus/storage-driver-supabase": "3.0.2",
171
+ "@directus/system-data": "3.0.1",
172
+ "@directus/utils": "13.0.2",
173
+ "@directus/validation": "2.0.2",
174
+ "directus": "11.5.1",
175
+ "@directus/storage-driver-local": "12.0.0"
176
176
  },
177
177
  "devDependencies": {
178
178
  "@directus/tsconfig": "3.0.0",