@directus/api 30.0.0 → 32.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 +7 -0
- package/dist/auth/auth.d.ts +2 -1
- package/dist/auth/auth.js +7 -2
- package/dist/auth/drivers/ldap.d.ts +0 -2
- package/dist/auth/drivers/ldap.js +9 -7
- package/dist/auth/drivers/oauth2.d.ts +0 -2
- package/dist/auth/drivers/oauth2.js +28 -11
- package/dist/auth/drivers/openid.d.ts +0 -2
- package/dist/auth/drivers/openid.js +28 -11
- package/dist/auth/drivers/saml.d.ts +0 -2
- package/dist/auth/drivers/saml.js +5 -5
- package/dist/auth.js +1 -2
- package/dist/cli/commands/bootstrap/index.js +12 -33
- package/dist/cli/commands/init/index.js +1 -1
- package/dist/cli/commands/schema/apply.d.ts +4 -0
- package/dist/cli/commands/schema/apply.js +26 -3
- package/dist/controllers/collections.js +7 -2
- package/dist/controllers/fields.js +31 -8
- package/dist/controllers/mcp.d.ts +2 -0
- package/dist/controllers/mcp.js +33 -0
- package/dist/controllers/server.js +26 -1
- package/dist/controllers/settings.js +9 -2
- 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/helpers/fn/types.js +3 -3
- package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/cockroachdb.js +13 -0
- package/dist/database/helpers/schema/dialects/mssql.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/mssql.js +23 -0
- package/dist/database/helpers/schema/dialects/mysql.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/mysql.js +25 -0
- package/dist/database/helpers/schema/dialects/oracle.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/oracle.js +13 -0
- package/dist/database/helpers/schema/dialects/postgres.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/postgres.js +13 -0
- package/dist/database/helpers/schema/types.d.ts +5 -0
- package/dist/database/helpers/schema/types.js +6 -0
- package/dist/database/migrations/20250813A-add-mcp.d.ts +3 -0
- package/dist/database/migrations/20250813A-add-mcp.js +18 -0
- package/dist/database/migrations/20251012A-add-field-searchable.d.ts +3 -0
- package/dist/database/migrations/20251012A-add-field-searchable.js +10 -0
- package/dist/database/migrations/20251014A-add-project-owner.d.ts +3 -0
- package/dist/database/migrations/20251014A-add-project-owner.js +37 -0
- package/dist/database/migrations/20251028A-add-retention-indexes.d.ts +3 -0
- package/dist/database/migrations/20251028A-add-retention-indexes.js +42 -0
- package/dist/database/run-ast/README.md +46 -0
- package/dist/database/run-ast/lib/apply-query/add-join.js +2 -2
- package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.d.ts +2 -2
- package/dist/database/run-ast/lib/apply-query/index.d.ts +0 -1
- package/dist/database/run-ast/lib/apply-query/index.js +4 -6
- package/dist/database/run-ast/lib/apply-query/search.js +2 -0
- package/dist/database/run-ast/lib/get-db-query.js +7 -6
- package/dist/database/run-ast/utils/generate-alias.d.ts +6 -0
- package/dist/database/run-ast/utils/generate-alias.js +57 -0
- package/dist/flows.js +1 -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 +103 -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/metrics/lib/create-metrics.js +16 -25
- package/dist/middleware/collection-exists.js +2 -2
- package/dist/operations/mail/index.js +3 -1
- package/dist/operations/mail/rate-limiter.d.ts +1 -0
- package/dist/operations/mail/rate-limiter.js +29 -0
- package/dist/permissions/modules/process-payload/process-payload.js +3 -10
- package/dist/permissions/modules/validate-access/validate-access.js +2 -3
- package/dist/schedules/metrics.js +6 -2
- package/dist/schedules/project.d.ts +4 -0
- package/dist/schedules/project.js +27 -0
- package/dist/services/authentication.js +36 -0
- package/dist/services/collections.d.ts +3 -3
- package/dist/services/collections.js +16 -1
- package/dist/services/fields.d.ts +21 -5
- package/dist/services/fields.js +109 -32
- package/dist/services/graphql/resolvers/query.js +1 -1
- package/dist/services/graphql/resolvers/system-admin.js +49 -5
- package/dist/services/graphql/schema/parse-query.js +8 -8
- package/dist/services/graphql/utils/aggregate-query.d.ts +1 -1
- package/dist/services/graphql/utils/aggregate-query.js +5 -1
- package/dist/services/graphql/utils/filter-replace-m2a.js +2 -1
- package/dist/services/import-export.d.ts +9 -1
- package/dist/services/import-export.js +287 -101
- package/dist/services/items.d.ts +1 -1
- package/dist/services/items.js +50 -24
- package/dist/services/mail/index.js +2 -0
- package/dist/services/mail/rate-limiter.d.ts +1 -0
- package/dist/services/mail/rate-limiter.js +29 -0
- package/dist/services/meta.js +28 -24
- package/dist/services/payload.d.ts +7 -3
- package/dist/services/payload.js +26 -12
- package/dist/services/schema.js +4 -1
- package/dist/services/server.d.ts +1 -0
- package/dist/services/server.js +15 -18
- package/dist/services/settings.d.ts +2 -1
- package/dist/services/settings.js +15 -0
- package/dist/services/tfa.d.ts +1 -1
- package/dist/services/tfa.js +20 -5
- package/dist/services/tus/server.js +14 -9
- package/dist/services/versions.d.ts +6 -4
- package/dist/services/versions.js +84 -25
- package/dist/telemetry/lib/get-report.js +4 -4
- package/dist/telemetry/lib/send-report.d.ts +6 -1
- package/dist/telemetry/lib/send-report.js +3 -1
- package/dist/telemetry/types/report.d.ts +17 -1
- package/dist/telemetry/utils/get-settings.d.ts +9 -0
- package/dist/telemetry/utils/get-settings.js +14 -0
- package/dist/test-utils/README.md +760 -0
- package/dist/test-utils/cache.d.ts +51 -0
- package/dist/test-utils/cache.js +59 -0
- package/dist/test-utils/database.d.ts +48 -0
- package/dist/test-utils/database.js +52 -0
- package/dist/test-utils/emitter.d.ts +35 -0
- package/dist/test-utils/emitter.js +38 -0
- package/dist/test-utils/fields-service.d.ts +28 -0
- package/dist/test-utils/fields-service.js +36 -0
- package/dist/test-utils/items-service.d.ts +23 -0
- package/dist/test-utils/items-service.js +37 -0
- package/dist/test-utils/knex.d.ts +164 -0
- package/dist/test-utils/knex.js +268 -0
- package/dist/test-utils/schema.d.ts +26 -0
- package/dist/test-utils/schema.js +35 -0
- package/dist/types/auth.d.ts +2 -3
- package/dist/utils/apply-diff.js +15 -0
- package/dist/utils/create-admin.d.ts +11 -0
- package/dist/utils/create-admin.js +50 -0
- package/dist/utils/get-schema.js +5 -3
- package/dist/utils/get-snapshot-diff.js +49 -5
- package/dist/utils/get-snapshot.js +13 -7
- package/dist/utils/sanitize-schema.d.ts +11 -4
- package/dist/utils/sanitize-schema.js +9 -6
- package/dist/utils/schedule.js +15 -19
- package/dist/utils/validate-diff.js +31 -0
- package/dist/utils/validate-snapshot.js +7 -0
- 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/dist/websocket/controllers/hooks.js +12 -20
- package/dist/websocket/messages.d.ts +3 -3
- package/package.json +65 -66
- package/dist/cli/utils/defaults.d.ts +0 -4
- package/dist/cli/utils/defaults.js +0 -17
- package/dist/telemetry/utils/get-project-id.d.ts +0 -2
- package/dist/telemetry/utils/get-project-id.js +0 -4
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
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { useEnv } from '@directus/env';
|
|
7
7
|
import { supportsTus } from '@directus/storage';
|
|
8
8
|
import { toArray } from '@directus/utils';
|
|
9
|
-
import { Server
|
|
9
|
+
import { Server } from '@tus/server';
|
|
10
10
|
import { RESUMABLE_UPLOADS } from '../../constants.js';
|
|
11
11
|
import { getStorage } from '../../storage/index.js';
|
|
12
12
|
import { extractMetadata } from '../files/lib/extract-metadata.js';
|
|
@@ -16,6 +16,7 @@ import { getTusLocker } from './lockers.js';
|
|
|
16
16
|
import { pick } from 'lodash-es';
|
|
17
17
|
import emitter from '../../emitter.js';
|
|
18
18
|
import getDatabase from '../../database/index.js';
|
|
19
|
+
import { getSchema } from '../../utils/get-schema.js';
|
|
19
20
|
async function createTusStore(context) {
|
|
20
21
|
const env = useEnv();
|
|
21
22
|
const storage = await getStorage();
|
|
@@ -40,16 +41,17 @@ export async function createTusServer(context) {
|
|
|
40
41
|
datastore: store,
|
|
41
42
|
locker: getTusLocker(),
|
|
42
43
|
...(RESUMABLE_UPLOADS.MAX_SIZE !== null && { maxSize: RESUMABLE_UPLOADS.MAX_SIZE }),
|
|
43
|
-
async onUploadFinish(req,
|
|
44
|
+
async onUploadFinish(req, upload) {
|
|
45
|
+
const schema = await getSchema();
|
|
44
46
|
const service = new ItemsService('directus_files', {
|
|
45
|
-
schema
|
|
47
|
+
schema,
|
|
46
48
|
});
|
|
47
49
|
const file = (await service.readByQuery({
|
|
48
50
|
filter: { tus_id: { _eq: upload.id } },
|
|
49
51
|
limit: 1,
|
|
50
52
|
}))[0];
|
|
51
53
|
if (!file)
|
|
52
|
-
return
|
|
54
|
+
return {};
|
|
53
55
|
let fileData;
|
|
54
56
|
// update metadata when file is replaced
|
|
55
57
|
if (file.tus_data?.['metadata']?.['replace_id']) {
|
|
@@ -91,19 +93,22 @@ export async function createTusServer(context) {
|
|
|
91
93
|
collection: 'directus_files',
|
|
92
94
|
}, {
|
|
93
95
|
database: getDatabase(),
|
|
94
|
-
schema
|
|
96
|
+
schema,
|
|
95
97
|
accountability: req.accountability,
|
|
96
98
|
});
|
|
97
|
-
return
|
|
99
|
+
return {
|
|
100
|
+
headers: {
|
|
101
|
+
'Directus-File-Id': upload.metadata['id'],
|
|
102
|
+
},
|
|
103
|
+
};
|
|
98
104
|
},
|
|
99
105
|
generateUrl(_req, opts) {
|
|
100
106
|
return env['PUBLIC_URL'] + '/files/tus/' + opts.id;
|
|
101
107
|
},
|
|
108
|
+
allowedHeaders: env['CORS_ALLOWED_HEADERS'],
|
|
109
|
+
exposedHeaders: env['CORS_EXPOSED_HEADERS'],
|
|
102
110
|
relativeLocation: String(env['PUBLIC_URL']).startsWith('http'),
|
|
103
111
|
});
|
|
104
|
-
server.on(EVENTS.POST_CREATE, async (_req, res, upload) => {
|
|
105
|
-
res.setHeader('Directus-File-Id', upload.metadata['id']);
|
|
106
|
-
});
|
|
107
112
|
return [server, cleanup];
|
|
108
113
|
function cleanup() {
|
|
109
114
|
server.removeAllListeners();
|
|
@@ -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
|
}
|
|
@@ -7,8 +7,8 @@ import { getExtensionCount } from '../utils/get-extension-count.js';
|
|
|
7
7
|
import { getFieldCount } from '../utils/get-field-count.js';
|
|
8
8
|
import { getFilesizeSum } from '../utils/get-filesize-sum.js';
|
|
9
9
|
import { getItemCount } from '../utils/get-item-count.js';
|
|
10
|
+
import { getSettings } from '../utils/get-settings.js';
|
|
10
11
|
import { getUserItemCount } from '../utils/get-user-item-count.js';
|
|
11
|
-
import { getProjectId } from '../utils/get-project-id.js';
|
|
12
12
|
const basicCountTasks = [
|
|
13
13
|
{ collection: 'directus_dashboards' },
|
|
14
14
|
{ collection: 'directus_files' },
|
|
@@ -26,7 +26,7 @@ export const getReport = async () => {
|
|
|
26
26
|
const db = getDatabase();
|
|
27
27
|
const env = useEnv();
|
|
28
28
|
const helpers = getHelpers(db);
|
|
29
|
-
const [basicCounts, userCounts, userItemCount, fieldsCounts, extensionsCounts, databaseSize, filesizes,
|
|
29
|
+
const [basicCounts, userCounts, userItemCount, fieldsCounts, extensionsCounts, databaseSize, filesizes, settings] = await Promise.all([
|
|
30
30
|
getItemCount(db, basicCountTasks),
|
|
31
31
|
fetchUserCount({ knex: db }),
|
|
32
32
|
getUserItemCount(db),
|
|
@@ -34,7 +34,7 @@ export const getReport = async () => {
|
|
|
34
34
|
getExtensionCount(db),
|
|
35
35
|
helpers.schema.getDatabaseSize(),
|
|
36
36
|
getFilesizeSum(db),
|
|
37
|
-
|
|
37
|
+
getSettings(db),
|
|
38
38
|
]);
|
|
39
39
|
return {
|
|
40
40
|
url: env['PUBLIC_URL'],
|
|
@@ -55,6 +55,6 @@ export const getReport = async () => {
|
|
|
55
55
|
extensions: extensionsCounts.totalEnabled,
|
|
56
56
|
database_size: databaseSize ?? 0,
|
|
57
57
|
files_size_total: filesizes.total,
|
|
58
|
-
|
|
58
|
+
...settings,
|
|
59
59
|
};
|
|
60
60
|
};
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { TelemetryReport } from '../types/report.js';
|
|
2
|
+
import type { OwnerInformation } from '@directus/types';
|
|
3
|
+
export type OwnerReport = OwnerInformation & {
|
|
4
|
+
project_id: string;
|
|
5
|
+
version: string;
|
|
6
|
+
};
|
|
2
7
|
/**
|
|
3
8
|
* Post an anonymous usage report to the centralized intake server
|
|
4
9
|
*/
|
|
5
|
-
export declare const sendReport: (report: TelemetryReport) => Promise<void>;
|
|
10
|
+
export declare const sendReport: (report: TelemetryReport | OwnerReport) => Promise<void>;
|
|
@@ -5,7 +5,9 @@ import { URL } from 'node:url';
|
|
|
5
5
|
*/
|
|
6
6
|
export const sendReport = async (report) => {
|
|
7
7
|
const env = useEnv();
|
|
8
|
-
const url =
|
|
8
|
+
const url = 'project_owner' in report
|
|
9
|
+
? new URL('/v1/owner', String(env['COMPLIANCE_URL']))
|
|
10
|
+
: new URL('/v1/metrics', String(env['TELEMETRY_URL']));
|
|
9
11
|
const headers = {
|
|
10
12
|
'Content-Type': 'application/json',
|
|
11
13
|
};
|
|
@@ -74,5 +74,21 @@ export interface TelemetryReport {
|
|
|
74
74
|
/**
|
|
75
75
|
* Unique project identifier
|
|
76
76
|
*/
|
|
77
|
-
project_id
|
|
77
|
+
project_id: string;
|
|
78
|
+
/**
|
|
79
|
+
* Whether the project has enabled MCP
|
|
80
|
+
*/
|
|
81
|
+
mcp_enabled: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Whether the project allows deletes in MCP
|
|
84
|
+
*/
|
|
85
|
+
mcp_allow_deletes: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Whether the project has enabled MCP system prompt
|
|
88
|
+
*/
|
|
89
|
+
mcp_system_prompt_enabled: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Number of Visual Editor URLs configured in the system
|
|
92
|
+
*/
|
|
93
|
+
visual_editor_urls: number;
|
|
78
94
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Knex } from 'knex';
|
|
2
|
+
export type TelemetrySettings = {
|
|
3
|
+
project_id: string;
|
|
4
|
+
mcp_enabled: boolean;
|
|
5
|
+
mcp_allow_deletes: boolean;
|
|
6
|
+
mcp_system_prompt_enabled: boolean;
|
|
7
|
+
visual_editor_urls: number;
|
|
8
|
+
};
|
|
9
|
+
export declare const getSettings: (db: Knex) => Promise<TelemetrySettings>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { toBoolean } from '@directus/utils';
|
|
2
|
+
export const getSettings = async (db) => {
|
|
3
|
+
const settings = await db
|
|
4
|
+
.select('project_id', 'mcp_enabled', 'mcp_allow_deletes', 'mcp_system_prompt_enabled', 'visual_editor_urls')
|
|
5
|
+
.from('directus_settings')
|
|
6
|
+
.first();
|
|
7
|
+
return {
|
|
8
|
+
project_id: settings.project_id,
|
|
9
|
+
mcp_enabled: toBoolean(settings?.mcp_enabled),
|
|
10
|
+
mcp_allow_deletes: toBoolean(settings?.mcp_allow_deletes),
|
|
11
|
+
mcp_system_prompt_enabled: toBoolean(settings?.mcp_system_prompt_enabled),
|
|
12
|
+
visual_editor_urls: settings.visual_editor_urls ? JSON.parse(settings.visual_editor_urls).length : 0,
|
|
13
|
+
};
|
|
14
|
+
};
|