@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.
- package/dist/services/collections.js +31 -1
- package/dist/services/fields/build-collection-and-field-relations.d.ts +21 -0
- package/dist/services/fields/build-collection-and-field-relations.js +55 -0
- package/dist/services/fields/get-collection-meta-updates.d.ts +11 -0
- package/dist/services/fields/get-collection-meta-updates.js +72 -0
- package/dist/services/fields/get-collection-relation-list.d.ts +5 -0
- package/dist/services/fields/get-collection-relation-list.js +28 -0
- package/dist/services/fields.js +17 -12
- package/dist/services/meta.js +4 -2
- package/package.json +19 -19
|
@@ -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
|
-
|
|
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
|
+
}
|
package/dist/services/fields.js
CHANGED
|
@@ -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
|
|
560
|
-
|
|
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
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
-
|
|
572
|
-
|
|
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
|
package/dist/services/meta.js
CHANGED
|
@@ -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
|
-
|
|
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]?.['
|
|
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.
|
|
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/
|
|
158
|
-
"@directus/extensions-sdk": "13.0.
|
|
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/
|
|
161
|
-
"@directus/pressure": "3.0.
|
|
162
|
-
"@directus/
|
|
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
|
|
167
|
-
"@directus/storage-driver-
|
|
168
|
-
"@directus/storage-driver-cloudinary": "12.0.
|
|
169
|
-
"@directus/storage-driver-
|
|
170
|
-
"@directus/storage-driver-
|
|
171
|
-
"@directus/storage-driver-
|
|
172
|
-
"@directus/system-data": "3.0.
|
|
173
|
-
"@directus/utils": "13.0.
|
|
174
|
-
"@directus/validation": "2.0.
|
|
175
|
-
"directus": "11.5.
|
|
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",
|