@directus/api 20.1.0 → 21.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 (97) hide show
  1. package/dist/app.js +2 -2
  2. package/dist/auth/drivers/ldap.js +1 -1
  3. package/dist/auth/drivers/oauth2.js +1 -1
  4. package/dist/auth/drivers/openid.js +1 -1
  5. package/dist/auth/drivers/saml.js +1 -1
  6. package/dist/auth.js +1 -1
  7. package/dist/cache.d.ts +0 -1
  8. package/dist/cache.js +8 -23
  9. package/dist/cli/commands/bootstrap/index.js +1 -1
  10. package/dist/cli/commands/count/index.js +1 -1
  11. package/dist/cli/commands/database/install.js +1 -1
  12. package/dist/cli/commands/database/migrate.js +1 -1
  13. package/dist/cli/commands/roles/create.js +1 -1
  14. package/dist/cli/commands/schema/apply.js +1 -1
  15. package/dist/cli/commands/schema/snapshot.js +1 -1
  16. package/dist/cli/commands/users/create.js +1 -1
  17. package/dist/cli/commands/users/passwd.js +1 -1
  18. package/dist/cli/load-extensions.js +1 -1
  19. package/dist/constants.js +2 -2
  20. package/dist/controllers/assets.js +1 -1
  21. package/dist/controllers/auth.js +1 -1
  22. package/dist/controllers/files.js +1 -1
  23. package/dist/controllers/schema.js +1 -1
  24. package/dist/controllers/tus.js +5 -3
  25. package/dist/database/index.js +11 -7
  26. package/dist/database/migrations/20210518A-add-foreign-key-constraints.js +1 -1
  27. package/dist/database/migrations/20210519A-add-system-fk-triggers.js +1 -1
  28. package/dist/database/migrations/20210802A-replace-groups.js +1 -1
  29. package/dist/database/migrations/20230721A-require-shares-fields.js +1 -1
  30. package/dist/database/migrations/20240305A-change-useragent-type.js +1 -1
  31. package/dist/database/migrations/20240716A-update-files-date-fields.d.ts +3 -0
  32. package/dist/database/migrations/20240716A-update-files-date-fields.js +33 -0
  33. package/dist/database/migrations/run.js +1 -1
  34. package/dist/emitter.js +1 -1
  35. package/dist/extensions/lib/get-shared-deps-mapping.js +1 -1
  36. package/dist/extensions/lib/installation/manager.js +1 -1
  37. package/dist/extensions/lib/sandbox/register/call-reference.js +1 -1
  38. package/dist/extensions/lib/sandbox/sdk/generators/log.js +1 -1
  39. package/dist/extensions/lib/sync-extensions.js +1 -1
  40. package/dist/extensions/manager.js +1 -1
  41. package/dist/flows.js +1 -1
  42. package/dist/{logger.js → logger/index.js} +3 -9
  43. package/dist/logger/redact-query.d.ts +1 -0
  44. package/dist/logger/redact-query.js +13 -0
  45. package/dist/mailer.js +1 -1
  46. package/dist/middleware/cache.js +1 -1
  47. package/dist/middleware/check-ip.js +1 -1
  48. package/dist/middleware/error-handler.d.ts +2 -2
  49. package/dist/middleware/error-handler.js +55 -52
  50. package/dist/middleware/rate-limiter-global.js +1 -1
  51. package/dist/middleware/respond.js +1 -1
  52. package/dist/operations/log/index.js +1 -1
  53. package/dist/operations/mail/index.js +1 -1
  54. package/dist/request/is-denied-ip.js +1 -1
  55. package/dist/server.js +1 -1
  56. package/dist/services/activity.js +1 -1
  57. package/dist/services/assets.js +3 -6
  58. package/dist/services/fields.d.ts +3 -0
  59. package/dist/services/fields.js +29 -5
  60. package/dist/services/files/lib/get-sharp-instance.d.ts +2 -0
  61. package/dist/services/files/lib/get-sharp-instance.js +10 -0
  62. package/dist/services/files/utils/get-metadata.js +8 -7
  63. package/dist/services/files.js +6 -1
  64. package/dist/services/graphql/utils/process-error.js +1 -1
  65. package/dist/services/graphql/utils/sanitize-gql-schema.js +1 -1
  66. package/dist/services/import-export.js +1 -1
  67. package/dist/services/mail/index.d.ts +1 -1
  68. package/dist/services/mail/index.js +10 -2
  69. package/dist/services/notifications.js +1 -1
  70. package/dist/services/relations.d.ts +3 -1
  71. package/dist/services/relations.js +27 -5
  72. package/dist/services/server.js +1 -1
  73. package/dist/services/shares.js +1 -1
  74. package/dist/services/tus/data-store.js +5 -6
  75. package/dist/services/tus/server.d.ts +1 -1
  76. package/dist/services/tus/server.js +9 -2
  77. package/dist/services/users.js +1 -1
  78. package/dist/services/webhooks.js +1 -1
  79. package/dist/telemetry/lib/track.js +1 -1
  80. package/dist/utils/apply-diff.js +1 -1
  81. package/dist/utils/apply-query.js +8 -2
  82. package/dist/utils/delete-from-require-cache.js +1 -1
  83. package/dist/utils/get-default-value.js +1 -1
  84. package/dist/utils/get-ip-from-req.js +1 -1
  85. package/dist/utils/get-permissions.js +1 -1
  86. package/dist/utils/get-schema.js +4 -4
  87. package/dist/utils/is-url-allowed.js +1 -1
  88. package/dist/utils/sanitize-query.js +1 -1
  89. package/dist/utils/transaction.js +1 -1
  90. package/dist/utils/validate-env.js +1 -1
  91. package/dist/utils/validate-storage.js +1 -1
  92. package/dist/websocket/controllers/base.js +1 -1
  93. package/dist/websocket/controllers/graphql.js +1 -1
  94. package/dist/websocket/controllers/rest.js +1 -1
  95. package/dist/websocket/errors.js +1 -1
  96. package/package.json +32 -32
  97. /package/dist/{logger.d.ts → logger/index.d.ts} +0 -0
@@ -1,6 +1,6 @@
1
1
  import { InvalidIpError } from '@directus/errors';
2
2
  import getDatabase from '../database/index.js';
3
- import { useLogger } from '../logger.js';
3
+ import { useLogger } from '../logger/index.js';
4
4
  import asyncHandler from '../utils/async-handler.js';
5
5
  import { ipInNetworks } from '../utils/ip-in-networks.js';
6
6
  export const checkIP = asyncHandler(async (req, _res, next) => {
@@ -1,3 +1,3 @@
1
+ /// <reference types="qs" />
1
2
  import type { ErrorRequestHandler } from 'express';
2
- declare const errorHandler: ErrorRequestHandler;
3
- export default errorHandler;
3
+ export declare const errorHandler: (err: any, req: import("express-serve-static-core").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>, res: import("express-serve-static-core").Response<any, Record<string, any>, number>, next: import("express-serve-static-core").NextFunction) => Promise<ReturnType<ErrorRequestHandler>>;
@@ -1,40 +1,38 @@
1
- import { ErrorCode, MethodNotAllowedError, isDirectusError } from '@directus/errors';
2
- import { isObject, toArray } from '@directus/utils';
1
+ import { ErrorCode, InternalServerError, isDirectusError } from '@directus/errors';
2
+ import { isObject } from '@directus/utils';
3
3
  import { getNodeEnv } from '@directus/utils/node';
4
4
  import getDatabase from '../database/index.js';
5
5
  import emitter from '../emitter.js';
6
- import { useLogger } from '../logger.js';
7
- // Note: keep all 4 parameters here. That's how Express recognizes it's the error handler, even if
8
- // we don't use next
9
- const errorHandler = (err, req, res, _next) => {
6
+ import { useLogger } from '../logger/index.js';
7
+ const FALLBACK_ERROR = new InternalServerError();
8
+ export const errorHandler = asyncErrorHandler(async (err, req, res) => {
10
9
  const logger = useLogger();
11
- let payload = {
12
- errors: [],
13
- };
14
- const errors = toArray(err);
10
+ let errors = [];
15
11
  let status = null;
16
- for (const error of errors) {
17
- if (getNodeEnv() === 'development') {
18
- if (isObject(error)) {
19
- error['extensions'] = {
20
- ...(error['extensions'] || {}),
21
- stack: error['stack'],
22
- };
23
- }
12
+ // It can be assumed that at least one error is given
13
+ const receivedErrors = Array.isArray(err) ? err : [err];
14
+ for (const error of receivedErrors) {
15
+ // In dev mode, if available, expose stack trace under error's extensions data
16
+ if (getNodeEnv() === 'development' && error instanceof Error && error.stack) {
17
+ (error.extensions ??= {})['stack'] = error.stack;
24
18
  }
25
19
  if (isDirectusError(error)) {
26
20
  logger.debug(error);
27
- if (!status) {
21
+ if (status === null) {
22
+ // Use current error status as response status
28
23
  status = error.status;
29
24
  }
30
25
  else if (status !== error.status) {
31
- status = 500;
26
+ // Fallback if status has already been set by a preceding error
27
+ // and doesn't match the current one
28
+ status = FALLBACK_ERROR.status;
32
29
  }
33
- payload.errors.push({
30
+ errors.push({
34
31
  message: error.message,
35
32
  extensions: {
36
- code: error.code,
37
33
  ...(error.extensions ?? {}),
34
+ // Expose error code under error's extensions data
35
+ code: error.code,
38
36
  },
39
37
  });
40
38
  if (isDirectusError(error, ErrorCode.MethodNotAllowed)) {
@@ -43,45 +41,50 @@ const errorHandler = (err, req, res, _next) => {
43
41
  }
44
42
  else {
45
43
  logger.error(error);
46
- status = 500;
44
+ status = FALLBACK_ERROR.status;
47
45
  if (req.accountability?.admin === true) {
48
46
  const localError = isObject(error) ? error : {};
49
- const message = localError['message'] ?? typeof error === 'string' ? error : null;
50
- payload = {
51
- errors: [
52
- {
53
- message: message || 'An unexpected error occurred.',
54
- extensions: {
55
- code: 'INTERNAL_SERVER_ERROR',
56
- ...(localError['extensions'] ?? {}),
57
- },
47
+ // Use 'message' prop if available, otherwise if 'error' is a string use that
48
+ const message = (typeof localError['message'] === 'string' ? localError['message'] : null) ??
49
+ (typeof error === 'string' ? error : null);
50
+ errors = [
51
+ {
52
+ message: message || FALLBACK_ERROR.message,
53
+ extensions: {
54
+ code: FALLBACK_ERROR.code,
55
+ ...(localError['extensions'] ?? {}),
58
56
  },
59
- ],
60
- };
57
+ },
58
+ ];
61
59
  }
62
60
  else {
63
- payload = {
64
- errors: [
65
- {
66
- message: 'An unexpected error occurred.',
67
- extensions: {
68
- code: 'INTERNAL_SERVER_ERROR',
69
- },
70
- },
71
- ],
72
- };
61
+ // Don't expose unknown errors to non-admin users
62
+ errors = [{ message: FALLBACK_ERROR.message, extensions: { code: FALLBACK_ERROR.code } }];
73
63
  }
74
64
  }
75
65
  }
76
- res.status(status ?? 500);
77
- emitter
78
- .emitFilter('request.error', payload.errors, {}, {
66
+ res.status(status ?? FALLBACK_ERROR.status);
67
+ const updatedErrors = await emitter.emitFilter('request.error', errors, {}, {
79
68
  database: getDatabase(),
80
69
  schema: req.schema,
81
70
  accountability: req.accountability ?? null,
82
- })
83
- .then((updatedErrors) => {
84
- return res.json({ ...payload, errors: updatedErrors });
85
71
  });
86
- };
87
- export default errorHandler;
72
+ return res.json({ errors: updatedErrors });
73
+ });
74
+ function asyncErrorHandler(fn) {
75
+ return (err, req, res, next) => fn(err, req, res, next).catch((error) => {
76
+ // To be on the safe side and ensure this doesn't lead to an unhandled (potentially crashing) error
77
+ try {
78
+ const logger = useLogger();
79
+ logger.error(error, 'Unexpected error in error handler');
80
+ }
81
+ catch {
82
+ // Ignore
83
+ }
84
+ // Delegate to default error handler to close the connection
85
+ if (res.headersSent)
86
+ return next(err);
87
+ res.status(FALLBACK_ERROR.status);
88
+ return res.json({ errors: [{ message: FALLBACK_ERROR.message, extensions: { code: FALLBACK_ERROR.code } }] });
89
+ });
90
+ }
@@ -1,6 +1,6 @@
1
1
  import { useEnv } from '@directus/env';
2
2
  import { HitRateLimitError } from '@directus/errors';
3
- import { useLogger } from '../logger.js';
3
+ import { useLogger } from '../logger/index.js';
4
4
  import { createRateLimiter } from '../rate-limiter.js';
5
5
  import asyncHandler from '../utils/async-handler.js';
6
6
  import { validateEnv } from '../utils/validate-env.js';
@@ -1,7 +1,7 @@
1
1
  import { useEnv } from '@directus/env';
2
2
  import { parse as parseBytesConfiguration } from 'bytes';
3
3
  import { getCache, setCacheValue } from '../cache.js';
4
- import { useLogger } from '../logger.js';
4
+ import { useLogger } from '../logger/index.js';
5
5
  import { ExportService } from '../services/import-export.js';
6
6
  import asyncHandler from '../utils/async-handler.js';
7
7
  import { getCacheControlHeader } from '../utils/get-cache-headers.js';
@@ -1,6 +1,6 @@
1
1
  import { defineOperationApi } from '@directus/extensions';
2
2
  import { optionToString } from '@directus/utils';
3
- import { useLogger } from '../../logger.js';
3
+ import { useLogger } from '../../logger/index.js';
4
4
  export default defineOperationApi({
5
5
  id: 'log',
6
6
  handler: ({ message }) => {
@@ -1,7 +1,7 @@
1
1
  import { defineOperationApi } from '@directus/extensions';
2
2
  import { MailService } from '../../services/mail/index.js';
3
3
  import { md } from '../../utils/md.js';
4
- import { useLogger } from '../../logger.js';
4
+ import { useLogger } from '../../logger/index.js';
5
5
  const logger = useLogger();
6
6
  export default defineOperationApi({
7
7
  id: 'mail',
@@ -1,6 +1,6 @@
1
1
  import { useEnv } from '@directus/env';
2
2
  import os from 'node:os';
3
- import { useLogger } from '../logger.js';
3
+ import { useLogger } from '../logger/index.js';
4
4
  import { ipInNetworks } from '../utils/ip-in-networks.js';
5
5
  export function isDeniedIp(ip) {
6
6
  const env = useEnv();
package/dist/server.js CHANGED
@@ -10,7 +10,7 @@ import url from 'url';
10
10
  import createApp from './app.js';
11
11
  import getDatabase from './database/index.js';
12
12
  import emitter from './emitter.js';
13
- import { useLogger } from './logger.js';
13
+ import { useLogger } from './logger/index.js';
14
14
  import { getConfigFromEnv } from './utils/get-config-from-env.js';
15
15
  import { getIPFromReq } from './utils/get-ip-from-req.js';
16
16
  import { createSubscriptionController, createWebSocketController, getSubscriptionController, getWebSocketController, } from './websocket/controllers/index.js';
@@ -2,7 +2,7 @@ import { Action } from '@directus/constants';
2
2
  import { useEnv } from '@directus/env';
3
3
  import { ErrorCode, isDirectusError } from '@directus/errors';
4
4
  import { uniq } from 'lodash-es';
5
- import { useLogger } from '../logger.js';
5
+ import { useLogger } from '../logger/index.js';
6
6
  import { getPermissions } from '../utils/get-permissions.js';
7
7
  import { isValidUuid } from '../utils/is-valid-uuid.js';
8
8
  import { Url } from '../utils/url.js';
@@ -7,13 +7,14 @@ import path from 'path';
7
7
  import sharp from 'sharp';
8
8
  import { SUPPORTED_IMAGE_TRANSFORM_FORMATS } from '../constants.js';
9
9
  import getDatabase from '../database/index.js';
10
- import { useLogger } from '../logger.js';
10
+ import { useLogger } from '../logger/index.js';
11
11
  import { getStorage } from '../storage/index.js';
12
12
  import { getMilliseconds } from '../utils/get-milliseconds.js';
13
13
  import { isValidUuid } from '../utils/is-valid-uuid.js';
14
14
  import * as TransformationUtils from '../utils/transformations.js';
15
15
  import { AuthorizationService } from './authorization.js';
16
16
  import { FilesService } from './files.js';
17
+ import { getSharpInstance } from './files/lib/get-sharp-instance.js';
17
18
  const env = useEnv();
18
19
  const logger = useLogger();
19
20
  export class AssetsService {
@@ -116,11 +117,7 @@ export class AssetsService {
116
117
  });
117
118
  }
118
119
  const readStream = await storage.location(file.storage).read(file.filename_disk, range);
119
- const transformer = sharp({
120
- limitInputPixels: Math.pow(env['ASSETS_TRANSFORM_IMAGE_MAX_DIMENSION'], 2),
121
- sequentialRead: true,
122
- failOn: env['ASSETS_INVALID_IMAGE_SENSITIVITY_LEVEL'],
123
- });
120
+ const transformer = getSharpInstance();
124
121
  transformer.timeout({
125
122
  seconds: clamp(Math.round(getMilliseconds(env['ASSETS_TRANSFORM_TIMEOUT'], 0) / 1000), 1, 3600),
126
123
  });
@@ -16,8 +16,11 @@ export declare class FieldsService {
16
16
  schema: SchemaOverview;
17
17
  cache: Keyv<any> | null;
18
18
  systemCache: Keyv<any>;
19
+ schemaCache: Keyv<any>;
19
20
  constructor(options: AbstractServiceOptions);
20
21
  private get hasReadAccess();
22
+ columnInfo(collection?: string): Promise<Column[]>;
23
+ columnInfo(collection: string, field: string): Promise<Column>;
21
24
  readAll(collection?: string): Promise<Field[]>;
22
25
  readOne(collection: string, field: string): Promise<Record<string, any>>;
23
26
  createField(collection: string, field: Partial<Field> & {
@@ -3,7 +3,7 @@ import { ForbiddenError, InvalidPayloadError } from '@directus/errors';
3
3
  import { createInspector } from '@directus/schema';
4
4
  import { addFieldFlag, toArray } from '@directus/utils';
5
5
  import { isEqual, isNil, merge } from 'lodash-es';
6
- import { clearSystemCache, getCache } from '../cache.js';
6
+ import { clearSystemCache, getCache, getCacheValue, setCacheValue } from '../cache.js';
7
7
  import { ALIAS_TYPES } from '../constants.js';
8
8
  import { translateDatabaseError } from '../database/errors/translate.js';
9
9
  import { getHelpers } from '../database/helpers/index.js';
@@ -19,7 +19,9 @@ import { transaction } from '../utils/transaction.js';
19
19
  import { ItemsService } from './items.js';
20
20
  import { PayloadService } from './payload.js';
21
21
  import { RelationsService } from './relations.js';
22
+ import { useEnv } from '@directus/env';
22
23
  const systemFieldRows = getSystemFieldRowsWithAuthProviders();
24
+ const env = useEnv();
23
25
  export class FieldsService {
24
26
  knex;
25
27
  helpers;
@@ -30,6 +32,7 @@ export class FieldsService {
30
32
  schema;
31
33
  cache;
32
34
  systemCache;
35
+ schemaCache;
33
36
  constructor(options) {
34
37
  this.knex = options.knex || getDatabase();
35
38
  this.helpers = getHelpers(this.knex);
@@ -38,15 +41,36 @@ export class FieldsService {
38
41
  this.itemsService = new ItemsService('directus_fields', options);
39
42
  this.payloadService = new PayloadService('directus_fields', options);
40
43
  this.schema = options.schema;
41
- const { cache, systemCache } = getCache();
44
+ const { cache, systemCache, localSchemaCache } = getCache();
42
45
  this.cache = cache;
43
46
  this.systemCache = systemCache;
47
+ this.schemaCache = localSchemaCache;
44
48
  }
45
49
  get hasReadAccess() {
46
50
  return !!this.accountability?.permissions?.find((permission) => {
47
51
  return permission.collection === 'directus_fields' && permission.action === 'read';
48
52
  });
49
53
  }
54
+ async columnInfo(collection, field) {
55
+ const schemaCacheIsEnabled = Boolean(env['CACHE_SCHEMA']);
56
+ let columnInfo = null;
57
+ if (schemaCacheIsEnabled) {
58
+ columnInfo = await getCacheValue(this.schemaCache, 'columnInfo');
59
+ }
60
+ if (!columnInfo) {
61
+ columnInfo = await this.schemaInspector.columnInfo();
62
+ if (schemaCacheIsEnabled) {
63
+ setCacheValue(this.schemaCache, 'columnInfo', columnInfo);
64
+ }
65
+ }
66
+ if (collection) {
67
+ columnInfo = columnInfo.filter((column) => column.table === collection);
68
+ }
69
+ if (field) {
70
+ return columnInfo.find((column) => column.name === field);
71
+ }
72
+ return columnInfo;
73
+ }
50
74
  async readAll(collection) {
51
75
  let fields;
52
76
  if (this.accountability && this.accountability.admin !== true && this.hasReadAccess === false) {
@@ -67,7 +91,7 @@ export class FieldsService {
67
91
  fields = (await nonAuthorizedItemsService.readByQuery({ limit: -1 }));
68
92
  fields.push(...systemFieldRows);
69
93
  }
70
- const columns = (await this.schemaInspector.columnInfo(collection)).map((column) => ({
94
+ const columns = (await this.columnInfo(collection)).map((column) => ({
71
95
  ...column,
72
96
  default_value: getDefaultValue(column, fields.find((field) => field.collection === column.table && field.field === column.name)),
73
97
  }));
@@ -175,7 +199,7 @@ export class FieldsService {
175
199
  fieldInfo ||
176
200
  systemFieldRows.find((fieldMeta) => fieldMeta.collection === collection && fieldMeta.field === field);
177
201
  try {
178
- column = await this.schemaInspector.columnInfo(collection, field);
202
+ column = await this.columnInfo(collection, field);
179
203
  }
180
204
  catch {
181
205
  // Do nothing
@@ -330,7 +354,7 @@ export class FieldsService {
330
354
  throw new InvalidPayloadError({ reason: 'Alias type cannot be changed' });
331
355
  }
332
356
  if (hookAdjustedField.schema) {
333
- const existingColumn = await this.schemaInspector.columnInfo(collection, hookAdjustedField.field);
357
+ const existingColumn = await this.columnInfo(collection, hookAdjustedField.field);
334
358
  if (hookAdjustedField.schema?.is_nullable === true && existingColumn.is_primary_key) {
335
359
  throw new InvalidPayloadError({ reason: 'Primary key cannot be null' });
336
360
  }
@@ -0,0 +1,2 @@
1
+ import { type Sharp } from 'sharp';
2
+ export declare function getSharpInstance(): Sharp;
@@ -0,0 +1,10 @@
1
+ import { useEnv } from '@directus/env';
2
+ import sharp, {} from 'sharp';
3
+ export function getSharpInstance() {
4
+ const env = useEnv();
5
+ return sharp({
6
+ limitInputPixels: Math.trunc(Math.pow(env['ASSETS_TRANSFORM_IMAGE_MAX_DIMENSION'], 2)),
7
+ sequentialRead: true,
8
+ failOn: env['ASSETS_INVALID_IMAGE_SENSITIVITY_LEVEL'],
9
+ });
10
+ }
@@ -1,19 +1,20 @@
1
+ import { useEnv } from '@directus/env';
1
2
  import exif, {} from 'exif-reader';
2
3
  import { parse as parseIcc } from 'icc';
3
4
  import { pick } from 'lodash-es';
4
5
  import { pipeline } from 'node:stream/promises';
5
- import sharp from 'sharp';
6
- import { useEnv } from '@directus/env';
7
- import { useLogger } from '../../../logger.js';
6
+ import { useLogger } from '../../../logger/index.js';
7
+ import { getSharpInstance } from '../lib/get-sharp-instance.js';
8
8
  import { parseIptc, parseXmp } from './parse-image-metadata.js';
9
9
  const env = useEnv();
10
10
  const logger = useLogger();
11
11
  export async function getMetadata(stream, allowList = env['FILE_METADATA_ALLOW_LIST']) {
12
- return new Promise((resolve, reject) => {
13
- pipeline(stream, sharp().metadata(async (err, sharpMetadata) => {
12
+ const transformer = getSharpInstance();
13
+ return new Promise((resolve) => {
14
+ pipeline(stream, transformer.metadata(async (err, sharpMetadata) => {
14
15
  if (err) {
15
- reject(err);
16
- return;
16
+ logger.error(err);
17
+ return resolve({});
17
18
  }
18
19
  const metadata = {};
19
20
  if (sharpMetadata.orientation && sharpMetadata.orientation >= 5) {
@@ -11,7 +11,7 @@ import path from 'path';
11
11
  import url from 'url';
12
12
  import { RESUMABLE_UPLOADS } from '../constants.js';
13
13
  import emitter from '../emitter.js';
14
- import { useLogger } from '../logger.js';
14
+ import { useLogger } from '../logger/index.js';
15
15
  import { getAxios } from '../request/index.js';
16
16
  import { getStorage } from '../storage/index.js';
17
17
  import { extractMetadata } from './files/lib/extract-metadata.js';
@@ -57,6 +57,10 @@ export class FilesService extends ItemsService {
57
57
  const fileExtension = path.extname(payload.filename_download) || (payload.type && '.' + extension(payload.type)) || '';
58
58
  // The filename_disk is the FINAL filename on disk
59
59
  payload.filename_disk ||= primaryKey + (fileExtension || '');
60
+ // If the filename_disk extension doesn't match the new mimetype, update it
61
+ if (isReplacement === true && path.extname(payload.filename_disk) !== fileExtension) {
62
+ payload.filename_disk = primaryKey + (fileExtension || '');
63
+ }
60
64
  // Temp filename is used for replacements
61
65
  const tempFilenameDisk = 'temp_' + payload.filename_disk;
62
66
  if (!payload.type) {
@@ -125,6 +129,7 @@ export class FilesService extends ItemsService {
125
129
  const { size } = await storage.location(data.storage).stat(payload.filename_disk);
126
130
  payload.filesize = size;
127
131
  const metadata = await extractMetadata(data.storage, payload);
132
+ payload.uploaded_on = new Date().toISOString();
128
133
  // We do this in a service without accountability. Even if you don't have update permissions to the file,
129
134
  // we still want to be able to set the extracted values from the file on create
130
135
  const sudoService = new ItemsService('directus_files', {
@@ -1,5 +1,5 @@
1
1
  import { isDirectusError } from '@directus/errors';
2
- import { useLogger } from '../../../logger.js';
2
+ import { useLogger } from '../../../logger/index.js';
3
3
  const processError = (accountability, error) => {
4
4
  const logger = useLogger();
5
5
  logger.error(error);
@@ -1,4 +1,4 @@
1
- import { useLogger } from '../../../logger.js';
1
+ import { useLogger } from '../../../logger/index.js';
2
2
  /**
3
3
  * Regex was taken from the spec
4
4
  * https://spec.graphql.org/June2018/#sec-Names
@@ -14,7 +14,7 @@ import Papa from 'papaparse';
14
14
  import StreamArray from 'stream-json/streamers/StreamArray.js';
15
15
  import getDatabase from '../database/index.js';
16
16
  import emitter from '../emitter.js';
17
- import { useLogger } from '../logger.js';
17
+ import { useLogger } from '../logger/index.js';
18
18
  import { getDateFormatted } from '../utils/get-date-formatted.js';
19
19
  import { getService } from '../utils/get-service.js';
20
20
  import { transaction } from '../utils/transaction.js';
@@ -14,7 +14,7 @@ export declare class MailService {
14
14
  knex: Knex;
15
15
  mailer: Transporter;
16
16
  constructor(opts: AbstractServiceOptions);
17
- send<T>(options: EmailOptions): Promise<T>;
17
+ send<T>(options: EmailOptions): Promise<T | null>;
18
18
  private renderTemplate;
19
19
  private getDefaultTemplateData;
20
20
  }
@@ -5,9 +5,10 @@ import { Liquid } from 'liquidjs';
5
5
  import path from 'path';
6
6
  import { fileURLToPath } from 'url';
7
7
  import getDatabase from '../../database/index.js';
8
- import { useLogger } from '../../logger.js';
8
+ import { useLogger } from '../../logger/index.js';
9
9
  import getMailer from '../../mailer.js';
10
10
  import { Url } from '../../utils/url.js';
11
+ import emitter from '../../emitter.js';
11
12
  const env = useEnv();
12
13
  const logger = useLogger();
13
14
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -35,7 +36,14 @@ export class MailService {
35
36
  }
36
37
  }
37
38
  async send(options) {
38
- const { template, ...emailOptions } = options;
39
+ const payload = await emitter.emitFilter(`email.send`, options, {
40
+ database: getDatabase(),
41
+ schema: null,
42
+ accountability: null,
43
+ });
44
+ if (!payload)
45
+ return null;
46
+ const { template, ...emailOptions } = payload;
39
47
  let { html } = options;
40
48
  const defaultTemplateData = await this.getDefaultTemplateData();
41
49
  const from = `${defaultTemplateData.projectName} <${options.from || env['EMAIL_FROM']}>`;
@@ -1,5 +1,5 @@
1
1
  import { useEnv } from '@directus/env';
2
- import { useLogger } from '../logger.js';
2
+ import { useLogger } from '../logger/index.js';
3
3
  import { md } from '../utils/md.js';
4
4
  import { Url } from '../utils/url.js';
5
5
  import { ItemsService } from './items.js';
@@ -1,4 +1,4 @@
1
- import type { SchemaInspector } from '@directus/schema';
1
+ import type { ForeignKey, SchemaInspector } from '@directus/schema';
2
2
  import type { Accountability, Relation, RelationMeta, SchemaOverview } from '@directus/types';
3
3
  import type Keyv from 'keyv';
4
4
  import type { Knex } from 'knex';
@@ -14,8 +14,10 @@ export declare class RelationsService {
14
14
  schema: SchemaOverview;
15
15
  relationsItemService: ItemsService<RelationMeta>;
16
16
  systemCache: Keyv<any>;
17
+ schemaCache: Keyv<any>;
17
18
  helpers: Helpers;
18
19
  constructor(options: AbstractServiceOptions);
20
+ foreignKeys(collection?: string): Promise<ForeignKey[]>;
19
21
  readAll(collection?: string, opts?: QueryOptions): Promise<Relation[]>;
20
22
  readOne(collection: string, field: string): Promise<Relation>;
21
23
  /**
@@ -2,7 +2,7 @@ import { ForbiddenError, InvalidPayloadError } from '@directus/errors';
2
2
  import { createInspector } from '@directus/schema';
3
3
  import { systemRelationRows } from '@directus/system-data';
4
4
  import { toArray } from '@directus/utils';
5
- import { clearSystemCache, getCache } from '../cache.js';
5
+ import { clearSystemCache, getCache, getCacheValue, setCacheValue } from '../cache.js';
6
6
  import { getHelpers } from '../database/helpers/index.js';
7
7
  import getDatabase, { getSchemaInspector } from '../database/index.js';
8
8
  import emitter from '../emitter.js';
@@ -11,6 +11,8 @@ import { getSchema } from '../utils/get-schema.js';
11
11
  import { transaction } from '../utils/transaction.js';
12
12
  import { ItemsService } from './items.js';
13
13
  import { PermissionsService } from './permissions/index.js';
14
+ import { useEnv } from '@directus/env';
15
+ const env = useEnv();
14
16
  export class RelationsService {
15
17
  knex;
16
18
  permissionsService;
@@ -19,6 +21,7 @@ export class RelationsService {
19
21
  schema;
20
22
  relationsItemService;
21
23
  systemCache;
24
+ schemaCache;
22
25
  helpers;
23
26
  constructor(options) {
24
27
  this.knex = options.knex || getDatabase();
@@ -33,9 +36,28 @@ export class RelationsService {
33
36
  // allowed to extract the relations regardless of permissions to directus_relations. This
34
37
  // happens in `filterForbidden` down below
35
38
  });
36
- this.systemCache = getCache().systemCache;
39
+ const cache = getCache();
40
+ this.systemCache = cache.systemCache;
41
+ this.schemaCache = cache.localSchemaCache;
37
42
  this.helpers = getHelpers(this.knex);
38
43
  }
44
+ async foreignKeys(collection) {
45
+ const schemaCacheIsEnabled = Boolean(env['CACHE_SCHEMA']);
46
+ let foreignKeys = null;
47
+ if (schemaCacheIsEnabled) {
48
+ foreignKeys = await getCacheValue(this.schemaCache, 'foreignKeys');
49
+ }
50
+ if (!foreignKeys) {
51
+ foreignKeys = await this.schemaInspector.foreignKeys();
52
+ if (schemaCacheIsEnabled) {
53
+ setCacheValue(this.schemaCache, 'foreignKeys', foreignKeys);
54
+ }
55
+ }
56
+ if (collection) {
57
+ return foreignKeys.filter((row) => row.table === collection);
58
+ }
59
+ return foreignKeys;
60
+ }
39
61
  async readAll(collection, opts) {
40
62
  if (this.accountability && this.accountability.admin !== true && this.hasReadAccess === false) {
41
63
  throw new ForbiddenError();
@@ -58,7 +80,7 @@ export class RelationsService {
58
80
  return true;
59
81
  return metaRow.many_collection === collection;
60
82
  });
61
- const schemaRows = await this.schemaInspector.foreignKeys(collection);
83
+ const schemaRows = await this.foreignKeys(collection);
62
84
  const results = this.stitchRelations(metaRows, schemaRows);
63
85
  return await this.filterForbidden(results);
64
86
  }
@@ -95,7 +117,7 @@ export class RelationsService {
95
117
  ],
96
118
  },
97
119
  });
98
- const schemaRow = (await this.schemaInspector.foreignKeys(collection)).find((foreignKey) => foreignKey.column === field);
120
+ const schemaRow = (await this.foreignKeys(collection)).find((foreignKey) => foreignKey.column === field);
99
121
  const stitched = this.stitchRelations(metaRow, schemaRow ? [schemaRow] : []);
100
122
  const results = await this.filterForbidden(stitched);
101
123
  if (results.length === 0) {
@@ -310,7 +332,7 @@ export class RelationsService {
310
332
  const nestedActionEvents = [];
311
333
  try {
312
334
  await transaction(this.knex, async (trx) => {
313
- const existingConstraints = await this.schemaInspector.foreignKeys();
335
+ const existingConstraints = await this.foreignKeys();
314
336
  const constraintNames = existingConstraints.map((key) => key.constraint_name);
315
337
  if (existingRelation.schema?.constraint_name &&
316
338
  constraintNames.includes(existingRelation.schema.constraint_name)) {
@@ -6,7 +6,7 @@ import { Readable } from 'node:stream';
6
6
  import { performance } from 'perf_hooks';
7
7
  import { getCache } from '../cache.js';
8
8
  import getDatabase, { hasDatabaseConnection } from '../database/index.js';
9
- import { useLogger } from '../logger.js';
9
+ import { useLogger } from '../logger/index.js';
10
10
  import getMailer from '../mailer.js';
11
11
  import { rateLimiterGlobal } from '../middleware/rate-limiter-global.js';
12
12
  import { rateLimiter } from '../middleware/rate-limiter-ip.js';
@@ -2,7 +2,7 @@ import { useEnv } from '@directus/env';
2
2
  import { ForbiddenError, InvalidCredentialsError } from '@directus/errors';
3
3
  import argon2 from 'argon2';
4
4
  import jwt from 'jsonwebtoken';
5
- import { useLogger } from '../logger.js';
5
+ import { useLogger } from '../logger/index.js';
6
6
  import { getMilliseconds } from '../utils/get-milliseconds.js';
7
7
  import { getSecret } from '../utils/get-secret.js';
8
8
  import { md } from '../utils/md.js';