@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.
- package/dist/app.js +10 -4
- package/dist/auth/drivers/oauth2.js +2 -3
- package/dist/auth/drivers/openid.js +2 -3
- package/dist/cache.d.ts +2 -2
- package/dist/cache.js +20 -7
- package/dist/controllers/assets.js +2 -2
- package/dist/controllers/metrics.d.ts +2 -0
- package/dist/controllers/metrics.js +33 -0
- package/dist/controllers/server.js +1 -1
- package/dist/database/helpers/number/dialects/mssql.d.ts +2 -2
- package/dist/database/helpers/number/dialects/mssql.js +3 -3
- package/dist/database/helpers/number/dialects/oracle.d.ts +2 -2
- package/dist/database/helpers/number/dialects/oracle.js +2 -2
- package/dist/database/helpers/number/dialects/sqlite.d.ts +2 -2
- package/dist/database/helpers/number/dialects/sqlite.js +2 -2
- package/dist/database/helpers/number/types.d.ts +2 -2
- package/dist/database/helpers/number/types.js +2 -2
- package/dist/database/index.js +3 -0
- package/dist/metrics/index.d.ts +1 -0
- package/dist/metrics/index.js +1 -0
- package/dist/metrics/lib/create-metrics.d.ts +15 -0
- package/dist/metrics/lib/create-metrics.js +239 -0
- package/dist/metrics/lib/use-metrics.d.ts +17 -0
- package/dist/metrics/lib/use-metrics.js +15 -0
- package/dist/metrics/types/metric.d.ts +1 -0
- package/dist/metrics/types/metric.js +1 -0
- package/dist/middleware/respond.js +7 -1
- package/dist/operations/condition/index.js +7 -2
- package/dist/schedules/metrics.d.ts +7 -0
- package/dist/schedules/metrics.js +44 -0
- package/dist/services/assets.d.ts +6 -1
- package/dist/services/assets.js +8 -6
- 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 +18 -13
- package/dist/services/graphql/errors/format.d.ts +6 -0
- package/dist/services/graphql/errors/format.js +14 -0
- package/dist/services/graphql/index.d.ts +5 -53
- package/dist/services/graphql/index.js +5 -2720
- package/dist/services/graphql/resolvers/mutation.d.ts +4 -0
- package/dist/services/graphql/resolvers/mutation.js +74 -0
- package/dist/services/graphql/resolvers/query.d.ts +8 -0
- package/dist/services/graphql/resolvers/query.js +87 -0
- package/dist/services/graphql/resolvers/system-admin.d.ts +5 -0
- package/dist/services/graphql/resolvers/system-admin.js +236 -0
- package/dist/services/graphql/resolvers/system-global.d.ts +7 -0
- package/dist/services/graphql/resolvers/system-global.js +435 -0
- package/dist/services/graphql/resolvers/system.d.ts +11 -0
- package/dist/services/graphql/resolvers/system.js +554 -0
- package/dist/services/graphql/schema/get-types.d.ts +12 -0
- package/dist/services/graphql/schema/get-types.js +223 -0
- package/dist/services/graphql/schema/index.d.ts +32 -0
- package/dist/services/graphql/schema/index.js +190 -0
- package/dist/services/graphql/schema/parse-args.d.ts +9 -0
- package/dist/services/graphql/schema/parse-args.js +35 -0
- package/dist/services/graphql/schema/parse-query.d.ts +7 -0
- package/dist/services/graphql/schema/parse-query.js +98 -0
- package/dist/services/graphql/schema/read.d.ts +12 -0
- package/dist/services/graphql/schema/read.js +653 -0
- package/dist/services/graphql/schema/write.d.ts +9 -0
- package/dist/services/graphql/schema/write.js +142 -0
- package/dist/services/graphql/subscription.d.ts +1 -1
- package/dist/services/graphql/subscription.js +7 -6
- package/dist/services/graphql/utils/aggrgate-query.d.ts +6 -0
- package/dist/services/graphql/utils/aggrgate-query.js +32 -0
- package/dist/services/graphql/utils/replace-fragments.d.ts +6 -0
- package/dist/services/graphql/utils/replace-fragments.js +21 -0
- package/dist/services/graphql/utils/replace-funcs.d.ts +5 -0
- package/dist/services/graphql/utils/replace-funcs.js +21 -0
- package/dist/services/graphql/utils/sanitize-gql-schema.d.ts +1 -1
- package/dist/services/graphql/utils/sanitize-gql-schema.js +5 -5
- package/dist/services/items.js +0 -2
- package/dist/services/meta.js +27 -84
- package/dist/services/users.d.ts +4 -0
- package/dist/services/users.js +23 -1
- package/dist/utils/apply-query.d.ts +1 -1
- package/dist/utils/apply-query.js +58 -21
- package/dist/utils/freeze-schema.d.ts +3 -0
- package/dist/utils/freeze-schema.js +31 -0
- package/dist/utils/get-accountability-for-token.js +1 -0
- package/dist/utils/get-milliseconds.js +1 -1
- package/dist/utils/get-schema.js +10 -5
- package/dist/utils/permissions-cachable.d.ts +8 -0
- package/dist/utils/permissions-cachable.js +38 -0
- package/dist/utils/sanitize-schema.d.ts +1 -1
- package/dist/websocket/messages.d.ts +6 -6
- 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
|
-
|
|
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,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
|
}
|
package/dist/services/assets.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
|
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:
|
|
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
|
-
|
|
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';
|
|
@@ -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
|
|
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
|
|
@@ -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 {
|
|
2
|
-
import type {
|
|
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:
|
|
10
|
+
scope: GQLScope;
|
|
13
11
|
constructor(options: AbstractServiceOptions & {
|
|
14
|
-
scope:
|
|
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
|
}
|