@directus/api 12.1.3 → 13.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.
@@ -148,8 +148,8 @@ CACHE_ENABLED=false
148
148
  # How long the cache is persisted ["5m"]
149
149
  # CACHE_TTL="30m"
150
150
 
151
- # How to scope the cache data ["directus-cache"]
152
- # CACHE_NAMESPACE="directus-cache"
151
+ # How to scope the cache data ["system-cache"]
152
+ # CACHE_NAMESPACE="system-cache"
153
153
 
154
154
  # Automatically purge the cache on create, update, and delete actions. [false]
155
155
  # CACHE_AUTO_PURGE=true
@@ -1,12 +1,28 @@
1
1
  export async function up(knex) {
2
2
  await knex.schema.alterTable('directus_shares', (table) => {
3
+ if (knex.client.constructor.name === 'Client_MySQL') {
4
+ // Temporary drop foreign key constraint, see https://github.com/directus/directus/issues/19399
5
+ table.dropForeign('collection', 'directus_shares_collection_foreign');
6
+ }
3
7
  table.dropNullable('collection');
8
+ if (knex.client.constructor.name === 'Client_MySQL') {
9
+ // Recreate foreign key constraint, from 20211211A-add-shares.ts
10
+ table.foreign('collection').references('directus_collections.collection').onDelete('CASCADE');
11
+ }
4
12
  table.dropNullable('item');
5
13
  });
6
14
  }
7
15
  export async function down(knex) {
8
16
  await knex.schema.alterTable('directus_shares', (table) => {
17
+ if (knex.client.constructor.name === 'Client_MySQL') {
18
+ // Temporary drop foreign key constraint, see https://github.com/directus/directus/issues/19399
19
+ table.dropForeign('collection', 'directus_shares_collection_foreign');
20
+ }
9
21
  table.setNullable('collection');
22
+ if (knex.client.constructor.name === 'Client_MySQL') {
23
+ // Recreate foreign key constraint, from 20211211A-add-shares.ts
24
+ table.foreign('collection').references('directus_collections.collection').onDelete('CASCADE');
25
+ }
10
26
  table.setNullable('item');
11
27
  });
12
28
  }
package/dist/env.js CHANGED
@@ -192,8 +192,9 @@ const allowedEnvironmentVars = [
192
192
  'RELATIONAL_BATCH_SIZE',
193
193
  'EXPORT_BATCH_SIZE',
194
194
  // flows
195
- 'FLOWS_EXEC_ALLOWED_MODULES',
196
195
  'FLOWS_ENV_ALLOW_LIST',
196
+ 'FLOWS_RUN_SCRIPT_MAX_MEMORY',
197
+ 'FLOWS_RUN_SCRIPT_TIMEOUT',
197
198
  // websockets
198
199
  'WEBSOCKETS_.+',
199
200
  ].map((name) => new RegExp(`^${name}$`));
@@ -282,8 +283,9 @@ const defaults = {
282
283
  WEBSOCKETS_GRAPHQL_PATH: '/graphql',
283
284
  WEBSOCKETS_HEARTBEAT_ENABLED: true,
284
285
  WEBSOCKETS_HEARTBEAT_PERIOD: 30,
285
- FLOWS_EXEC_ALLOWED_MODULES: false,
286
286
  FLOWS_ENV_ALLOW_LIST: false,
287
+ FLOWS_RUN_SCRIPT_MAX_MEMORY: 32,
288
+ FLOWS_RUN_SCRIPT_TIMEOUT: 10000,
287
289
  PRESSURE_LIMITER_ENABLED: true,
288
290
  PRESSURE_LIMITER_SAMPLE_INTERVAL: 250,
289
291
  PRESSURE_LIMITER_MAX_EVENT_LOOP_UTILIZATION: 0.99,
package/dist/flows.d.ts CHANGED
@@ -7,6 +7,7 @@ declare class FlowManager {
7
7
  private operationFlowHandlers;
8
8
  private webhookFlowHandlers;
9
9
  private reloadQueue;
10
+ private envs;
10
11
  constructor();
11
12
  initialize(): Promise<void>;
12
13
  reload(): Promise<void>;
package/dist/flows.js CHANGED
@@ -1,5 +1,5 @@
1
- import { Action, REDACTED_TEXT } from '@directus/constants';
2
- import { applyOptionsData, isValidJSON, parseJSON, toArray } from '@directus/utils';
1
+ import { Action } from '@directus/constants';
2
+ import { applyOptionsData, getRedactedString, isValidJSON, parseJSON, toArray } from '@directus/utils';
3
3
  import { omit, pick } from 'lodash-es';
4
4
  import { get } from 'micromustache';
5
5
  import getDatabase from './database/index.js';
@@ -16,7 +16,7 @@ import { constructFlowTree } from './utils/construct-flow-tree.js';
16
16
  import { getSchema } from './utils/get-schema.js';
17
17
  import { JobQueue } from './utils/job-queue.js';
18
18
  import { mapValuesDeep } from './utils/map-values-deep.js';
19
- import { redact } from './utils/redact.js';
19
+ import { redactObject } from './utils/redact-object.js';
20
20
  import { sanitizeError } from './utils/sanitize-error.js';
21
21
  import { scheduleSynchronizedJob, validateCron } from './utils/schedule.js';
22
22
  let flowManager;
@@ -38,8 +38,10 @@ class FlowManager {
38
38
  operationFlowHandlers = {};
39
39
  webhookFlowHandlers = {};
40
40
  reloadQueue;
41
+ envs;
41
42
  constructor() {
42
43
  this.reloadQueue = new JobQueue();
44
+ this.envs = env['FLOWS_ENV_ALLOW_LIST'] ? pick(env, toArray(env['FLOWS_ENV_ALLOW_LIST'])) : {};
43
45
  const messenger = getMessenger();
44
46
  messenger.subscribe('flows', (event) => {
45
47
  if (event['type'] === 'reload') {
@@ -238,7 +240,7 @@ class FlowManager {
238
240
  [TRIGGER_KEY]: data,
239
241
  [LAST_KEY]: data,
240
242
  [ACCOUNTABILITY_KEY]: context?.['accountability'] ?? null,
241
- [ENV_KEY]: pick(env, env['FLOWS_ENV_ALLOW_LIST'] ? toArray(env['FLOWS_ENV_ALLOW_LIST']) : []),
243
+ [ENV_KEY]: this.envs,
242
244
  };
243
245
  let nextOperation = flow.operation;
244
246
  let lastOperationStatus = 'unknown';
@@ -276,14 +278,17 @@ class FlowManager {
276
278
  collection: 'directus_flows',
277
279
  item: flow.id,
278
280
  data: {
279
- steps: steps,
280
- data: redact(omit(keyedData, '$accountability.permissions'), // Permissions is a ton of data, and is just a copy of what's in the directus_permissions table
281
- [
282
- ['**', 'headers', 'authorization'],
283
- ['**', 'headers', 'cookie'],
284
- ['**', 'query', 'access_token'],
285
- ['**', 'payload', 'password'],
286
- ], REDACTED_TEXT),
281
+ steps: steps.map((step) => redactObject(step, { values: this.envs }, getRedactedString)),
282
+ data: redactObject(omit(keyedData, '$accountability.permissions'), // Permissions is a ton of data, and is just a copy of what's in the directus_permissions table
283
+ {
284
+ keys: [
285
+ ['**', 'headers', 'authorization'],
286
+ ['**', 'headers', 'cookie'],
287
+ ['**', 'query', 'access_token'],
288
+ ['**', 'payload', 'password'],
289
+ ],
290
+ values: this.envs,
291
+ }, getRedactedString),
287
292
  },
288
293
  });
289
294
  }
package/dist/logger.js CHANGED
@@ -1,5 +1,4 @@
1
- import { REDACTED_TEXT } from '@directus/constants';
2
- import { toArray } from '@directus/utils';
1
+ import { REDACTED_TEXT, toArray } from '@directus/utils';
3
2
  import { merge } from 'lodash-es';
4
3
  import { pino } from 'pino';
5
4
  import { pinoHttp, stdSerializers } from 'pino-http';
@@ -1,38 +1,47 @@
1
- import { defineOperationApi, toArray } from '@directus/utils';
2
- import { isBuiltin } from 'node:module';
3
- import { NodeVM, VMScript } from 'vm2';
1
+ import { defineOperationApi } from '@directus/utils';
2
+ import { createRequire } from 'node:module';
3
+ const require = createRequire(import.meta.url);
4
+ const ivm = require('isolated-vm');
5
+ /**
6
+ * A helper for making the logs prettier.
7
+ * The logger prints arrays with their indices but this looks "bad" when you have only one argument.
8
+ */
9
+ function unpackArgs(args) {
10
+ return args.length === 1 ? args[0] : args;
11
+ }
4
12
  export default defineOperationApi({
5
13
  id: 'exec',
6
- handler: async ({ code }, { data, env }) => {
7
- const allowedModules = env['FLOWS_EXEC_ALLOWED_MODULES'] ? toArray(env['FLOWS_EXEC_ALLOWED_MODULES']) : [];
8
- const allowedModulesBuiltIn = [];
9
- const allowedModulesExternal = [];
14
+ handler: async ({ code }, { data, env, logger }) => {
10
15
  const allowedEnv = data['$env'] ?? {};
11
- const opts = {
12
- eval: false,
13
- wasm: false,
14
- env: allowedEnv,
15
- };
16
- for (const module of allowedModules) {
17
- if (isBuiltin(module)) {
18
- allowedModulesBuiltIn.push(module);
19
- }
20
- else {
21
- allowedModulesExternal.push(module);
22
- }
23
- }
24
- if (allowedModules.length > 0) {
25
- opts.require = {
26
- builtin: allowedModulesBuiltIn,
27
- external: {
28
- modules: allowedModulesExternal,
29
- transitive: false,
30
- },
31
- };
32
- }
33
- const vm = new NodeVM(opts);
34
- const script = new VMScript(code).compile();
35
- const fn = await vm.run(script);
36
- return await fn(data);
16
+ const isolateSizeMb = env['FLOWS_RUN_SCRIPT_MAX_MEMORY'];
17
+ const scriptTimeoutMs = env['FLOWS_RUN_SCRIPT_TIMEOUT'];
18
+ const isolate = new ivm.Isolate({ memoryLimit: isolateSizeMb });
19
+ const context = isolate.createContextSync();
20
+ const jail = context.global;
21
+ jail.setSync('global', jail.derefInto());
22
+ jail.setSync('process', { env: allowedEnv }, { copy: true });
23
+ jail.setSync('module', { exports: null }, { copy: true });
24
+ jail.setSync('console', {
25
+ log: new ivm.Callback((...args) => logger.info(unpackArgs(args)), { sync: true }),
26
+ info: new ivm.Callback((...args) => logger.info(unpackArgs(args)), { sync: true }),
27
+ warn: new ivm.Callback((...args) => logger.warn(unpackArgs(args)), { sync: true }),
28
+ error: new ivm.Callback((...args) => logger.error(unpackArgs(args)), { sync: true }),
29
+ trace: new ivm.Callback((...args) => logger.trace(unpackArgs(args)), { sync: true }),
30
+ debug: new ivm.Callback((...args) => logger.debug(unpackArgs(args)), { sync: true }),
31
+ }, { copy: true });
32
+ // Run the operation once to define the module.exports function
33
+ await context.eval(code, { timeout: scriptTimeoutMs });
34
+ const inputData = new ivm.ExternalCopy({ data });
35
+ const resultRef = await context.evalClosure(`return module.exports($0.data)`, [inputData.copyInto()], {
36
+ result: { reference: true, promise: true },
37
+ timeout: scriptTimeoutMs,
38
+ });
39
+ const result = await resultRef.copy();
40
+ // Memory cleanup
41
+ resultRef.release();
42
+ inputData.release();
43
+ context.release();
44
+ isolate.dispose();
45
+ return result;
37
46
  },
38
47
  });
@@ -0,0 +1,10 @@
1
+ import jwt from 'jsonwebtoken';
2
+ type Options = {
3
+ operation: string;
4
+ payload?: Record<string, any> | string;
5
+ token?: string;
6
+ secret?: jwt.Secret;
7
+ options?: any;
8
+ };
9
+ declare const _default: import("@directus/types").OperationApiConfig<Options>;
10
+ export default _default;
@@ -0,0 +1,35 @@
1
+ import { defineOperationApi, optionToObject, optionToString } from '@directus/utils';
2
+ import jwt from 'jsonwebtoken';
3
+ export default defineOperationApi({
4
+ id: 'json-web-token',
5
+ handler: async ({ operation, payload, token, secret, options }) => {
6
+ if (operation === 'sign') {
7
+ if (!payload)
8
+ throw new Error('Undefined JSON Web Token payload');
9
+ if (!secret)
10
+ throw new Error('Undefined JSON Web Token secret');
11
+ const payloadObject = optionToObject(payload);
12
+ const secretString = optionToString(secret);
13
+ const optionsObject = optionToObject(options);
14
+ return jwt.sign(payloadObject, secretString, optionsObject);
15
+ }
16
+ else if (operation === 'verify') {
17
+ if (!token)
18
+ throw new Error('Undefined JSON Web Token token');
19
+ if (!secret)
20
+ throw new Error('Undefined JSON Web Token secret');
21
+ const tokenString = optionToString(token);
22
+ const secretString = optionToString(secret);
23
+ const optionsObject = optionToObject(options);
24
+ return jwt.verify(tokenString, secretString, optionsObject);
25
+ }
26
+ else if (operation === 'decode') {
27
+ if (!token)
28
+ throw new Error('Undefined JSON Web Token token');
29
+ const tokenString = optionToString(token);
30
+ const optionsObject = optionToObject(options);
31
+ return jwt.decode(tokenString, optionsObject);
32
+ }
33
+ throw new Error('Undefined "Operation" for JSON Web Token');
34
+ },
35
+ });
@@ -24,8 +24,8 @@ export default defineOperationApi({
24
24
  knex: database,
25
25
  });
26
26
  const messageString = message ? optionToString(message) : null;
27
- const collectionString = message ? optionToString(collection) : null;
28
- const itemString = message ? optionToString(item) : null;
27
+ const collectionString = collection ? optionToString(collection) : null;
28
+ const itemString = item ? optionToString(item) : null;
29
29
  const payload = toArray(recipient).map((userId) => {
30
30
  return {
31
31
  recipient: userId,
@@ -67,7 +67,7 @@ export class FieldsService {
67
67
  }
68
68
  const columns = (await this.schemaInspector.columnInfo(collection)).map((column) => ({
69
69
  ...column,
70
- default_value: getDefaultValue(column),
70
+ default_value: getDefaultValue(column, fields.find((field) => field.collection === column.table && field.field === column.name)),
71
71
  }));
72
72
  const columnsWithSystem = columns.map((column) => {
73
73
  const field = fields.find((field) => {
@@ -184,7 +184,7 @@ export class FieldsService {
184
184
  const columnWithCastDefaultValue = column
185
185
  ? {
186
186
  ...column,
187
- default_value: getDefaultValue(column),
187
+ default_value: getDefaultValue(column, fieldInfo),
188
188
  }
189
189
  : null;
190
190
  const data = {
@@ -1532,16 +1532,6 @@ export class GraphQLService {
1532
1532
  }),
1533
1533
  }
1534
1534
  : GraphQLBoolean,
1535
- flows: {
1536
- type: new GraphQLObjectType({
1537
- name: 'server_info_flows',
1538
- fields: {
1539
- execAllowedModules: {
1540
- type: new GraphQLList(GraphQLString),
1541
- },
1542
- },
1543
- }),
1544
- },
1545
1535
  websocket: toBoolean(env['WEBSOCKETS_ENABLED'])
1546
1536
  ? {
1547
1537
  type: new GraphQLObjectType({
@@ -60,9 +60,6 @@ export class ServerService {
60
60
  else {
61
61
  info['rateLimitGlobal'] = false;
62
62
  }
63
- info['flows'] = {
64
- execAllowedModules: env['FLOWS_EXEC_ALLOWED_MODULES'] ? toArray(env['FLOWS_EXEC_ALLOWED_MODULES']) : [],
65
- };
66
63
  info['queryLimit'] = {
67
64
  default: env['QUERY_LIMIT_DEFAULT'],
68
65
  max: Number.isFinite(env['QUERY_LIMIT_MAX']) ? env['QUERY_LIMIT_MAX'] : -1,
@@ -186,6 +186,7 @@ class OASSpecsService {
186
186
  },
187
187
  responses: {
188
188
  '200': {
189
+ description: 'Successful request',
189
190
  content: method === 'delete'
190
191
  ? undefined
191
192
  : {
@@ -376,7 +377,7 @@ class OASSpecsService {
376
377
  {
377
378
  type: 'string',
378
379
  },
379
- relatedTags.map((tag) => ({
380
+ ...relatedTags.map((tag) => ({
380
381
  $ref: `#/components/schemas/${tag.name}`,
381
382
  })),
382
383
  ],
@@ -1,3 +1,6 @@
1
1
  import type { SchemaOverview } from '@directus/schema/types/overview';
2
2
  import type { Column } from '@directus/schema';
3
- export default function getDefaultValue(column: SchemaOverview[string]['columns'][string] | Column): string | boolean | number | Record<string, any> | any[] | null;
3
+ import type { FieldMeta } from '@directus/types';
4
+ export default function getDefaultValue(column: SchemaOverview[string]['columns'][string] | Column, field?: {
5
+ special?: FieldMeta['special'];
6
+ }): string | boolean | number | Record<string, any> | any[] | null;
@@ -2,8 +2,8 @@ import { parseJSON } from '@directus/utils';
2
2
  import env from '../env.js';
3
3
  import logger from '../logger.js';
4
4
  import getLocalType from './get-local-type.js';
5
- export default function getDefaultValue(column) {
6
- const type = getLocalType(column);
5
+ export default function getDefaultValue(column, field) {
6
+ const type = getLocalType(column, field);
7
7
  const defaultValue = column.default_value ?? null;
8
8
  if (defaultValue === null)
9
9
  return null;
@@ -0,0 +1,23 @@
1
+ import type { UnknownObject } from '@directus/types';
2
+ type Keys = string[][];
3
+ type Values = Record<string, any>;
4
+ type Replacement = (key?: string) => string;
5
+ /**
6
+ * Redact values in an object.
7
+ *
8
+ * @param input Input object in which values should be redacted.
9
+ * @param redact The key paths at which and values itself which should be redacted.
10
+ * @param redact.keys Nested array of key paths at which values should be redacted. (Supports `*` for shallow matching, `**` for deep matching.)
11
+ * @param redact.values Value names and the corresponding values that should be redacted.
12
+ * @param replacement Replacement function with which the values are redacted.
13
+ * @returns Redacted object.
14
+ */
15
+ export declare function redactObject(input: UnknownObject, redact: {
16
+ keys?: Keys;
17
+ values?: Values;
18
+ }, replacement: Replacement): UnknownObject;
19
+ /**
20
+ * Replace values and extract Error objects for use with JSON.stringify()
21
+ */
22
+ export declare function getReplacer(replacement: Replacement, values?: Values): (_key: string, value: unknown) => unknown;
23
+ export {};
@@ -1,26 +1,32 @@
1
1
  import { isObject } from '@directus/utils';
2
2
  /**
3
- * Redact values at certain paths in an object.
3
+ * Redact values in an object.
4
+ *
4
5
  * @param input Input object in which values should be redacted.
5
- * @param paths Nested array of object paths to be redacted (supports `*` for shallow matching, `**` for deep matching).
6
- * @param replacement Replacement the values are redacted by.
6
+ * @param redact The key paths at which and values itself which should be redacted.
7
+ * @param redact.keys Nested array of key paths at which values should be redacted. (Supports `*` for shallow matching, `**` for deep matching.)
8
+ * @param redact.values Value names and the corresponding values that should be redacted.
9
+ * @param replacement Replacement function with which the values are redacted.
7
10
  * @returns Redacted object.
8
11
  */
9
- export function redact(input, paths, replacement) {
12
+ export function redactObject(input, redact, replacement) {
10
13
  const wildcardChars = ['*', '**'];
11
- const clone = JSON.parse(JSON.stringify(input, errorReplacer));
14
+ const clone = JSON.parse(JSON.stringify(input, getReplacer(replacement, redact.values)));
12
15
  const visited = new WeakSet();
13
- traverse(clone, paths);
16
+ if (redact.keys) {
17
+ traverse(clone, redact.keys);
18
+ }
14
19
  return clone;
15
- function traverse(object, checkPaths) {
16
- if (checkPaths.length === 0) {
20
+ function traverse(object, checkKeyPaths) {
21
+ if (checkKeyPaths.length === 0) {
17
22
  return;
18
23
  }
19
24
  visited.add(object);
25
+ const REDACTED_TEXT = replacement();
20
26
  const globalCheckPaths = [];
21
27
  for (const key of Object.keys(object)) {
22
28
  const localCheckPaths = [];
23
- for (const [index, path] of [...checkPaths].entries()) {
29
+ for (const [index, path] of [...checkKeyPaths].entries()) {
24
30
  const [current, ...remaining] = path;
25
31
  const escapedKey = wildcardChars.includes(key) ? `\\${key}` : key;
26
32
  switch (current) {
@@ -29,17 +35,17 @@ export function redact(input, paths, replacement) {
29
35
  localCheckPaths.push(remaining);
30
36
  }
31
37
  else {
32
- object[key] = replacement;
33
- checkPaths.splice(index, 1);
38
+ object[key] = REDACTED_TEXT;
39
+ checkKeyPaths.splice(index, 1);
34
40
  }
35
41
  break;
36
42
  case '*':
37
43
  if (remaining.length > 0) {
38
44
  globalCheckPaths.push(remaining);
39
- checkPaths.splice(index, 1);
45
+ checkKeyPaths.splice(index, 1);
40
46
  }
41
47
  else {
42
- object[key] = replacement;
48
+ object[key] = REDACTED_TEXT;
43
49
  }
44
50
  break;
45
51
  case '**':
@@ -47,7 +53,7 @@ export function redact(input, paths, replacement) {
47
53
  const [next, ...nextRemaining] = remaining;
48
54
  if (next === escapedKey) {
49
55
  if (nextRemaining.length === 0) {
50
- object[key] = replacement;
56
+ object[key] = REDACTED_TEXT;
51
57
  }
52
58
  else {
53
59
  localCheckPaths.push(nextRemaining);
@@ -61,7 +67,7 @@ export function redact(input, paths, replacement) {
61
67
  }
62
68
  }
63
69
  else {
64
- object[key] = replacement;
70
+ object[key] = REDACTED_TEXT;
65
71
  }
66
72
  break;
67
73
  }
@@ -74,16 +80,29 @@ export function redact(input, paths, replacement) {
74
80
  }
75
81
  }
76
82
  /**
77
- * Extract values from Error objects for use with JSON.stringify()
83
+ * Replace values and extract Error objects for use with JSON.stringify()
78
84
  */
79
- export function errorReplacer(_key, value) {
80
- if (value instanceof Error) {
81
- return {
82
- name: value.name,
83
- message: value.message,
84
- stack: value.stack,
85
- cause: value.cause,
86
- };
87
- }
88
- return value;
85
+ export function getReplacer(replacement, values) {
86
+ const filteredValues = values
87
+ ? Object.entries(values).filter(([_k, v]) => typeof v === 'string' && v.length > 0)
88
+ : [];
89
+ return (_key, value) => {
90
+ if (value instanceof Error) {
91
+ return {
92
+ name: value.name,
93
+ message: value.message,
94
+ stack: value.stack,
95
+ cause: value.cause,
96
+ };
97
+ }
98
+ if (!values || filteredValues.length === 0 || typeof value !== 'string')
99
+ return value;
100
+ let finalValue = value;
101
+ for (const [redactKey, valueToRedact] of filteredValues) {
102
+ if (finalValue.includes(valueToRedact)) {
103
+ finalValue = finalValue.replace(new RegExp(valueToRedact, 'g'), replacement(redactKey));
104
+ }
105
+ }
106
+ return finalValue;
107
+ };
89
108
  }
@@ -146,22 +146,26 @@ function sanitizeDeep(deep, accountability) {
146
146
  parse(deep);
147
147
  return result;
148
148
  function parse(level, path = []) {
149
+ const subQuery = {};
149
150
  const parsedLevel = {};
150
151
  for (const [key, value] of Object.entries(level)) {
151
152
  if (!key)
152
153
  break;
153
154
  if (key.startsWith('_')) {
154
- // Sanitize query only accepts non-underscore-prefixed query options
155
- const parsedSubQuery = sanitizeQuery({ [key.substring(1)]: value }, accountability);
156
- // ...however we want to keep them for the nested structure of deep, otherwise there's no
157
- // way of knowing when to keep nesting and when to stop
158
- const [parsedKey, parsedValue] = Object.entries(parsedSubQuery)[0];
159
- parsedLevel[`_${parsedKey}`] = parsedValue;
155
+ // Collect all sub query parameters without the leading underscore
156
+ subQuery[key.substring(1)] = value;
160
157
  }
161
158
  else if (isPlainObject(value)) {
162
159
  parse(value, [...path, key]);
163
160
  }
164
161
  }
162
+ if (Object.keys(subQuery).length > 0) {
163
+ // Sanitize the entire sub query
164
+ const parsedSubQuery = sanitizeQuery(subQuery, accountability);
165
+ for (const [parsedKey, parsedValue] of Object.entries(parsedSubQuery)) {
166
+ parsedLevel[`_${parsedKey}`] = parsedValue;
167
+ }
168
+ }
165
169
  if (Object.keys(parsedLevel).length > 0) {
166
170
  set(result, path, merge({}, get(result, path, {}), parsedLevel));
167
171
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@directus/api",
3
- "version": "12.1.3",
3
+ "version": "13.0.0",
4
4
  "description": "Directus is a real-time API and App dashboard for managing SQL database content",
5
5
  "keywords": [
6
6
  "directus",
@@ -97,11 +97,12 @@
97
97
  "icc": "3.0.0",
98
98
  "inquirer": "9.2.4",
99
99
  "ioredis": "5.3.2",
100
+ "isolated-vm": "4.6.0",
100
101
  "joi": "17.9.2",
101
102
  "js-yaml": "4.1.0",
102
103
  "js2xmlparser": "5.0.0",
103
104
  "json2csv": "5.0.7",
104
- "jsonwebtoken": "9.0.0",
105
+ "jsonwebtoken": "9.0.1",
105
106
  "keyv": "4.5.2",
106
107
  "knex": "2.4.2",
107
108
  "ldapjs": "2.3.3",
@@ -138,27 +139,26 @@
138
139
  "tsx": "3.12.7",
139
140
  "uuid": "9.0.0",
140
141
  "uuid-validate": "0.0.3",
141
- "vm2": "3.9.19",
142
142
  "wellknown": "0.5.0",
143
143
  "ws": "8.12.1",
144
144
  "zod": "3.21.4",
145
145
  "zod-validation-error": "1.0.1",
146
- "@directus/app": "10.6.2",
147
- "@directus/constants": "10.2.2",
146
+ "@directus/app": "10.7.0",
147
+ "@directus/constants": "10.2.3",
148
148
  "@directus/errors": "0.0.2",
149
- "@directus/extensions-sdk": "10.1.8",
150
- "@directus/pressure": "1.0.7",
149
+ "@directus/extensions-sdk": "10.1.9",
150
+ "@directus/pressure": "1.0.8",
151
151
  "@directus/schema": "10.0.2",
152
- "@directus/specs": "10.1.1",
152
+ "@directus/specs": "10.2.0",
153
153
  "@directus/storage": "10.0.5",
154
- "@directus/storage-driver-azure": "10.0.8",
155
- "@directus/storage-driver-cloudinary": "10.0.8",
156
- "@directus/storage-driver-gcs": "10.0.8",
157
- "@directus/storage-driver-local": "10.0.8",
158
- "@directus/storage-driver-s3": "10.0.8",
159
- "@directus/storage-driver-supabase": "1.0.0",
160
- "@directus/utils": "10.0.8",
161
- "@directus/validation": "0.0.3"
154
+ "@directus/storage-driver-azure": "10.0.9",
155
+ "@directus/storage-driver-cloudinary": "10.0.9",
156
+ "@directus/storage-driver-gcs": "10.0.9",
157
+ "@directus/storage-driver-local": "10.0.9",
158
+ "@directus/storage-driver-s3": "10.0.9",
159
+ "@directus/storage-driver-supabase": "1.0.1",
160
+ "@directus/utils": "10.0.9",
161
+ "@directus/validation": "0.0.4"
162
162
  },
163
163
  "devDependencies": {
164
164
  "@ngneat/falso": "6.4.0",
@@ -207,7 +207,7 @@
207
207
  "vitest": "0.31.1",
208
208
  "@directus/random": "0.2.2",
209
209
  "@directus/tsconfig": "1.0.0",
210
- "@directus/types": "10.1.4"
210
+ "@directus/types": "10.1.5"
211
211
  },
212
212
  "optionalDependencies": {
213
213
  "@keyv/redis": "2.5.8",
@@ -1,15 +0,0 @@
1
- import type { UnknownObject } from '@directus/types';
2
- type Paths = string[][];
3
- /**
4
- * Redact values at certain paths in an object.
5
- * @param input Input object in which values should be redacted.
6
- * @param paths Nested array of object paths to be redacted (supports `*` for shallow matching, `**` for deep matching).
7
- * @param replacement Replacement the values are redacted by.
8
- * @returns Redacted object.
9
- */
10
- export declare function redact(input: UnknownObject, paths: Paths, replacement: string): UnknownObject;
11
- /**
12
- * Extract values from Error objects for use with JSON.stringify()
13
- */
14
- export declare function errorReplacer(_key: string, value: unknown): unknown;
15
- export {};