@directus/api 11.1.0 → 12.0.1

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 (246) hide show
  1. package/dist/app.js +3 -4
  2. package/dist/auth/auth.d.ts +4 -4
  3. package/dist/auth/auth.js +2 -2
  4. package/dist/auth/drivers/ldap.js +20 -17
  5. package/dist/auth/drivers/local.js +5 -5
  6. package/dist/auth/drivers/oauth2.js +16 -16
  7. package/dist/auth/drivers/openid.js +18 -17
  8. package/dist/auth/drivers/saml.js +6 -7
  9. package/dist/auth.js +3 -2
  10. package/dist/cache.js +3 -13
  11. package/dist/cli/utils/create-env/env-stub.liquid +5 -7
  12. package/dist/controllers/activity.js +7 -6
  13. package/dist/controllers/assets.js +25 -12
  14. package/dist/controllers/auth.js +8 -7
  15. package/dist/controllers/collections.js +4 -3
  16. package/dist/controllers/dashboards.js +5 -4
  17. package/dist/controllers/extensions.js +3 -3
  18. package/dist/controllers/fields.js +9 -8
  19. package/dist/controllers/files.js +11 -11
  20. package/dist/controllers/flows.js +5 -4
  21. package/dist/controllers/folders.js +5 -4
  22. package/dist/controllers/items.js +14 -13
  23. package/dist/controllers/not-found.js +2 -2
  24. package/dist/controllers/notifications.js +5 -4
  25. package/dist/controllers/operations.js +5 -4
  26. package/dist/controllers/panels.js +5 -4
  27. package/dist/controllers/permissions.js +5 -4
  28. package/dist/controllers/presets.js +5 -4
  29. package/dist/controllers/relations.js +6 -5
  30. package/dist/controllers/roles.js +5 -4
  31. package/dist/controllers/schema.js +8 -8
  32. package/dist/controllers/server.js +2 -2
  33. package/dist/controllers/settings.js +3 -2
  34. package/dist/controllers/shares.js +7 -6
  35. package/dist/controllers/translations.js +6 -5
  36. package/dist/controllers/users.js +22 -21
  37. package/dist/controllers/utils.js +10 -10
  38. package/dist/controllers/webhooks.js +5 -4
  39. package/dist/{exceptions/database → database/errors}/dialects/mssql.js +8 -18
  40. package/dist/{exceptions/database → database/errors}/dialects/mysql.js +9 -19
  41. package/dist/{exceptions/database → database/errors}/dialects/oracle.js +2 -2
  42. package/dist/{exceptions/database → database/errors}/dialects/postgres.js +7 -18
  43. package/dist/{exceptions/database → database/errors}/dialects/sqlite.js +7 -10
  44. package/dist/{exceptions/database → database/errors}/translate.js +1 -1
  45. package/dist/database/migrations/run.js +10 -1
  46. package/dist/env.js +6 -13
  47. package/dist/errors/codes.d.ts +29 -0
  48. package/dist/errors/codes.js +30 -0
  49. package/dist/errors/contains-null-values.d.ts +7 -0
  50. package/dist/errors/contains-null-values.js +4 -0
  51. package/dist/errors/content-too-large.d.ts +1 -0
  52. package/dist/errors/content-too-large.js +3 -0
  53. package/dist/errors/forbidden.d.ts +1 -0
  54. package/dist/errors/forbidden.js +3 -0
  55. package/dist/errors/hit-rate-limit.d.ts +6 -0
  56. package/dist/errors/hit-rate-limit.js +8 -0
  57. package/dist/errors/illegal-asset-transformation.d.ts +4 -0
  58. package/dist/errors/illegal-asset-transformation.js +3 -0
  59. package/dist/errors/index.d.ts +28 -0
  60. package/dist/errors/index.js +28 -0
  61. package/dist/errors/invalid-credentials.d.ts +1 -0
  62. package/dist/errors/invalid-credentials.js +3 -0
  63. package/dist/errors/invalid-foreign-key.d.ts +6 -0
  64. package/dist/errors/invalid-foreign-key.js +14 -0
  65. package/dist/errors/invalid-ip.d.ts +1 -0
  66. package/dist/errors/invalid-ip.js +3 -0
  67. package/dist/errors/invalid-otp.d.ts +1 -0
  68. package/dist/errors/invalid-otp.js +3 -0
  69. package/dist/errors/invalid-payload.d.ts +5 -0
  70. package/dist/errors/invalid-payload.js +4 -0
  71. package/dist/errors/invalid-provider-config.d.ts +5 -0
  72. package/dist/errors/invalid-provider-config.js +3 -0
  73. package/dist/errors/invalid-provider.d.ts +1 -0
  74. package/dist/errors/invalid-provider.js +3 -0
  75. package/dist/errors/invalid-query.d.ts +5 -0
  76. package/dist/errors/invalid-query.js +4 -0
  77. package/dist/errors/invalid-token.d.ts +1 -0
  78. package/dist/errors/invalid-token.js +3 -0
  79. package/dist/errors/method-not-allowed.d.ts +6 -0
  80. package/dist/errors/method-not-allowed.js +6 -0
  81. package/dist/errors/not-null-violation.d.ts +6 -0
  82. package/dist/errors/not-null-violation.js +14 -0
  83. package/dist/errors/range-not-satisfiable.d.ts +7 -0
  84. package/dist/errors/range-not-satisfiable.js +7 -0
  85. package/dist/errors/record-not-unique.d.ts +6 -0
  86. package/dist/errors/record-not-unique.js +14 -0
  87. package/dist/errors/route-not-found.d.ts +5 -0
  88. package/dist/errors/route-not-found.js +4 -0
  89. package/dist/errors/service-unavailable.d.ts +7 -0
  90. package/dist/errors/service-unavailable.js +4 -0
  91. package/dist/errors/token-expired.d.ts +1 -0
  92. package/dist/errors/token-expired.js +3 -0
  93. package/dist/errors/unexpected-response.d.ts +1 -0
  94. package/dist/errors/unexpected-response.js +3 -0
  95. package/dist/errors/unprocessable-content.d.ts +5 -0
  96. package/dist/errors/unprocessable-content.js +4 -0
  97. package/dist/errors/unsupported-media-type.d.ts +6 -0
  98. package/dist/errors/unsupported-media-type.js +4 -0
  99. package/dist/errors/user-suspended.d.ts +1 -0
  100. package/dist/errors/user-suspended.js +3 -0
  101. package/dist/errors/value-out-of-range.d.ts +6 -0
  102. package/dist/errors/value-out-of-range.js +14 -0
  103. package/dist/errors/value-too-long.d.ts +6 -0
  104. package/dist/errors/value-too-long.js +14 -0
  105. package/dist/extensions.js +0 -4
  106. package/dist/flows.js +6 -8
  107. package/dist/index.d.ts +0 -2
  108. package/dist/index.js +0 -2
  109. package/dist/messenger.js +4 -4
  110. package/dist/middleware/authenticate.js +1 -1
  111. package/dist/middleware/check-ip.js +2 -2
  112. package/dist/middleware/collection-exists.js +2 -2
  113. package/dist/middleware/error-handler.js +7 -7
  114. package/dist/middleware/graphql.js +11 -9
  115. package/dist/middleware/rate-limiter-global.d.ts +2 -2
  116. package/dist/middleware/rate-limiter-global.js +2 -3
  117. package/dist/middleware/rate-limiter-ip.d.ts +2 -2
  118. package/dist/middleware/rate-limiter-ip.js +2 -3
  119. package/dist/middleware/validate-batch.js +3 -4
  120. package/dist/rate-limiter.js +2 -9
  121. package/dist/services/activity.js +3 -2
  122. package/dist/services/assets.js +9 -10
  123. package/dist/services/authentication.js +12 -11
  124. package/dist/services/authorization.d.ts +1 -1
  125. package/dist/services/authorization.js +16 -16
  126. package/dist/services/collections.js +34 -18
  127. package/dist/services/fields.js +24 -16
  128. package/dist/services/files.js +7 -6
  129. package/dist/services/graphql/errors/execution.d.ts +6 -0
  130. package/dist/services/graphql/errors/execution.js +2 -0
  131. package/dist/services/graphql/errors/index.d.ts +2 -0
  132. package/dist/services/graphql/errors/index.js +2 -0
  133. package/dist/services/graphql/errors/validation.d.ts +6 -0
  134. package/dist/services/graphql/errors/validation.js +2 -0
  135. package/dist/services/graphql/index.d.ts +2 -2
  136. package/dist/services/graphql/index.js +30 -12
  137. package/dist/services/graphql/utils/process-error.js +3 -3
  138. package/dist/services/import-export.js +7 -7
  139. package/dist/services/index.d.ts +1 -0
  140. package/dist/services/index.js +1 -0
  141. package/dist/services/items.js +14 -13
  142. package/dist/services/mail/index.js +3 -3
  143. package/dist/services/meta.js +3 -4
  144. package/dist/services/payload.js +11 -7
  145. package/dist/services/relations.js +32 -22
  146. package/dist/services/revisions.js +3 -3
  147. package/dist/services/roles.js +10 -9
  148. package/dist/services/schema.js +5 -5
  149. package/dist/services/shares.js +4 -4
  150. package/dist/services/tfa.js +6 -6
  151. package/dist/services/translations.d.ts +2 -2
  152. package/dist/services/translations.js +4 -4
  153. package/dist/services/users.js +26 -29
  154. package/dist/services/utils.js +4 -4
  155. package/dist/synchronization.js +3 -3
  156. package/dist/types/items.d.ts +2 -2
  157. package/dist/utils/apply-diff.js +2 -2
  158. package/dist/utils/apply-query.js +17 -7
  159. package/dist/utils/get-accountability-for-role.js +1 -2
  160. package/dist/utils/get-accountability-for-token.js +3 -3
  161. package/dist/utils/get-column-path.js +5 -3
  162. package/dist/utils/get-column.js +3 -3
  163. package/dist/utils/get-service.d.ts +1 -1
  164. package/dist/utils/get-service.js +1 -1
  165. package/dist/utils/jwt.js +5 -5
  166. package/dist/utils/validate-diff.js +23 -9
  167. package/dist/utils/validate-keys.js +3 -3
  168. package/dist/utils/validate-query.d.ts +2 -0
  169. package/dist/utils/validate-query.js +27 -21
  170. package/dist/utils/validate-snapshot.js +11 -5
  171. package/dist/websocket/authenticate.js +12 -15
  172. package/dist/websocket/controllers/base.js +18 -15
  173. package/dist/websocket/controllers/graphql.js +2 -2
  174. package/dist/websocket/controllers/index.js +3 -7
  175. package/dist/websocket/controllers/rest.js +3 -3
  176. package/dist/websocket/{exceptions.d.ts → errors.d.ts} +5 -5
  177. package/dist/websocket/{exceptions.js → errors.js} +10 -10
  178. package/dist/websocket/handlers/items.js +5 -5
  179. package/dist/websocket/handlers/subscribe.js +8 -8
  180. package/package.json +15 -15
  181. package/dist/exceptions/content-too-large.d.ts +0 -4
  182. package/dist/exceptions/content-too-large.js +0 -6
  183. package/dist/exceptions/database/contains-null-values.d.ts +0 -9
  184. package/dist/exceptions/database/contains-null-values.js +0 -6
  185. package/dist/exceptions/database/invalid-foreign-key.d.ts +0 -10
  186. package/dist/exceptions/database/invalid-foreign-key.js +0 -11
  187. package/dist/exceptions/database/not-null-violation.d.ts +0 -9
  188. package/dist/exceptions/database/not-null-violation.js +0 -6
  189. package/dist/exceptions/database/record-not-unique.d.ts +0 -10
  190. package/dist/exceptions/database/record-not-unique.js +0 -11
  191. package/dist/exceptions/database/value-out-of-range.d.ts +0 -10
  192. package/dist/exceptions/database/value-out-of-range.js +0 -11
  193. package/dist/exceptions/database/value-too-long.d.ts +0 -9
  194. package/dist/exceptions/database/value-too-long.js +0 -11
  195. package/dist/exceptions/forbidden.d.ts +0 -6
  196. package/dist/exceptions/forbidden.js +0 -13
  197. package/dist/exceptions/graphql-validation.d.ts +0 -4
  198. package/dist/exceptions/graphql-validation.js +0 -6
  199. package/dist/exceptions/hit-rate-limit.d.ts +0 -9
  200. package/dist/exceptions/hit-rate-limit.js +0 -6
  201. package/dist/exceptions/illegal-asset-transformation.d.ts +0 -4
  202. package/dist/exceptions/illegal-asset-transformation.js +0 -6
  203. package/dist/exceptions/index.d.ts +0 -21
  204. package/dist/exceptions/index.js +0 -21
  205. package/dist/exceptions/invalid-config.d.ts +0 -4
  206. package/dist/exceptions/invalid-config.js +0 -6
  207. package/dist/exceptions/invalid-credentials.d.ts +0 -4
  208. package/dist/exceptions/invalid-credentials.js +0 -6
  209. package/dist/exceptions/invalid-ip.d.ts +0 -4
  210. package/dist/exceptions/invalid-ip.js +0 -6
  211. package/dist/exceptions/invalid-otp.d.ts +0 -4
  212. package/dist/exceptions/invalid-otp.js +0 -6
  213. package/dist/exceptions/invalid-payload.d.ts +0 -4
  214. package/dist/exceptions/invalid-payload.js +0 -6
  215. package/dist/exceptions/invalid-provider.d.ts +0 -4
  216. package/dist/exceptions/invalid-provider.js +0 -6
  217. package/dist/exceptions/invalid-query.d.ts +0 -4
  218. package/dist/exceptions/invalid-query.js +0 -6
  219. package/dist/exceptions/invalid-token.d.ts +0 -4
  220. package/dist/exceptions/invalid-token.js +0 -6
  221. package/dist/exceptions/method-not-allowed.d.ts +0 -8
  222. package/dist/exceptions/method-not-allowed.js +0 -6
  223. package/dist/exceptions/range-not-satisfiable.d.ts +0 -5
  224. package/dist/exceptions/range-not-satisfiable.js +0 -9
  225. package/dist/exceptions/route-not-found.d.ts +0 -4
  226. package/dist/exceptions/route-not-found.js +0 -6
  227. package/dist/exceptions/service-unavailable.d.ts +0 -9
  228. package/dist/exceptions/service-unavailable.js +0 -6
  229. package/dist/exceptions/token-expired.d.ts +0 -4
  230. package/dist/exceptions/token-expired.js +0 -6
  231. package/dist/exceptions/unexpected-response.d.ts +0 -4
  232. package/dist/exceptions/unexpected-response.js +0 -6
  233. package/dist/exceptions/unprocessable-entity.d.ts +0 -4
  234. package/dist/exceptions/unprocessable-entity.js +0 -6
  235. package/dist/exceptions/unsupported-media-type.d.ts +0 -4
  236. package/dist/exceptions/unsupported-media-type.js +0 -6
  237. package/dist/exceptions/user-suspended.d.ts +0 -4
  238. package/dist/exceptions/user-suspended.js +0 -6
  239. /package/dist/{exceptions/database → database/errors}/dialects/mssql.d.ts +0 -0
  240. /package/dist/{exceptions/database → database/errors}/dialects/mysql.d.ts +0 -0
  241. /package/dist/{exceptions/database → database/errors}/dialects/oracle.d.ts +0 -0
  242. /package/dist/{exceptions/database → database/errors}/dialects/postgres.d.ts +0 -0
  243. /package/dist/{exceptions/database → database/errors}/dialects/sqlite.d.ts +0 -0
  244. /package/dist/{exceptions/database → database/errors}/dialects/types.d.ts +0 -0
  245. /package/dist/{exceptions/database → database/errors}/dialects/types.js +0 -0
  246. /package/dist/{exceptions/database → database/errors}/translate.d.ts +0 -0
@@ -1,6 +1,5 @@
1
- import ms from 'ms';
2
1
  import env from '../env.js';
3
- import { HitRateLimitException } from '../exceptions/index.js';
2
+ import { HitRateLimitError } from '../errors/index.js';
4
3
  import logger from '../logger.js';
5
4
  import { createRateLimiter } from '../rate-limiter.js';
6
5
  import asyncHandler from '../utils/async-handler.js';
@@ -20,7 +19,7 @@ if (env['RATE_LIMITER_GLOBAL_ENABLED'] === true) {
20
19
  if (rateLimiterRes instanceof Error)
21
20
  throw rateLimiterRes;
22
21
  res.set('Retry-After', String(Math.round(rateLimiterRes.msBeforeNext / 1000)));
23
- throw new HitRateLimitException(`Too many requests, retry after ${ms(rateLimiterRes.msBeforeNext)}.`, {
22
+ throw new HitRateLimitError({
24
23
  limit: +env['RATE_LIMITER_GLOBAL_POINTS'],
25
24
  reset: new Date(Date.now() + rateLimiterRes.msBeforeNext),
26
25
  });
@@ -1,5 +1,5 @@
1
1
  import type { RequestHandler } from 'express';
2
- import type { RateLimiterMemcache, RateLimiterMemory, RateLimiterRedis } from 'rate-limiter-flexible';
2
+ import type { RateLimiterMemory, RateLimiterRedis } from 'rate-limiter-flexible';
3
3
  declare let checkRateLimit: RequestHandler;
4
- export declare let rateLimiter: RateLimiterRedis | RateLimiterMemcache | RateLimiterMemory;
4
+ export declare let rateLimiter: RateLimiterRedis | RateLimiterMemory;
5
5
  export default checkRateLimit;
@@ -1,6 +1,5 @@
1
- import ms from 'ms';
2
1
  import env from '../env.js';
3
- import { HitRateLimitException } from '../exceptions/index.js';
2
+ import { HitRateLimitError } from '../errors/index.js';
4
3
  import { createRateLimiter } from '../rate-limiter.js';
5
4
  import asyncHandler from '../utils/async-handler.js';
6
5
  import { getIPFromReq } from '../utils/get-ip-from-req.js';
@@ -18,7 +17,7 @@ if (env['RATE_LIMITER_ENABLED'] === true) {
18
17
  if (rateLimiterRes instanceof Error)
19
18
  throw rateLimiterRes;
20
19
  res.set('Retry-After', String(Math.round(rateLimiterRes.msBeforeNext / 1000)));
21
- throw new HitRateLimitException(`Too many requests, retry after ${ms(rateLimiterRes.msBeforeNext)}.`, {
20
+ throw new HitRateLimitError({
22
21
  limit: +env['RATE_LIMITER_POINTS'],
23
22
  reset: new Date(Date.now() + rateLimiterRes.msBeforeNext),
24
23
  });
@@ -1,6 +1,5 @@
1
- import { FailedValidationException } from '@directus/exceptions';
2
1
  import Joi from 'joi';
3
- import { InvalidPayloadException } from '../exceptions/index.js';
2
+ import { InvalidPayloadError } from '../errors/index.js';
4
3
  import asyncHandler from '../utils/async-handler.js';
5
4
  import { sanitizeQuery } from '../utils/sanitize-query.js';
6
5
  export const validateBatch = (scope) => asyncHandler(async (req, _res, next) => {
@@ -12,7 +11,7 @@ export const validateBatch = (scope) => asyncHandler(async (req, _res, next) =>
12
11
  return next();
13
12
  }
14
13
  if (!req.body)
15
- throw new InvalidPayloadException('Payload in body is required');
14
+ throw new InvalidPayloadError({ reason: 'Payload in body is required' });
16
15
  if (['update', 'delete'].includes(scope) && Array.isArray(req.body)) {
17
16
  return next();
18
17
  }
@@ -36,7 +35,7 @@ export const validateBatch = (scope) => asyncHandler(async (req, _res, next) =>
36
35
  }
37
36
  const { error } = batchSchema.validate(req.body);
38
37
  if (error) {
39
- throw new FailedValidationException(error.details[0]);
38
+ throw new InvalidPayloadError({ reason: error.details[0].message });
40
39
  }
41
40
  return next();
42
41
  });
@@ -1,5 +1,5 @@
1
1
  import { merge } from 'lodash-es';
2
- import { RateLimiterMemcache, RateLimiterMemory, RateLimiterRedis } from 'rate-limiter-flexible';
2
+ import { RateLimiterMemory, RateLimiterRedis } from 'rate-limiter-flexible';
3
3
  import env from './env.js';
4
4
  import { getConfigFromEnv } from './utils/get-config-from-env.js';
5
5
  import { createRequire } from 'node:module';
@@ -8,8 +8,6 @@ export function createRateLimiter(configPrefix = 'RATE_LIMITER', configOverrides
8
8
  switch (env['RATE_LIMITER_STORE']) {
9
9
  case 'redis':
10
10
  return new RateLimiterRedis(getConfig('redis', configPrefix, configOverrides));
11
- case 'memcache':
12
- return new RateLimiterMemcache(getConfig('memcache', configPrefix, configOverrides));
13
11
  case 'memory':
14
12
  default:
15
13
  return new RateLimiterMemory(getConfig('memory', configPrefix, configOverrides));
@@ -19,12 +17,7 @@ function getConfig(store = 'memory', configPrefix = 'RATE_LIMITER', overrides) {
19
17
  const config = getConfigFromEnv(`${configPrefix}_`, `${configPrefix}_${store}_`);
20
18
  if (store === 'redis') {
21
19
  const Redis = require('ioredis');
22
- delete config.redis;
23
- config.storeClient = new Redis(env[`${configPrefix}_REDIS`] || getConfigFromEnv(`${configPrefix}_REDIS_`));
24
- }
25
- if (store === 'memcache') {
26
- const Memcached = require('memcached');
27
- config.storeClient = new Memcached(env[`${configPrefix}_MEMCACHE`], getConfigFromEnv(`${configPrefix}_MEMCACHE_`));
20
+ config.storeClient = new Redis(env[`REDIS`] || getConfigFromEnv(`REDIS_`));
28
21
  }
29
22
  delete config.enabled;
30
23
  delete config.store;
@@ -1,8 +1,9 @@
1
1
  import { Action } from '@directus/constants';
2
+ import { isDirectusError } from '@directus/errors';
2
3
  import { uniq } from 'lodash-es';
3
4
  import validateUUID from 'uuid-validate';
4
5
  import env from '../env.js';
5
- import { ForbiddenException } from '../exceptions/forbidden.js';
6
+ import { ErrorCode } from '../errors/index.js';
6
7
  import logger from '../logger.js';
7
8
  import { getPermissions } from '../utils/get-permissions.js';
8
9
  import { Url } from '../utils/url.js';
@@ -80,7 +81,7 @@ ${comment}
80
81
  });
81
82
  }
82
83
  catch (err) {
83
- if (err instanceof ForbiddenException) {
84
+ if (isDirectusError(err, ErrorCode.Forbidden)) {
84
85
  logger.warn(`User ${userID} doesn't have proper permissions to receive notification for this item.`);
85
86
  }
86
87
  else {
@@ -7,10 +7,7 @@ import validateUUID from 'uuid-validate';
7
7
  import { SUPPORTED_IMAGE_TRANSFORM_FORMATS } from '../constants.js';
8
8
  import getDatabase from '../database/index.js';
9
9
  import env from '../env.js';
10
- import { ForbiddenException } from '../exceptions/forbidden.js';
11
- import { IllegalAssetTransformation } from '../exceptions/illegal-asset-transformation.js';
12
- import { RangeNotSatisfiableException } from '../exceptions/range-not-satisfiable.js';
13
- import { ServiceUnavailableException } from '../exceptions/service-unavailable.js';
10
+ import { ForbiddenError, IllegalAssetTransformationError, RangeNotSatisfiableError, ServiceUnavailableError, } from '../errors/index.js';
14
11
  import logger from '../logger.js';
15
12
  import { getStorage } from '../storage/index.js';
16
13
  import { getMilliseconds } from '../utils/get-milliseconds.js';
@@ -39,23 +36,23 @@ export class AssetsService {
39
36
  */
40
37
  const isValidUUID = validateUUID(id, 4);
41
38
  if (isValidUUID === false)
42
- throw new ForbiddenException();
39
+ throw new ForbiddenError();
43
40
  if (systemPublicKeys.includes(id) === false && this.accountability?.admin !== true) {
44
41
  await this.authorizationService.checkAccess('read', 'directus_files', id);
45
42
  }
46
43
  const file = (await this.knex.select('*').from('directus_files').where({ id }).first());
47
44
  if (!file)
48
- throw new ForbiddenException();
45
+ throw new ForbiddenError();
49
46
  const exists = await storage.location(file.storage).exists(file.filename_disk);
50
47
  if (!exists)
51
- throw new ForbiddenException();
48
+ throw new ForbiddenError();
52
49
  if (range) {
53
50
  const missingRangeLimits = range.start === undefined && range.end === undefined;
54
51
  const endBeforeStart = range.start !== undefined && range.end !== undefined && range.end <= range.start;
55
52
  const startOverflow = range.start !== undefined && range.start >= file.filesize;
56
53
  const endUnderflow = range.end !== undefined && range.end <= 0;
57
54
  if (missingRangeLimits || endBeforeStart || startOverflow || endUnderflow) {
58
- throw new RangeNotSatisfiableException(range);
55
+ throw new RangeNotSatisfiableError({ range });
59
56
  }
60
57
  const lastByte = file.filesize - 1;
61
58
  if (range.end) {
@@ -106,12 +103,14 @@ export class AssetsService {
106
103
  !height ||
107
104
  width > env['ASSETS_TRANSFORM_IMAGE_MAX_DIMENSION'] ||
108
105
  height > env['ASSETS_TRANSFORM_IMAGE_MAX_DIMENSION']) {
109
- throw new IllegalAssetTransformation(`Image is too large to be transformed, or image size couldn't be determined.`);
106
+ logger.warn(`Image is too large to be transformed, or image size couldn't be determined.`);
107
+ throw new IllegalAssetTransformationError({ invalidTransformations: ['width', 'height'] });
110
108
  }
111
109
  const { queue, process } = sharp.counters();
112
110
  if (queue + process > env['ASSETS_TRANSFORM_MAX_CONCURRENT']) {
113
- throw new ServiceUnavailableException('Server too busy', {
111
+ throw new ServiceUnavailableError({
114
112
  service: 'files',
113
+ reason: 'Server too busy',
115
114
  });
116
115
  }
117
116
  const readStream = await storage.location(file.storage).read(file.filename_disk, range);
@@ -7,7 +7,8 @@ import { DEFAULT_AUTH_PROVIDER } from '../constants.js';
7
7
  import getDatabase from '../database/index.js';
8
8
  import emitter from '../emitter.js';
9
9
  import env from '../env.js';
10
- import { InvalidCredentialsException, InvalidOTPException, InvalidProviderException, UserSuspendedException, } from '../exceptions/index.js';
10
+ import { InvalidCredentialsError, InvalidProviderError, UserSuspendedError } from '../errors/index.js';
11
+ import { InvalidOtpError } from '../errors/index.js';
11
12
  import { createRateLimiter } from '../rate-limiter.js';
12
13
  import { getMilliseconds } from '../utils/get-milliseconds.js';
13
14
  import { stall } from '../utils/stall.js';
@@ -76,16 +77,16 @@ export class AuthenticationService {
76
77
  emitStatus('fail');
77
78
  if (user?.status === 'suspended') {
78
79
  await stall(STALL_TIME, timeStart);
79
- throw new UserSuspendedException();
80
+ throw new UserSuspendedError();
80
81
  }
81
82
  else {
82
83
  await stall(STALL_TIME, timeStart);
83
- throw new InvalidCredentialsException();
84
+ throw new InvalidCredentialsError();
84
85
  }
85
86
  }
86
87
  else if (user.provider !== providerName) {
87
88
  await stall(STALL_TIME, timeStart);
88
- throw new InvalidProviderException();
89
+ throw new InvalidProviderError();
89
90
  }
90
91
  const settingsService = new SettingsService({
91
92
  knex: this.knex,
@@ -117,7 +118,7 @@ export class AuthenticationService {
117
118
  if (user.tfa_secret && !otp) {
118
119
  emitStatus('fail');
119
120
  await stall(STALL_TIME, timeStart);
120
- throw new InvalidOTPException(`"otp" is required`);
121
+ throw new InvalidOtpError();
121
122
  }
122
123
  if (user.tfa_secret && otp) {
123
124
  const tfaService = new TFAService({ knex: this.knex, schema: this.schema });
@@ -125,7 +126,7 @@ export class AuthenticationService {
125
126
  if (otpValid === false) {
126
127
  emitStatus('fail');
127
128
  await stall(STALL_TIME, timeStart);
128
- throw new InvalidOTPException(`"otp" is invalid`);
129
+ throw new InvalidOtpError();
129
130
  }
130
131
  }
131
132
  const tokenPayload = {
@@ -188,7 +189,7 @@ export class AuthenticationService {
188
189
  const STALL_TIME = env['LOGIN_STALL_TIME'];
189
190
  const timeStart = performance.now();
190
191
  if (!refreshToken) {
191
- throw new InvalidCredentialsException();
192
+ throw new InvalidCredentialsError();
192
193
  }
193
194
  const record = await this.knex
194
195
  .select({
@@ -230,17 +231,17 @@ export class AuthenticationService {
230
231
  })
231
232
  .first();
232
233
  if (!record || (!record.share_id && !record.user_id)) {
233
- throw new InvalidCredentialsException();
234
+ throw new InvalidCredentialsError();
234
235
  }
235
236
  if (record.user_id && record.user_status !== 'active') {
236
237
  await this.knex('directus_sessions').where({ token: refreshToken }).del();
237
238
  if (record.user_status === 'suspended') {
238
239
  await stall(STALL_TIME, timeStart);
239
- throw new UserSuspendedException();
240
+ throw new UserSuspendedError();
240
241
  }
241
242
  else {
242
243
  await stall(STALL_TIME, timeStart);
243
- throw new InvalidCredentialsException();
244
+ throw new InvalidCredentialsError();
244
245
  }
245
246
  }
246
247
  if (record.user_id) {
@@ -330,7 +331,7 @@ export class AuthenticationService {
330
331
  .where('id', userID)
331
332
  .first();
332
333
  if (!user) {
333
- throw new InvalidCredentialsException();
334
+ throw new InvalidCredentialsError();
334
335
  }
335
336
  const provider = getAuthProvider(user.provider);
336
337
  await provider.verify(clone(user), password);
@@ -1,6 +1,6 @@
1
1
  import type { Accountability, PermissionsAction, SchemaOverview } from '@directus/types';
2
2
  import type { Knex } from 'knex';
3
- import type { AbstractServiceOptions, AST, Item, PrimaryKey } from '../types/index.js';
3
+ import type { AST, AbstractServiceOptions, Item, PrimaryKey } from '../types/index.js';
4
4
  import { PayloadService } from './payload.js';
5
5
  export declare class AuthorizationService {
6
6
  knex: Knex;
@@ -1,9 +1,9 @@
1
- import { FailedValidationException } from '@directus/exceptions';
2
1
  import { validatePayload } from '@directus/utils';
2
+ import { FailedValidationError, joiValidationErrorItemToErrorExtensions } from '@directus/validation';
3
3
  import { cloneDeep, flatten, isArray, isNil, merge, reduce, uniq, uniqWith } from 'lodash-es';
4
4
  import { GENERATE_SPECIAL } from '../constants.js';
5
5
  import getDatabase from '../database/index.js';
6
- import { ForbiddenException } from '../exceptions/index.js';
6
+ import { ForbiddenError } from '../errors/forbidden.js';
7
7
  import { getRelationInfo } from '../utils/get-relation-info.js';
8
8
  import { stripFunction } from '../utils/strip-function.js';
9
9
  import { ItemsService } from './items.js';
@@ -31,7 +31,7 @@ export class AuthorizationService {
31
31
  // If the permissions don't match the collections, you don't have permission to read all of them
32
32
  const uniqueCollectionsRequestedCount = uniq(collectionsRequested.map(({ collection }) => collection)).length;
33
33
  if (uniqueCollectionsRequestedCount !== permissionsForCollections.length) {
34
- throw new ForbiddenException();
34
+ throw new ForbiddenError();
35
35
  }
36
36
  validateFields(ast);
37
37
  validateFilterPermissions(ast, this.schema, action, this.accountability);
@@ -94,7 +94,7 @@ export class AuthorizationService {
94
94
  if (column === '*')
95
95
  continue;
96
96
  if (allowedFields.includes(column) === false)
97
- throw new ForbiddenException();
97
+ throw new ForbiddenError();
98
98
  }
99
99
  }
100
100
  }
@@ -107,7 +107,7 @@ export class AuthorizationService {
107
107
  continue;
108
108
  const fieldKey = stripFunction(childNode.name);
109
109
  if (allowedFields.includes(fieldKey) === false) {
110
- throw new ForbiddenException();
110
+ throw new ForbiddenError();
111
111
  }
112
112
  }
113
113
  }
@@ -189,7 +189,7 @@ export class AuthorizationService {
189
189
  });
190
190
  // Filter key not found in parent collection
191
191
  if (!relation)
192
- throw new ForbiddenException();
192
+ throw new ForbiddenError();
193
193
  const relatedCollectionName = relation.related_collection === parentCollection ? relation.collection : relation.related_collection;
194
194
  // Add the `item` field to the required permissions
195
195
  (result[relatedCollectionName] || (result[relatedCollectionName] = new Set())).add(field);
@@ -211,7 +211,7 @@ export class AuthorizationService {
211
211
  });
212
212
  // Filter key not found in parent collection
213
213
  if (!relation)
214
- throw new ForbiddenException();
214
+ throw new ForbiddenError();
215
215
  parentCollection =
216
216
  relation.related_collection === parentCollection ? relation.collection : relation.related_collection;
217
217
  (result[parentCollection] || (result[parentCollection] = new Set())).add(filterKey);
@@ -262,14 +262,14 @@ export class AuthorizationService {
262
262
  if (action !== 'read' && collection === rootCollection) {
263
263
  const actionPermission = accountability?.permissions?.find((permission) => permission.collection === collection && permission.action === action);
264
264
  if (!actionPermission || !actionPermission.fields) {
265
- throw new ForbiddenException();
265
+ throw new ForbiddenError();
266
266
  }
267
267
  allowedFields = permission?.fields
268
268
  ? [...permission.fields, schema.collections[collection].primary]
269
269
  : [schema.collections[collection].primary];
270
270
  }
271
271
  else if (!permission || !permission.fields) {
272
- throw new ForbiddenException();
272
+ throw new ForbiddenError();
273
273
  }
274
274
  else {
275
275
  allowedFields = permission.fields;
@@ -288,7 +288,7 @@ export class AuthorizationService {
288
288
  originalFieldName = aliasMap[fieldName];
289
289
  }
290
290
  if (!allowedFields.includes(originalFieldName)) {
291
- throw new ForbiddenException();
291
+ throw new ForbiddenError();
292
292
  }
293
293
  }
294
294
  }
@@ -356,14 +356,14 @@ export class AuthorizationService {
356
356
  return permission.collection === collection && permission.action === action;
357
357
  });
358
358
  if (!permission)
359
- throw new ForbiddenException();
359
+ throw new ForbiddenError();
360
360
  // Check if you have permission to access the fields you're trying to access
361
361
  const allowedFields = permission.fields || [];
362
362
  if (allowedFields.includes('*') === false) {
363
363
  const keysInData = Object.keys(payload);
364
364
  const invalidKeys = keysInData.filter((fieldKey) => allowedFields.includes(fieldKey) === false);
365
365
  if (invalidKeys.length > 0) {
366
- throw new ForbiddenException();
366
+ throw new ForbiddenError();
367
367
  }
368
368
  }
369
369
  }
@@ -412,7 +412,7 @@ export class AuthorizationService {
412
412
  }
413
413
  }
414
414
  const validationErrors = [];
415
- validationErrors.push(...flatten(validatePayload(permission.validation, payloadWithPresets).map((error) => error.details.map((details) => new FailedValidationException(details)))));
415
+ validationErrors.push(...flatten(validatePayload(permission.validation, payloadWithPresets).map((error) => error.details.map((details) => new FailedValidationError(joiValidationErrorItemToErrorExtensions(details))))));
416
416
  if (validationErrors.length > 0)
417
417
  throw validationErrors;
418
418
  return payloadWithPresets;
@@ -431,14 +431,14 @@ export class AuthorizationService {
431
431
  if (Array.isArray(pk)) {
432
432
  const result = await itemsService.readMany(pk, { ...query, limit: pk.length }, { permissionsAction: action });
433
433
  if (!result)
434
- throw new ForbiddenException();
434
+ throw new ForbiddenError();
435
435
  if (result.length !== pk.length)
436
- throw new ForbiddenException();
436
+ throw new ForbiddenError();
437
437
  }
438
438
  else {
439
439
  const result = await itemsService.readOne(pk, query, { permissionsAction: action });
440
440
  if (!result)
441
- throw new ForbiddenException();
441
+ throw new ForbiddenError();
442
442
  }
443
443
  }
444
444
  }
@@ -1,6 +1,6 @@
1
1
  import { createInspector } from '@directus/schema';
2
2
  import { addFieldFlag } from '@directus/utils';
3
- import { chunk, omit } from 'lodash-es';
3
+ import { chunk, groupBy, merge, omit } from 'lodash-es';
4
4
  import { clearSystemCache, getCache } from '../cache.js';
5
5
  import { ALIAS_TYPES } from '../constants.js';
6
6
  import { getHelpers } from '../database/helpers/index.js';
@@ -8,7 +8,7 @@ import getDatabase, { getSchemaInspector } from '../database/index.js';
8
8
  import { systemCollectionRows } from '../database/system-data/collections/index.js';
9
9
  import emitter from '../emitter.js';
10
10
  import env from '../env.js';
11
- import { ForbiddenException, InvalidPayloadException } from '../exceptions/index.js';
11
+ import { ForbiddenError, InvalidPayloadError } from '../errors/index.js';
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';
@@ -36,12 +36,12 @@ export class CollectionsService {
36
36
  */
37
37
  async createOne(payload, opts) {
38
38
  if (this.accountability && this.accountability.admin !== true) {
39
- throw new ForbiddenException();
39
+ throw new ForbiddenError();
40
40
  }
41
41
  if (!payload.collection)
42
- throw new InvalidPayloadException(`"collection" is required`);
42
+ throw new InvalidPayloadError({ reason: `"collection" is required` });
43
43
  if (payload.collection.startsWith('directus_')) {
44
- throw new InvalidPayloadException(`Collections can't start with "directus_"`);
44
+ throw new InvalidPayloadError({ reason: `Collections can't start with "directus_"` });
45
45
  }
46
46
  const nestedActionEvents = [];
47
47
  try {
@@ -51,7 +51,7 @@ export class CollectionsService {
51
51
  ...Object.keys(this.schema.collections),
52
52
  ];
53
53
  if (existingCollections.includes(payload.collection)) {
54
- throw new InvalidPayloadException(`Collection "${payload.collection}" already exists.`);
54
+ throw new InvalidPayloadError({ reason: `Collection "${payload.collection}" already exists` });
55
55
  }
56
56
  // Create the collection/fields in a transaction so it'll be reverted in case of errors or
57
57
  // permission problems. This might not work reliably in MySQL, as it doesn't support DDL in
@@ -108,7 +108,22 @@ export class CollectionsService {
108
108
  schema: this.schema,
109
109
  });
110
110
  const fieldPayloads = payload.fields.filter((field) => field.meta).map((field) => field.meta);
111
- await fieldItemsService.createMany(fieldPayloads, {
111
+ // Sort new fields that does not have any group defined, in ascending order.
112
+ // Lodash merge is used so that the "sort" can be overridden if defined.
113
+ let sortedFieldPayloads = fieldPayloads
114
+ .filter((field) => field?.group === undefined || field?.group === null)
115
+ .map((field, index) => merge({ sort: index + 1 }, field));
116
+ // Sort remaining new fields with group defined, if any, in ascending order.
117
+ // sortedFieldPayloads will be less than fieldPayloads if it filtered out any fields with group defined.
118
+ if (sortedFieldPayloads.length < fieldPayloads.length) {
119
+ const fieldsWithGroups = groupBy(fieldPayloads.filter((field) => field?.group), (field) => field?.group);
120
+ // The sort order is restarted from 1 for fields in each group and appended to sortedFieldPayloads.
121
+ // Lodash merge is used so that the "sort" can be overridden if defined.
122
+ for (const [_group, fields] of Object.entries(fieldsWithGroups)) {
123
+ sortedFieldPayloads = sortedFieldPayloads.concat(fields.map((field, index) => merge({ sort: index + 1 }, field)));
124
+ }
125
+ }
126
+ await fieldItemsService.createMany(sortedFieldPayloads, {
112
127
  bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params),
113
128
  bypassLimits: true,
114
129
  });
@@ -255,7 +270,7 @@ export class CollectionsService {
255
270
  async readOne(collectionKey) {
256
271
  const result = await this.readMany([collectionKey]);
257
272
  if (result.length === 0)
258
- throw new ForbiddenException();
273
+ throw new ForbiddenError();
259
274
  return result[0];
260
275
  }
261
276
  /**
@@ -270,7 +285,7 @@ export class CollectionsService {
270
285
  const collectionsYouHavePermissionToRead = permissions.map(({ collection }) => collection);
271
286
  for (const collectionKey of collectionKeys) {
272
287
  if (collectionsYouHavePermissionToRead.includes(collectionKey) === false) {
273
- throw new ForbiddenException();
288
+ throw new ForbiddenError();
274
289
  }
275
290
  }
276
291
  }
@@ -283,7 +298,7 @@ export class CollectionsService {
283
298
  */
284
299
  async updateOne(collectionKey, data, opts) {
285
300
  if (this.accountability && this.accountability.admin !== true) {
286
- throw new ForbiddenException();
301
+ throw new ForbiddenError();
287
302
  }
288
303
  const nestedActionEvents = [];
289
304
  try {
@@ -336,10 +351,10 @@ export class CollectionsService {
336
351
  */
337
352
  async updateBatch(data, opts) {
338
353
  if (this.accountability && this.accountability.admin !== true) {
339
- throw new ForbiddenException();
354
+ throw new ForbiddenError();
340
355
  }
341
356
  if (!Array.isArray(data)) {
342
- throw new InvalidPayloadException('Input should be an array of collection changes.');
357
+ throw new InvalidPayloadError({ reason: 'Input should be an array of collection changes' });
343
358
  }
344
359
  const collectionKey = 'collection';
345
360
  const collectionKeys = [];
@@ -352,8 +367,9 @@ export class CollectionsService {
352
367
  schema: this.schema,
353
368
  });
354
369
  for (const payload of data) {
355
- if (!payload[collectionKey])
356
- throw new InvalidPayloadException(`Collection in update misses collection key.`);
370
+ if (!payload[collectionKey]) {
371
+ throw new InvalidPayloadError({ reason: `Collection in update misses collection key` });
372
+ }
357
373
  await collectionItemsService.updateOne(payload[collectionKey], omit(payload, collectionKey), {
358
374
  autoPurgeCache: false,
359
375
  autoPurgeSystemCache: false,
@@ -385,7 +401,7 @@ export class CollectionsService {
385
401
  */
386
402
  async updateMany(collectionKeys, data, opts) {
387
403
  if (this.accountability && this.accountability.admin !== true) {
388
- throw new ForbiddenException();
404
+ throw new ForbiddenError();
389
405
  }
390
406
  const nestedActionEvents = [];
391
407
  try {
@@ -427,14 +443,14 @@ export class CollectionsService {
427
443
  */
428
444
  async deleteOne(collectionKey, opts) {
429
445
  if (this.accountability && this.accountability.admin !== true) {
430
- throw new ForbiddenException();
446
+ throw new ForbiddenError();
431
447
  }
432
448
  const nestedActionEvents = [];
433
449
  try {
434
450
  const collections = await this.readByQuery();
435
451
  const collectionToBeDeleted = collections.find((collection) => collection.collection === collectionKey);
436
452
  if (!!collectionToBeDeleted === false) {
437
- throw new ForbiddenException();
453
+ throw new ForbiddenError();
438
454
  }
439
455
  await this.knex.transaction(async (trx) => {
440
456
  if (collectionToBeDeleted.schema) {
@@ -531,7 +547,7 @@ export class CollectionsService {
531
547
  */
532
548
  async deleteMany(collectionKeys, opts) {
533
549
  if (this.accountability && this.accountability.admin !== true) {
534
- throw new ForbiddenException();
550
+ throw new ForbiddenError();
535
551
  }
536
552
  const nestedActionEvents = [];
537
553
  try {