@directus/api 10.1.0 → 11.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 (68) hide show
  1. package/dist/app.js +4 -3
  2. package/dist/auth/drivers/oauth2.js +1 -1
  3. package/dist/auth/drivers/openid.js +1 -1
  4. package/dist/cli/utils/create-env/env-stub.liquid +7 -0
  5. package/dist/constants.d.ts +0 -1
  6. package/dist/constants.js +0 -1
  7. package/dist/controllers/assets.js +6 -10
  8. package/dist/controllers/files.js +19 -1
  9. package/dist/controllers/permissions.js +7 -4
  10. package/dist/controllers/translations.d.ts +2 -0
  11. package/dist/controllers/translations.js +149 -0
  12. package/dist/controllers/users.js +1 -1
  13. package/dist/database/migrations/20230525A-add-preview-settings.d.ts +3 -0
  14. package/dist/database/migrations/20230525A-add-preview-settings.js +10 -0
  15. package/dist/database/migrations/20230526A-migrate-translation-strings.d.ts +3 -0
  16. package/dist/database/migrations/20230526A-migrate-translation-strings.js +54 -0
  17. package/dist/database/run-ast.js +3 -3
  18. package/dist/database/system-data/app-access-permissions/app-access-permissions.yaml +3 -0
  19. package/dist/database/system-data/collections/collections.yaml +23 -0
  20. package/dist/database/system-data/fields/collections.yaml +16 -0
  21. package/dist/database/system-data/fields/settings.yaml +0 -5
  22. package/dist/database/system-data/fields/translations.yaml +27 -0
  23. package/dist/env.js +17 -0
  24. package/dist/exceptions/content-too-large.d.ts +4 -0
  25. package/dist/exceptions/content-too-large.js +6 -0
  26. package/dist/extensions.js +13 -11
  27. package/dist/flows.d.ts +1 -1
  28. package/dist/flows.js +20 -19
  29. package/dist/logger.d.ts +1 -1
  30. package/dist/logger.js +6 -6
  31. package/dist/server.js +0 -11
  32. package/dist/services/assets.d.ts +2 -2
  33. package/dist/services/collections.js +8 -7
  34. package/dist/services/fields.js +7 -5
  35. package/dist/services/files.d.ts +2 -2
  36. package/dist/services/files.js +4 -9
  37. package/dist/services/graphql/index.js +4 -41
  38. package/dist/services/index.d.ts +1 -0
  39. package/dist/services/index.js +1 -0
  40. package/dist/services/items.js +10 -9
  41. package/dist/services/revisions.d.ts +6 -1
  42. package/dist/services/revisions.js +24 -0
  43. package/dist/services/server.js +3 -17
  44. package/dist/services/specifications.d.ts +2 -2
  45. package/dist/services/specifications.js +6 -5
  46. package/dist/services/translations.d.ts +10 -0
  47. package/dist/services/translations.js +36 -0
  48. package/dist/synchronization.d.ts +7 -0
  49. package/dist/synchronization.js +120 -0
  50. package/dist/types/assets.d.ts +6 -1
  51. package/dist/types/events.d.ts +2 -2
  52. package/dist/utils/apply-query.d.ts +9 -2
  53. package/dist/utils/apply-query.js +43 -16
  54. package/dist/utils/md.js +1 -1
  55. package/dist/utils/redact.d.ts +11 -0
  56. package/dist/utils/redact.js +75 -0
  57. package/dist/utils/sanitize-query.js +10 -1
  58. package/dist/utils/schedule.d.ts +5 -0
  59. package/dist/utils/schedule.js +27 -0
  60. package/dist/utils/should-clear-cache.d.ts +10 -0
  61. package/dist/utils/should-clear-cache.js +18 -0
  62. package/dist/utils/should-skip-cache.js +18 -2
  63. package/dist/utils/transformations.d.ts +2 -2
  64. package/dist/utils/transformations.js +27 -10
  65. package/dist/utils/validate-query.js +3 -1
  66. package/package.json +49 -53
  67. package/dist/utils/get-os-info.d.ts +0 -9
  68. package/dist/utils/get-os-info.js +0 -40
package/dist/env.js CHANGED
@@ -25,6 +25,8 @@ const allowedEnvironmentVars = [
25
25
  'GRAPHQL_INTROSPECTION',
26
26
  'MAX_BATCH_MUTATION',
27
27
  'LOGGER_.+',
28
+ 'QUERY_LIMIT_MAX',
29
+ 'QUERY_LIMIT_DEFAULT',
28
30
  'ROBOTS_TXT',
29
31
  // server
30
32
  'SERVER_.+',
@@ -66,6 +68,7 @@ const allowedEnvironmentVars = [
66
68
  'CACHE_TTL',
67
69
  'CACHE_CONTROL_S_MAXAGE',
68
70
  'CACHE_AUTO_PURGE',
71
+ 'CACHE_AUTO_PURGE_IGNORE_LIST',
69
72
  'CACHE_SYSTEM_TTL',
70
73
  'CACHE_SCHEMA',
71
74
  'CACHE_PERMISSIONS',
@@ -100,6 +103,9 @@ const allowedEnvironmentVars = [
100
103
  'STORAGE_.+_HEALTHCHECK_THRESHOLD',
101
104
  // metadata
102
105
  'FILE_METADATA_ALLOW_LIST',
106
+ // files
107
+ 'FILES_MAX_UPLOAD_SIZE',
108
+ 'FILES_CONTENT_TYPE_ALLOW_LIST',
103
109
  // assets
104
110
  'ASSETS_CACHE_TTL',
105
111
  'ASSETS_TRANSFORM_MAX_CONCURRENT',
@@ -155,6 +161,13 @@ const allowedEnvironmentVars = [
155
161
  'MESSENGER_REDIS_HOST',
156
162
  'MESSENGER_REDIS_PORT',
157
163
  'MESSENGER_REDIS_PASSWORD',
164
+ // synchronization
165
+ 'SYNCHRONIZATION_STORE',
166
+ 'SYNCHRONIZATION_NAMESPACE',
167
+ 'SYNCHRONIZATION_REDIS',
168
+ 'SYNCHRONIZATION_REDIS_HOST',
169
+ 'SYNCHRONIZATION_REDIS_PORT',
170
+ 'SYNCHRONIZATION_REDIS_PASSWORD',
158
171
  // emails
159
172
  'EMAIL_FROM',
160
173
  'EMAIL_TRANSPORT',
@@ -196,6 +209,7 @@ const defaults = {
196
209
  PUBLIC_URL: '/',
197
210
  MAX_PAYLOAD_SIZE: '1mb',
198
211
  MAX_RELATIONAL_DEPTH: 10,
212
+ QUERY_LIMIT_DEFAULT: 100,
199
213
  MAX_BATCH_MUTATION: Infinity,
200
214
  ROBOTS_TXT: 'User-agent: *\nDisallow: /',
201
215
  DB_EXCLUDE_TABLES: 'spatial_ref_sys,sysdiagrams',
@@ -230,6 +244,7 @@ const defaults = {
230
244
  CACHE_TTL: '5m',
231
245
  CACHE_NAMESPACE: 'system-cache',
232
246
  CACHE_AUTO_PURGE: false,
247
+ CACHE_AUTO_PURGE_IGNORE_LIST: 'directus_activity,directus_presets',
233
248
  CACHE_CONTROL_S_MAXAGE: '0',
234
249
  CACHE_SCHEMA: true,
235
250
  CACHE_PERMISSIONS: true,
@@ -269,6 +284,7 @@ const defaults = {
269
284
  PRESSURE_LIMITER_MAX_MEMORY_RSS: false,
270
285
  PRESSURE_LIMITER_MAX_MEMORY_HEAP_USED: false,
271
286
  PRESSURE_LIMITER_RETRY_AFTER: false,
287
+ FILES_MIME_TYPE_ALLOW_LIST: '*/*',
272
288
  };
273
289
  // Allows us to force certain environment variable into a type, instead of relying
274
290
  // on the auto-parsed type in processValues. ref #3705
@@ -282,6 +298,7 @@ const typeMap = {
282
298
  DB_PORT: 'number',
283
299
  DB_EXCLUDE_TABLES: 'array',
284
300
  CACHE_SKIP_ALLOWED: 'boolean',
301
+ CACHE_AUTO_PURGE_IGNORE_LIST: 'array',
285
302
  IMPORT_IP_DENY_LIST: 'array',
286
303
  FILE_METADATA_ALLOW_LIST: 'array',
287
304
  GRAPHQL_INTROSPECTION: 'boolean',
@@ -0,0 +1,4 @@
1
+ import { BaseException } from '@directus/exceptions';
2
+ export declare class ContentTooLargeException extends BaseException {
3
+ constructor(message: string);
4
+ }
@@ -0,0 +1,6 @@
1
+ import { BaseException } from '@directus/exceptions';
2
+ export class ContentTooLargeException extends BaseException {
3
+ constructor(message) {
4
+ super(message, 413, 'CONTENT_TOO_LARGE');
5
+ }
6
+ }
@@ -8,7 +8,6 @@ import virtualDefault from '@rollup/plugin-virtual';
8
8
  import chokidar, { FSWatcher } from 'chokidar';
9
9
  import express, { Router } from 'express';
10
10
  import { clone, escapeRegExp } from 'lodash-es';
11
- import { schedule, validate } from 'node-cron';
12
11
  import { readdir } from 'node:fs/promises';
13
12
  import { createRequire } from 'node:module';
14
13
  import { dirname } from 'node:path';
@@ -25,6 +24,7 @@ import * as services from './services/index.js';
25
24
  import getModuleDefault from './utils/get-module-default.js';
26
25
  import { getSchema } from './utils/get-schema.js';
27
26
  import { JobQueue } from './utils/job-queue.js';
27
+ import { scheduleSynchronizedJob, validateCron } from './utils/schedule.js';
28
28
  import { Url } from './utils/url.js';
29
29
  // Workaround for https://github.com/rollup/plugins/issues/1329
30
30
  const virtual = virtualDefault;
@@ -189,7 +189,7 @@ class ExtensionManager {
189
189
  this.isLoaded = true;
190
190
  }
191
191
  async unload() {
192
- this.unregisterApiExtensions();
192
+ await this.unregisterApiExtensions();
193
193
  this.apiEmitter.offAll();
194
194
  if (env['SERVE_APP']) {
195
195
  this.appExtensions = null;
@@ -299,7 +299,7 @@ class ExtensionManager {
299
299
  const hookPath = path.resolve(hook.path, hook.entrypoint);
300
300
  const hookInstance = await import(`./${pathToRelativeUrl(hookPath, __dirname)}?t=${Date.now()}`);
301
301
  const config = getModuleDefault(hookInstance);
302
- this.registerHook(config);
302
+ this.registerHook(config, hook.name);
303
303
  this.apiExtensions.push({ path: hookPath });
304
304
  }
305
305
  catch (error) {
@@ -353,8 +353,8 @@ class ExtensionManager {
353
353
  const bundlePath = path.resolve(bundle.path, bundle.entrypoint.api);
354
354
  const bundleInstances = await import(`./${pathToRelativeUrl(bundlePath, __dirname)}?t=${Date.now()}`);
355
355
  const configs = getModuleDefault(bundleInstances);
356
- for (const { config } of configs.hooks) {
357
- this.registerHook(config);
356
+ for (const { config, name } of configs.hooks) {
357
+ this.registerHook(config, name);
358
358
  }
359
359
  for (const { config, name } of configs.endpoints) {
360
360
  this.registerEndpoint(config, name);
@@ -370,7 +370,8 @@ class ExtensionManager {
370
370
  }
371
371
  }
372
372
  }
373
- registerHook(register) {
373
+ registerHook(register, name) {
374
+ let scheduleIndex = 0;
374
375
  const registerFunctions = {
375
376
  filter: (event, handler) => {
376
377
  emitter.onFilter(event, handler);
@@ -397,8 +398,8 @@ class ExtensionManager {
397
398
  });
398
399
  },
399
400
  schedule: (cron, handler) => {
400
- if (validate(cron)) {
401
- const task = schedule(cron, async () => {
401
+ if (validateCron(cron)) {
402
+ const job = scheduleSynchronizedJob(`${name}:${scheduleIndex}`, cron, async () => {
402
403
  if (this.options.schedule) {
403
404
  try {
404
405
  await handler();
@@ -408,9 +409,10 @@ class ExtensionManager {
408
409
  }
409
410
  }
410
411
  });
412
+ scheduleIndex++;
411
413
  this.hookEvents.push({
412
414
  type: 'schedule',
413
- task,
415
+ job,
414
416
  });
415
417
  }
416
418
  else {
@@ -460,7 +462,7 @@ class ExtensionManager {
460
462
  const flowManager = getFlowManager();
461
463
  flowManager.addOperation(config.id, config.handler);
462
464
  }
463
- unregisterApiExtensions() {
465
+ async unregisterApiExtensions() {
464
466
  for (const event of this.hookEvents) {
465
467
  switch (event.type) {
466
468
  case 'filter':
@@ -473,7 +475,7 @@ class ExtensionManager {
473
475
  emitter.offInit(event.name, event.handler);
474
476
  break;
475
477
  case 'schedule':
476
- event.task.stop();
478
+ await event.job.stop();
477
479
  break;
478
480
  }
479
481
  }
package/dist/flows.d.ts CHANGED
@@ -15,7 +15,7 @@ declare class FlowManager {
15
15
  runOperationFlow(id: string, data: unknown, context: Record<string, unknown>): Promise<unknown>;
16
16
  runWebhookFlow(id: string, data: unknown, context: Record<string, unknown>): Promise<{
17
17
  result: unknown;
18
- cacheEnabled: boolean;
18
+ cacheEnabled?: boolean;
19
19
  }>;
20
20
  private load;
21
21
  private unload;
package/dist/flows.js CHANGED
@@ -1,10 +1,8 @@
1
+ import { Action, REDACTED_TEXT } from '@directus/constants';
1
2
  import * as sharedExceptions from '@directus/exceptions';
2
- import { Action } from '@directus/constants';
3
3
  import { applyOptionsData, isValidJSON, parseJSON, toArray } from '@directus/utils';
4
- import fastRedact from 'fast-redact';
5
4
  import { omit, pick } from 'lodash-es';
6
5
  import { get } from 'micromustache';
7
- import { schedule, validate } from 'node-cron';
8
6
  import getDatabase from './database/index.js';
9
7
  import emitter from './emitter.js';
10
8
  import env from './env.js';
@@ -12,20 +10,17 @@ import * as exceptions from './exceptions/index.js';
12
10
  import logger from './logger.js';
13
11
  import { getMessenger } from './messenger.js';
14
12
  import { ActivityService } from './services/activity.js';
15
- import * as services from './services/index.js';
16
13
  import { FlowsService } from './services/flows.js';
14
+ import * as services from './services/index.js';
17
15
  import { RevisionsService } from './services/revisions.js';
18
16
  import { constructFlowTree } from './utils/construct-flow-tree.js';
19
17
  import { getSchema } from './utils/get-schema.js';
20
18
  import { JobQueue } from './utils/job-queue.js';
21
19
  import { mapValuesDeep } from './utils/map-values-deep.js';
20
+ import { redact } from './utils/redact.js';
22
21
  import { sanitizeError } from './utils/sanitize-error.js';
22
+ import { scheduleSynchronizedJob, validateCron } from './utils/schedule.js';
23
23
  let flowManager;
24
- const redactLogs = fastRedact({
25
- censor: '--redacted--',
26
- paths: ['*.headers.authorization', '*.access_token', '*.headers.cookie'],
27
- serialize: false,
28
- });
29
24
  export function getFlowManager() {
30
25
  if (flowManager) {
31
26
  return flowManager;
@@ -147,8 +142,8 @@ class FlowManager {
147
142
  }
148
143
  }
149
144
  else if (flow.trigger === 'schedule') {
150
- if (validate(flow.options['cron'])) {
151
- const task = schedule(flow.options['cron'], async () => {
145
+ if (validateCron(flow.options['cron'])) {
146
+ const job = scheduleSynchronizedJob(flow.id, flow.options['cron'], async () => {
152
147
  try {
153
148
  await this.executeFlow(flow);
154
149
  }
@@ -156,7 +151,7 @@ class FlowManager {
156
151
  logger.error(error);
157
152
  }
158
153
  });
159
- this.triggerHandlers.push({ id: flow.id, events: [{ type: flow.trigger, task }] });
154
+ this.triggerHandlers.push({ id: flow.id, events: [{ type: flow.trigger, job }] });
160
155
  }
161
156
  else {
162
157
  logger.warn(`Couldn't register cron trigger. Provided cron is invalid: ${flow.options['cron']}`);
@@ -186,7 +181,7 @@ class FlowManager {
186
181
  this.webhookFlowHandlers[`${method}-${flow.id}`] = handler;
187
182
  }
188
183
  else if (flow.trigger === 'manual') {
189
- const handler = (data, context) => {
184
+ const handler = async (data, context) => {
190
185
  const enabledCollections = flow.options?.['collections'] ?? [];
191
186
  const targetCollection = data?.['body'].collection;
192
187
  if (!targetCollection) {
@@ -203,10 +198,10 @@ class FlowManager {
203
198
  }
204
199
  if (flow.options['async']) {
205
200
  this.executeFlow(flow, data, context);
206
- return undefined;
201
+ return { result: undefined };
207
202
  }
208
203
  else {
209
- return this.executeFlow(flow, data, context);
204
+ return { result: await this.executeFlow(flow, data, context) };
210
205
  }
211
206
  };
212
207
  // Default return to $last for manual
@@ -218,7 +213,7 @@ class FlowManager {
218
213
  }
219
214
  async unload() {
220
215
  for (const trigger of this.triggerHandlers) {
221
- trigger.events.forEach((event) => {
216
+ for (const event of trigger.events) {
222
217
  switch (event.type) {
223
218
  case 'filter':
224
219
  emitter.offFilter(event.name, event.handler);
@@ -227,10 +222,10 @@ class FlowManager {
227
222
  emitter.offAction(event.name, event.handler);
228
223
  break;
229
224
  case 'schedule':
230
- event.task.stop();
225
+ await event.job.stop();
231
226
  break;
232
227
  }
233
- });
228
+ }
234
229
  }
235
230
  this.triggerHandlers = [];
236
231
  this.operationFlowHandlers = {};
@@ -283,7 +278,13 @@ class FlowManager {
283
278
  item: flow.id,
284
279
  data: {
285
280
  steps: steps,
286
- data: redactLogs(omit(keyedData, '$accountability.permissions')), // Permissions is a ton of data, and is just a copy of what's in the directus_permissions table
281
+ 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
282
+ [
283
+ ['**', 'headers', 'authorization'],
284
+ ['**', 'headers', 'cookie'],
285
+ ['**', 'query', 'access_token'],
286
+ ['**', 'payload', 'password'],
287
+ ], REDACTED_TEXT),
287
288
  },
288
289
  });
289
290
  }
package/dist/logger.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /// <reference types="qs" />
2
- import type { LoggerOptions } from 'pino';
3
2
  import type { RequestHandler } from 'express';
3
+ import type { LoggerOptions } from 'pino';
4
4
  export declare const httpLoggerOptions: LoggerOptions;
5
5
  declare const logger: import("pino").Logger<LoggerOptions & Record<string, any>>;
6
6
  export declare const expressLogger: RequestHandler<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
package/dist/logger.js CHANGED
@@ -1,23 +1,23 @@
1
+ import { REDACTED_TEXT } from '@directus/constants';
1
2
  import { toArray } from '@directus/utils';
2
3
  import { merge } from 'lodash-es';
3
4
  import { pino } from 'pino';
4
5
  import { pinoHttp, stdSerializers } from 'pino-http';
5
6
  import { URL } from 'url';
6
7
  import env from './env.js';
7
- import { REDACT_TEXT } from './constants.js';
8
8
  import { getConfigFromEnv } from './utils/get-config-from-env.js';
9
9
  const pinoOptions = {
10
10
  level: env['LOG_LEVEL'] || 'info',
11
11
  redact: {
12
12
  paths: ['req.headers.authorization', 'req.headers.cookie'],
13
- censor: REDACT_TEXT,
13
+ censor: REDACTED_TEXT,
14
14
  },
15
15
  };
16
16
  export const httpLoggerOptions = {
17
17
  level: env['LOG_LEVEL'] || 'info',
18
18
  redact: {
19
19
  paths: ['req.headers.authorization', 'req.headers.cookie'],
20
- censor: REDACT_TEXT,
20
+ censor: REDACTED_TEXT,
21
21
  },
22
22
  };
23
23
  if (env['LOG_STYLE'] !== 'raw') {
@@ -48,11 +48,11 @@ if (env['LOG_STYLE'] === 'raw') {
48
48
  const path = pathParts.join('.');
49
49
  if (path === 'res.headers') {
50
50
  if ('set-cookie' in value) {
51
- value['set-cookie'] = REDACT_TEXT;
51
+ value['set-cookie'] = REDACTED_TEXT;
52
52
  }
53
53
  return value;
54
54
  }
55
- return REDACT_TEXT;
55
+ return REDACTED_TEXT;
56
56
  },
57
57
  };
58
58
  }
@@ -99,7 +99,7 @@ export default logger;
99
99
  function redactQuery(originalPath) {
100
100
  const url = new URL(originalPath, 'http://example.com/');
101
101
  if (url.searchParams.has('access_token')) {
102
- url.searchParams.set('access_token', REDACT_TEXT);
102
+ url.searchParams.set('access_token', REDACTED_TEXT);
103
103
  }
104
104
  return url.pathname + url.search;
105
105
  }
package/dist/server.js CHANGED
@@ -1,4 +1,3 @@
1
- import { isUpToDate } from '@directus/update-check';
2
1
  import { createTerminus } from '@godaddy/terminus';
3
2
  import * as http from 'http';
4
3
  import * as https from 'https';
@@ -11,7 +10,6 @@ import emitter from './emitter.js';
11
10
  import env from './env.js';
12
11
  import logger from './logger.js';
13
12
  import { getConfigFromEnv } from './utils/get-config-from-env.js';
14
- import * as pkg from './utils/package.js';
15
13
  export let SERVER_ONLINE = true;
16
14
  export async function createServer() {
17
15
  const server = http.createServer(await createApp());
@@ -107,15 +105,6 @@ export async function startServer() {
107
105
  const port = env['PORT'];
108
106
  server
109
107
  .listen(port, host, () => {
110
- isUpToDate(pkg.name, pkg.version)
111
- .then((update) => {
112
- if (update) {
113
- logger.warn(`Update available: ${pkg.version} -> ${update}`);
114
- }
115
- })
116
- .catch(() => {
117
- // No need to log/warn here. The update message is only an informative nice-to-have
118
- });
119
108
  logger.info(`Server started at http://${host}:${port}`);
120
109
  emitter.emitAction('server.start', { server }, {
121
110
  database: getDatabase(),
@@ -3,14 +3,14 @@ import type { Range, Stat } from '@directus/storage';
3
3
  import type { Accountability } from '@directus/types';
4
4
  import type { Knex } from 'knex';
5
5
  import type { Readable } from 'node:stream';
6
- import type { AbstractServiceOptions, TransformationParams } from '../types/index.js';
6
+ import type { AbstractServiceOptions, TransformationSet } from '../types/index.js';
7
7
  import { AuthorizationService } from './authorization.js';
8
8
  export declare class AssetsService {
9
9
  knex: Knex;
10
10
  accountability: Accountability | null;
11
11
  authorizationService: AuthorizationService;
12
12
  constructor(options: AbstractServiceOptions);
13
- getAsset(id: string, transformation: TransformationParams, range?: Range): Promise<{
13
+ getAsset(id: string, transformation: TransformationSet, range?: Range): Promise<{
14
14
  stream: Readable;
15
15
  file: any;
16
16
  stat: Stat;
@@ -12,6 +12,7 @@ import { ForbiddenException, InvalidPayloadException } from '../exceptions/index
12
12
  import { FieldsService } from '../services/fields.js';
13
13
  import { ItemsService } from '../services/items.js';
14
14
  import { getSchema } from '../utils/get-schema.js';
15
+ import { shouldClearCache } from '../utils/should-clear-cache.js';
15
16
  export class CollectionsService {
16
17
  knex;
17
18
  helpers;
@@ -130,7 +131,7 @@ export class CollectionsService {
130
131
  return payload.collection;
131
132
  }
132
133
  finally {
133
- if (this.cache && env['CACHE_AUTO_PURGE'] && opts?.autoPurgeCache !== false) {
134
+ if (shouldClearCache(this.cache, opts)) {
134
135
  await this.cache.clear();
135
136
  }
136
137
  if (opts?.autoPurgeSystemCache !== false) {
@@ -171,7 +172,7 @@ export class CollectionsService {
171
172
  return collections;
172
173
  }
173
174
  finally {
174
- if (this.cache && env['CACHE_AUTO_PURGE'] && opts?.autoPurgeCache !== false) {
175
+ if (shouldClearCache(this.cache, opts)) {
175
176
  await this.cache.clear();
176
177
  }
177
178
  if (opts?.autoPurgeSystemCache !== false) {
@@ -315,7 +316,7 @@ export class CollectionsService {
315
316
  return collectionKey;
316
317
  }
317
318
  finally {
318
- if (this.cache && env['CACHE_AUTO_PURGE'] && opts?.autoPurgeCache !== false) {
319
+ if (shouldClearCache(this.cache, opts)) {
319
320
  await this.cache.clear();
320
321
  }
321
322
  if (opts?.autoPurgeSystemCache !== false) {
@@ -363,7 +364,7 @@ export class CollectionsService {
363
364
  });
364
365
  }
365
366
  finally {
366
- if (this.cache && env['CACHE_AUTO_PURGE'] && opts?.autoPurgeCache !== false) {
367
+ if (shouldClearCache(this.cache, opts)) {
367
368
  await this.cache.clear();
368
369
  }
369
370
  if (opts?.autoPurgeSystemCache !== false) {
@@ -405,7 +406,7 @@ export class CollectionsService {
405
406
  return collectionKeys;
406
407
  }
407
408
  finally {
408
- if (this.cache && env['CACHE_AUTO_PURGE'] && opts?.autoPurgeCache !== false) {
409
+ if (shouldClearCache(this.cache, opts)) {
409
410
  await this.cache.clear();
410
411
  }
411
412
  if (opts?.autoPurgeSystemCache !== false) {
@@ -510,7 +511,7 @@ export class CollectionsService {
510
511
  return collectionKey;
511
512
  }
512
513
  finally {
513
- if (this.cache && env['CACHE_AUTO_PURGE'] && opts?.autoPurgeCache !== false) {
514
+ if (shouldClearCache(this.cache, opts)) {
514
515
  await this.cache.clear();
515
516
  }
516
517
  if (opts?.autoPurgeSystemCache !== false) {
@@ -551,7 +552,7 @@ export class CollectionsService {
551
552
  return collectionKeys;
552
553
  }
553
554
  finally {
554
- if (this.cache && env['CACHE_AUTO_PURGE'] && opts?.autoPurgeCache !== false) {
555
+ if (shouldClearCache(this.cache, opts)) {
555
556
  await this.cache.clear();
556
557
  }
557
558
  if (opts?.autoPurgeSystemCache !== false) {
@@ -8,7 +8,6 @@ import { getHelpers } from '../database/helpers/index.js';
8
8
  import getDatabase, { getSchemaInspector } from '../database/index.js';
9
9
  import { systemFieldRows } from '../database/system-data/fields/index.js';
10
10
  import emitter from '../emitter.js';
11
- import env from '../env.js';
12
11
  import { translateDatabaseError } from '../exceptions/database/translate.js';
13
12
  import { ForbiddenException, InvalidPayloadException } from '../exceptions/index.js';
14
13
  import { ItemsService } from '../services/items.js';
@@ -17,6 +16,7 @@ import getDefaultValue from '../utils/get-default-value.js';
17
16
  import getLocalType from '../utils/get-local-type.js';
18
17
  import { getSchema } from '../utils/get-schema.js';
19
18
  import { sanitizeColumn } from '../utils/sanitize-schema.js';
19
+ import { shouldClearCache } from '../utils/should-clear-cache.js';
20
20
  import { RelationsService } from './relations.js';
21
21
  export class FieldsService {
22
22
  knex;
@@ -270,7 +270,7 @@ export class FieldsService {
270
270
  if (runPostColumnChange) {
271
271
  await this.helpers.schema.postColumnChange();
272
272
  }
273
- if (this.cache && env['CACHE_AUTO_PURGE'] && opts?.autoPurgeCache !== false) {
273
+ if (shouldClearCache(this.cache, opts)) {
274
274
  await this.cache.clear();
275
275
  }
276
276
  if (opts?.autoPurgeSystemCache !== false) {
@@ -311,7 +311,9 @@ export class FieldsService {
311
311
  }
312
312
  if (hookAdjustedField.schema) {
313
313
  const existingColumn = await this.schemaInspector.columnInfo(collection, hookAdjustedField.field);
314
- if (!isEqual(sanitizeColumn(existingColumn), hookAdjustedField.schema)) {
314
+ // Sanitize column only when applying snapshot diff as opts is only passed from /utils/apply-diff.ts
315
+ const columnToCompare = opts?.bypassLimits && opts.autoPurgeSystemCache === false ? sanitizeColumn(existingColumn) : existingColumn;
316
+ if (!isEqual(columnToCompare, hookAdjustedField.schema)) {
315
317
  try {
316
318
  await this.knex.schema.alterTable(collection, (table) => {
317
319
  if (!hookAdjustedField.schema)
@@ -365,7 +367,7 @@ export class FieldsService {
365
367
  if (runPostColumnChange) {
366
368
  await this.helpers.schema.postColumnChange();
367
369
  }
368
- if (this.cache && env['CACHE_AUTO_PURGE'] && opts?.autoPurgeCache !== false) {
370
+ if (shouldClearCache(this.cache, opts)) {
369
371
  await this.cache.clear();
370
372
  }
371
373
  if (opts?.autoPurgeSystemCache !== false) {
@@ -495,7 +497,7 @@ export class FieldsService {
495
497
  if (runPostColumnChange) {
496
498
  await this.helpers.schema.postColumnChange();
497
499
  }
498
- if (this.cache && env['CACHE_AUTO_PURGE'] && opts?.autoPurgeCache !== false) {
500
+ if (shouldClearCache(this.cache, opts)) {
499
501
  await this.cache.clear();
500
502
  }
501
503
  if (opts?.autoPurgeSystemCache !== false) {
@@ -26,9 +26,9 @@ export declare class FilesService extends ItemsService {
26
26
  /**
27
27
  * Delete a file
28
28
  */
29
- deleteOne(key: PrimaryKey, opts?: MutationOptions): Promise<PrimaryKey>;
29
+ deleteOne(key: PrimaryKey): Promise<PrimaryKey>;
30
30
  /**
31
31
  * Delete multiple files
32
32
  */
33
- deleteMany(keys: PrimaryKey[], opts?: MutationOptions): Promise<PrimaryKey[]>;
33
+ deleteMany(keys: PrimaryKey[]): Promise<PrimaryKey[]>;
34
34
  }
@@ -66,6 +66,7 @@ export class FilesService extends ItemsService {
66
66
  catch (err) {
67
67
  logger.warn(`Couldn't save file ${payload.filename_disk}`);
68
68
  logger.warn(err);
69
+ await this.deleteOne(primaryKey);
69
70
  throw new ServiceUnavailableException(`Couldn't save file ${payload.filename_disk}`, { service: 'files' });
70
71
  }
71
72
  const { size } = await storage.location(data.storage).stat(payload.filename_disk);
@@ -87,9 +88,6 @@ export class FilesService extends ItemsService {
87
88
  schema: this.schema,
88
89
  });
89
90
  await sudoService.updateOne(primaryKey, payload, { emitEvents: false });
90
- if (this.cache && env['CACHE_AUTO_PURGE'] && opts?.autoPurgeCache !== false) {
91
- await this.cache.clear();
92
- }
93
91
  if (opts?.emitEvents !== false) {
94
92
  emitter.emitAction('files.upload', {
95
93
  payload,
@@ -244,14 +242,14 @@ export class FilesService extends ItemsService {
244
242
  /**
245
243
  * Delete a file
246
244
  */
247
- async deleteOne(key, opts) {
248
- await this.deleteMany([key], opts);
245
+ async deleteOne(key) {
246
+ await this.deleteMany([key]);
249
247
  return key;
250
248
  }
251
249
  /**
252
250
  * Delete multiple files
253
251
  */
254
- async deleteMany(keys, opts) {
252
+ async deleteMany(keys) {
255
253
  const storage = await getStorage();
256
254
  const files = await super.readMany(keys, { fields: ['id', 'storage'], limit: -1 });
257
255
  if (!files) {
@@ -265,9 +263,6 @@ export class FilesService extends ItemsService {
265
263
  await disk.delete(filepath);
266
264
  }
267
265
  }
268
- if (this.cache && env['CACHE_AUTO_PURGE'] && opts?.autoPurgeCache !== false) {
269
- await this.cache.clear();
270
- }
271
266
  return keys;
272
267
  }
273
268
  }
@@ -1539,49 +1539,12 @@ export class GraphQLService {
1539
1539
  },
1540
1540
  }),
1541
1541
  },
1542
- });
1543
- }
1544
- if (this.accountability?.admin === true) {
1545
- ServerInfo.addFields({
1546
- directus: {
1547
- type: new GraphQLObjectType({
1548
- name: 'server_info_directus',
1549
- fields: {
1550
- version: {
1551
- type: GraphQLString,
1552
- },
1553
- },
1554
- }),
1555
- },
1556
- node: {
1557
- type: new GraphQLObjectType({
1558
- name: 'server_info_node',
1559
- fields: {
1560
- version: {
1561
- type: GraphQLString,
1562
- },
1563
- uptime: {
1564
- type: GraphQLInt,
1565
- },
1566
- },
1567
- }),
1568
- },
1569
- os: {
1542
+ queryLimit: {
1570
1543
  type: new GraphQLObjectType({
1571
- name: 'server_info_os',
1544
+ name: 'server_info_query_limit',
1572
1545
  fields: {
1573
- type: {
1574
- type: GraphQLString,
1575
- },
1576
- version: {
1577
- type: GraphQLString,
1578
- },
1579
- uptime: {
1580
- type: GraphQLInt,
1581
- },
1582
- totalmem: {
1583
- type: GraphQLInt,
1584
- },
1546
+ default: { type: GraphQLInt },
1547
+ max: { type: GraphQLInt },
1585
1548
  },
1586
1549
  }),
1587
1550
  },
@@ -28,6 +28,7 @@ export * from './settings.js';
28
28
  export * from './shares.js';
29
29
  export * from './specifications.js';
30
30
  export * from './tfa.js';
31
+ export * from './translations.js';
31
32
  export * from './users.js';
32
33
  export * from './utils.js';
33
34
  export * from './webhooks.js';
@@ -28,6 +28,7 @@ export * from './settings.js';
28
28
  export * from './shares.js';
29
29
  export * from './specifications.js';
30
30
  export * from './tfa.js';
31
+ export * from './translations.js';
31
32
  export * from './users.js';
32
33
  export * from './utils.js';
33
34
  export * from './webhooks.js';