@directus/api 30.0.0 → 31.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/app.js +5 -0
- package/dist/auth/drivers/oauth2.js +17 -3
- package/dist/auth/drivers/openid.js +17 -3
- package/dist/controllers/mcp.d.ts +2 -0
- package/dist/controllers/mcp.js +33 -0
- package/dist/controllers/users.js +17 -7
- package/dist/controllers/versions.js +3 -2
- package/dist/database/errors/dialects/mssql.d.ts +1 -1
- package/dist/database/errors/dialects/mssql.js +18 -10
- package/dist/database/migrations/20250813A-add-mcp.d.ts +3 -0
- package/dist/database/migrations/20250813A-add-mcp.js +18 -0
- package/dist/database/run-ast/README.md +46 -0
- package/dist/mcp/define.d.ts +2 -0
- package/dist/mcp/define.js +3 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +1 -0
- package/dist/mcp/schema.d.ts +485 -0
- package/dist/mcp/schema.js +219 -0
- package/dist/mcp/server.d.ts +97 -0
- package/dist/mcp/server.js +310 -0
- package/dist/mcp/tools/assets.d.ts +3 -0
- package/dist/mcp/tools/assets.js +54 -0
- package/dist/mcp/tools/collections.d.ts +84 -0
- package/dist/mcp/tools/collections.js +90 -0
- package/dist/mcp/tools/fields.d.ts +101 -0
- package/dist/mcp/tools/fields.js +157 -0
- package/dist/mcp/tools/files.d.ts +235 -0
- package/dist/mcp/tools/files.js +103 -0
- package/dist/mcp/tools/flows.d.ts +323 -0
- package/dist/mcp/tools/flows.js +85 -0
- package/dist/mcp/tools/folders.d.ts +95 -0
- package/dist/mcp/tools/folders.js +96 -0
- package/dist/mcp/tools/index.d.ts +15 -0
- package/dist/mcp/tools/index.js +29 -0
- package/dist/mcp/tools/items.d.ts +87 -0
- package/dist/mcp/tools/items.js +141 -0
- package/dist/mcp/tools/operations.d.ts +171 -0
- package/dist/mcp/tools/operations.js +77 -0
- package/dist/mcp/tools/prompts/assets.md +8 -0
- package/dist/mcp/tools/prompts/collections.md +336 -0
- package/dist/mcp/tools/prompts/fields.md +521 -0
- package/dist/mcp/tools/prompts/files.md +180 -0
- package/dist/mcp/tools/prompts/flows.md +495 -0
- package/dist/mcp/tools/prompts/folders.md +34 -0
- package/dist/mcp/tools/prompts/index.d.ts +16 -0
- package/dist/mcp/tools/prompts/index.js +19 -0
- package/dist/mcp/tools/prompts/items.md +317 -0
- package/dist/mcp/tools/prompts/operations.md +721 -0
- package/dist/mcp/tools/prompts/relations.md +386 -0
- package/dist/mcp/tools/prompts/schema.md +130 -0
- package/dist/mcp/tools/prompts/system-prompt-description.md +1 -0
- package/dist/mcp/tools/prompts/system-prompt.md +44 -0
- package/dist/mcp/tools/prompts/trigger-flow.md +214 -0
- package/dist/mcp/tools/relations.d.ts +73 -0
- package/dist/mcp/tools/relations.js +93 -0
- package/dist/mcp/tools/schema.d.ts +54 -0
- package/dist/mcp/tools/schema.js +317 -0
- package/dist/mcp/tools/system.d.ts +3 -0
- package/dist/mcp/tools/system.js +22 -0
- package/dist/mcp/tools/trigger-flow.d.ts +8 -0
- package/dist/mcp/tools/trigger-flow.js +48 -0
- package/dist/mcp/transport.d.ts +13 -0
- package/dist/mcp/transport.js +18 -0
- package/dist/mcp/types.d.ts +56 -0
- package/dist/mcp/types.js +1 -0
- package/dist/services/authentication.js +36 -0
- package/dist/services/fields.js +4 -4
- package/dist/services/items.js +14 -4
- package/dist/services/payload.d.ts +7 -3
- package/dist/services/payload.js +26 -12
- package/dist/services/server.js +1 -0
- package/dist/services/tfa.d.ts +1 -1
- package/dist/services/tfa.js +20 -5
- package/dist/services/versions.d.ts +6 -4
- package/dist/services/versions.js +84 -25
- package/dist/types/auth.d.ts +2 -1
- package/dist/utils/versioning/deep-map-with-schema.d.ts +23 -0
- package/dist/utils/versioning/deep-map-with-schema.js +81 -0
- package/dist/utils/versioning/handle-version.d.ts +2 -2
- package/dist/utils/versioning/handle-version.js +47 -43
- package/dist/utils/versioning/split-recursive.d.ts +4 -0
- package/dist/utils/versioning/split-recursive.js +27 -0
- package/package.json +30 -29
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AbstractServiceOptions, Accountability, Aggregate, FieldOverview, Item, MutationOptions, PayloadAction, PayloadServiceProcessRelationResult, PrimaryKey, SchemaOverview } from '@directus/types';
|
|
1
|
+
import type { AbstractServiceOptions, Accountability, Aggregate, DefaultOverwrite, FieldOverview, Item, MutationOptions, PayloadAction, PayloadServiceProcessRelationResult, PrimaryKey, SchemaOverview } from '@directus/types';
|
|
2
2
|
import type { Knex } from 'knex';
|
|
3
3
|
import type { Helpers } from '../database/helpers/index.js';
|
|
4
4
|
type Transformers = {
|
|
@@ -9,6 +9,7 @@ type Transformers = {
|
|
|
9
9
|
accountability: Accountability | null;
|
|
10
10
|
specials: string[];
|
|
11
11
|
helpers: Helpers;
|
|
12
|
+
overwriteDefaults: DefaultOverwrite | undefined;
|
|
12
13
|
}) => Promise<any>;
|
|
13
14
|
};
|
|
14
15
|
/**
|
|
@@ -22,7 +23,10 @@ export declare class PayloadService {
|
|
|
22
23
|
collection: string;
|
|
23
24
|
schema: SchemaOverview;
|
|
24
25
|
nested: string[];
|
|
25
|
-
|
|
26
|
+
overwriteDefaults: DefaultOverwrite | undefined;
|
|
27
|
+
constructor(collection: string, options: AbstractServiceOptions & {
|
|
28
|
+
overwriteDefaults?: DefaultOverwrite | undefined;
|
|
29
|
+
});
|
|
26
30
|
transformers: Transformers;
|
|
27
31
|
processValues(action: PayloadAction, payloads: Partial<Item>[]): Promise<Partial<Item>[]>;
|
|
28
32
|
processValues(action: PayloadAction, payload: Partial<Item>): Promise<Partial<Item>>;
|
|
@@ -62,6 +66,6 @@ export declare class PayloadService {
|
|
|
62
66
|
* Transforms the input partial payload to match the output structure, to have consistency
|
|
63
67
|
* between delta and data
|
|
64
68
|
*/
|
|
65
|
-
prepareDelta(
|
|
69
|
+
prepareDelta(delta: Partial<Item>): Promise<string | null>;
|
|
66
70
|
}
|
|
67
71
|
export {};
|
package/dist/services/payload.js
CHANGED
|
@@ -21,6 +21,7 @@ export class PayloadService {
|
|
|
21
21
|
collection;
|
|
22
22
|
schema;
|
|
23
23
|
nested;
|
|
24
|
+
overwriteDefaults;
|
|
24
25
|
constructor(collection, options) {
|
|
25
26
|
this.accountability = options.accountability || null;
|
|
26
27
|
this.knex = options.knex || getDatabase();
|
|
@@ -28,6 +29,7 @@ export class PayloadService {
|
|
|
28
29
|
this.collection = collection;
|
|
29
30
|
this.schema = options.schema;
|
|
30
31
|
this.nested = options.nested ?? [];
|
|
32
|
+
this.overwriteDefaults = options.overwriteDefaults;
|
|
31
33
|
return this;
|
|
32
34
|
}
|
|
33
35
|
transformers = {
|
|
@@ -77,14 +79,14 @@ export class PayloadService {
|
|
|
77
79
|
return value ? '**********' : null;
|
|
78
80
|
return value;
|
|
79
81
|
},
|
|
80
|
-
async 'user-created'({ action, value, accountability }) {
|
|
82
|
+
async 'user-created'({ action, value, accountability, overwriteDefaults }) {
|
|
81
83
|
if (action === 'create')
|
|
82
|
-
return accountability?.user
|
|
84
|
+
return (overwriteDefaults ? overwriteDefaults._user : accountability?.user) ?? null;
|
|
83
85
|
return value;
|
|
84
86
|
},
|
|
85
|
-
async 'user-updated'({ action, value, accountability }) {
|
|
87
|
+
async 'user-updated'({ action, value, accountability, overwriteDefaults }) {
|
|
86
88
|
if (action === 'update')
|
|
87
|
-
return accountability?.user
|
|
89
|
+
return (overwriteDefaults ? overwriteDefaults._user : accountability?.user) ?? null;
|
|
88
90
|
return value;
|
|
89
91
|
},
|
|
90
92
|
async 'role-created'({ action, value, accountability }) {
|
|
@@ -97,14 +99,14 @@ export class PayloadService {
|
|
|
97
99
|
return accountability?.role || null;
|
|
98
100
|
return value;
|
|
99
101
|
},
|
|
100
|
-
async 'date-created'({ action, value, helpers }) {
|
|
102
|
+
async 'date-created'({ action, value, helpers, overwriteDefaults }) {
|
|
101
103
|
if (action === 'create')
|
|
102
|
-
return new Date(helpers.date.writeTimestamp(new Date().toISOString()));
|
|
104
|
+
return new Date(overwriteDefaults ? overwriteDefaults._date : helpers.date.writeTimestamp(new Date().toISOString()));
|
|
103
105
|
return value;
|
|
104
106
|
},
|
|
105
|
-
async 'date-updated'({ action, value, helpers }) {
|
|
107
|
+
async 'date-updated'({ action, value, helpers, overwriteDefaults }) {
|
|
106
108
|
if (action === 'update')
|
|
107
|
-
return new Date(helpers.date.writeTimestamp(new Date().toISOString()));
|
|
109
|
+
return new Date(overwriteDefaults ? overwriteDefaults._date : helpers.date.writeTimestamp(new Date().toISOString()));
|
|
108
110
|
return value;
|
|
109
111
|
},
|
|
110
112
|
async 'cast-csv'({ action, value }) {
|
|
@@ -212,6 +214,7 @@ export class PayloadService {
|
|
|
212
214
|
accountability,
|
|
213
215
|
specials: fieldSpecials,
|
|
214
216
|
helpers: this.helpers,
|
|
217
|
+
overwriteDefaults: this.overwriteDefaults,
|
|
215
218
|
});
|
|
216
219
|
}
|
|
217
220
|
}
|
|
@@ -405,6 +408,7 @@ export class PayloadService {
|
|
|
405
408
|
autoPurgeCache: opts?.autoPurgeCache,
|
|
406
409
|
autoPurgeSystemCache: opts?.autoPurgeSystemCache,
|
|
407
410
|
skipTracking: opts?.skipTracking,
|
|
411
|
+
overwriteDefaults: opts?.overwriteDefaults?.[relation.field],
|
|
408
412
|
onItemCreate: opts?.onItemCreate,
|
|
409
413
|
mutationTracker: opts?.mutationTracker,
|
|
410
414
|
});
|
|
@@ -419,6 +423,7 @@ export class PayloadService {
|
|
|
419
423
|
autoPurgeCache: opts?.autoPurgeCache,
|
|
420
424
|
autoPurgeSystemCache: opts?.autoPurgeSystemCache,
|
|
421
425
|
skipTracking: opts?.skipTracking,
|
|
426
|
+
overwriteDefaults: opts?.overwriteDefaults?.[relation.field],
|
|
422
427
|
onItemCreate: opts?.onItemCreate,
|
|
423
428
|
mutationTracker: opts?.mutationTracker,
|
|
424
429
|
});
|
|
@@ -479,6 +484,7 @@ export class PayloadService {
|
|
|
479
484
|
autoPurgeCache: opts?.autoPurgeCache,
|
|
480
485
|
autoPurgeSystemCache: opts?.autoPurgeSystemCache,
|
|
481
486
|
skipTracking: opts?.skipTracking,
|
|
487
|
+
overwriteDefaults: opts?.overwriteDefaults?.[relation.field],
|
|
482
488
|
onItemCreate: opts?.onItemCreate,
|
|
483
489
|
mutationTracker: opts?.mutationTracker,
|
|
484
490
|
});
|
|
@@ -493,6 +499,7 @@ export class PayloadService {
|
|
|
493
499
|
autoPurgeCache: opts?.autoPurgeCache,
|
|
494
500
|
autoPurgeSystemCache: opts?.autoPurgeSystemCache,
|
|
495
501
|
skipTracking: opts?.skipTracking,
|
|
502
|
+
overwriteDefaults: opts?.overwriteDefaults?.[relation.field],
|
|
496
503
|
onItemCreate: opts?.onItemCreate,
|
|
497
504
|
mutationTracker: opts?.mutationTracker,
|
|
498
505
|
});
|
|
@@ -589,6 +596,7 @@ export class PayloadService {
|
|
|
589
596
|
autoPurgeCache: opts?.autoPurgeCache,
|
|
590
597
|
autoPurgeSystemCache: opts?.autoPurgeSystemCache,
|
|
591
598
|
skipTracking: opts?.skipTracking,
|
|
599
|
+
overwriteDefaults: opts?.overwriteDefaults?.[relation.meta.one_field],
|
|
592
600
|
onItemCreate: opts?.onItemCreate,
|
|
593
601
|
mutationTracker: opts?.mutationTracker,
|
|
594
602
|
})));
|
|
@@ -619,6 +627,7 @@ export class PayloadService {
|
|
|
619
627
|
autoPurgeCache: opts?.autoPurgeCache,
|
|
620
628
|
autoPurgeSystemCache: opts?.autoPurgeSystemCache,
|
|
621
629
|
skipTracking: opts?.skipTracking,
|
|
630
|
+
overwriteDefaults: opts?.overwriteDefaults?.[relation.meta.one_field],
|
|
622
631
|
onItemCreate: opts?.onItemCreate,
|
|
623
632
|
mutationTracker: opts?.mutationTracker,
|
|
624
633
|
});
|
|
@@ -632,6 +641,7 @@ export class PayloadService {
|
|
|
632
641
|
autoPurgeCache: opts?.autoPurgeCache,
|
|
633
642
|
autoPurgeSystemCache: opts?.autoPurgeSystemCache,
|
|
634
643
|
skipTracking: opts?.skipTracking,
|
|
644
|
+
overwriteDefaults: opts?.overwriteDefaults?.[relation.meta.one_field],
|
|
635
645
|
onItemCreate: opts?.onItemCreate,
|
|
636
646
|
mutationTracker: opts?.mutationTracker,
|
|
637
647
|
});
|
|
@@ -679,13 +689,14 @@ export class PayloadService {
|
|
|
679
689
|
autoPurgeCache: opts?.autoPurgeCache,
|
|
680
690
|
autoPurgeSystemCache: opts?.autoPurgeSystemCache,
|
|
681
691
|
skipTracking: opts?.skipTracking,
|
|
692
|
+
overwriteDefaults: opts?.overwriteDefaults?.[relation.meta.one_field]?.['create'],
|
|
682
693
|
onItemCreate: opts?.onItemCreate,
|
|
683
694
|
mutationTracker: opts?.mutationTracker,
|
|
684
695
|
});
|
|
685
696
|
}
|
|
686
697
|
if (alterations.update) {
|
|
687
|
-
for (const
|
|
688
|
-
const { [relatedPrimaryKeyField]: key, ...record } =
|
|
698
|
+
for (const index in alterations.update) {
|
|
699
|
+
const { [relatedPrimaryKeyField]: key, ...record } = alterations.update[index];
|
|
689
700
|
const existingRecord = await this.knex
|
|
690
701
|
.select(relatedPrimaryKeyField, relation.field)
|
|
691
702
|
.from(relation.collection)
|
|
@@ -702,6 +713,7 @@ export class PayloadService {
|
|
|
702
713
|
autoPurgeCache: opts?.autoPurgeCache,
|
|
703
714
|
autoPurgeSystemCache: opts?.autoPurgeSystemCache,
|
|
704
715
|
skipTracking: opts?.skipTracking,
|
|
716
|
+
overwriteDefaults: opts?.overwriteDefaults?.[relation.meta.one_field]?.['update'][index],
|
|
705
717
|
onItemCreate: opts?.onItemCreate,
|
|
706
718
|
mutationTracker: opts?.mutationTracker,
|
|
707
719
|
});
|
|
@@ -733,6 +745,7 @@ export class PayloadService {
|
|
|
733
745
|
autoPurgeCache: opts?.autoPurgeCache,
|
|
734
746
|
autoPurgeSystemCache: opts?.autoPurgeSystemCache,
|
|
735
747
|
skipTracking: opts?.skipTracking,
|
|
748
|
+
overwriteDefaults: opts?.overwriteDefaults?.[relation.meta.one_field]?.['delete'],
|
|
736
749
|
onItemCreate: opts?.onItemCreate,
|
|
737
750
|
mutationTracker: opts?.mutationTracker,
|
|
738
751
|
});
|
|
@@ -746,6 +759,7 @@ export class PayloadService {
|
|
|
746
759
|
autoPurgeCache: opts?.autoPurgeCache,
|
|
747
760
|
autoPurgeSystemCache: opts?.autoPurgeSystemCache,
|
|
748
761
|
skipTracking: opts?.skipTracking,
|
|
762
|
+
overwriteDefaults: opts?.overwriteDefaults?.[relation.meta.one_field]?.['delete'],
|
|
749
763
|
onItemCreate: opts?.onItemCreate,
|
|
750
764
|
mutationTracker: opts?.mutationTracker,
|
|
751
765
|
});
|
|
@@ -759,8 +773,8 @@ export class PayloadService {
|
|
|
759
773
|
* Transforms the input partial payload to match the output structure, to have consistency
|
|
760
774
|
* between delta and data
|
|
761
775
|
*/
|
|
762
|
-
async prepareDelta(
|
|
763
|
-
let payload = cloneDeep(
|
|
776
|
+
async prepareDelta(delta) {
|
|
777
|
+
let payload = cloneDeep(delta);
|
|
764
778
|
for (const key in payload) {
|
|
765
779
|
if (payload[key]?.isRawInstance) {
|
|
766
780
|
payload[key] = payload[key].bindings[0];
|
package/dist/services/server.js
CHANGED
package/dist/services/tfa.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export declare class TFAService {
|
|
|
6
6
|
itemsService: ItemsService;
|
|
7
7
|
constructor(options: AbstractServiceOptions);
|
|
8
8
|
verifyOTP(key: PrimaryKey, otp: string, secret?: string): Promise<boolean>;
|
|
9
|
-
generateTFA(key: PrimaryKey): Promise<Record<string, string>>;
|
|
9
|
+
generateTFA(key: PrimaryKey, requiresPassword?: boolean): Promise<Record<string, string>>;
|
|
10
10
|
enableTFA(key: PrimaryKey, otp: string, secret: string): Promise<void>;
|
|
11
11
|
disableTFA(key: PrimaryKey): Promise<void>;
|
|
12
12
|
}
|
package/dist/services/tfa.js
CHANGED
|
@@ -2,6 +2,7 @@ import { InvalidPayloadError } from '@directus/errors';
|
|
|
2
2
|
import { authenticator } from 'otplib';
|
|
3
3
|
import getDatabase from '../database/index.js';
|
|
4
4
|
import { ItemsService } from './items.js';
|
|
5
|
+
import { DEFAULT_AUTH_PROVIDER } from '../constants.js';
|
|
5
6
|
export class TFAService {
|
|
6
7
|
knex;
|
|
7
8
|
itemsService;
|
|
@@ -19,26 +20,40 @@ export class TFAService {
|
|
|
19
20
|
}
|
|
20
21
|
return authenticator.check(otp, user.tfa_secret);
|
|
21
22
|
}
|
|
22
|
-
async generateTFA(key) {
|
|
23
|
-
const user = await this.knex
|
|
23
|
+
async generateTFA(key, requiresPassword = true) {
|
|
24
|
+
const user = await this.knex
|
|
25
|
+
.select('email', 'tfa_secret', 'provider', 'external_identifier')
|
|
26
|
+
.from('directus_users')
|
|
27
|
+
.where({ id: key })
|
|
28
|
+
.first();
|
|
24
29
|
if (user?.tfa_secret !== null) {
|
|
25
30
|
throw new InvalidPayloadError({ reason: 'TFA Secret is already set for this user' });
|
|
26
31
|
}
|
|
27
|
-
|
|
32
|
+
// Only require email for non-OAuth users
|
|
33
|
+
if (user?.provider === DEFAULT_AUTH_PROVIDER && !user?.email) {
|
|
28
34
|
throw new InvalidPayloadError({ reason: 'User must have a valid email to enable TFA' });
|
|
29
35
|
}
|
|
36
|
+
if (!requiresPassword && user?.provider === DEFAULT_AUTH_PROVIDER) {
|
|
37
|
+
throw new InvalidPayloadError({ reason: 'This method is only available for OAuth users' });
|
|
38
|
+
}
|
|
30
39
|
const secret = authenticator.generateSecret();
|
|
31
40
|
const project = await this.knex.select('project_name').from('directus_settings').limit(1).first();
|
|
41
|
+
// For OAuth users without email, use external_identifier as fallback
|
|
42
|
+
const accountName = user.email || user.external_identifier || `user_${key}`;
|
|
32
43
|
return {
|
|
33
44
|
secret,
|
|
34
|
-
url: authenticator.keyuri(
|
|
45
|
+
url: authenticator.keyuri(accountName, project?.project_name || 'Directus', secret),
|
|
35
46
|
};
|
|
36
47
|
}
|
|
37
48
|
async enableTFA(key, otp, secret) {
|
|
38
|
-
const user = await this.knex.select('tfa_secret').from('directus_users').where({ id: key }).first();
|
|
49
|
+
const user = await this.knex.select('tfa_secret', 'provider').from('directus_users').where({ id: key }).first();
|
|
50
|
+
const requiresPassword = user?.['provider'] === DEFAULT_AUTH_PROVIDER;
|
|
39
51
|
if (user?.tfa_secret !== null) {
|
|
40
52
|
throw new InvalidPayloadError({ reason: 'TFA Secret is already set for this user' });
|
|
41
53
|
}
|
|
54
|
+
if (!requiresPassword && user?.provider === DEFAULT_AUTH_PROVIDER) {
|
|
55
|
+
throw new InvalidPayloadError({ reason: 'This method is only available for OAuth users' });
|
|
56
|
+
}
|
|
42
57
|
if (!authenticator.check(otp, secret)) {
|
|
43
58
|
throw new InvalidPayloadError({ reason: `"otp" is invalid` });
|
|
44
59
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { AbstractServiceOptions, Item, MutationOptions, PrimaryKey, Query } from '@directus/types';
|
|
1
|
+
import type { AbstractServiceOptions, ContentVersion, Item, MutationOptions, PrimaryKey, Query, QueryOptions } from '@directus/types';
|
|
2
2
|
import { ItemsService } from './items.js';
|
|
3
|
-
export declare class VersionsService extends ItemsService {
|
|
3
|
+
export declare class VersionsService extends ItemsService<ContentVersion> {
|
|
4
4
|
constructor(options: AbstractServiceOptions);
|
|
5
5
|
private validateCreateData;
|
|
6
6
|
getMainItem(collection: string, item: PrimaryKey, query?: Query): Promise<Item>;
|
|
@@ -8,10 +8,12 @@ export declare class VersionsService extends ItemsService {
|
|
|
8
8
|
outdated: boolean;
|
|
9
9
|
mainHash: string;
|
|
10
10
|
}>;
|
|
11
|
-
|
|
11
|
+
getVersionSave(key: string, collection: string, item: string, mapDelta?: boolean): Promise<ContentVersion | undefined>;
|
|
12
12
|
createOne(data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey>;
|
|
13
|
+
readOne(key: PrimaryKey, query?: Query, opts?: QueryOptions): Promise<ContentVersion>;
|
|
13
14
|
createMany(data: Partial<Item>[], opts?: MutationOptions): Promise<PrimaryKey[]>;
|
|
14
15
|
updateMany(keys: PrimaryKey[], data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey[]>;
|
|
15
|
-
save(key: PrimaryKey,
|
|
16
|
+
save(key: PrimaryKey, delta: Partial<Item>): Promise<Partial<Item>>;
|
|
16
17
|
promote(version: PrimaryKey, mainHash: string, fields?: string[]): Promise<PrimaryKey>;
|
|
18
|
+
private mapDelta;
|
|
17
19
|
}
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { Action } from '@directus/constants';
|
|
2
2
|
import { ForbiddenError, InvalidPayloadError, UnprocessableContentError } from '@directus/errors';
|
|
3
3
|
import Joi from 'joi';
|
|
4
|
-
import { assign, pick } from 'lodash-es';
|
|
4
|
+
import { assign, get, isEqual, isPlainObject, pick } from 'lodash-es';
|
|
5
5
|
import objectHash from 'object-hash';
|
|
6
6
|
import { getCache } from '../cache.js';
|
|
7
|
+
import { getHelpers } from '../database/helpers/index.js';
|
|
7
8
|
import emitter from '../emitter.js';
|
|
8
9
|
import { validateAccess } from '../permissions/modules/validate-access/validate-access.js';
|
|
9
10
|
import { shouldClearCache } from '../utils/should-clear-cache.js';
|
|
11
|
+
import { splitRecursive } from '../utils/versioning/split-recursive.js';
|
|
10
12
|
import { ActivityService } from './activity.js';
|
|
11
13
|
import { ItemsService } from './items.js';
|
|
12
14
|
import { PayloadService } from './payload.js';
|
|
13
15
|
import { RevisionsService } from './revisions.js';
|
|
16
|
+
import { deepMapWithSchema } from '../utils/versioning/deep-map-with-schema.js';
|
|
14
17
|
export class VersionsService extends ItemsService {
|
|
15
18
|
constructor(options) {
|
|
16
19
|
super('directus_versions', options);
|
|
@@ -59,10 +62,10 @@ export class VersionsService extends ItemsService {
|
|
|
59
62
|
knex: this.knex,
|
|
60
63
|
schema: this.schema,
|
|
61
64
|
});
|
|
62
|
-
const existingVersions = await sudoService.readByQuery({
|
|
65
|
+
const existingVersions = (await sudoService.readByQuery({
|
|
63
66
|
aggregate: { count: ['*'] },
|
|
64
67
|
filter: { key: { _eq: data['key'] }, collection: { _eq: data['collection'] }, item: { _eq: data['item'] } },
|
|
65
|
-
});
|
|
68
|
+
}));
|
|
66
69
|
if (existingVersions[0]['count'] > 0) {
|
|
67
70
|
throw new UnprocessableContentError({
|
|
68
71
|
reason: `Version "${data['key']}" already exists for item "${data['item']}" in collection "${data['collection']}"`,
|
|
@@ -82,21 +85,17 @@ export class VersionsService extends ItemsService {
|
|
|
82
85
|
const mainHash = objectHash(mainItem);
|
|
83
86
|
return { outdated: hash !== mainHash, mainHash };
|
|
84
87
|
}
|
|
85
|
-
async
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (versions[0]['delta']) {
|
|
97
|
-
return [versions[0]['delta']];
|
|
98
|
-
}
|
|
99
|
-
return null;
|
|
88
|
+
async getVersionSave(key, collection, item, mapDelta = true) {
|
|
89
|
+
const version = (await this.readByQuery({
|
|
90
|
+
filter: {
|
|
91
|
+
key: { _eq: key },
|
|
92
|
+
collection: { _eq: collection },
|
|
93
|
+
item: { _eq: item },
|
|
94
|
+
},
|
|
95
|
+
}))[0];
|
|
96
|
+
if (mapDelta && version?.delta)
|
|
97
|
+
version.delta = this.mapDelta(version);
|
|
98
|
+
return version;
|
|
100
99
|
}
|
|
101
100
|
async createOne(data, opts) {
|
|
102
101
|
await this.validateCreateData(data);
|
|
@@ -104,6 +103,12 @@ export class VersionsService extends ItemsService {
|
|
|
104
103
|
data['hash'] = objectHash(mainItem);
|
|
105
104
|
return super.createOne(data, opts);
|
|
106
105
|
}
|
|
106
|
+
async readOne(key, query = {}, opts) {
|
|
107
|
+
const version = await super.readOne(key, query, opts);
|
|
108
|
+
if (version?.delta)
|
|
109
|
+
version.delta = this.mapDelta(version);
|
|
110
|
+
return version;
|
|
111
|
+
}
|
|
107
112
|
async createMany(data, opts) {
|
|
108
113
|
if (!Array.isArray(data)) {
|
|
109
114
|
throw new InvalidPayloadError({ reason: 'Input should be an array of items' });
|
|
@@ -156,7 +161,7 @@ export class VersionsService extends ItemsService {
|
|
|
156
161
|
}
|
|
157
162
|
return super.updateMany(keys, data, opts);
|
|
158
163
|
}
|
|
159
|
-
async save(key,
|
|
164
|
+
async save(key, delta) {
|
|
160
165
|
const version = await super.readOne(key);
|
|
161
166
|
const payloadService = new PayloadService(this.collection, {
|
|
162
167
|
accountability: this.accountability,
|
|
@@ -171,7 +176,7 @@ export class VersionsService extends ItemsService {
|
|
|
171
176
|
knex: this.knex,
|
|
172
177
|
schema: this.schema,
|
|
173
178
|
});
|
|
174
|
-
const { item, collection } = version;
|
|
179
|
+
const { item, collection, delta: existingDelta } = version;
|
|
175
180
|
const activity = await activityService.createOne({
|
|
176
181
|
action: Action.VERSION_SAVE,
|
|
177
182
|
user: this.accountability?.user ?? null,
|
|
@@ -181,7 +186,8 @@ export class VersionsService extends ItemsService {
|
|
|
181
186
|
origin: this.accountability?.origin ?? null,
|
|
182
187
|
item,
|
|
183
188
|
});
|
|
184
|
-
const
|
|
189
|
+
const helpers = getHelpers(this.knex);
|
|
190
|
+
let revisionDelta = await payloadService.prepareDelta(delta);
|
|
185
191
|
await revisionsService.createOne({
|
|
186
192
|
activity,
|
|
187
193
|
version: key,
|
|
@@ -190,10 +196,23 @@ export class VersionsService extends ItemsService {
|
|
|
190
196
|
data: revisionDelta,
|
|
191
197
|
delta: revisionDelta,
|
|
192
198
|
});
|
|
193
|
-
|
|
199
|
+
revisionDelta = revisionDelta ? JSON.parse(revisionDelta) : null;
|
|
200
|
+
const date = new Date(helpers.date.writeTimestamp(new Date().toISOString()));
|
|
201
|
+
deepMapObjects(revisionDelta, (object, path) => {
|
|
202
|
+
const existing = get(existingDelta, path);
|
|
203
|
+
if (existing && isEqual(existing, object))
|
|
204
|
+
return;
|
|
205
|
+
object['_user'] = this.accountability?.user;
|
|
206
|
+
object['_date'] = date;
|
|
207
|
+
});
|
|
208
|
+
const finalVersionDelta = assign({}, existingDelta, revisionDelta);
|
|
194
209
|
const sudoService = new ItemsService(this.collection, {
|
|
195
210
|
knex: this.knex,
|
|
196
211
|
schema: this.schema,
|
|
212
|
+
accountability: {
|
|
213
|
+
...this.accountability,
|
|
214
|
+
admin: true,
|
|
215
|
+
},
|
|
197
216
|
});
|
|
198
217
|
await sudoService.updateOne(key, { delta: finalVersionDelta });
|
|
199
218
|
const { cache } = getCache();
|
|
@@ -203,7 +222,7 @@ export class VersionsService extends ItemsService {
|
|
|
203
222
|
return finalVersionDelta;
|
|
204
223
|
}
|
|
205
224
|
async promote(version, mainHash, fields) {
|
|
206
|
-
const { collection, item, delta } = (await
|
|
225
|
+
const { collection, item, delta } = (await super.readOne(version));
|
|
207
226
|
// will throw an error if the accountability does not have permission to update the item
|
|
208
227
|
if (this.accountability) {
|
|
209
228
|
await validateAccess({
|
|
@@ -227,7 +246,8 @@ export class VersionsService extends ItemsService {
|
|
|
227
246
|
reason: `Main item has changed since this version was last updated`,
|
|
228
247
|
});
|
|
229
248
|
}
|
|
230
|
-
const
|
|
249
|
+
const { rawDelta, defaultOverwrites } = splitRecursive(delta);
|
|
250
|
+
const payloadToUpdate = fields ? pick(rawDelta, fields) : rawDelta;
|
|
231
251
|
const itemsService = new ItemsService(collection, {
|
|
232
252
|
accountability: this.accountability,
|
|
233
253
|
knex: this.knex,
|
|
@@ -242,7 +262,9 @@ export class VersionsService extends ItemsService {
|
|
|
242
262
|
schema: this.schema,
|
|
243
263
|
accountability: this.accountability,
|
|
244
264
|
});
|
|
245
|
-
const updatedItemKey = await itemsService.updateOne(item, payloadAfterHooks
|
|
265
|
+
const updatedItemKey = await itemsService.updateOne(item, payloadAfterHooks, {
|
|
266
|
+
overwriteDefaults: defaultOverwrites,
|
|
267
|
+
});
|
|
246
268
|
emitter.emitAction(['items.promote', `${collection}.items.promote`], {
|
|
247
269
|
payload: payloadAfterHooks,
|
|
248
270
|
collection,
|
|
@@ -255,4 +277,41 @@ export class VersionsService extends ItemsService {
|
|
|
255
277
|
});
|
|
256
278
|
return updatedItemKey;
|
|
257
279
|
}
|
|
280
|
+
mapDelta(version) {
|
|
281
|
+
const delta = version.delta ?? {};
|
|
282
|
+
delta[this.schema.collections[version.collection].primary] = version.item;
|
|
283
|
+
return deepMapWithSchema(delta, ([key, value], context) => {
|
|
284
|
+
if (key === '_user' || key === '_date')
|
|
285
|
+
return;
|
|
286
|
+
if (context.collection.primary in context.object) {
|
|
287
|
+
if (context.field.special.includes('user-updated')) {
|
|
288
|
+
return [key, context.object['_user']];
|
|
289
|
+
}
|
|
290
|
+
if (context.field.special.includes('date-updated')) {
|
|
291
|
+
return [key, context.object['_date']];
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
if (context.field.special.includes('user-created')) {
|
|
296
|
+
return [key, context.object['_user']];
|
|
297
|
+
}
|
|
298
|
+
if (context.field.special.includes('date-created')) {
|
|
299
|
+
return [key, context.object['_date']];
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (key in context.object)
|
|
303
|
+
return [key, value];
|
|
304
|
+
return undefined;
|
|
305
|
+
}, { collection: version.collection, schema: this.schema }, { mapNonExistentFields: true, detailedUpdateSyntax: true });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
/** Deeply maps all objects of a structure. Only calls the callback for objects, not for arrays. Objects in arrays will continued to be mapped. */
|
|
309
|
+
function deepMapObjects(object, fn, path = []) {
|
|
310
|
+
if (isPlainObject(object) && typeof object === 'object' && object !== null) {
|
|
311
|
+
fn(object, path);
|
|
312
|
+
Object.entries(object).map(([key, value]) => deepMapObjects(value, fn, [...path, key]));
|
|
313
|
+
}
|
|
314
|
+
else if (Array.isArray(object)) {
|
|
315
|
+
object.map((value, index) => deepMapObjects(value, fn, [...path, String(index)]));
|
|
316
|
+
}
|
|
258
317
|
}
|
package/dist/types/auth.d.ts
CHANGED
|
@@ -27,10 +27,11 @@ export interface Session {
|
|
|
27
27
|
export type DirectusTokenPayload = {
|
|
28
28
|
id?: string;
|
|
29
29
|
role: string | null;
|
|
30
|
-
session?: string;
|
|
31
30
|
app_access: boolean | number;
|
|
32
31
|
admin_access: boolean | number;
|
|
33
32
|
share?: string;
|
|
33
|
+
session?: string;
|
|
34
|
+
enforce_tfa?: boolean;
|
|
34
35
|
};
|
|
35
36
|
export type ShareData = {
|
|
36
37
|
share_id: string;
|
|
@@ -0,0 +1,23 @@
|
|
|
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 data like a response or delta changes with collection, field and relation context for each entry.
|
|
5
|
+
* Bottom to Top depth first mapping of values.
|
|
6
|
+
*/
|
|
7
|
+
export declare function deepMapWithSchema(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
|
+
object: Record<string, any>;
|
|
14
|
+
}) => [key: string | number, value: unknown] | undefined, context: {
|
|
15
|
+
schema: SchemaOverview;
|
|
16
|
+
collection: string;
|
|
17
|
+
relationInfo?: RelationInfo;
|
|
18
|
+
}, options?: {
|
|
19
|
+
/** If set to true, non-existent fields will be included in the mapping and will have a value of undefined */
|
|
20
|
+
mapNonExistentFields?: boolean;
|
|
21
|
+
/** If set to true, it will map the "create", "update" and "delete" syntax for o2m relations found in deltas */
|
|
22
|
+
detailedUpdateSyntax?: boolean;
|
|
23
|
+
}): any;
|
|
@@ -0,0 +1,81 @@
|
|
|
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 data like a response or delta changes with collection, field and relation context for each entry.
|
|
7
|
+
* Bottom to Top depth first mapping of values.
|
|
8
|
+
*/
|
|
9
|
+
export function deepMapWithSchema(object, callback, context, options) {
|
|
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
|
+
let fields;
|
|
13
|
+
if (options?.mapNonExistentFields) {
|
|
14
|
+
fields = Object.entries(collection.fields);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
fields = Object.keys(object).map((key) => [key, collection.fields[key]]);
|
|
18
|
+
}
|
|
19
|
+
return Object.fromEntries(fields
|
|
20
|
+
.map(([key, field]) => {
|
|
21
|
+
let value = object[key];
|
|
22
|
+
if (!field)
|
|
23
|
+
return [key, value];
|
|
24
|
+
const relationInfo = getRelationInfo(context.schema.relations, collection.collection, field.field);
|
|
25
|
+
let leaf = true;
|
|
26
|
+
if (relationInfo.relation && typeof value === 'object' && value !== null && isPlainObject(object)) {
|
|
27
|
+
switch (relationInfo.relationType) {
|
|
28
|
+
case 'm2o':
|
|
29
|
+
value = deepMapWithSchema(value, callback, {
|
|
30
|
+
schema: context.schema,
|
|
31
|
+
collection: relationInfo.relation.related_collection,
|
|
32
|
+
relationInfo,
|
|
33
|
+
}, options);
|
|
34
|
+
leaf = false;
|
|
35
|
+
break;
|
|
36
|
+
case 'o2m': {
|
|
37
|
+
function map(childValue) {
|
|
38
|
+
if (isPlainObject(childValue) && typeof childValue === 'object' && childValue !== null) {
|
|
39
|
+
leaf = false;
|
|
40
|
+
return deepMapWithSchema(childValue, callback, {
|
|
41
|
+
schema: context.schema,
|
|
42
|
+
collection: relationInfo.relation.collection,
|
|
43
|
+
relationInfo,
|
|
44
|
+
}, options);
|
|
45
|
+
}
|
|
46
|
+
else
|
|
47
|
+
return childValue;
|
|
48
|
+
}
|
|
49
|
+
if (Array.isArray(value)) {
|
|
50
|
+
value = value.map(map);
|
|
51
|
+
}
|
|
52
|
+
else if (options?.detailedUpdateSyntax && isPlainObject(value)) {
|
|
53
|
+
value = {
|
|
54
|
+
create: value['create']?.map(map),
|
|
55
|
+
update: value['update']?.map(map),
|
|
56
|
+
delete: value['delete']?.map(map),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
case 'a2o': {
|
|
62
|
+
const related_collection = object[relationInfo.relation.meta.one_collection_field];
|
|
63
|
+
if (!related_collection) {
|
|
64
|
+
throw new InvalidQueryError({
|
|
65
|
+
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 `,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
value = deepMapWithSchema(value, callback, {
|
|
69
|
+
schema: context.schema,
|
|
70
|
+
collection: related_collection,
|
|
71
|
+
relationInfo,
|
|
72
|
+
}, options);
|
|
73
|
+
leaf = false;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return callback([key, value], { collection, field, ...relationInfo, leaf, object });
|
|
79
|
+
})
|
|
80
|
+
.filter((f) => f));
|
|
81
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { PrimaryKey, Query, QueryOptions } from '@directus/types';
|
|
2
2
|
import type { ItemsService as ItemsServiceType } from '../../services/index.js';
|
|
3
|
-
export declare function handleVersion(self: ItemsServiceType, key: PrimaryKey, queryWithKey: Query, opts?: QueryOptions): Promise<
|
|
3
|
+
export declare function handleVersion(self: ItemsServiceType, key: PrimaryKey, queryWithKey: Query, opts?: QueryOptions): Promise<any>;
|