@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.
Files changed (123) hide show
  1. package/dist/app.js +3 -3
  2. package/dist/auth/drivers/oauth2.d.ts +2 -0
  3. package/dist/auth/drivers/oauth2.js +40 -2
  4. package/dist/auth/drivers/openid.js +8 -1
  5. package/dist/controllers/access.js +2 -2
  6. package/dist/controllers/comments.js +2 -2
  7. package/dist/controllers/dashboards.js +2 -2
  8. package/dist/controllers/files.js +2 -2
  9. package/dist/controllers/flows.js +2 -2
  10. package/dist/controllers/folders.js +2 -2
  11. package/dist/controllers/items.js +2 -2
  12. package/dist/controllers/notifications.js +2 -2
  13. package/dist/controllers/operations.js +2 -2
  14. package/dist/controllers/panels.js +2 -2
  15. package/dist/controllers/permissions.js +2 -2
  16. package/dist/controllers/policies.js +2 -2
  17. package/dist/controllers/presets.js +2 -2
  18. package/dist/controllers/roles.js +2 -2
  19. package/dist/controllers/shares.js +2 -2
  20. package/dist/controllers/translations.js +2 -2
  21. package/dist/controllers/users.js +2 -2
  22. package/dist/controllers/utils.js +8 -3
  23. package/dist/controllers/versions.js +2 -2
  24. package/dist/controllers/webhooks.js +1 -1
  25. package/dist/database/helpers/capabilities/dialects/default.d.ts +3 -0
  26. package/dist/database/helpers/capabilities/dialects/default.js +3 -0
  27. package/dist/database/helpers/capabilities/dialects/mysql.d.ts +4 -0
  28. package/dist/database/helpers/capabilities/dialects/mysql.js +9 -0
  29. package/dist/database/helpers/capabilities/dialects/postgres.d.ts +5 -0
  30. package/dist/database/helpers/capabilities/dialects/postgres.js +14 -0
  31. package/dist/database/helpers/capabilities/index.d.ts +7 -0
  32. package/dist/database/helpers/capabilities/index.js +7 -0
  33. package/dist/database/helpers/capabilities/types.d.ts +11 -0
  34. package/dist/database/helpers/capabilities/types.js +15 -0
  35. package/dist/database/helpers/index.d.ts +2 -0
  36. package/dist/database/helpers/index.js +2 -0
  37. package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +1 -2
  38. package/dist/database/helpers/schema/dialects/cockroachdb.js +0 -4
  39. package/dist/database/helpers/schema/dialects/postgres.d.ts +1 -2
  40. package/dist/database/helpers/schema/dialects/postgres.js +0 -4
  41. package/dist/database/index.js +1 -1
  42. package/dist/database/migrations/20250224A-visual-editor.d.ts +3 -0
  43. package/dist/database/migrations/20250224A-visual-editor.js +35 -0
  44. package/dist/database/run-ast/lib/get-db-query.js +16 -4
  45. package/dist/logger/index.js +3 -3
  46. package/dist/middleware/sanitize-query.js +17 -7
  47. package/dist/middleware/validate-batch.js +1 -1
  48. package/dist/operations/item-delete/index.js +1 -1
  49. package/dist/operations/item-read/index.js +1 -1
  50. package/dist/operations/item-update/index.js +1 -1
  51. package/dist/permissions/lib/fetch-permissions.js +6 -4
  52. package/dist/permissions/modules/process-ast/utils/context-has-dynamic-variables.d.ts +2 -0
  53. package/dist/permissions/modules/process-ast/utils/context-has-dynamic-variables.js +3 -0
  54. package/dist/permissions/modules/process-payload/process-payload.d.ts +1 -0
  55. package/dist/permissions/modules/process-payload/process-payload.js +13 -4
  56. package/dist/permissions/types.d.ts +2 -1
  57. package/dist/permissions/utils/extract-required-dynamic-variable-context.d.ts +3 -2
  58. package/dist/permissions/utils/extract-required-dynamic-variable-context.js +24 -5
  59. package/dist/permissions/utils/fetch-dynamic-variable-data.d.ts +9 -0
  60. package/dist/permissions/utils/{fetch-dynamic-variable-context.js → fetch-dynamic-variable-data.js} +11 -12
  61. package/dist/rate-limiter.js +1 -1
  62. package/dist/services/assets.js +12 -2
  63. package/dist/services/authentication.js +2 -2
  64. package/dist/services/collections.js +39 -3
  65. package/dist/services/fields/build-collection-and-field-relations.d.ts +21 -0
  66. package/dist/services/fields/build-collection-and-field-relations.js +55 -0
  67. package/dist/services/fields/get-collection-meta-updates.d.ts +11 -0
  68. package/dist/services/fields/get-collection-meta-updates.js +72 -0
  69. package/dist/services/fields/get-collection-relation-list.d.ts +5 -0
  70. package/dist/services/fields/get-collection-relation-list.js +28 -0
  71. package/dist/services/fields.js +17 -12
  72. package/dist/services/graphql/resolvers/get-collection-type.d.ts +3 -0
  73. package/dist/services/graphql/resolvers/get-collection-type.js +34 -0
  74. package/dist/services/graphql/resolvers/get-field-type.d.ts +3 -0
  75. package/dist/services/graphql/resolvers/get-field-type.js +51 -0
  76. package/dist/services/graphql/resolvers/get-relation-type.d.ts +3 -0
  77. package/dist/services/graphql/resolvers/get-relation-type.js +39 -0
  78. package/dist/services/graphql/resolvers/mutation.js +1 -1
  79. package/dist/services/graphql/resolvers/query.js +4 -4
  80. package/dist/services/graphql/resolvers/system-admin.d.ts +2 -2
  81. package/dist/services/graphql/resolvers/system-admin.js +207 -199
  82. package/dist/services/graphql/resolvers/system.d.ts +1 -7
  83. package/dist/services/graphql/resolvers/system.js +12 -113
  84. package/dist/services/graphql/schema/index.js +1 -1
  85. package/dist/services/graphql/schema/parse-query.d.ts +2 -2
  86. package/dist/services/graphql/schema/parse-query.js +6 -6
  87. package/dist/services/graphql/schema/read.d.ts +2 -2
  88. package/dist/services/graphql/schema/read.js +86 -2
  89. package/dist/services/graphql/schema-cache.d.ts +2 -2
  90. package/dist/services/graphql/schema-cache.js +1 -3
  91. package/dist/services/graphql/subscription.d.ts +3 -3
  92. package/dist/services/graphql/subscription.js +3 -3
  93. package/dist/services/graphql/utils/{aggrgate-query.d.ts → aggregate-query.d.ts} +2 -2
  94. package/dist/services/graphql/utils/{aggrgate-query.js → aggregate-query.js} +3 -3
  95. package/dist/services/items.d.ts +1 -0
  96. package/dist/services/items.js +30 -16
  97. package/dist/services/meta.js +4 -2
  98. package/dist/services/payload.d.ts +1 -0
  99. package/dist/services/payload.js +32 -17
  100. package/dist/services/shares.js +1 -1
  101. package/dist/services/specifications.js +10 -5
  102. package/dist/services/tus/lockers.d.ts +1 -1
  103. package/dist/services/tus/lockers.js +6 -5
  104. package/dist/services/tus/server.js +24 -0
  105. package/dist/services/users.js +1 -0
  106. package/dist/types/services.d.ts +2 -0
  107. package/dist/utils/apply-query.d.ts +1 -0
  108. package/dist/utils/apply-query.js +42 -31
  109. package/dist/utils/generate-hash.js +1 -1
  110. package/dist/utils/get-config-from-env.d.ts +6 -1
  111. package/dist/utils/get-config-from-env.js +16 -11
  112. package/dist/utils/get-graphql-type.js +3 -1
  113. package/dist/utils/is-login-redirect-allowed.js +2 -0
  114. package/dist/utils/redact-object.js +5 -1
  115. package/dist/utils/sanitize-query.d.ts +5 -2
  116. package/dist/utils/sanitize-query.js +34 -9
  117. package/dist/websocket/controllers/base.d.ts +2 -2
  118. package/dist/websocket/handlers/items.js +4 -4
  119. package/dist/websocket/handlers/subscribe.js +2 -2
  120. package/dist/websocket/messages.d.ts +7 -7
  121. package/dist/websocket/messages.js +1 -1
  122. package/package.json +58 -58
  123. 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, abortController.signal),
45
- this.acquireLock(this.id, cancelReq, abortController.signal),
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 false;
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) {
@@ -137,6 +137,7 @@ export class UsersService extends ItemsService {
137
137
  throw new FailedValidationError({
138
138
  field: 'email',
139
139
  type: 'email',
140
+ path: [],
140
141
  });
141
142
  }
142
143
  }
@@ -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>;
@@ -7,6 +7,7 @@ type ApplyQueryOptions = {
7
7
  isInnerQuery?: boolean;
8
8
  hasMultiRelationalSort?: boolean | undefined;
9
9
  groupWhenCases?: number[][] | undefined;
10
+ groupColumnPositions?: number[] | undefined;
10
11
  };
11
12
  /**
12
13
  * Apply the Query to a given Knex query builder instance
@@ -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
- columns = rawColumns.map((column, index) => applyCaseWhen({
54
- columnCases: options.groupWhenCases[index].map((caseIndex) => cases[caseIndex]),
55
- column,
56
- aliasMap,
57
- cases,
58
- table: collection,
59
- permissions,
60
- }, {
61
- knex,
62
- schema,
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
- if (childKey === '_none' || childKey === '_some') {
356
- const subQueryBuilder = (filter, cases) => (subQueryKnex) => {
357
- const field = relation.field;
358
- const collection = relation.collection;
359
- const column = `${collection}.${field}`;
360
- subQueryKnex
361
- .select({ [field]: column })
362
- .from(collection)
363
- .whereNotNull(column);
364
- applyQuery(knex, relation.collection, subQueryKnex, { filter }, schema, cases, permissions);
365
- };
366
- const { cases: subCases } = getCases(relation.collection, permissions, []);
367
- if (childKey === '_none') {
368
- dbQuery[logical].whereNotIn(pkField, subQueryBuilder(Object.values(value)[0], subCases));
369
- continue;
370
- }
371
- else if (childKey === '_some') {
372
- dbQuery[logical].whereIn(pkField, subQueryBuilder(Object.values(value)[0], subCases));
373
- continue;
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 declare function getConfigFromEnv(prefix: string, omitPrefix?: string | string[], type?: 'camelcase' | 'underscore'): Record<string, any>;
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, omitPrefix, type = 'camelcase') {
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
- if (key.toLowerCase().startsWith(prefix.toLowerCase()) === false)
13
+ const lowerCaseKey = key.toLowerCase();
14
+ if (lowerCaseKey.startsWith(lowerCasePrefix) === false)
9
15
  continue;
10
- if (omitPrefix) {
11
- let matches = false;
12
- if (Array.isArray(omitPrefix)) {
13
- matches = omitPrefix.some((prefix) => key.toLowerCase().startsWith(prefix.toLowerCase()));
14
- }
15
- else {
16
- matches = key.toLowerCase().startsWith(omitPrefix.toLowerCase());
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
- finalValue = finalValue.replace(new RegExp(valueToRedact, 'g'), replacement(redactKey));
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
- export declare function sanitizeQuery(rawQuery: Record<string, any>, accountability?: Accountability | null): Query;
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
- export function sanitizeQuery(rawQuery, accountability) {
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
- return parseFilter(filters, accountability);
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: WebSocket.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 ('query' in message) {
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.ZodType<Query, z.ZodTypeDef, Query>>;
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.ZodType<Query, z.ZodTypeDef, Query>>;
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.ZodType<Query, z.ZodTypeDef, Query>>;
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.custom().optional(),
38
+ query: z.record(z.any()).optional(),
39
39
  }),
40
40
  WebSocketMessage.extend({
41
41
  type: z.literal('unsubscribe'),