@directus/api 25.0.0 → 26.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 +3 -3
- package/dist/auth/drivers/oauth2.d.ts +2 -0
- package/dist/auth/drivers/oauth2.js +40 -2
- package/dist/auth/drivers/openid.js +8 -1
- package/dist/controllers/access.js +2 -2
- package/dist/controllers/comments.js +2 -2
- package/dist/controllers/dashboards.js +2 -2
- package/dist/controllers/files.js +2 -2
- package/dist/controllers/flows.js +2 -2
- package/dist/controllers/folders.js +2 -2
- package/dist/controllers/items.js +2 -2
- package/dist/controllers/notifications.js +2 -2
- package/dist/controllers/operations.js +2 -2
- package/dist/controllers/panels.js +2 -2
- package/dist/controllers/permissions.js +2 -2
- package/dist/controllers/policies.js +2 -2
- package/dist/controllers/presets.js +2 -2
- package/dist/controllers/roles.js +2 -2
- package/dist/controllers/shares.js +2 -2
- package/dist/controllers/translations.js +2 -2
- package/dist/controllers/users.js +2 -2
- package/dist/controllers/utils.js +8 -3
- package/dist/controllers/versions.js +2 -2
- package/dist/controllers/webhooks.js +1 -1
- package/dist/database/helpers/capabilities/dialects/default.d.ts +3 -0
- package/dist/database/helpers/capabilities/dialects/default.js +3 -0
- package/dist/database/helpers/capabilities/dialects/mysql.d.ts +4 -0
- package/dist/database/helpers/capabilities/dialects/mysql.js +9 -0
- package/dist/database/helpers/capabilities/dialects/postgres.d.ts +5 -0
- package/dist/database/helpers/capabilities/dialects/postgres.js +14 -0
- package/dist/database/helpers/capabilities/index.d.ts +7 -0
- package/dist/database/helpers/capabilities/index.js +7 -0
- package/dist/database/helpers/capabilities/types.d.ts +11 -0
- package/dist/database/helpers/capabilities/types.js +15 -0
- package/dist/database/helpers/index.d.ts +2 -0
- package/dist/database/helpers/index.js +2 -0
- package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +1 -2
- package/dist/database/helpers/schema/dialects/cockroachdb.js +0 -4
- package/dist/database/helpers/schema/dialects/postgres.d.ts +1 -2
- package/dist/database/helpers/schema/dialects/postgres.js +0 -4
- package/dist/database/index.js +1 -1
- package/dist/database/migrations/20250224A-visual-editor.d.ts +3 -0
- package/dist/database/migrations/20250224A-visual-editor.js +35 -0
- package/dist/database/run-ast/lib/get-db-query.js +16 -4
- package/dist/logger/index.js +3 -3
- package/dist/middleware/sanitize-query.js +17 -7
- package/dist/middleware/validate-batch.js +1 -1
- package/dist/operations/item-delete/index.js +1 -1
- package/dist/operations/item-read/index.js +1 -1
- package/dist/operations/item-update/index.js +1 -1
- package/dist/permissions/lib/fetch-permissions.js +6 -4
- package/dist/permissions/modules/process-ast/utils/context-has-dynamic-variables.d.ts +2 -0
- package/dist/permissions/modules/process-ast/utils/context-has-dynamic-variables.js +3 -0
- package/dist/permissions/modules/process-payload/process-payload.d.ts +1 -0
- package/dist/permissions/modules/process-payload/process-payload.js +13 -4
- package/dist/permissions/types.d.ts +2 -1
- package/dist/permissions/utils/extract-required-dynamic-variable-context.d.ts +3 -2
- package/dist/permissions/utils/extract-required-dynamic-variable-context.js +24 -5
- package/dist/permissions/utils/fetch-dynamic-variable-data.d.ts +9 -0
- package/dist/permissions/utils/{fetch-dynamic-variable-context.js → fetch-dynamic-variable-data.js} +11 -12
- package/dist/rate-limiter.js +1 -1
- package/dist/services/assets.js +12 -2
- package/dist/services/authentication.js +2 -2
- package/dist/services/collections.js +39 -3
- package/dist/services/fields/build-collection-and-field-relations.d.ts +21 -0
- package/dist/services/fields/build-collection-and-field-relations.js +55 -0
- package/dist/services/fields/get-collection-meta-updates.d.ts +11 -0
- package/dist/services/fields/get-collection-meta-updates.js +72 -0
- package/dist/services/fields/get-collection-relation-list.d.ts +5 -0
- package/dist/services/fields/get-collection-relation-list.js +28 -0
- package/dist/services/fields.js +17 -12
- package/dist/services/graphql/resolvers/get-collection-type.d.ts +3 -0
- package/dist/services/graphql/resolvers/get-collection-type.js +34 -0
- package/dist/services/graphql/resolvers/get-field-type.d.ts +3 -0
- package/dist/services/graphql/resolvers/get-field-type.js +51 -0
- package/dist/services/graphql/resolvers/get-relation-type.d.ts +3 -0
- package/dist/services/graphql/resolvers/get-relation-type.js +39 -0
- package/dist/services/graphql/resolvers/mutation.js +1 -1
- package/dist/services/graphql/resolvers/query.js +4 -4
- package/dist/services/graphql/resolvers/system-admin.d.ts +2 -2
- package/dist/services/graphql/resolvers/system-admin.js +207 -199
- package/dist/services/graphql/resolvers/system.d.ts +1 -7
- package/dist/services/graphql/resolvers/system.js +12 -113
- package/dist/services/graphql/schema/index.js +1 -1
- package/dist/services/graphql/schema/parse-query.d.ts +2 -2
- package/dist/services/graphql/schema/parse-query.js +6 -6
- package/dist/services/graphql/schema/read.d.ts +2 -2
- package/dist/services/graphql/schema/read.js +86 -2
- package/dist/services/graphql/schema-cache.d.ts +2 -2
- package/dist/services/graphql/schema-cache.js +1 -3
- package/dist/services/graphql/subscription.d.ts +3 -3
- package/dist/services/graphql/subscription.js +3 -3
- package/dist/services/graphql/utils/{aggrgate-query.d.ts → aggregate-query.d.ts} +2 -2
- package/dist/services/graphql/utils/{aggrgate-query.js → aggregate-query.js} +3 -3
- package/dist/services/items.d.ts +1 -0
- package/dist/services/items.js +30 -16
- package/dist/services/meta.js +4 -2
- package/dist/services/payload.d.ts +1 -0
- package/dist/services/payload.js +32 -17
- package/dist/services/shares.js +1 -1
- package/dist/services/specifications.js +10 -5
- package/dist/services/tus/lockers.d.ts +1 -1
- package/dist/services/tus/lockers.js +6 -5
- package/dist/services/tus/server.js +24 -0
- package/dist/services/users.js +1 -0
- package/dist/types/services.d.ts +2 -0
- package/dist/utils/apply-query.d.ts +1 -0
- package/dist/utils/apply-query.js +42 -31
- package/dist/utils/generate-hash.js +1 -1
- package/dist/utils/get-config-from-env.d.ts +6 -1
- package/dist/utils/get-config-from-env.js +16 -11
- package/dist/utils/get-graphql-type.js +3 -1
- package/dist/utils/is-login-redirect-allowed.js +2 -0
- package/dist/utils/redact-object.js +5 -1
- package/dist/utils/sanitize-query.d.ts +5 -2
- package/dist/utils/sanitize-query.js +34 -9
- package/dist/websocket/controllers/base.d.ts +2 -2
- package/dist/websocket/handlers/items.js +4 -4
- package/dist/websocket/handlers/subscribe.js +2 -2
- package/dist/websocket/messages.d.ts +7 -7
- package/dist/websocket/messages.js +1 -1
- package/package.json +58 -58
- package/dist/permissions/utils/fetch-dynamic-variable-context.d.ts +0 -8
|
@@ -38,11 +38,12 @@ export class KvLock {
|
|
|
38
38
|
this.acquireTimeout = acquireTimeout;
|
|
39
39
|
this.kv = useLock();
|
|
40
40
|
}
|
|
41
|
-
async lock(cancelReq) {
|
|
41
|
+
async lock(signal, cancelReq) {
|
|
42
42
|
const abortController = new AbortController();
|
|
43
|
+
const abortSignal = AbortSignal.any([signal, abortController.signal]);
|
|
43
44
|
const lock = await Promise.race([
|
|
44
|
-
waitTimeout(this.acquireTimeout,
|
|
45
|
-
this.acquireLock(this.id, cancelReq,
|
|
45
|
+
waitTimeout(this.acquireTimeout, abortSignal),
|
|
46
|
+
this.acquireLock(this.id, cancelReq, abortSignal),
|
|
46
47
|
]);
|
|
47
48
|
abortController.abort();
|
|
48
49
|
if (!lock) {
|
|
@@ -50,10 +51,10 @@ export class KvLock {
|
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
53
|
async acquireLock(id, requestRelease, signal) {
|
|
54
|
+
const lockTime = await this.kv.get(id);
|
|
53
55
|
if (signal.aborted) {
|
|
54
|
-
return
|
|
56
|
+
return typeof lockTime !== 'undefined';
|
|
55
57
|
}
|
|
56
|
-
const lockTime = await this.kv.get(id);
|
|
57
58
|
const now = Date.now();
|
|
58
59
|
if (!lockTime || Number(lockTime) < now - this.lockTimeout) {
|
|
59
60
|
await this.kv.set(id, now);
|
|
@@ -14,6 +14,8 @@ import { ItemsService } from '../index.js';
|
|
|
14
14
|
import { TusDataStore } from './data-store.js';
|
|
15
15
|
import { getTusLocker } from './lockers.js';
|
|
16
16
|
import { pick } from 'lodash-es';
|
|
17
|
+
import emitter from '../../emitter.js';
|
|
18
|
+
import getDatabase from '../../database/index.js';
|
|
17
19
|
async function createTusStore(context) {
|
|
18
20
|
const env = useEnv();
|
|
19
21
|
const storage = await getStorage();
|
|
@@ -48,6 +50,7 @@ export async function createTusServer(context) {
|
|
|
48
50
|
}))[0];
|
|
49
51
|
if (!file)
|
|
50
52
|
return res;
|
|
53
|
+
let fileData;
|
|
51
54
|
// update metadata when file is replaced
|
|
52
55
|
if (file.tus_data?.['metadata']?.['replace_id']) {
|
|
53
56
|
const newFile = await service.readOne(file.tus_data['metadata']['replace_id']);
|
|
@@ -60,6 +63,12 @@ export async function createTusServer(context) {
|
|
|
60
63
|
...updateFields,
|
|
61
64
|
...metadata,
|
|
62
65
|
});
|
|
66
|
+
fileData = {
|
|
67
|
+
...newFile,
|
|
68
|
+
...updateFields,
|
|
69
|
+
...metadata,
|
|
70
|
+
id: file.tus_data['metadata']['replace_id'],
|
|
71
|
+
};
|
|
63
72
|
await service.deleteOne(file.id);
|
|
64
73
|
}
|
|
65
74
|
else {
|
|
@@ -69,7 +78,22 @@ export async function createTusServer(context) {
|
|
|
69
78
|
tus_id: null,
|
|
70
79
|
tus_data: null,
|
|
71
80
|
});
|
|
81
|
+
fileData = {
|
|
82
|
+
...file,
|
|
83
|
+
...metadata,
|
|
84
|
+
tus_id: null,
|
|
85
|
+
tus_data: null,
|
|
86
|
+
};
|
|
72
87
|
}
|
|
88
|
+
emitter.emitAction('files.upload', {
|
|
89
|
+
payload: fileData,
|
|
90
|
+
key: fileData.id,
|
|
91
|
+
collection: 'directus_files',
|
|
92
|
+
}, {
|
|
93
|
+
database: getDatabase(),
|
|
94
|
+
schema: req.schema,
|
|
95
|
+
accountability: req.accountability,
|
|
96
|
+
});
|
|
73
97
|
return res;
|
|
74
98
|
},
|
|
75
99
|
generateUrl(_req, opts) {
|
package/dist/services/users.js
CHANGED
package/dist/types/services.d.ts
CHANGED
|
@@ -4,10 +4,12 @@ export type AbstractServiceOptions = {
|
|
|
4
4
|
knex?: Knex | undefined;
|
|
5
5
|
accountability?: Accountability | null | undefined;
|
|
6
6
|
schema: SchemaOverview;
|
|
7
|
+
nested?: string[];
|
|
7
8
|
};
|
|
8
9
|
export interface AbstractService {
|
|
9
10
|
knex: Knex;
|
|
10
11
|
accountability: Accountability | null | undefined;
|
|
12
|
+
nested: string[];
|
|
11
13
|
createOne(data: Partial<Item>): Promise<PrimaryKey>;
|
|
12
14
|
createMany(data: Partial<Item>[]): Promise<PrimaryKey[]>;
|
|
13
15
|
readOne(key: PrimaryKey, query?: Query): Promise<Item>;
|
|
@@ -47,20 +47,28 @@ export default function applyQuery(knex, collection, dbQuery, query, schema, cas
|
|
|
47
47
|
hasMultiRelationalFilter = filterResult.hasMultiRelationalFilter;
|
|
48
48
|
}
|
|
49
49
|
if (query.group) {
|
|
50
|
+
const helpers = getHelpers(knex);
|
|
50
51
|
const rawColumns = query.group.map((column) => getColumn(knex, collection, column, false, schema));
|
|
51
52
|
let columns;
|
|
52
53
|
if (options?.groupWhenCases) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
column,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
54
|
+
if (helpers.capabilities.supportsColumnPositionInGroupBy() && options.groupColumnPositions) {
|
|
55
|
+
// This can be streamlined for databases that support reusing the alias in group by expressions
|
|
56
|
+
columns = query.group.map((column, index) => options.groupColumnPositions[index] !== undefined ? knex.raw(options.groupColumnPositions[index]) : column);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
// Reconstruct the columns with the case/when logic
|
|
60
|
+
columns = rawColumns.map((column, index) => applyCaseWhen({
|
|
61
|
+
columnCases: options.groupWhenCases[index].map((caseIndex) => cases[caseIndex]),
|
|
62
|
+
column,
|
|
63
|
+
aliasMap,
|
|
64
|
+
cases,
|
|
65
|
+
table: collection,
|
|
66
|
+
permissions,
|
|
67
|
+
}, {
|
|
68
|
+
knex,
|
|
69
|
+
schema,
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
64
72
|
if (query.sort && query.sort.length === 1 && query.sort[0] === query.group[0]) {
|
|
65
73
|
// Special case, where the sort query is injected by the group by operation
|
|
66
74
|
dbQuery.clear('order');
|
|
@@ -352,26 +360,29 @@ export function applyFilter(knex, schema, rootQuery, rootFilter, collection, ali
|
|
|
352
360
|
pkField = knex.raw(getHelpers(knex).schema.castA2oPrimaryKey(), [pkField]);
|
|
353
361
|
}
|
|
354
362
|
const childKey = Object.keys(value)?.[0];
|
|
355
|
-
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
363
|
+
const subQueryBuilder = (filter, cases) => (subQueryKnex) => {
|
|
364
|
+
const field = relation.field;
|
|
365
|
+
const collection = relation.collection;
|
|
366
|
+
const column = `${collection}.${field}`;
|
|
367
|
+
subQueryKnex
|
|
368
|
+
.select({ [field]: column })
|
|
369
|
+
.from(collection)
|
|
370
|
+
.whereNotNull(column);
|
|
371
|
+
applyQuery(knex, relation.collection, subQueryKnex, { filter }, schema, cases, permissions);
|
|
372
|
+
};
|
|
373
|
+
const { cases: subCases } = getCases(relation.collection, permissions, []);
|
|
374
|
+
if (childKey === '_none') {
|
|
375
|
+
dbQuery[logical].whereNotIn(pkField, subQueryBuilder(Object.values(value)[0], subCases));
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
else if (childKey === '_some') {
|
|
379
|
+
dbQuery[logical].whereIn(pkField, subQueryBuilder(Object.values(value)[0], subCases));
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
// Add implicit _some behavior when no operator is provided
|
|
384
|
+
dbQuery[logical].whereIn(pkField, subQueryBuilder(value, subCases));
|
|
385
|
+
continue;
|
|
375
386
|
}
|
|
376
387
|
}
|
|
377
388
|
if (filterPath.includes('_none') || filterPath.includes('_some')) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import argon2 from 'argon2';
|
|
2
2
|
import { getConfigFromEnv } from './get-config-from-env.js';
|
|
3
3
|
export function generateHash(stringToHash) {
|
|
4
|
-
const argon2HashConfigOptions = getConfigFromEnv('HASH_', 'HASH_RAW'); // Disallow the HASH_RAW option, see https://github.com/directus/directus/discussions/7670#discussioncomment-1255805
|
|
4
|
+
const argon2HashConfigOptions = getConfigFromEnv('HASH_', { omitPrefix: 'HASH_RAW' }); // Disallow the HASH_RAW option, see https://github.com/directus/directus/discussions/7670#discussioncomment-1255805
|
|
5
5
|
// associatedData, if specified, must be passed as a Buffer to argon2.hash, see https://github.com/ranisalt/node-argon2/wiki/Options#associateddata
|
|
6
6
|
if ('associatedData' in argon2HashConfigOptions)
|
|
7
7
|
argon2HashConfigOptions['associatedData'] = Buffer.from(argon2HashConfigOptions['associatedData']);
|
|
@@ -1 +1,6 @@
|
|
|
1
|
-
export
|
|
1
|
+
export interface GetConfigFromEnvOptions {
|
|
2
|
+
omitPrefix?: string | string[];
|
|
3
|
+
omitKey?: string | string[];
|
|
4
|
+
type?: 'camelcase' | 'underscore';
|
|
5
|
+
}
|
|
6
|
+
export declare function getConfigFromEnv(prefix: string, options?: GetConfigFromEnvOptions): Record<string, any>;
|
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
import { useEnv } from '@directus/env';
|
|
2
|
+
import { toArray } from '@directus/utils';
|
|
2
3
|
import camelcase from 'camelcase';
|
|
3
4
|
import { set } from 'lodash-es';
|
|
4
|
-
export function getConfigFromEnv(prefix,
|
|
5
|
+
export function getConfigFromEnv(prefix, options) {
|
|
5
6
|
const env = useEnv();
|
|
7
|
+
const type = options?.type ?? 'camelcase';
|
|
6
8
|
const config = {};
|
|
9
|
+
const lowerCasePrefix = prefix.toLowerCase();
|
|
10
|
+
const omitKeys = toArray(options?.omitKey ?? []).map((key) => key.toLowerCase());
|
|
11
|
+
const omitPrefixes = toArray(options?.omitPrefix ?? []).map((prefix) => prefix.toLowerCase());
|
|
7
12
|
for (const [key, value] of Object.entries(env)) {
|
|
8
|
-
|
|
13
|
+
const lowerCaseKey = key.toLowerCase();
|
|
14
|
+
if (lowerCaseKey.startsWith(lowerCasePrefix) === false)
|
|
9
15
|
continue;
|
|
10
|
-
if (
|
|
11
|
-
|
|
12
|
-
if (
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (matches)
|
|
16
|
+
if (omitKeys.length > 0) {
|
|
17
|
+
const isKeyInOmitKeys = omitKeys.some((keyToOmit) => lowerCaseKey === keyToOmit);
|
|
18
|
+
if (isKeyInOmitKeys)
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (omitPrefixes.length > 0) {
|
|
22
|
+
const keyStartsWithAnyPrefix = omitPrefixes.some((prefix) => lowerCaseKey.startsWith(prefix));
|
|
23
|
+
if (keyStartsWithAnyPrefix)
|
|
19
24
|
continue;
|
|
20
25
|
}
|
|
21
26
|
if (key.includes('__')) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GraphQLBoolean, GraphQLFloat, GraphQLInt, GraphQLList, GraphQLScalarType, GraphQLString } from 'graphql';
|
|
1
|
+
import { GraphQLBoolean, GraphQLFloat, GraphQLID, GraphQLInt, GraphQLList, GraphQLScalarType, GraphQLString, } from 'graphql';
|
|
2
2
|
import { GraphQLJSON } from 'graphql-compose';
|
|
3
3
|
import { GraphQLBigInt } from '../services/graphql/types/bigint.js';
|
|
4
4
|
import { GraphQLDate } from '../services/graphql/types/date.js';
|
|
@@ -31,6 +31,8 @@ export function getGraphQLType(localType, special) {
|
|
|
31
31
|
return GraphQLDate;
|
|
32
32
|
case 'hash':
|
|
33
33
|
return GraphQLHash;
|
|
34
|
+
case 'uuid':
|
|
35
|
+
return GraphQLID;
|
|
34
36
|
default:
|
|
35
37
|
return GraphQLString;
|
|
36
38
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEnv } from '@directus/env';
|
|
2
2
|
import { toArray } from '@directus/utils';
|
|
3
|
+
import { useLogger } from '../logger/index.js';
|
|
3
4
|
import isUrlAllowed from './is-url-allowed.js';
|
|
4
5
|
/**
|
|
5
6
|
* Checks if the defined redirect after successful SSO login is in the allow list
|
|
@@ -26,6 +27,7 @@ export function isLoginRedirectAllowed(redirect, provider) {
|
|
|
26
27
|
return true;
|
|
27
28
|
}
|
|
28
29
|
if (URL.canParse(publicUrl) === false) {
|
|
30
|
+
useLogger().error('Invalid PUBLIC_URL for login redirect');
|
|
29
31
|
return false;
|
|
30
32
|
}
|
|
31
33
|
// allow redirects to the defined PUBLIC_URL
|
|
@@ -116,7 +116,8 @@ export function getReplacer(replacement, values) {
|
|
|
116
116
|
let finalValue = value;
|
|
117
117
|
for (const [redactKey, valueToRedact] of filteredValues) {
|
|
118
118
|
if (finalValue.includes(valueToRedact)) {
|
|
119
|
-
|
|
119
|
+
const regexp = new RegExp(escapeRegexString(valueToRedact), 'g');
|
|
120
|
+
finalValue = finalValue.replace(regexp, replacement(redactKey));
|
|
120
121
|
}
|
|
121
122
|
}
|
|
122
123
|
return finalValue;
|
|
@@ -125,3 +126,6 @@ export function getReplacer(replacement, values) {
|
|
|
125
126
|
const seen = new WeakSet();
|
|
126
127
|
return replacer(seen);
|
|
127
128
|
}
|
|
129
|
+
function escapeRegexString(string) {
|
|
130
|
+
return string.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
|
|
131
|
+
}
|
|
@@ -1,2 +1,5 @@
|
|
|
1
|
-
import type { Accountability, Query } from '@directus/types';
|
|
2
|
-
|
|
1
|
+
import type { Accountability, Query, SchemaOverview } from '@directus/types';
|
|
2
|
+
/**
|
|
3
|
+
* Sanitize the query parameters and parse them where necessary.
|
|
4
|
+
*/
|
|
5
|
+
export declare function sanitizeQuery(rawQuery: Record<string, any>, schema: SchemaOverview, accountability?: Accountability | null): Promise<Query>;
|
|
@@ -2,9 +2,17 @@ import { useEnv } from '@directus/env';
|
|
|
2
2
|
import { InvalidQueryError } from '@directus/errors';
|
|
3
3
|
import { parseFilter, parseJSON } from '@directus/utils';
|
|
4
4
|
import { flatten, get, isPlainObject, merge, set } from 'lodash-es';
|
|
5
|
+
import getDatabase from '../database/index.js';
|
|
5
6
|
import { useLogger } from '../logger/index.js';
|
|
7
|
+
import { fetchPolicies } from '../permissions/lib/fetch-policies.js';
|
|
8
|
+
import { contextHasDynamicVariables } from '../permissions/modules/process-ast/utils/context-has-dynamic-variables.js';
|
|
9
|
+
import { extractRequiredDynamicVariableContext } from '../permissions/utils/extract-required-dynamic-variable-context.js';
|
|
10
|
+
import { fetchDynamicVariableData } from '../permissions/utils/fetch-dynamic-variable-data.js';
|
|
6
11
|
import { Meta } from '../types/index.js';
|
|
7
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Sanitize the query parameters and parse them where necessary.
|
|
14
|
+
*/
|
|
15
|
+
export async function sanitizeQuery(rawQuery, schema, accountability) {
|
|
8
16
|
const env = useEnv();
|
|
9
17
|
const query = {};
|
|
10
18
|
const hasMaxLimit = 'QUERY_LIMIT_MAX' in env &&
|
|
@@ -33,7 +41,7 @@ export function sanitizeQuery(rawQuery, accountability) {
|
|
|
33
41
|
query.sort = sanitizeSort(rawQuery['sort']);
|
|
34
42
|
}
|
|
35
43
|
if (rawQuery['filter']) {
|
|
36
|
-
query.filter = sanitizeFilter(rawQuery['filter'], accountability || null);
|
|
44
|
+
query.filter = await sanitizeFilter(rawQuery['filter'], schema, accountability || null);
|
|
37
45
|
}
|
|
38
46
|
if (rawQuery['offset'] !== undefined) {
|
|
39
47
|
query.offset = sanitizeOffset(rawQuery['offset']);
|
|
@@ -58,7 +66,7 @@ export function sanitizeQuery(rawQuery, accountability) {
|
|
|
58
66
|
if (rawQuery['deep']) {
|
|
59
67
|
if (!query.deep)
|
|
60
68
|
query.deep = {};
|
|
61
|
-
query.deep = sanitizeDeep(rawQuery['deep'], accountability);
|
|
69
|
+
query.deep = await sanitizeDeep(rawQuery['deep'], schema, accountability);
|
|
62
70
|
}
|
|
63
71
|
if (rawQuery['alias']) {
|
|
64
72
|
query.alias = sanitizeAlias(rawQuery['alias']);
|
|
@@ -106,7 +114,7 @@ function sanitizeAggregate(rawAggregate) {
|
|
|
106
114
|
}
|
|
107
115
|
return aggregate;
|
|
108
116
|
}
|
|
109
|
-
function sanitizeFilter(rawFilter, accountability) {
|
|
117
|
+
async function sanitizeFilter(rawFilter, schema, accountability) {
|
|
110
118
|
let filters = rawFilter;
|
|
111
119
|
if (typeof filters === 'string') {
|
|
112
120
|
try {
|
|
@@ -117,7 +125,24 @@ function sanitizeFilter(rawFilter, accountability) {
|
|
|
117
125
|
}
|
|
118
126
|
}
|
|
119
127
|
try {
|
|
120
|
-
|
|
128
|
+
let filterContext;
|
|
129
|
+
if (accountability) {
|
|
130
|
+
const dynamicVariableContext = extractRequiredDynamicVariableContext(filters);
|
|
131
|
+
if (contextHasDynamicVariables(dynamicVariableContext)) {
|
|
132
|
+
const context = {
|
|
133
|
+
schema,
|
|
134
|
+
knex: getDatabase(),
|
|
135
|
+
};
|
|
136
|
+
const policies = await fetchPolicies(accountability, context);
|
|
137
|
+
context.accountability = accountability;
|
|
138
|
+
filterContext = await fetchDynamicVariableData({
|
|
139
|
+
dynamicVariableContext,
|
|
140
|
+
accountability,
|
|
141
|
+
policies,
|
|
142
|
+
}, context);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return parseFilter(filters, accountability, filterContext);
|
|
121
146
|
}
|
|
122
147
|
catch {
|
|
123
148
|
throw new InvalidQueryError({ reason: 'Invalid filter object' });
|
|
@@ -146,7 +171,7 @@ function sanitizeMeta(rawMeta) {
|
|
|
146
171
|
}
|
|
147
172
|
return [rawMeta];
|
|
148
173
|
}
|
|
149
|
-
function sanitizeDeep(deep, accountability) {
|
|
174
|
+
async function sanitizeDeep(deep, schema, accountability) {
|
|
150
175
|
const logger = useLogger();
|
|
151
176
|
const result = {};
|
|
152
177
|
if (typeof deep === 'string') {
|
|
@@ -157,9 +182,9 @@ function sanitizeDeep(deep, accountability) {
|
|
|
157
182
|
logger.warn('Invalid value passed for deep query parameter.');
|
|
158
183
|
}
|
|
159
184
|
}
|
|
160
|
-
parse(deep);
|
|
185
|
+
await parse(deep);
|
|
161
186
|
return result;
|
|
162
|
-
function parse(level, path = []) {
|
|
187
|
+
async function parse(level, path = []) {
|
|
163
188
|
const subQuery = {};
|
|
164
189
|
const parsedLevel = {};
|
|
165
190
|
for (const [key, value] of Object.entries(level)) {
|
|
@@ -175,7 +200,7 @@ function sanitizeDeep(deep, accountability) {
|
|
|
175
200
|
}
|
|
176
201
|
if (Object.keys(subQuery).length > 0) {
|
|
177
202
|
// Sanitize the entire sub query
|
|
178
|
-
const parsedSubQuery = sanitizeQuery(subQuery, accountability);
|
|
203
|
+
const parsedSubQuery = await sanitizeQuery(subQuery, schema, accountability);
|
|
179
204
|
for (const [parsedKey, parsedValue] of Object.entries(parsedSubQuery)) {
|
|
180
205
|
parsedLevel[`_${parsedKey}`] = parsedValue;
|
|
181
206
|
}
|
|
@@ -2,11 +2,11 @@ import type { Accountability } from '@directus/types';
|
|
|
2
2
|
import type { IncomingMessage, Server as httpServer } from 'http';
|
|
3
3
|
import type { RateLimiterAbstract } from 'rate-limiter-flexible';
|
|
4
4
|
import type internal from 'stream';
|
|
5
|
-
import WebSocket from 'ws';
|
|
5
|
+
import WebSocket, { type Server } from 'ws';
|
|
6
6
|
import { WebSocketAuthMessage, WebSocketMessage } from '../messages.js';
|
|
7
7
|
import type { AuthenticationState, UpgradeContext, WebSocketAuthentication, WebSocketClient } from '../types.js';
|
|
8
8
|
export default abstract class SocketController {
|
|
9
|
-
server:
|
|
9
|
+
server: Server;
|
|
10
10
|
clients: Set<WebSocketClient>;
|
|
11
11
|
authentication: WebSocketAuthentication;
|
|
12
12
|
endpoint: string;
|
|
@@ -35,7 +35,7 @@ export class ItemsHandler {
|
|
|
35
35
|
const metaService = new MetaService({ schema, accountability });
|
|
36
36
|
let result, meta;
|
|
37
37
|
if (message.action === 'create') {
|
|
38
|
-
const query = sanitizeQuery(message?.query ?? {}, accountability);
|
|
38
|
+
const query = await sanitizeQuery(message?.query ?? {}, schema, accountability);
|
|
39
39
|
if (Array.isArray(message.data)) {
|
|
40
40
|
const keys = await service.createMany(message.data);
|
|
41
41
|
result = await service.readMany(keys, query);
|
|
@@ -46,7 +46,7 @@ export class ItemsHandler {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
if (message.action === 'read') {
|
|
49
|
-
const query = sanitizeQuery(message.query ?? {}, accountability);
|
|
49
|
+
const query = await sanitizeQuery(message.query ?? {}, schema, accountability);
|
|
50
50
|
if (message.id) {
|
|
51
51
|
result = await service.readOne(message.id, query);
|
|
52
52
|
}
|
|
@@ -62,7 +62,7 @@ export class ItemsHandler {
|
|
|
62
62
|
meta = await metaService.getMetaForQuery(message.collection, query);
|
|
63
63
|
}
|
|
64
64
|
if (message.action === 'update') {
|
|
65
|
-
const query = sanitizeQuery(message.query ?? {}, accountability);
|
|
65
|
+
const query = await sanitizeQuery(message.query ?? {}, schema, accountability);
|
|
66
66
|
if (message.id) {
|
|
67
67
|
const key = await service.updateOne(message.id, message.data);
|
|
68
68
|
result = await service.readOne(key);
|
|
@@ -92,7 +92,7 @@ export class ItemsHandler {
|
|
|
92
92
|
result = message.ids;
|
|
93
93
|
}
|
|
94
94
|
else if (message.query) {
|
|
95
|
-
const query = sanitizeQuery(message.query, accountability);
|
|
95
|
+
const query = await sanitizeQuery(message.query, schema, accountability);
|
|
96
96
|
result = await service.deleteByQuery(query);
|
|
97
97
|
}
|
|
98
98
|
else {
|
|
@@ -140,8 +140,8 @@ export class SubscribeHandler {
|
|
|
140
140
|
if ('event' in message) {
|
|
141
141
|
subscription.event = message.event;
|
|
142
142
|
}
|
|
143
|
-
if (
|
|
144
|
-
subscription.query = sanitizeQuery(message.query, accountability);
|
|
143
|
+
if (message.query) {
|
|
144
|
+
subscription.query = await sanitizeQuery(message.query, schema, accountability);
|
|
145
145
|
}
|
|
146
146
|
if ('item' in message)
|
|
147
147
|
subscription.item = String(message.item);
|
|
@@ -150,7 +150,7 @@ export declare const WebSocketSubscribeMessage: z.ZodDiscriminatedUnion<"type",
|
|
|
150
150
|
collection: z.ZodString;
|
|
151
151
|
event: z.ZodOptional<z.ZodUnion<[z.ZodLiteral<"create">, z.ZodLiteral<"update">, z.ZodLiteral<"delete">]>>;
|
|
152
152
|
item: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNumber]>>;
|
|
153
|
-
query: z.ZodOptional<z.
|
|
153
|
+
query: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
|
|
154
154
|
}>, "passthrough", z.ZodTypeAny, z.objectOutputType<z.objectUtil.extendShape<{
|
|
155
155
|
type: z.ZodString;
|
|
156
156
|
uid: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNumber]>>;
|
|
@@ -159,7 +159,7 @@ export declare const WebSocketSubscribeMessage: z.ZodDiscriminatedUnion<"type",
|
|
|
159
159
|
collection: z.ZodString;
|
|
160
160
|
event: z.ZodOptional<z.ZodUnion<[z.ZodLiteral<"create">, z.ZodLiteral<"update">, z.ZodLiteral<"delete">]>>;
|
|
161
161
|
item: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNumber]>>;
|
|
162
|
-
query: z.ZodOptional<z.
|
|
162
|
+
query: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
|
|
163
163
|
}>, z.ZodTypeAny, "passthrough">, z.objectInputType<z.objectUtil.extendShape<{
|
|
164
164
|
type: z.ZodString;
|
|
165
165
|
uid: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNumber]>>;
|
|
@@ -168,7 +168,7 @@ export declare const WebSocketSubscribeMessage: z.ZodDiscriminatedUnion<"type",
|
|
|
168
168
|
collection: z.ZodString;
|
|
169
169
|
event: z.ZodOptional<z.ZodUnion<[z.ZodLiteral<"create">, z.ZodLiteral<"update">, z.ZodLiteral<"delete">]>>;
|
|
170
170
|
item: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNumber]>>;
|
|
171
|
-
query: z.ZodOptional<z.
|
|
171
|
+
query: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
|
|
172
172
|
}>, z.ZodTypeAny, "passthrough">>, z.ZodObject<z.objectUtil.extendShape<{
|
|
173
173
|
type: z.ZodString;
|
|
174
174
|
uid: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNumber]>>;
|
|
@@ -336,13 +336,13 @@ export declare const WebSocketEvent: z.ZodDiscriminatedUnion<"action", [z.ZodObj
|
|
|
336
336
|
keys: z.ZodArray<z.ZodUnion<[z.ZodString, z.ZodNumber]>, "many">;
|
|
337
337
|
}, "strip", z.ZodTypeAny, {
|
|
338
338
|
collection: string;
|
|
339
|
-
action: "update";
|
|
340
339
|
keys: (string | number)[];
|
|
340
|
+
action: "update";
|
|
341
341
|
payload?: Record<string, any> | undefined;
|
|
342
342
|
}, {
|
|
343
343
|
collection: string;
|
|
344
|
-
action: "update";
|
|
345
344
|
keys: (string | number)[];
|
|
345
|
+
action: "update";
|
|
346
346
|
payload?: Record<string, any> | undefined;
|
|
347
347
|
}>, z.ZodObject<{
|
|
348
348
|
action: z.ZodLiteral<"delete">;
|
|
@@ -351,13 +351,13 @@ export declare const WebSocketEvent: z.ZodDiscriminatedUnion<"action", [z.ZodObj
|
|
|
351
351
|
keys: z.ZodArray<z.ZodUnion<[z.ZodString, z.ZodNumber]>, "many">;
|
|
352
352
|
}, "strip", z.ZodTypeAny, {
|
|
353
353
|
collection: string;
|
|
354
|
-
action: "delete";
|
|
355
354
|
keys: (string | number)[];
|
|
355
|
+
action: "delete";
|
|
356
356
|
payload?: Record<string, any> | undefined;
|
|
357
357
|
}, {
|
|
358
358
|
collection: string;
|
|
359
|
-
action: "delete";
|
|
360
359
|
keys: (string | number)[];
|
|
360
|
+
action: "delete";
|
|
361
361
|
payload?: Record<string, any> | undefined;
|
|
362
362
|
}>]>;
|
|
363
363
|
export type WebSocketEvent = z.infer<typeof WebSocketEvent>;
|
|
@@ -35,7 +35,7 @@ export const WebSocketSubscribeMessage = z.discriminatedUnion('type', [
|
|
|
35
35
|
collection: z.string(),
|
|
36
36
|
event: z.union([z.literal('create'), z.literal('update'), z.literal('delete')]).optional(),
|
|
37
37
|
item: zodStringOrNumber.optional(),
|
|
38
|
-
query: z.
|
|
38
|
+
query: z.record(z.any()).optional(),
|
|
39
39
|
}),
|
|
40
40
|
WebSocketMessage.extend({
|
|
41
41
|
type: z.literal('unsubscribe'),
|