@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,15 +1,15 @@
1
1
  import { KNEX_TYPES, REGEX_BETWEEN_PARENS } from '@directus/constants';
2
2
  import { createInspector } from '@directus/schema';
3
3
  import { addFieldFlag, toArray } from '@directus/utils';
4
- import { isEqual, isNil } from 'lodash-es';
4
+ import { isEqual, isNil, merge } from 'lodash-es';
5
5
  import { clearSystemCache, getCache } from '../cache.js';
6
6
  import { ALIAS_TYPES } from '../constants.js';
7
+ import { translateDatabaseError } from '../database/errors/translate.js';
7
8
  import { getHelpers } from '../database/helpers/index.js';
8
9
  import getDatabase, { getSchemaInspector } from '../database/index.js';
9
10
  import { systemFieldRows } from '../database/system-data/fields/index.js';
10
11
  import emitter from '../emitter.js';
11
- import { translateDatabaseError } from '../exceptions/database/translate.js';
12
- import { ForbiddenException, InvalidPayloadException } from '../exceptions/index.js';
12
+ import { ForbiddenError, InvalidPayloadError } from '../errors/index.js';
13
13
  import { ItemsService } from '../services/items.js';
14
14
  import { PayloadService } from '../services/payload.js';
15
15
  import getDefaultValue from '../utils/get-default-value.js';
@@ -48,7 +48,7 @@ export class FieldsService {
48
48
  async readAll(collection) {
49
49
  let fields;
50
50
  if (this.accountability && this.accountability.admin !== true && this.hasReadAccess === false) {
51
- throw new ForbiddenException();
51
+ throw new ForbiddenError();
52
52
  }
53
53
  const nonAuthorizedItemsService = new ItemsService('directus_fields', {
54
54
  knex: this.knex,
@@ -125,7 +125,7 @@ export class FieldsService {
125
125
  allowedFieldsInCollection[permission.collection] = permission.fields ?? [];
126
126
  });
127
127
  if (collection && collection in allowedFieldsInCollection === false) {
128
- throw new ForbiddenException();
128
+ throw new ForbiddenError();
129
129
  }
130
130
  return result.filter((field) => {
131
131
  if (field.collection in allowedFieldsInCollection === false)
@@ -151,17 +151,17 @@ export class FieldsService {
151
151
  async readOne(collection, field) {
152
152
  if (this.accountability && this.accountability.admin !== true) {
153
153
  if (this.hasReadAccess === false) {
154
- throw new ForbiddenException();
154
+ throw new ForbiddenError();
155
155
  }
156
156
  const permissions = this.accountability.permissions.find((permission) => {
157
157
  return permission.action === 'read' && permission.collection === collection;
158
158
  });
159
159
  if (!permissions || !permissions.fields)
160
- throw new ForbiddenException();
160
+ throw new ForbiddenError();
161
161
  if (permissions.fields.includes('*') === false) {
162
162
  const allowedFields = permissions.fields;
163
163
  if (allowedFields.includes(field) === false)
164
- throw new ForbiddenException();
164
+ throw new ForbiddenError();
165
165
  }
166
166
  }
167
167
  let column = undefined;
@@ -179,7 +179,7 @@ export class FieldsService {
179
179
  // Do nothing
180
180
  }
181
181
  if (!column && !fieldInfo)
182
- throw new ForbiddenException();
182
+ throw new ForbiddenError();
183
183
  const type = getLocalType(column, fieldInfo);
184
184
  const columnWithCastDefaultValue = column
185
185
  ? {
@@ -199,7 +199,7 @@ export class FieldsService {
199
199
  async createField(collection, field, table, // allows collection creation to
200
200
  opts) {
201
201
  if (this.accountability && this.accountability.admin !== true) {
202
- throw new ForbiddenException();
202
+ throw new ForbiddenError();
203
203
  }
204
204
  const runPostColumnChange = await this.helpers.schema.preColumnChange();
205
205
  const nestedActionEvents = [];
@@ -208,7 +208,9 @@ export class FieldsService {
208
208
  isNil(await this.knex.select('id').from('directus_fields').where({ collection, field: field.field }).first()) === false;
209
209
  // Check if field already exists, either as a column, or as a row in directus_fields
210
210
  if (exists) {
211
- throw new InvalidPayloadException(`Field "${field.field}" already exists in collection "${collection}"`);
211
+ throw new InvalidPayloadError({
212
+ reason: `Field "${field.field}" already exists in collection "${collection}"`,
213
+ });
212
214
  }
213
215
  // Add flag for specific database type overrides
214
216
  const flagToAdd = this.helpers.date.fieldFlagForField(field.type);
@@ -239,8 +241,14 @@ export class FieldsService {
239
241
  }
240
242
  }
241
243
  if (hookAdjustedField.meta) {
244
+ const existingSortRecord = await trx
245
+ .from('directus_fields')
246
+ .where(hookAdjustedField.meta?.group ? { collection, group: hookAdjustedField.meta.group } : { collection })
247
+ .max('sort', { as: 'max' })
248
+ .first();
249
+ const newSortValue = existingSortRecord?.max ? existingSortRecord.max + 1 : 1;
242
250
  await itemsService.createOne({
243
- ...hookAdjustedField.meta,
251
+ ...merge({ sort: newSortValue }, hookAdjustedField.meta),
244
252
  collection: collection,
245
253
  field: hookAdjustedField.field,
246
254
  }, { emitEvents: false });
@@ -287,7 +295,7 @@ export class FieldsService {
287
295
  }
288
296
  async updateField(collection, field, opts) {
289
297
  if (this.accountability && this.accountability.admin !== true) {
290
- throw new ForbiddenException();
298
+ throw new ForbiddenError();
291
299
  }
292
300
  const runPostColumnChange = await this.helpers.schema.preColumnChange();
293
301
  const nestedActionEvents = [];
@@ -307,7 +315,7 @@ export class FieldsService {
307
315
  (hookAdjustedField.type === 'alias' ||
308
316
  this.schema.collections[collection].fields[field.field]?.type === 'alias') &&
309
317
  hookAdjustedField.type !== (this.schema.collections[collection].fields[field.field]?.type ?? 'alias')) {
310
- throw new InvalidPayloadException('Alias type cannot be changed');
318
+ throw new InvalidPayloadError({ reason: 'Alias type cannot be changed' });
311
319
  }
312
320
  if (hookAdjustedField.schema) {
313
321
  const existingColumn = await this.schemaInspector.columnInfo(collection, hookAdjustedField.field);
@@ -384,7 +392,7 @@ export class FieldsService {
384
392
  }
385
393
  async deleteField(collection, field, opts) {
386
394
  if (this.accountability && this.accountability.admin !== true) {
387
- throw new ForbiddenException();
395
+ throw new ForbiddenError();
388
396
  }
389
397
  const runPostColumnChange = await this.helpers.schema.preColumnChange();
390
398
  const nestedActionEvents = [];
@@ -551,7 +559,7 @@ export class FieldsService {
551
559
  column = table[field.type](field.field);
552
560
  }
553
561
  else {
554
- throw new InvalidPayloadException(`Illegal type passed: "${field.type}"`);
562
+ throw new InvalidPayloadError({ reason: `Illegal type passed: "${field.type}"` });
555
563
  }
556
564
  if (field.schema?.default_value !== undefined) {
557
565
  if (typeof field.schema.default_value === 'string' &&
@@ -12,7 +12,7 @@ import url from 'url';
12
12
  import { SUPPORTED_IMAGE_METADATA_FORMATS } from '../constants.js';
13
13
  import emitter from '../emitter.js';
14
14
  import env from '../env.js';
15
- import { ForbiddenException, InvalidPayloadException, ServiceUnavailableException } from '../exceptions/index.js';
15
+ import { ForbiddenError, InvalidPayloadError, ServiceUnavailableError } from '../errors/index.js';
16
16
  import logger from '../logger.js';
17
17
  import { getAxios } from '../request/index.js';
18
18
  import { getStorage } from '../storage/index.js';
@@ -67,7 +67,7 @@ export class FilesService extends ItemsService {
67
67
  logger.warn(`Couldn't save file ${payload.filename_disk}`);
68
68
  logger.warn(err);
69
69
  await this.deleteOne(primaryKey);
70
- throw new ServiceUnavailableException(`Couldn't save file ${payload.filename_disk}`, { service: 'files' });
70
+ throw new ServiceUnavailableError({ service: 'files', reason: `Couldn't save file ${payload.filename_disk}` });
71
71
  }
72
72
  const { size } = await storage.location(data.storage).stat(payload.filename_disk);
73
73
  payload.filesize = size;
@@ -202,7 +202,7 @@ export class FilesService extends ItemsService {
202
202
  async importOne(importURL, body) {
203
203
  const fileCreatePermissions = this.accountability?.permissions?.find((permission) => permission.collection === 'directus_files' && permission.action === 'create');
204
204
  if (this.accountability && this.accountability?.admin !== true && !fileCreatePermissions) {
205
- throw new ForbiddenException();
205
+ throw new ForbiddenError();
206
206
  }
207
207
  let fileResponse;
208
208
  try {
@@ -213,8 +213,9 @@ export class FilesService extends ItemsService {
213
213
  }
214
214
  catch (err) {
215
215
  logger.warn(err, `Couldn't fetch file from URL "${importURL}"`);
216
- throw new ServiceUnavailableException(`Couldn't fetch file from url "${importURL}"`, {
216
+ throw new ServiceUnavailableError({
217
217
  service: 'external-file',
218
+ reason: `Couldn't fetch file from url "${importURL}"`,
218
219
  });
219
220
  }
220
221
  const parsedURL = url.parse(fileResponse.request.res.responseUrl);
@@ -234,7 +235,7 @@ export class FilesService extends ItemsService {
234
235
  */
235
236
  async createOne(data, opts) {
236
237
  if (!data.type) {
237
- throw new InvalidPayloadException(`"type" is required`);
238
+ throw new InvalidPayloadError({ reason: `"type" is required` });
238
239
  }
239
240
  const key = await super.createOne(data, opts);
240
241
  return key;
@@ -253,7 +254,7 @@ export class FilesService extends ItemsService {
253
254
  const storage = await getStorage();
254
255
  const files = await super.readMany(keys, { fields: ['id', 'storage'], limit: -1 });
255
256
  if (!files) {
256
- throw new ForbiddenException();
257
+ throw new ForbiddenError();
257
258
  }
258
259
  await super.deleteMany(keys);
259
260
  for (const file of files) {
@@ -0,0 +1,6 @@
1
+ import type { GraphQLError } from 'graphql';
2
+ interface GraphQLExecutionErrorExtensions {
3
+ errors: GraphQLError[];
4
+ }
5
+ export declare const GraphQLExecutionError: import("@directus/errors").DirectusErrorConstructor<GraphQLExecutionErrorExtensions>;
6
+ export {};
@@ -0,0 +1,2 @@
1
+ import { createError } from '@directus/errors';
2
+ export const GraphQLExecutionError = createError('GRAPHQL_EXECUTION', 'GraphQL execution error.', 400);
@@ -0,0 +1,2 @@
1
+ export { GraphQLExecutionError } from './execution.js';
2
+ export { GraphQLValidationError } from './validation.js';
@@ -0,0 +1,2 @@
1
+ export { GraphQLExecutionError } from './execution.js';
2
+ export { GraphQLValidationError } from './validation.js';
@@ -0,0 +1,6 @@
1
+ import type { GraphQLError } from 'graphql';
2
+ interface GraphQLValidationErrorExtensions {
3
+ errors: GraphQLError[];
4
+ }
5
+ export declare const GraphQLValidationError: import("@directus/errors").DirectusErrorConstructor<GraphQLValidationErrorExtensions>;
6
+ export {};
@@ -0,0 +1,2 @@
1
+ import { createError } from '@directus/errors';
2
+ export const GraphQLValidationError = createError('GRAPHQL_VALIDATION', 'GraphQL validation error.', 400);
@@ -1,4 +1,4 @@
1
- import type { BaseException } from '@directus/exceptions';
1
+ import type { DirectusError } from '@directus/errors';
2
2
  import type { Accountability, Filter, Query, SchemaOverview } from '@directus/types';
3
3
  import type { ArgumentNode, FormattedExecutionResult, FragmentDefinitionNode, GraphQLResolveInfo, SelectionNode } from 'graphql';
4
4
  import { GraphQLError, GraphQLSchema } from 'graphql';
@@ -61,7 +61,7 @@ export declare class GraphQLService {
61
61
  /**
62
62
  * Convert Directus-Exception into a GraphQL format, so it can be returned by GraphQL properly.
63
63
  */
64
- formatError(error: BaseException | BaseException[]): GraphQLError;
64
+ formatError(error: DirectusError | DirectusError[]): GraphQLError;
65
65
  /**
66
66
  * Replace all fragments in a selectionset for the actual selection set as defined in the fragment
67
67
  * Effectively merges the selections with the fragments used in those selections
@@ -1,4 +1,5 @@
1
1
  import { Action, FUNCTIONS } from '@directus/constants';
2
+ import { isDirectusError } from '@directus/errors';
2
3
  import { parseFilterFunctionPath } from '@directus/utils';
3
4
  import argon2 from 'argon2';
4
5
  import { GraphQLBoolean, GraphQLEnumType, GraphQLError, GraphQLFloat, GraphQLID, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, GraphQLSchema, GraphQLString, GraphQLUnionType, NoSchemaIntrospectionCustomRule, execute, specifiedRules, validate, } from 'graphql';
@@ -8,13 +9,15 @@ import { clearSystemCache, getCache } from '../../cache.js';
8
9
  import { DEFAULT_AUTH_PROVIDER, GENERATE_SPECIAL } from '../../constants.js';
9
10
  import getDatabase from '../../database/index.js';
10
11
  import env from '../../env.js';
11
- import { ForbiddenException, GraphQLValidationException, InvalidPayloadException } from '../../exceptions/index.js';
12
+ import { ErrorCode, ForbiddenError, InvalidPayloadError } from '../../errors/index.js';
12
13
  import { getExtensionManager } from '../../extensions.js';
13
14
  import { generateHash } from '../../utils/generate-hash.js';
14
15
  import { getGraphQLType } from '../../utils/get-graphql-type.js';
15
16
  import { getMilliseconds } from '../../utils/get-milliseconds.js';
17
+ import { getService } from '../../utils/get-service.js';
16
18
  import { reduceSchema } from '../../utils/reduce-schema.js';
17
19
  import { sanitizeQuery } from '../../utils/sanitize-query.js';
20
+ import { toBoolean } from '../../utils/to-boolean.js';
18
21
  import { validateQuery } from '../../utils/validate-query.js';
19
22
  import { ActivityService } from '../activity.js';
20
23
  import { AuthenticationService } from '../authentication.js';
@@ -28,6 +31,8 @@ import { SpecificationService } from '../specifications.js';
28
31
  import { TFAService } from '../tfa.js';
29
32
  import { UsersService } from '../users.js';
30
33
  import { UtilsService } from '../utils.js';
34
+ import { GraphQLExecutionError, GraphQLValidationError } from './errors/index.js';
35
+ import { createSubscriptionGenerator } from './subscription.js';
31
36
  import { GraphQLBigInt } from './types/bigint.js';
32
37
  import { GraphQLDate } from './types/date.js';
33
38
  import { GraphQLGeoJSON } from './types/geojson.js';
@@ -36,9 +41,6 @@ import { GraphQLStringOrFloat } from './types/string-or-float.js';
36
41
  import { GraphQLVoid } from './types/void.js';
37
42
  import { addPathToValidationError } from './utils/add-path-to-validation-error.js';
38
43
  import processError from './utils/process-error.js';
39
- import { createSubscriptionGenerator } from './subscription.js';
40
- import { getService } from '../../utils/get-service.js';
41
- import { toBoolean } from '../../utils/to-boolean.js';
42
44
  const validationRules = Array.from(specifiedRules);
43
45
  if (env['GRAPHQL_INTROSPECTION'] === false) {
44
46
  validationRules.push(NoSchemaIntrospectionCustomRule);
@@ -72,7 +74,7 @@ export class GraphQLService {
72
74
  const schema = this.getSchema();
73
75
  const validationErrors = validate(schema, document, validationRules).map((validationError) => addPathToValidationError(validationError));
74
76
  if (validationErrors.length > 0) {
75
- throw new GraphQLValidationException({ graphqlErrors: validationErrors });
77
+ throw new GraphQLValidationError({ errors: validationErrors });
76
78
  }
77
79
  let result;
78
80
  try {
@@ -85,7 +87,7 @@ export class GraphQLService {
85
87
  });
86
88
  }
87
89
  catch (err) {
88
- throw new InvalidPayloadException('GraphQL execution error.', { graphqlErrors: [err.message] });
90
+ throw new GraphQLExecutionError({ errors: [err.message] });
89
91
  }
90
92
  const formattedResult = {};
91
93
  if (result['data'])
@@ -431,12 +433,24 @@ export class GraphQLService {
431
433
  _nstarts_with: {
432
434
  type: GraphQLString,
433
435
  },
436
+ _istarts_with: {
437
+ type: GraphQLString,
438
+ },
439
+ _nistarts_with: {
440
+ type: GraphQLString,
441
+ },
434
442
  _ends_with: {
435
443
  type: GraphQLString,
436
444
  },
437
445
  _nends_with: {
438
446
  type: GraphQLString,
439
447
  },
448
+ _iends_with: {
449
+ type: GraphQLString,
450
+ },
451
+ _niends_with: {
452
+ type: GraphQLString,
453
+ },
440
454
  _in: {
441
455
  type: new GraphQLList(GraphQLString),
442
456
  },
@@ -1739,7 +1753,9 @@ export class GraphQLService {
1739
1753
  });
1740
1754
  const currentRefreshToken = args['refresh_token'] || req?.cookies[env['REFRESH_TOKEN_COOKIE_NAME']];
1741
1755
  if (!currentRefreshToken) {
1742
- throw new InvalidPayloadException(`"refresh_token" is required in either the JSON payload or Cookie`);
1756
+ throw new InvalidPayloadError({
1757
+ reason: `"refresh_token" is required in either the JSON payload or Cookie`,
1758
+ });
1743
1759
  }
1744
1760
  const result = await authenticationService.refresh(currentRefreshToken);
1745
1761
  if (args['mode'] === 'cookie') {
@@ -1779,7 +1795,9 @@ export class GraphQLService {
1779
1795
  });
1780
1796
  const currentRefreshToken = args['refresh_token'] || req?.cookies[env['REFRESH_TOKEN_COOKIE_NAME']];
1781
1797
  if (!currentRefreshToken) {
1782
- throw new InvalidPayloadException(`"refresh_token" is required in either the JSON payload or Cookie`);
1798
+ throw new InvalidPayloadError({
1799
+ reason: `"refresh_token" is required in either the JSON payload or Cookie`,
1800
+ });
1783
1801
  }
1784
1802
  await authenticationService.logout(currentRefreshToken);
1785
1803
  return true;
@@ -1806,7 +1824,7 @@ export class GraphQLService {
1806
1824
  await service.requestPasswordReset(args['email'], args['reset_url'] || null);
1807
1825
  }
1808
1826
  catch (err) {
1809
- if (err instanceof InvalidPayloadException) {
1827
+ if (isDirectusError(err, ErrorCode.InvalidPayload)) {
1810
1828
  throw err;
1811
1829
  }
1812
1830
  }
@@ -1892,7 +1910,7 @@ export class GraphQLService {
1892
1910
  });
1893
1911
  const otpValid = await service.verifyOTP(this.accountability.user, args['otp']);
1894
1912
  if (otpValid === false) {
1895
- throw new InvalidPayloadException(`"otp" is invalid`);
1913
+ throw new InvalidPayloadError({ reason: `"otp" is invalid` });
1896
1914
  }
1897
1915
  await service.disableTFA(this.accountability.user);
1898
1916
  return true;
@@ -1906,7 +1924,7 @@ export class GraphQLService {
1906
1924
  resolve: async (_, args) => {
1907
1925
  const { nanoid } = await import('nanoid');
1908
1926
  if (args['length'] && Number(args['length']) > 500) {
1909
- throw new InvalidPayloadException(`"length" can't be more than 500 characters`);
1927
+ throw new InvalidPayloadError({ reason: `"length" can't be more than 500 characters` });
1910
1928
  }
1911
1929
  return nanoid(args['length'] ? Number(args['length']) : 32);
1912
1930
  },
@@ -1965,7 +1983,7 @@ export class GraphQLService {
1965
1983
  type: GraphQLVoid,
1966
1984
  resolve: async () => {
1967
1985
  if (this.accountability?.admin !== true) {
1968
- throw new ForbiddenException();
1986
+ throw new ForbiddenError();
1969
1987
  }
1970
1988
  const { cache } = getCache();
1971
1989
  await cache?.clear();
@@ -1,14 +1,14 @@
1
- import { BaseException } from '@directus/exceptions';
1
+ import { isDirectusError } from '@directus/errors';
2
2
  import logger from '../../../logger.js';
3
3
  const processError = (accountability, error) => {
4
4
  logger.error(error);
5
5
  const { originalError } = error;
6
- if (originalError instanceof BaseException) {
6
+ if (isDirectusError(originalError)) {
7
7
  return {
8
8
  message: originalError.message,
9
9
  extensions: {
10
10
  code: originalError.code,
11
- ...originalError.extensions,
11
+ ...(originalError.extensions ?? {}),
12
12
  },
13
13
  };
14
14
  }
@@ -14,7 +14,7 @@ import { file as createTmpFile } from 'tmp-promise';
14
14
  import getDatabase from '../database/index.js';
15
15
  import emitter from '../emitter.js';
16
16
  import env from '../env.js';
17
- import { ForbiddenException, InvalidPayloadException, ServiceUnavailableException, UnsupportedMediaTypeException, } from '../exceptions/index.js';
17
+ import { ForbiddenError, InvalidPayloadError, ServiceUnavailableError, UnsupportedMediaTypeError, } from '../errors/index.js';
18
18
  import logger from '../logger.js';
19
19
  import { getDateFormatted } from '../utils/get-date-formatted.js';
20
20
  import { FilesService } from './files.js';
@@ -31,11 +31,11 @@ export class ImportService {
31
31
  }
32
32
  async import(collection, mimetype, stream) {
33
33
  if (this.accountability?.admin !== true && collection.startsWith('directus_'))
34
- throw new ForbiddenException();
34
+ throw new ForbiddenError();
35
35
  const createPermissions = this.accountability?.permissions?.find((permission) => permission.collection === collection && permission.action === 'create');
36
36
  const updatePermissions = this.accountability?.permissions?.find((permission) => permission.collection === collection && permission.action === 'update');
37
37
  if (this.accountability?.admin !== true && (!createPermissions || !updatePermissions)) {
38
- throw new ForbiddenException();
38
+ throw new ForbiddenError();
39
39
  }
40
40
  switch (mimetype) {
41
41
  case 'application/json':
@@ -44,7 +44,7 @@ export class ImportService {
44
44
  case 'application/vnd.ms-excel':
45
45
  return await this.importCSV(collection, stream);
46
46
  default:
47
- throw new UnsupportedMediaTypeException(`Can't import files of type "${mimetype}"`);
47
+ throw new UnsupportedMediaTypeError({ mediaType: mimetype, where: 'file import' });
48
48
  }
49
49
  }
50
50
  importJSON(collection, stream) {
@@ -67,7 +67,7 @@ export class ImportService {
67
67
  extractJSON.on('error', (err) => {
68
68
  destroyStream(stream);
69
69
  destroyStream(extractJSON);
70
- reject(new InvalidPayloadException(err.message));
70
+ reject(new InvalidPayloadError({ reason: err.message }));
71
71
  });
72
72
  saveQueue.error((err) => {
73
73
  reject(err);
@@ -122,7 +122,7 @@ export class ImportService {
122
122
  })
123
123
  .on('error', (err) => {
124
124
  destroyStream(stream);
125
- reject(new InvalidPayloadException(err.message));
125
+ reject(new InvalidPayloadError({ reason: err.message }));
126
126
  })
127
127
  .on('end', () => {
128
128
  saveQueue.drain(() => {
@@ -288,6 +288,6 @@ export class ExportService {
288
288
  if (format === 'yaml') {
289
289
  return toYAML(input);
290
290
  }
291
- throw new ServiceUnavailableException(`Illegal export type used: "${format}"`, { service: 'export' });
291
+ throw new ServiceUnavailableError({ service: 'export', reason: `Illegal export type used: "${format}"` });
292
292
  }
293
293
  }
@@ -32,3 +32,4 @@ export * from './translations.js';
32
32
  export * from './users.js';
33
33
  export * from './utils.js';
34
34
  export * from './webhooks.js';
35
+ export * from './websocket.js';
@@ -32,3 +32,4 @@ export * from './translations.js';
32
32
  export * from './users.js';
33
33
  export * from './utils.js';
34
34
  export * from './webhooks.js';
35
+ export * from './websocket.js';
@@ -6,8 +6,9 @@ import getDatabase from '../database/index.js';
6
6
  import runAST from '../database/run-ast.js';
7
7
  import emitter from '../emitter.js';
8
8
  import env from '../env.js';
9
- import { translateDatabaseError } from '../exceptions/database/translate.js';
10
- import { ForbiddenException, InvalidPayloadException } from '../exceptions/index.js';
9
+ import { ForbiddenError } from '../errors/index.js';
10
+ import { translateDatabaseError } from '../database/errors/translate.js';
11
+ import { InvalidPayloadError } from '../errors/index.js';
11
12
  import getASTFromQuery from '../utils/get-ast-from-query.js';
12
13
  import { shouldClearCache } from '../utils/should-clear-cache.js';
13
14
  import { validateKeys } from '../utils/validate-keys.js';
@@ -36,7 +37,7 @@ export class ItemsService {
36
37
  trackMutations(count) {
37
38
  mutationCount += count;
38
39
  if (mutationCount > maxCount) {
39
- throw new InvalidPayloadException(`Exceeded max batch mutation limit of ${maxCount}.`);
40
+ throw new InvalidPayloadError({ reason: `Exceeded max batch mutation limit of ${maxCount}` });
40
41
  }
41
42
  },
42
43
  getCount() {
@@ -108,8 +109,8 @@ export class ItemsService {
108
109
  const payloadWithPresets = this.accountability
109
110
  ? authorizationService.validatePayload('create', this.collection, payloadAfterHooks)
110
111
  : payloadAfterHooks;
111
- if (opts.preMutationException) {
112
- throw opts.preMutationException;
112
+ if (opts.preMutationError) {
113
+ throw opts.preMutationError;
113
114
  }
114
115
  const { payload: payloadWithM2O, revisions: revisionsM2O, nestedActionEvents: nestedActionEventsM2O, } = await payloadService.processM2O(payloadWithPresets, opts);
115
116
  const { payload: payloadWithA2O, revisions: revisionsA2O, nestedActionEvents: nestedActionEventsA2O, } = await payloadService.processA2O(payloadWithM2O, opts);
@@ -303,7 +304,7 @@ export class ItemsService {
303
304
  stripNonRequested: opts?.stripNonRequested !== undefined ? opts.stripNonRequested : true,
304
305
  });
305
306
  if (records === null) {
306
- throw new ForbiddenException();
307
+ throw new ForbiddenError();
307
308
  }
308
309
  const filteredRecords = opts?.emitEvents !== false
309
310
  ? await emitter.emitFilter(this.eventScope === 'items' ? ['items.read', `${this.collection}.items.read`] : `${this.eventScope}.read`, records, {
@@ -338,7 +339,7 @@ export class ItemsService {
338
339
  const queryWithKey = assign({}, query, { filter: filterWithKey });
339
340
  const results = await this.readByQuery(queryWithKey, opts);
340
341
  if (results.length === 0) {
341
- throw new ForbiddenException();
342
+ throw new ForbiddenError();
342
343
  }
343
344
  return results[0];
344
345
  }
@@ -380,7 +381,7 @@ export class ItemsService {
380
381
  */
381
382
  async updateBatch(data, opts = {}) {
382
383
  if (!Array.isArray(data)) {
383
- throw new InvalidPayloadException('Input should be an array of items.');
384
+ throw new InvalidPayloadError({ reason: 'Input should be an array of items' });
384
385
  }
385
386
  if (!opts.mutationTracker)
386
387
  opts.mutationTracker = this.createMutationTracker();
@@ -395,7 +396,7 @@ export class ItemsService {
395
396
  });
396
397
  for (const item of data) {
397
398
  if (!item[primaryKeyField])
398
- throw new InvalidPayloadException(`Item in update misses primary key.`);
399
+ throw new InvalidPayloadError({ reason: `Item in update misses primary key` });
399
400
  const combinedOpts = Object.assign({ autoPurgeCache: false }, opts);
400
401
  keys.push(await service.updateOne(item[primaryKeyField], omit(item, primaryKeyField), combinedOpts));
401
402
  }
@@ -454,8 +455,8 @@ export class ItemsService {
454
455
  const payloadWithPresets = this.accountability
455
456
  ? authorizationService.validatePayload('update', this.collection, payloadAfterHooks)
456
457
  : payloadAfterHooks;
457
- if (opts.preMutationException) {
458
- throw opts.preMutationException;
458
+ if (opts.preMutationError) {
459
+ throw opts.preMutationError;
459
460
  }
460
461
  await this.knex.transaction(async (trx) => {
461
462
  const payloadService = new PayloadService(this.collection, {
@@ -654,8 +655,8 @@ export class ItemsService {
654
655
  });
655
656
  await authorizationService.checkAccess('delete', this.collection, keys);
656
657
  }
657
- if (opts.preMutationException) {
658
- throw opts.preMutationException;
658
+ if (opts.preMutationError) {
659
+ throw opts.preMutationError;
659
660
  }
660
661
  if (opts.emitEvents !== false) {
661
662
  await emitter.emitFilter(this.eventScope === 'items' ? ['items.delete', `${this.collection}.items.delete`] : `${this.eventScope}.delete`, keys, {
@@ -1,13 +1,13 @@
1
1
  import fse from 'fs-extra';
2
2
  import { Liquid } from 'liquidjs';
3
3
  import path from 'path';
4
+ import { fileURLToPath } from 'url';
4
5
  import getDatabase from '../../database/index.js';
5
6
  import env from '../../env.js';
6
- import { InvalidPayloadException } from '../../exceptions/index.js';
7
+ import { InvalidPayloadError } from '../../errors/index.js';
7
8
  import logger from '../../logger.js';
8
9
  import getMailer from '../../mailer.js';
9
10
  import { Url } from '../../utils/url.js';
10
- import { fileURLToPath } from 'url';
11
11
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
12
  const liquidEngine = new Liquid({
13
13
  root: [path.resolve(env['EXTENSIONS_PATH'], 'templates'), path.resolve(__dirname, 'templates')],
@@ -60,7 +60,7 @@ export class MailService {
60
60
  const systemTemplatePath = path.join(__dirname, 'templates', template + '.liquid');
61
61
  const templatePath = (await fse.pathExists(customTemplatePath)) ? customTemplatePath : systemTemplatePath;
62
62
  if ((await fse.pathExists(templatePath)) === false) {
63
- throw new InvalidPayloadException(`Template "${template}" doesn't exist.`);
63
+ throw new InvalidPayloadError({ reason: `Template "${template}" doesn't exist` });
64
64
  }
65
65
  const templateString = await fse.readFile(templatePath, 'utf8');
66
66
  const html = await liquidEngine.parseAndRender(templateString, variables);
@@ -1,5 +1,5 @@
1
1
  import getDatabase from '../database/index.js';
2
- import { ForbiddenException } from '../exceptions/index.js';
2
+ import { ForbiddenError } from '../errors/index.js';
3
3
  import { applyFilter, applySearch } from '../utils/apply-query.js';
4
4
  export class MetaService {
5
5
  knex;
@@ -10,7 +10,6 @@ export class MetaService {
10
10
  this.accountability = options.accountability || null;
11
11
  this.schema = options.schema;
12
12
  }
13
- // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
14
13
  async getMetaForQuery(collection, query) {
15
14
  if (!query || !query.meta)
16
15
  return;
@@ -35,7 +34,7 @@ export class MetaService {
35
34
  return permission.action === 'read' && permission.collection === collection;
36
35
  });
37
36
  if (!permissionsRecord)
38
- throw new ForbiddenException();
37
+ throw new ForbiddenError();
39
38
  const permissions = permissionsRecord.permissions ?? {};
40
39
  applyFilter(this.knex, this.schema, dbQuery, permissions, collection, {});
41
40
  }
@@ -50,7 +49,7 @@ export class MetaService {
50
49
  return permission.action === 'read' && permission.collection === collection;
51
50
  });
52
51
  if (!permissionsRecord)
53
- throw new ForbiddenException();
52
+ throw new ForbiddenError();
54
53
  const permissions = permissionsRecord.permissions ?? {};
55
54
  if (Object.keys(filter).length > 0) {
56
55
  filter = { _and: [permissions, filter] };