@directus/api 29.1.1 → 30.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +9 -1
- package/dist/controllers/items.js +3 -4
- package/dist/mailer.js +3 -3
- package/dist/middleware/respond.js +2 -2
- package/dist/services/graphql/index.d.ts +2 -2
- package/dist/services/graphql/index.js +6 -5
- package/dist/services/graphql/resolvers/query.js +4 -39
- package/dist/services/items.js +25 -9
- package/dist/services/payload.js +44 -0
- package/dist/utils/deep-map-response.d.ts +17 -0
- package/dist/utils/deep-map-response.js +61 -0
- package/dist/utils/get-relation-info.d.ts +1 -2
- package/dist/utils/permissions-cacheable.d.ts +8 -0
- package/dist/utils/{permissions-cachable.js → permissions-cacheable.js} +8 -6
- package/dist/utils/transaction.d.ts +1 -1
- package/dist/utils/transaction.js +18 -2
- package/dist/utils/versioning/handle-version.d.ts +3 -0
- package/dist/utils/versioning/handle-version.js +92 -0
- package/dist/utils/versioning/merge-version-data.d.ts +2 -0
- package/dist/utils/versioning/merge-version-data.js +10 -0
- package/package.json +17 -17
- package/dist/middleware/merge-content-versions.d.ts +0 -2
- package/dist/middleware/merge-content-versions.js +0 -26
- package/dist/utils/merge-version-data.d.ts +0 -3
- package/dist/utils/merge-version-data.js +0 -134
- package/dist/utils/permissions-cachable.d.ts +0 -8
package/dist/constants.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export declare const FILTER_VARIABLES: string[];
|
|
|
6
6
|
export declare const ALIAS_TYPES: string[];
|
|
7
7
|
export declare const DEFAULT_AUTH_PROVIDER = "default";
|
|
8
8
|
export declare const COLUMN_TRANSFORMS: string[];
|
|
9
|
-
export declare const GENERATE_SPECIAL: readonly ["uuid", "date-created", "role-created", "user-created"];
|
|
9
|
+
export declare const GENERATE_SPECIAL: readonly ["uuid", "date-created", "date-updated", "role-created", "role-updated", "user-created", "user-updated"];
|
|
10
10
|
export declare const UUID_REGEX = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
|
|
11
11
|
export declare const REFRESH_COOKIE_OPTIONS: CookieOptions;
|
|
12
12
|
export declare const SESSION_COOKIE_OPTIONS: CookieOptions;
|
package/dist/constants.js
CHANGED
|
@@ -51,7 +51,15 @@ export const FILTER_VARIABLES = ['$NOW', '$CURRENT_USER', '$CURRENT_ROLE'];
|
|
|
51
51
|
export const ALIAS_TYPES = ['alias', 'o2m', 'm2m', 'm2a', 'o2a', 'files', 'translations'];
|
|
52
52
|
export const DEFAULT_AUTH_PROVIDER = 'default';
|
|
53
53
|
export const COLUMN_TRANSFORMS = ['year', 'month', 'day', 'weekday', 'hour', 'minute', 'second'];
|
|
54
|
-
export const GENERATE_SPECIAL = [
|
|
54
|
+
export const GENERATE_SPECIAL = [
|
|
55
|
+
'uuid',
|
|
56
|
+
'date-created',
|
|
57
|
+
'date-updated',
|
|
58
|
+
'role-created',
|
|
59
|
+
'role-updated',
|
|
60
|
+
'user-created',
|
|
61
|
+
'user-updated',
|
|
62
|
+
];
|
|
55
63
|
export const UUID_REGEX = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}';
|
|
56
64
|
export const REFRESH_COOKIE_OPTIONS = {
|
|
57
65
|
httpOnly: true,
|
|
@@ -2,7 +2,6 @@ import { ErrorCode, ForbiddenError, RouteNotFoundError, isDirectusError } from '
|
|
|
2
2
|
import { isSystemCollection } from '@directus/system-data';
|
|
3
3
|
import express from 'express';
|
|
4
4
|
import collectionExists from '../middleware/collection-exists.js';
|
|
5
|
-
import { mergeContentVersions } from '../middleware/merge-content-versions.js';
|
|
6
5
|
import { respond } from '../middleware/respond.js';
|
|
7
6
|
import { validateBatch } from '../middleware/validate-batch.js';
|
|
8
7
|
import { ItemsService } from '../services/items.js';
|
|
@@ -75,8 +74,8 @@ const readHandler = asyncHandler(async (req, res, next) => {
|
|
|
75
74
|
};
|
|
76
75
|
return next();
|
|
77
76
|
});
|
|
78
|
-
router.search('/:collection', collectionExists, validateBatch('read'), readHandler,
|
|
79
|
-
router.get('/:collection', collectionExists, readHandler,
|
|
77
|
+
router.search('/:collection', collectionExists, validateBatch('read'), readHandler, respond);
|
|
78
|
+
router.get('/:collection', collectionExists, readHandler, respond);
|
|
80
79
|
router.get('/:collection/:pk', collectionExists, asyncHandler(async (req, res, next) => {
|
|
81
80
|
if (isSystemCollection(req.params['collection']))
|
|
82
81
|
throw new ForbiddenError();
|
|
@@ -89,7 +88,7 @@ router.get('/:collection/:pk', collectionExists, asyncHandler(async (req, res, n
|
|
|
89
88
|
data: result || null,
|
|
90
89
|
};
|
|
91
90
|
return next();
|
|
92
|
-
}),
|
|
91
|
+
}), respond);
|
|
93
92
|
router.patch('/:collection', collectionExists, validateBatch('update'), asyncHandler(async (req, res, next) => {
|
|
94
93
|
if (isSystemCollection(req.params['collection']))
|
|
95
94
|
throw new ForbiddenError();
|
package/dist/mailer.js
CHANGED
|
@@ -19,11 +19,11 @@ export default function getMailer() {
|
|
|
19
19
|
});
|
|
20
20
|
}
|
|
21
21
|
else if (transportName === 'ses') {
|
|
22
|
-
const
|
|
22
|
+
const { SESv2Client, SendEmailCommand } = require('@aws-sdk/client-sesv2');
|
|
23
23
|
const sesOptions = getConfigFromEnv('EMAIL_SES_');
|
|
24
|
-
const
|
|
24
|
+
const sesClient = new SESv2Client(sesOptions);
|
|
25
25
|
transporter = nodemailer.createTransport({
|
|
26
|
-
SES: {
|
|
26
|
+
SES: { sesClient, SendEmailCommand },
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
29
|
else if (transportName === 'smtp') {
|
|
@@ -10,7 +10,7 @@ import { getCacheKey } from '../utils/get-cache-key.js';
|
|
|
10
10
|
import { getDateFormatted } from '../utils/get-date-formatted.js';
|
|
11
11
|
import { getMilliseconds } from '../utils/get-milliseconds.js';
|
|
12
12
|
import { stringByteSize } from '../utils/get-string-byte-size.js';
|
|
13
|
-
import {
|
|
13
|
+
import { permissionsCacheable } from '../utils/permissions-cacheable.js';
|
|
14
14
|
export const respond = asyncHandler(async (req, res) => {
|
|
15
15
|
const env = useEnv();
|
|
16
16
|
const logger = useLogger();
|
|
@@ -29,7 +29,7 @@ export const respond = asyncHandler(async (req, res) => {
|
|
|
29
29
|
!req.sanitizedQuery.export &&
|
|
30
30
|
res.locals['cache'] !== false &&
|
|
31
31
|
exceedsMaxSize === false &&
|
|
32
|
-
(await
|
|
32
|
+
(await permissionsCacheable(req.collection, {
|
|
33
33
|
knex: getDatabase(),
|
|
34
34
|
schema: req.schema,
|
|
35
35
|
}, req.accountability))) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AbstractServiceOptions, Accountability, GraphQLParams, GQLScope, Item, Query, SchemaOverview } from '@directus/types';
|
|
1
|
+
import type { AbstractServiceOptions, Accountability, GraphQLParams, GQLScope, Item, Query, SchemaOverview, PrimaryKey } from '@directus/types';
|
|
2
2
|
import type { FormattedExecutionResult, GraphQLSchema } from 'graphql';
|
|
3
3
|
import type { Knex } from 'knex';
|
|
4
4
|
export declare class GraphQLService {
|
|
@@ -22,7 +22,7 @@ export declare class GraphQLService {
|
|
|
22
22
|
/**
|
|
23
23
|
* Execute the read action on the correct service. Checks for singleton as well.
|
|
24
24
|
*/
|
|
25
|
-
read(collection: string, query: Query): Promise<Partial<Item>>;
|
|
25
|
+
read(collection: string, query: Query, id?: PrimaryKey): Promise<Partial<Item>>;
|
|
26
26
|
/**
|
|
27
27
|
* Upsert and read singleton item
|
|
28
28
|
*/
|
|
@@ -61,16 +61,17 @@ export class GraphQLService {
|
|
|
61
61
|
/**
|
|
62
62
|
* Execute the read action on the correct service. Checks for singleton as well.
|
|
63
63
|
*/
|
|
64
|
-
async read(collection, query) {
|
|
64
|
+
async read(collection, query, id) {
|
|
65
65
|
const service = getService(collection, {
|
|
66
66
|
knex: this.knex,
|
|
67
67
|
accountability: this.accountability,
|
|
68
68
|
schema: this.schema,
|
|
69
69
|
});
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
if (this.schema.collections[collection].singleton)
|
|
71
|
+
return await service.readSingleton(query, { stripNonRequested: false });
|
|
72
|
+
if (id)
|
|
73
|
+
return await service.readOne(id, query, { stripNonRequested: false });
|
|
74
|
+
return await service.readByQuery(query, { stripNonRequested: false });
|
|
74
75
|
}
|
|
75
76
|
/**
|
|
76
77
|
* Upsert and read singleton item
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { parseFilterFunctionPath } from '@directus/utils';
|
|
2
2
|
import { omit } from 'lodash-es';
|
|
3
|
-
import { mergeVersionsRaw, mergeVersionsRecursive } from '../../../utils/merge-version-data.js';
|
|
4
|
-
import { VersionsService } from '../../versions.js';
|
|
5
3
|
import { parseArgs } from '../schema/parse-args.js';
|
|
6
4
|
import { getQuery } from '../schema/parse-query.js';
|
|
7
5
|
import { getAggregateQuery } from '../utils/aggregate-query.js';
|
|
@@ -19,7 +17,6 @@ export async function resolveQuery(gql, info) {
|
|
|
19
17
|
return null;
|
|
20
18
|
const args = parseArgs(info.fieldNodes[0].arguments || [], info.variableValues);
|
|
21
19
|
let query;
|
|
22
|
-
let versionRaw = false;
|
|
23
20
|
const isAggregate = collection.endsWith('_aggregated') && collection in gql.schema.collections === false;
|
|
24
21
|
if (isAggregate) {
|
|
25
22
|
query = await getAggregateQuery(args, selections, gql.schema, gql.accountability);
|
|
@@ -32,50 +29,18 @@ export async function resolveQuery(gql, info) {
|
|
|
32
29
|
}
|
|
33
30
|
if (collection.endsWith('_by_version') && collection in gql.schema.collections === false) {
|
|
34
31
|
collection = collection.slice(0, -11);
|
|
35
|
-
versionRaw = true;
|
|
32
|
+
query.versionRaw = true;
|
|
36
33
|
}
|
|
37
34
|
}
|
|
38
|
-
if (args['id']) {
|
|
39
|
-
query.filter = {
|
|
40
|
-
_and: [
|
|
41
|
-
query.filter || {},
|
|
42
|
-
{
|
|
43
|
-
[gql.schema.collections[collection].primary]: {
|
|
44
|
-
_eq: args['id'],
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
],
|
|
48
|
-
};
|
|
49
|
-
query.limit = 1;
|
|
50
|
-
}
|
|
51
35
|
// Transform count(a.b.c) into a.b.count(c)
|
|
52
36
|
if (query.fields?.length) {
|
|
53
37
|
for (let fieldIndex = 0; fieldIndex < query.fields.length; fieldIndex++) {
|
|
54
38
|
query.fields[fieldIndex] = parseFilterFunctionPath(query.fields[fieldIndex]);
|
|
55
39
|
}
|
|
56
40
|
}
|
|
57
|
-
const result = await gql.read(collection, query);
|
|
58
|
-
if (args['
|
|
59
|
-
|
|
60
|
-
const saves = await versionsService.getVersionSaves(args['version'], collection, args['id']);
|
|
61
|
-
if (saves) {
|
|
62
|
-
if (gql.schema.collections[collection].singleton) {
|
|
63
|
-
return versionRaw
|
|
64
|
-
? mergeVersionsRaw(result, saves)
|
|
65
|
-
: mergeVersionsRecursive(result, saves, collection, gql.schema);
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
if (result?.[0] === undefined)
|
|
69
|
-
return null;
|
|
70
|
-
return versionRaw
|
|
71
|
-
? mergeVersionsRaw(result[0], saves)
|
|
72
|
-
: mergeVersionsRecursive(result[0], saves, collection, gql.schema);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
if (args['id']) {
|
|
77
|
-
return result?.[0] || null;
|
|
78
|
-
}
|
|
41
|
+
const result = await gql.read(collection, query, args['id']);
|
|
42
|
+
if (args['id'])
|
|
43
|
+
return result;
|
|
79
44
|
if (query.group) {
|
|
80
45
|
// for every entry in result add a group field based on query.group;
|
|
81
46
|
const aggregateKeys = Object.keys(query.aggregate ?? {});
|
package/dist/services/items.js
CHANGED
|
@@ -18,6 +18,7 @@ import { shouldClearCache } from '../utils/should-clear-cache.js';
|
|
|
18
18
|
import { transaction } from '../utils/transaction.js';
|
|
19
19
|
import { validateKeys } from '../utils/validate-keys.js';
|
|
20
20
|
import { validateUserCountIntegrity } from '../utils/validate-user-count-integrity.js';
|
|
21
|
+
import { handleVersion } from '../utils/versioning/handle-version.js';
|
|
21
22
|
import { PayloadService } from './payload.js';
|
|
22
23
|
const env = useEnv();
|
|
23
24
|
export class ItemsService {
|
|
@@ -96,8 +97,6 @@ export class ItemsService {
|
|
|
96
97
|
if (!opts.bypassLimits) {
|
|
97
98
|
opts.mutationTracker.trackMutations(1);
|
|
98
99
|
}
|
|
99
|
-
const { ActivityService } = await import('./activity.js');
|
|
100
|
-
const { RevisionsService } = await import('./revisions.js');
|
|
101
100
|
const primaryKeyField = this.schema.collections[this.collection].primary;
|
|
102
101
|
const fields = Object.keys(this.schema.collections[this.collection].fields);
|
|
103
102
|
const aliases = Object.values(this.schema.collections[this.collection].fields)
|
|
@@ -226,7 +225,11 @@ export class ItemsService {
|
|
|
226
225
|
}
|
|
227
226
|
}
|
|
228
227
|
// If this is an authenticated action, and accountability tracking is enabled, save activity row
|
|
229
|
-
if (
|
|
228
|
+
if (opts.skipTracking !== true &&
|
|
229
|
+
this.accountability &&
|
|
230
|
+
this.schema.collections[this.collection].accountability !== null) {
|
|
231
|
+
const { ActivityService } = await import('./activity.js');
|
|
232
|
+
const { RevisionsService } = await import('./revisions.js');
|
|
230
233
|
const activityService = new ActivityService({
|
|
231
234
|
knex: trx,
|
|
232
235
|
schema: this.schema,
|
|
@@ -267,6 +270,9 @@ export class ItemsService {
|
|
|
267
270
|
if (autoIncrementSequenceNeedsToBeReset) {
|
|
268
271
|
await getHelpers(trx).sequence.resetAutoIncrementSequence(this.collection, primaryKeyField);
|
|
269
272
|
}
|
|
273
|
+
if (opts.onItemCreate) {
|
|
274
|
+
opts.onItemCreate(this.collection, primaryKey);
|
|
275
|
+
}
|
|
270
276
|
return primaryKey;
|
|
271
277
|
});
|
|
272
278
|
if (opts.emitEvents !== false) {
|
|
@@ -427,7 +433,13 @@ export class ItemsService {
|
|
|
427
433
|
validateKeys(this.schema, this.collection, primaryKeyField, key);
|
|
428
434
|
const filterWithKey = assign({}, query.filter, { [primaryKeyField]: { _eq: key } });
|
|
429
435
|
const queryWithKey = assign({}, query, { filter: filterWithKey });
|
|
430
|
-
|
|
436
|
+
let results = [];
|
|
437
|
+
if (query.version) {
|
|
438
|
+
results = (await handleVersion(this, key, queryWithKey, opts));
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
results = await this.readByQuery(queryWithKey, opts);
|
|
442
|
+
}
|
|
431
443
|
if (results.length === 0) {
|
|
432
444
|
throw new ForbiddenError();
|
|
433
445
|
}
|
|
@@ -522,8 +534,6 @@ export class ItemsService {
|
|
|
522
534
|
if (!opts.bypassLimits) {
|
|
523
535
|
opts.mutationTracker.trackMutations(keys.length);
|
|
524
536
|
}
|
|
525
|
-
const { ActivityService } = await import('./activity.js');
|
|
526
|
-
const { RevisionsService } = await import('./revisions.js');
|
|
527
537
|
const primaryKeyField = this.schema.collections[this.collection].primary;
|
|
528
538
|
validateKeys(this.schema, this.collection, primaryKeyField, keys);
|
|
529
539
|
const fields = Object.keys(this.schema.collections[this.collection].fields);
|
|
@@ -616,7 +626,11 @@ export class ItemsService {
|
|
|
616
626
|
}
|
|
617
627
|
}
|
|
618
628
|
// If this is an authenticated action, and accountability tracking is enabled, save activity row
|
|
619
|
-
if (
|
|
629
|
+
if (opts.skipTracking !== true &&
|
|
630
|
+
this.accountability &&
|
|
631
|
+
this.schema.collections[this.collection].accountability !== null) {
|
|
632
|
+
const { ActivityService } = await import('./activity.js');
|
|
633
|
+
const { RevisionsService } = await import('./revisions.js');
|
|
620
634
|
const activityService = new ActivityService({
|
|
621
635
|
knex: trx,
|
|
622
636
|
schema: this.schema,
|
|
@@ -780,7 +794,6 @@ export class ItemsService {
|
|
|
780
794
|
if (!opts.bypassLimits) {
|
|
781
795
|
opts.mutationTracker.trackMutations(keys.length);
|
|
782
796
|
}
|
|
783
|
-
const { ActivityService } = await import('./activity.js');
|
|
784
797
|
const primaryKeyField = this.schema.collections[this.collection].primary;
|
|
785
798
|
validateKeys(this.schema, this.collection, primaryKeyField, keys);
|
|
786
799
|
if (this.accountability) {
|
|
@@ -816,7 +829,10 @@ export class ItemsService {
|
|
|
816
829
|
await validateUserCountIntegrity({ flags: opts.userIntegrityCheckFlags, knex: trx });
|
|
817
830
|
}
|
|
818
831
|
}
|
|
819
|
-
if (
|
|
832
|
+
if (opts.skipTracking !== true &&
|
|
833
|
+
this.accountability &&
|
|
834
|
+
this.schema.collections[this.collection].accountability !== null) {
|
|
835
|
+
const { ActivityService } = await import('./activity.js');
|
|
820
836
|
const activityService = new ActivityService({
|
|
821
837
|
knex: trx,
|
|
822
838
|
schema: this.schema,
|
package/dist/services/payload.js
CHANGED
|
@@ -402,6 +402,10 @@ export class PayloadService {
|
|
|
402
402
|
onRequireUserIntegrityCheck: (flags) => (userIntegrityCheckFlags |= flags),
|
|
403
403
|
bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params),
|
|
404
404
|
emitEvents: opts?.emitEvents,
|
|
405
|
+
autoPurgeCache: opts?.autoPurgeCache,
|
|
406
|
+
autoPurgeSystemCache: opts?.autoPurgeSystemCache,
|
|
407
|
+
skipTracking: opts?.skipTracking,
|
|
408
|
+
onItemCreate: opts?.onItemCreate,
|
|
405
409
|
mutationTracker: opts?.mutationTracker,
|
|
406
410
|
});
|
|
407
411
|
}
|
|
@@ -412,6 +416,10 @@ export class PayloadService {
|
|
|
412
416
|
onRequireUserIntegrityCheck: (flags) => (userIntegrityCheckFlags |= flags),
|
|
413
417
|
bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params),
|
|
414
418
|
emitEvents: opts?.emitEvents,
|
|
419
|
+
autoPurgeCache: opts?.autoPurgeCache,
|
|
420
|
+
autoPurgeSystemCache: opts?.autoPurgeSystemCache,
|
|
421
|
+
skipTracking: opts?.skipTracking,
|
|
422
|
+
onItemCreate: opts?.onItemCreate,
|
|
415
423
|
mutationTracker: opts?.mutationTracker,
|
|
416
424
|
});
|
|
417
425
|
}
|
|
@@ -468,6 +476,10 @@ export class PayloadService {
|
|
|
468
476
|
onRequireUserIntegrityCheck: (flags) => (userIntegrityCheckFlags |= flags),
|
|
469
477
|
bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params),
|
|
470
478
|
emitEvents: opts?.emitEvents,
|
|
479
|
+
autoPurgeCache: opts?.autoPurgeCache,
|
|
480
|
+
autoPurgeSystemCache: opts?.autoPurgeSystemCache,
|
|
481
|
+
skipTracking: opts?.skipTracking,
|
|
482
|
+
onItemCreate: opts?.onItemCreate,
|
|
471
483
|
mutationTracker: opts?.mutationTracker,
|
|
472
484
|
});
|
|
473
485
|
}
|
|
@@ -478,6 +490,10 @@ export class PayloadService {
|
|
|
478
490
|
onRequireUserIntegrityCheck: (flags) => (userIntegrityCheckFlags |= flags),
|
|
479
491
|
bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params),
|
|
480
492
|
emitEvents: opts?.emitEvents,
|
|
493
|
+
autoPurgeCache: opts?.autoPurgeCache,
|
|
494
|
+
autoPurgeSystemCache: opts?.autoPurgeSystemCache,
|
|
495
|
+
skipTracking: opts?.skipTracking,
|
|
496
|
+
onItemCreate: opts?.onItemCreate,
|
|
481
497
|
mutationTracker: opts?.mutationTracker,
|
|
482
498
|
});
|
|
483
499
|
}
|
|
@@ -570,6 +586,10 @@ export class PayloadService {
|
|
|
570
586
|
onRequireUserIntegrityCheck: (flags) => (userIntegrityCheckFlags |= flags),
|
|
571
587
|
bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params),
|
|
572
588
|
emitEvents: opts?.emitEvents,
|
|
589
|
+
autoPurgeCache: opts?.autoPurgeCache,
|
|
590
|
+
autoPurgeSystemCache: opts?.autoPurgeSystemCache,
|
|
591
|
+
skipTracking: opts?.skipTracking,
|
|
592
|
+
onItemCreate: opts?.onItemCreate,
|
|
573
593
|
mutationTracker: opts?.mutationTracker,
|
|
574
594
|
})));
|
|
575
595
|
const query = {
|
|
@@ -596,6 +616,10 @@ export class PayloadService {
|
|
|
596
616
|
onRequireUserIntegrityCheck: (flags) => (userIntegrityCheckFlags |= flags),
|
|
597
617
|
bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params),
|
|
598
618
|
emitEvents: opts?.emitEvents,
|
|
619
|
+
autoPurgeCache: opts?.autoPurgeCache,
|
|
620
|
+
autoPurgeSystemCache: opts?.autoPurgeSystemCache,
|
|
621
|
+
skipTracking: opts?.skipTracking,
|
|
622
|
+
onItemCreate: opts?.onItemCreate,
|
|
599
623
|
mutationTracker: opts?.mutationTracker,
|
|
600
624
|
});
|
|
601
625
|
}
|
|
@@ -605,6 +629,10 @@ export class PayloadService {
|
|
|
605
629
|
onRequireUserIntegrityCheck: (flags) => (userIntegrityCheckFlags |= flags),
|
|
606
630
|
bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params),
|
|
607
631
|
emitEvents: opts?.emitEvents,
|
|
632
|
+
autoPurgeCache: opts?.autoPurgeCache,
|
|
633
|
+
autoPurgeSystemCache: opts?.autoPurgeSystemCache,
|
|
634
|
+
skipTracking: opts?.skipTracking,
|
|
635
|
+
onItemCreate: opts?.onItemCreate,
|
|
608
636
|
mutationTracker: opts?.mutationTracker,
|
|
609
637
|
});
|
|
610
638
|
}
|
|
@@ -648,6 +676,10 @@ export class PayloadService {
|
|
|
648
676
|
onRequireUserIntegrityCheck: (flags) => (userIntegrityCheckFlags |= flags),
|
|
649
677
|
bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params),
|
|
650
678
|
emitEvents: opts?.emitEvents,
|
|
679
|
+
autoPurgeCache: opts?.autoPurgeCache,
|
|
680
|
+
autoPurgeSystemCache: opts?.autoPurgeSystemCache,
|
|
681
|
+
skipTracking: opts?.skipTracking,
|
|
682
|
+
onItemCreate: opts?.onItemCreate,
|
|
651
683
|
mutationTracker: opts?.mutationTracker,
|
|
652
684
|
});
|
|
653
685
|
}
|
|
@@ -667,6 +699,10 @@ export class PayloadService {
|
|
|
667
699
|
onRequireUserIntegrityCheck: (flags) => (userIntegrityCheckFlags |= flags),
|
|
668
700
|
bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params),
|
|
669
701
|
emitEvents: opts?.emitEvents,
|
|
702
|
+
autoPurgeCache: opts?.autoPurgeCache,
|
|
703
|
+
autoPurgeSystemCache: opts?.autoPurgeSystemCache,
|
|
704
|
+
skipTracking: opts?.skipTracking,
|
|
705
|
+
onItemCreate: opts?.onItemCreate,
|
|
670
706
|
mutationTracker: opts?.mutationTracker,
|
|
671
707
|
});
|
|
672
708
|
}
|
|
@@ -694,6 +730,10 @@ export class PayloadService {
|
|
|
694
730
|
onRequireUserIntegrityCheck: (flags) => (userIntegrityCheckFlags |= flags),
|
|
695
731
|
bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params),
|
|
696
732
|
emitEvents: opts?.emitEvents,
|
|
733
|
+
autoPurgeCache: opts?.autoPurgeCache,
|
|
734
|
+
autoPurgeSystemCache: opts?.autoPurgeSystemCache,
|
|
735
|
+
skipTracking: opts?.skipTracking,
|
|
736
|
+
onItemCreate: opts?.onItemCreate,
|
|
697
737
|
mutationTracker: opts?.mutationTracker,
|
|
698
738
|
});
|
|
699
739
|
}
|
|
@@ -703,6 +743,10 @@ export class PayloadService {
|
|
|
703
743
|
onRequireUserIntegrityCheck: (flags) => (userIntegrityCheckFlags |= flags),
|
|
704
744
|
bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params),
|
|
705
745
|
emitEvents: opts?.emitEvents,
|
|
746
|
+
autoPurgeCache: opts?.autoPurgeCache,
|
|
747
|
+
autoPurgeSystemCache: opts?.autoPurgeSystemCache,
|
|
748
|
+
skipTracking: opts?.skipTracking,
|
|
749
|
+
onItemCreate: opts?.onItemCreate,
|
|
706
750
|
mutationTracker: opts?.mutationTracker,
|
|
707
751
|
});
|
|
708
752
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { CollectionOverview, FieldOverview, Relation, SchemaOverview } from '@directus/types';
|
|
2
|
+
import { type RelationInfo } from './get-relation-info.js';
|
|
3
|
+
/**
|
|
4
|
+
* Allows to deep map the response from the ItemsService with collection, field and relation context for each entry.
|
|
5
|
+
* Bottom to Top depth first mapping of values.
|
|
6
|
+
*/
|
|
7
|
+
export declare function deepMapResponse(object: Record<string, any>, callback: (entry: [key: string | number, value: unknown], context: {
|
|
8
|
+
collection: CollectionOverview;
|
|
9
|
+
field: FieldOverview;
|
|
10
|
+
relation: Relation | null;
|
|
11
|
+
leaf: boolean;
|
|
12
|
+
relationType: RelationInfo['relationType'] | null;
|
|
13
|
+
}) => [key: string | number, value: unknown], context: {
|
|
14
|
+
schema: SchemaOverview;
|
|
15
|
+
collection: string;
|
|
16
|
+
relationInfo?: RelationInfo;
|
|
17
|
+
}): any;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { isPlainObject } from 'lodash-es';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { getRelationInfo } from './get-relation-info.js';
|
|
4
|
+
import { InvalidQueryError } from '@directus/errors';
|
|
5
|
+
/**
|
|
6
|
+
* Allows to deep map the response from the ItemsService with collection, field and relation context for each entry.
|
|
7
|
+
* Bottom to Top depth first mapping of values.
|
|
8
|
+
*/
|
|
9
|
+
export function deepMapResponse(object, callback, context) {
|
|
10
|
+
const collection = context.schema.collections[context.collection];
|
|
11
|
+
assert(isPlainObject(object) && typeof object === 'object' && object !== null, `DeepMapResponse only works on objects, received ${JSON.stringify(object)}`);
|
|
12
|
+
return Object.fromEntries(Object.entries(object).map(([key, value]) => {
|
|
13
|
+
const field = collection?.fields[key];
|
|
14
|
+
if (!field)
|
|
15
|
+
return [key, value];
|
|
16
|
+
const relationInfo = getRelationInfo(context.schema.relations, collection.collection, field.field);
|
|
17
|
+
let leaf = true;
|
|
18
|
+
if (relationInfo.relation && typeof value === 'object' && value !== null && isPlainObject(object)) {
|
|
19
|
+
switch (relationInfo.relationType) {
|
|
20
|
+
case 'm2o':
|
|
21
|
+
value = deepMapResponse(value, callback, {
|
|
22
|
+
schema: context.schema,
|
|
23
|
+
collection: relationInfo.relation.related_collection,
|
|
24
|
+
relationInfo,
|
|
25
|
+
});
|
|
26
|
+
leaf = false;
|
|
27
|
+
break;
|
|
28
|
+
case 'o2m':
|
|
29
|
+
value = value.map((childValue) => {
|
|
30
|
+
if (isPlainObject(childValue) && typeof childValue === 'object' && childValue !== null) {
|
|
31
|
+
leaf = false;
|
|
32
|
+
return deepMapResponse(childValue, callback, {
|
|
33
|
+
schema: context.schema,
|
|
34
|
+
collection: relationInfo.relation.collection,
|
|
35
|
+
relationInfo,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
else
|
|
39
|
+
return childValue;
|
|
40
|
+
});
|
|
41
|
+
break;
|
|
42
|
+
case 'a2o': {
|
|
43
|
+
const related_collection = object[relationInfo.relation.meta.one_collection_field];
|
|
44
|
+
if (!related_collection) {
|
|
45
|
+
throw new InvalidQueryError({
|
|
46
|
+
reason: `When selecting '${collection.collection}.${field.field}', the field '${collection.collection}.${relationInfo.relation.meta.one_collection_field}' has to be selected when using versioning and m2a relations `,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
value = deepMapResponse(value, callback, {
|
|
50
|
+
schema: context.schema,
|
|
51
|
+
collection: related_collection,
|
|
52
|
+
relationInfo,
|
|
53
|
+
});
|
|
54
|
+
leaf = false;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return callback([key, value], { collection, field, ...relationInfo, leaf });
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { Relation } from '@directus/types';
|
|
2
|
-
type RelationInfo = {
|
|
2
|
+
export type RelationInfo = {
|
|
3
3
|
relation: Relation | null;
|
|
4
4
|
relationType: 'o2m' | 'm2o' | 'a2o' | 'o2a' | null;
|
|
5
5
|
};
|
|
6
6
|
export declare function getRelationInfo(relations: Relation[], collection: string, field: string): RelationInfo;
|
|
7
|
-
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Accountability, Filter } from '@directus/types';
|
|
2
|
+
import type { Context } from '../permissions/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Check if the read permissions for a collection contain the dynamic variable $NOW.
|
|
5
|
+
* If they do, the permissions are not cacheable.
|
|
6
|
+
*/
|
|
7
|
+
export declare function permissionsCacheable(collection: string | undefined, context: Context, accountability?: Accountability): Promise<boolean>;
|
|
8
|
+
export declare function filterHasNow(filter: Filter): boolean;
|
|
@@ -3,9 +3,9 @@ import { fetchPolicies } from '../permissions/lib/fetch-policies.js';
|
|
|
3
3
|
import { createDefaultAccountability } from '../permissions/utils/create-default-accountability.js';
|
|
4
4
|
/**
|
|
5
5
|
* Check if the read permissions for a collection contain the dynamic variable $NOW.
|
|
6
|
-
* If they do, the permissions are not
|
|
6
|
+
* If they do, the permissions are not cacheable.
|
|
7
7
|
*/
|
|
8
|
-
export async function
|
|
8
|
+
export async function permissionsCacheable(collection, context, accountability) {
|
|
9
9
|
if (!collection) {
|
|
10
10
|
return true;
|
|
11
11
|
}
|
|
@@ -18,17 +18,19 @@ export async function permissionsCachable(collection, context, accountability) {
|
|
|
18
18
|
if (!permission.permissions) {
|
|
19
19
|
return false;
|
|
20
20
|
}
|
|
21
|
-
return
|
|
21
|
+
return filterHasNow(permission.permissions);
|
|
22
22
|
});
|
|
23
23
|
return !has_now;
|
|
24
24
|
}
|
|
25
|
-
export function
|
|
25
|
+
export function filterHasNow(filter) {
|
|
26
|
+
if (filter === null)
|
|
27
|
+
return false;
|
|
26
28
|
return Object.entries(filter).some(([key, value]) => {
|
|
27
29
|
if (key === '_and' || key === '_or') {
|
|
28
|
-
return value.some((sub_filter) =>
|
|
30
|
+
return value.some((sub_filter) => filterHasNow(sub_filter));
|
|
29
31
|
}
|
|
30
32
|
else if (typeof value === 'object') {
|
|
31
|
-
return
|
|
33
|
+
return filterHasNow(value);
|
|
32
34
|
}
|
|
33
35
|
else if (typeof value === 'string') {
|
|
34
36
|
return value.startsWith('$NOW');
|
|
@@ -6,4 +6,4 @@ import { type Knex } from 'knex';
|
|
|
6
6
|
* Can be used to ensure the handler is run within a transaction,
|
|
7
7
|
* while preventing nested transactions.
|
|
8
8
|
*/
|
|
9
|
-
export declare const transaction: <T = unknown>(knex: Knex, handler: (knex: Knex) => Promise<T>) => Promise<T>;
|
|
9
|
+
export declare const transaction: <T = unknown>(knex: Knex, handler: (knex: Knex.Transaction) => Promise<T>) => Promise<T>;
|
|
@@ -63,7 +63,23 @@ function shouldRetryTransaction(client, error) {
|
|
|
63
63
|
* @link https://www.sqlite.org/rescode.html#busy
|
|
64
64
|
*/
|
|
65
65
|
const SQLITE_BUSY_ERROR_CODE = 'SQLITE_BUSY';
|
|
66
|
+
// Both mariadb and mysql
|
|
67
|
+
const MYSQL_DEADLOCK_CODE = 'ER_LOCK_DEADLOCK';
|
|
68
|
+
const POSTGRES_DEADLOCK_CODE = '40P01';
|
|
69
|
+
const ORACLE_DEADLOCK_CODE = 'ORA-00060';
|
|
70
|
+
const MSSQL_DEADLOCK_CODE = 'EREQUEST';
|
|
71
|
+
const MSSQL_DEADLOCK_NUMBER = '1205';
|
|
72
|
+
const codes = {
|
|
73
|
+
cockroachdb: [{ code: COCKROACH_RETRY_ERROR_CODE }],
|
|
74
|
+
sqlite: [{ code: SQLITE_BUSY_ERROR_CODE }],
|
|
75
|
+
mysql: [{ code: MYSQL_DEADLOCK_CODE }],
|
|
76
|
+
mssql: [{ code: MSSQL_DEADLOCK_CODE, number: MSSQL_DEADLOCK_NUMBER }],
|
|
77
|
+
oracle: [{ code: ORACLE_DEADLOCK_CODE }],
|
|
78
|
+
postgres: [{ code: POSTGRES_DEADLOCK_CODE }],
|
|
79
|
+
redshift: [],
|
|
80
|
+
};
|
|
66
81
|
return (isObject(error) &&
|
|
67
|
-
((
|
|
68
|
-
(
|
|
82
|
+
codes[client].some((code) => {
|
|
83
|
+
return Object.entries(code).every(([key, value]) => String(error[key]) === value);
|
|
84
|
+
}));
|
|
69
85
|
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { Item, PrimaryKey, Query, QueryOptions } from '@directus/types';
|
|
2
|
+
import type { ItemsService as ItemsServiceType } from '../../services/index.js';
|
|
3
|
+
export declare function handleVersion(self: ItemsServiceType, key: PrimaryKey, queryWithKey: Query, opts?: QueryOptions): Promise<(Item | undefined)[]>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { ForbiddenError } from '@directus/errors';
|
|
2
|
+
import { deepMapResponse } from '../deep-map-response.js';
|
|
3
|
+
import { transaction } from '../transaction.js';
|
|
4
|
+
import { mergeVersionsRaw } from './merge-version-data.js';
|
|
5
|
+
export async function handleVersion(self, key, queryWithKey, opts) {
|
|
6
|
+
const { VersionsService } = await import('../../services/versions.js');
|
|
7
|
+
const { ItemsService } = await import('../../services/items.js');
|
|
8
|
+
if (queryWithKey.versionRaw) {
|
|
9
|
+
const originalData = await self.readByQuery(queryWithKey, opts);
|
|
10
|
+
if (originalData.length === 0) {
|
|
11
|
+
throw new ForbiddenError();
|
|
12
|
+
}
|
|
13
|
+
const versionsService = new VersionsService({
|
|
14
|
+
schema: self.schema,
|
|
15
|
+
accountability: self.accountability,
|
|
16
|
+
knex: self.knex,
|
|
17
|
+
});
|
|
18
|
+
const versionData = await versionsService.getVersionSaves(queryWithKey.version, self.collection, key);
|
|
19
|
+
if (!versionData || versionData.length === 0)
|
|
20
|
+
return [originalData[0]];
|
|
21
|
+
return [mergeVersionsRaw(originalData[0], versionData)];
|
|
22
|
+
}
|
|
23
|
+
let results = [];
|
|
24
|
+
const versionsService = new VersionsService({
|
|
25
|
+
schema: self.schema,
|
|
26
|
+
accountability: self.accountability,
|
|
27
|
+
knex: self.knex,
|
|
28
|
+
});
|
|
29
|
+
const createdIDs = {};
|
|
30
|
+
const versionData = await versionsService.getVersionSaves(queryWithKey.version, self.collection, key);
|
|
31
|
+
await transaction(self.knex, async (trx) => {
|
|
32
|
+
const itemsServiceAdmin = new ItemsService(self.collection, {
|
|
33
|
+
schema: self.schema,
|
|
34
|
+
accountability: {
|
|
35
|
+
admin: true,
|
|
36
|
+
},
|
|
37
|
+
knex: trx,
|
|
38
|
+
});
|
|
39
|
+
await Promise.all((versionData ?? []).map((data) => {
|
|
40
|
+
return itemsServiceAdmin.updateOne(key, data, {
|
|
41
|
+
emitEvents: false,
|
|
42
|
+
autoPurgeCache: false,
|
|
43
|
+
skipTracking: true,
|
|
44
|
+
onItemCreate: (collection, pk) => {
|
|
45
|
+
if (collection in createdIDs === false)
|
|
46
|
+
createdIDs[collection] = [];
|
|
47
|
+
createdIDs[collection].push(pk);
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}));
|
|
51
|
+
const itemsServiceUser = new ItemsService(self.collection, {
|
|
52
|
+
schema: self.schema,
|
|
53
|
+
accountability: self.accountability,
|
|
54
|
+
knex: trx,
|
|
55
|
+
});
|
|
56
|
+
results = await itemsServiceUser.readByQuery(queryWithKey, opts);
|
|
57
|
+
await trx.rollback();
|
|
58
|
+
});
|
|
59
|
+
results = results.map((result) => {
|
|
60
|
+
return deepMapResponse(result, ([key, value], context) => {
|
|
61
|
+
if (context.relationType === 'm2o' || context.relationType === 'a2o') {
|
|
62
|
+
const ids = createdIDs[context.relation.related_collection];
|
|
63
|
+
const match = ids?.find((id) => String(id) === String(value));
|
|
64
|
+
if (match) {
|
|
65
|
+
return [key, null];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else if (context.relationType === 'o2m' && Array.isArray(value)) {
|
|
69
|
+
const ids = createdIDs[context.relation.collection];
|
|
70
|
+
return [
|
|
71
|
+
key,
|
|
72
|
+
value.map((val) => {
|
|
73
|
+
const match = ids?.find((id) => String(id) === String(val));
|
|
74
|
+
if (match) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return val;
|
|
78
|
+
}),
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
if (context.field.field === context.collection.primary) {
|
|
82
|
+
const ids = createdIDs[context.collection.collection];
|
|
83
|
+
const match = ids?.find((id) => String(id) === String(value));
|
|
84
|
+
if (match) {
|
|
85
|
+
return [key, null];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return [key, value];
|
|
89
|
+
}, { collection: self.collection, schema: self.schema });
|
|
90
|
+
});
|
|
91
|
+
return results;
|
|
92
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { cloneDeep } from 'lodash-es';
|
|
2
|
+
export function mergeVersionsRaw(item, versionData) {
|
|
3
|
+
const result = cloneDeep(item);
|
|
4
|
+
for (const versionRecord of versionData) {
|
|
5
|
+
for (const key of Object.keys(versionRecord)) {
|
|
6
|
+
result[key] = versionRecord[key];
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
return result;
|
|
10
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@directus/api",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "30.0.0",
|
|
4
4
|
"description": "Directus is a real-time API and App dashboard for managing SQL database content",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"directus",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
],
|
|
60
60
|
"dependencies": {
|
|
61
61
|
"@authenio/samlify-node-xmllint": "2.0.0",
|
|
62
|
-
"@aws-sdk/client-
|
|
62
|
+
"@aws-sdk/client-sesv2": "3.864.0",
|
|
63
63
|
"@godaddy/terminus": "4.12.1",
|
|
64
64
|
"@rollup/plugin-alias": "5.1.1",
|
|
65
65
|
"@rollup/plugin-node-resolve": "16.0.1",
|
|
@@ -153,30 +153,30 @@
|
|
|
153
153
|
"ws": "8.18.3",
|
|
154
154
|
"zod": "4.0.14",
|
|
155
155
|
"zod-validation-error": "4.0.1",
|
|
156
|
+
"@directus/app": "13.14.0",
|
|
156
157
|
"@directus/constants": "13.0.2",
|
|
157
158
|
"@directus/env": "5.1.2",
|
|
158
|
-
"@directus/app": "13.13.1",
|
|
159
|
-
"@directus/extensions": "3.0.9",
|
|
160
159
|
"@directus/errors": "2.0.3",
|
|
161
|
-
"@directus/extensions-sdk": "16.0.
|
|
162
|
-
"@directus/extensions-registry": "3.0.
|
|
163
|
-
"@directus/pressure": "3.0.8",
|
|
164
|
-
"@directus/memory": "3.0.8",
|
|
165
|
-
"@directus/schema": "13.0.2",
|
|
166
|
-
"@directus/schema-builder": "0.0.4",
|
|
167
|
-
"@directus/storage": "12.0.1",
|
|
160
|
+
"@directus/extensions-sdk": "16.0.1",
|
|
161
|
+
"@directus/extensions-registry": "3.0.10",
|
|
168
162
|
"@directus/format-title": "12.0.1",
|
|
163
|
+
"@directus/extensions": "3.0.10",
|
|
164
|
+
"@directus/memory": "3.0.8",
|
|
165
|
+
"@directus/pressure": "3.0.8",
|
|
169
166
|
"@directus/specs": "11.1.1",
|
|
167
|
+
"@directus/schema-builder": "0.0.5",
|
|
168
|
+
"@directus/storage": "12.0.1",
|
|
170
169
|
"@directus/storage-driver-azure": "12.0.8",
|
|
170
|
+
"@directus/schema": "13.0.2",
|
|
171
171
|
"@directus/storage-driver-cloudinary": "12.0.8",
|
|
172
|
-
"@directus/storage-driver-local": "12.0.1",
|
|
173
|
-
"@directus/storage-driver-s3": "12.0.8",
|
|
174
172
|
"@directus/storage-driver-gcs": "12.0.8",
|
|
173
|
+
"@directus/storage-driver-s3": "12.0.8",
|
|
175
174
|
"@directus/storage-driver-supabase": "3.0.8",
|
|
176
|
-
"@directus/
|
|
175
|
+
"@directus/storage-driver-local": "12.0.1",
|
|
177
176
|
"@directus/utils": "13.0.9",
|
|
177
|
+
"@directus/system-data": "3.2.1",
|
|
178
178
|
"@directus/validation": "2.0.8",
|
|
179
|
-
"directus": "11.
|
|
179
|
+
"directus": "11.11.0"
|
|
180
180
|
},
|
|
181
181
|
"devDependencies": {
|
|
182
182
|
"@directus/tsconfig": "3.0.0",
|
|
@@ -220,8 +220,8 @@
|
|
|
220
220
|
"knex-mock-client": "3.0.2",
|
|
221
221
|
"typescript": "5.8.3",
|
|
222
222
|
"vitest": "3.2.4",
|
|
223
|
-
"@directus/schema-builder": "0.0.
|
|
224
|
-
"@directus/types": "13.2.
|
|
223
|
+
"@directus/schema-builder": "0.0.5",
|
|
224
|
+
"@directus/types": "13.2.2"
|
|
225
225
|
},
|
|
226
226
|
"optionalDependencies": {
|
|
227
227
|
"@keyv/redis": "3.0.1",
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { isObject } from '@directus/utils';
|
|
2
|
-
import { VersionsService } from '../services/versions.js';
|
|
3
|
-
import asyncHandler from '../utils/async-handler.js';
|
|
4
|
-
import { mergeVersionsRaw, mergeVersionsRecursive } from '../utils/merge-version-data.js';
|
|
5
|
-
export const mergeContentVersions = asyncHandler(async (req, res, next) => {
|
|
6
|
-
if (req.sanitizedQuery.version &&
|
|
7
|
-
req.collection &&
|
|
8
|
-
(req.singleton || req.params['pk']) &&
|
|
9
|
-
'data' in res.locals['payload']) {
|
|
10
|
-
const originalData = res.locals['payload'].data;
|
|
11
|
-
// only act on single item requests
|
|
12
|
-
if (!isObject(originalData))
|
|
13
|
-
return next();
|
|
14
|
-
const versionsService = new VersionsService({ accountability: req.accountability ?? null, schema: req.schema });
|
|
15
|
-
const versionData = await versionsService.getVersionSaves(req.sanitizedQuery.version, req.collection, req.params['pk']);
|
|
16
|
-
if (!versionData || versionData.length === 0)
|
|
17
|
-
return next();
|
|
18
|
-
if (req.sanitizedQuery.versionRaw) {
|
|
19
|
-
res.locals['payload'].data = mergeVersionsRaw(originalData, versionData);
|
|
20
|
-
}
|
|
21
|
-
else {
|
|
22
|
-
res.locals['payload'].data = mergeVersionsRecursive(originalData, versionData, req.collection, req.schema);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
return next();
|
|
26
|
-
});
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
import type { Item, SchemaOverview } from '@directus/types';
|
|
2
|
-
export declare function mergeVersionsRaw(item: Item, versionData: Partial<Item>[]): Item;
|
|
3
|
-
export declare function mergeVersionsRecursive(item: Item, versionData: Item[], collection: string, schema: SchemaOverview): Item;
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { isObject } from '@directus/utils';
|
|
2
|
-
import Joi from 'joi';
|
|
3
|
-
import { cloneDeep } from 'lodash-es';
|
|
4
|
-
const alterationSchema = Joi.object({
|
|
5
|
-
create: Joi.array().items(Joi.object().unknown()),
|
|
6
|
-
update: Joi.array().items(Joi.object().unknown()),
|
|
7
|
-
delete: Joi.array().items(Joi.string(), Joi.number()),
|
|
8
|
-
});
|
|
9
|
-
export function mergeVersionsRaw(item, versionData) {
|
|
10
|
-
const result = cloneDeep(item);
|
|
11
|
-
for (const versionRecord of versionData) {
|
|
12
|
-
for (const key of Object.keys(versionRecord)) {
|
|
13
|
-
result[key] = versionRecord[key];
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
return result;
|
|
17
|
-
}
|
|
18
|
-
export function mergeVersionsRecursive(item, versionData, collection, schema) {
|
|
19
|
-
if (versionData.length === 0)
|
|
20
|
-
return item;
|
|
21
|
-
return recursiveMerging(item, versionData, collection, schema);
|
|
22
|
-
}
|
|
23
|
-
function recursiveMerging(data, versionData, collection, schema) {
|
|
24
|
-
const result = cloneDeep(data);
|
|
25
|
-
const relations = getRelations(collection, schema);
|
|
26
|
-
for (const versionRecord of versionData) {
|
|
27
|
-
if (!isObject(versionRecord)) {
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
30
|
-
for (const key of Object.keys(data)) {
|
|
31
|
-
if (key in versionRecord === false) {
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
const currentValue = data[key];
|
|
35
|
-
const newValue = versionRecord[key];
|
|
36
|
-
if (typeof newValue !== 'object' || newValue === null) {
|
|
37
|
-
// primitive type substitution, json and non relational array values are handled in the next check
|
|
38
|
-
result[key] = newValue;
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
|
-
if (key in relations === false) {
|
|
42
|
-
// check for m2a exception
|
|
43
|
-
if (isManyToAnyCollection(collection, schema) && key === 'item') {
|
|
44
|
-
const item = addMissingKeys(isObject(currentValue) ? currentValue : {}, newValue);
|
|
45
|
-
result[key] = recursiveMerging(item, [newValue], data['collection'], schema);
|
|
46
|
-
}
|
|
47
|
-
else {
|
|
48
|
-
// item is not a relation
|
|
49
|
-
result[key] = newValue;
|
|
50
|
-
}
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
const { error } = alterationSchema.validate(newValue);
|
|
54
|
-
if (error) {
|
|
55
|
-
if (typeof newValue === 'object' && key in relations) {
|
|
56
|
-
const newItem = !currentValue || typeof currentValue !== 'object' ? newValue : currentValue;
|
|
57
|
-
result[key] = recursiveMerging(newItem, [newValue], relations[key], schema);
|
|
58
|
-
}
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
const alterations = newValue;
|
|
62
|
-
const currentPrimaryKeyField = schema.collections[collection].primary;
|
|
63
|
-
const relatedPrimaryKeyField = schema.collections[relations[key]].primary;
|
|
64
|
-
const mergedRelation = [];
|
|
65
|
-
if (Array.isArray(currentValue)) {
|
|
66
|
-
if (alterations.delete.length > 0) {
|
|
67
|
-
for (const currentItem of currentValue) {
|
|
68
|
-
const currentId = typeof currentItem === 'object' ? currentItem[currentPrimaryKeyField] : currentItem;
|
|
69
|
-
if (alterations.delete.includes(currentId) === false) {
|
|
70
|
-
mergedRelation.push(currentItem);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
else {
|
|
75
|
-
mergedRelation.push(...currentValue);
|
|
76
|
-
}
|
|
77
|
-
if (alterations.update.length > 0) {
|
|
78
|
-
for (const updatedItem of alterations.update) {
|
|
79
|
-
// find existing item to update
|
|
80
|
-
const itemIndex = mergedRelation.findIndex((currentItem) => currentItem[relatedPrimaryKeyField] === updatedItem[currentPrimaryKeyField]);
|
|
81
|
-
if (itemIndex === -1) {
|
|
82
|
-
// check for raw primary keys
|
|
83
|
-
const pkIndex = mergedRelation.findIndex((currentItem) => currentItem === updatedItem[currentPrimaryKeyField]);
|
|
84
|
-
if (pkIndex === -1) {
|
|
85
|
-
// nothing to update so add the item as is
|
|
86
|
-
mergedRelation.push(updatedItem);
|
|
87
|
-
}
|
|
88
|
-
else {
|
|
89
|
-
mergedRelation[pkIndex] = updatedItem;
|
|
90
|
-
}
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
const item = addMissingKeys(mergedRelation[itemIndex], updatedItem);
|
|
94
|
-
mergedRelation[itemIndex] = recursiveMerging(item, [updatedItem], relations[key], schema);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
if (alterations.create.length > 0) {
|
|
99
|
-
for (const createdItem of alterations.create) {
|
|
100
|
-
const item = addMissingKeys({}, createdItem);
|
|
101
|
-
mergedRelation.push(recursiveMerging(item, [createdItem], relations[key], schema));
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
result[key] = mergedRelation;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return result;
|
|
108
|
-
}
|
|
109
|
-
function addMissingKeys(item, edits) {
|
|
110
|
-
const result = { ...item };
|
|
111
|
-
for (const key of Object.keys(edits)) {
|
|
112
|
-
if (key in item === false) {
|
|
113
|
-
result[key] = null;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
return result;
|
|
117
|
-
}
|
|
118
|
-
function isManyToAnyCollection(collection, schema) {
|
|
119
|
-
const relation = schema.relations.find((relation) => relation.collection === collection && relation.meta?.many_collection === collection);
|
|
120
|
-
if (!relation || !relation.meta?.one_field || !relation.related_collection)
|
|
121
|
-
return false;
|
|
122
|
-
return Boolean(schema.collections[relation.related_collection]?.fields[relation.meta.one_field]?.special.includes('m2a'));
|
|
123
|
-
}
|
|
124
|
-
function getRelations(collection, schema) {
|
|
125
|
-
return schema.relations.reduce((result, relation) => {
|
|
126
|
-
if (relation.related_collection === collection && relation.meta?.one_field) {
|
|
127
|
-
result[relation.meta.one_field] = relation.collection;
|
|
128
|
-
}
|
|
129
|
-
if (relation.collection === collection && relation.related_collection) {
|
|
130
|
-
result[relation.field] = relation.related_collection;
|
|
131
|
-
}
|
|
132
|
-
return result;
|
|
133
|
-
}, {});
|
|
134
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { Accountability, Filter } from '@directus/types';
|
|
2
|
-
import type { Context } from '../permissions/types.js';
|
|
3
|
-
/**
|
|
4
|
-
* Check if the read permissions for a collection contain the dynamic variable $NOW.
|
|
5
|
-
* If they do, the permissions are not cachable.
|
|
6
|
-
*/
|
|
7
|
-
export declare function permissionsCachable(collection: string | undefined, context: Context, accountability?: Accountability): Promise<boolean>;
|
|
8
|
-
export declare function filter_has_now(filter: Filter): boolean;
|