@directus/api 11.0.1 → 12.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 (283) 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/emitter.d.ts +3 -2
  47. package/dist/emitter.js +12 -4
  48. package/dist/env.js +21 -17
  49. package/dist/errors/codes.d.ts +29 -0
  50. package/dist/errors/codes.js +30 -0
  51. package/dist/errors/contains-null-values.d.ts +7 -0
  52. package/dist/errors/contains-null-values.js +4 -0
  53. package/dist/errors/content-too-large.d.ts +1 -0
  54. package/dist/errors/content-too-large.js +3 -0
  55. package/dist/errors/forbidden.d.ts +1 -0
  56. package/dist/errors/forbidden.js +3 -0
  57. package/dist/errors/hit-rate-limit.d.ts +6 -0
  58. package/dist/errors/hit-rate-limit.js +8 -0
  59. package/dist/errors/illegal-asset-transformation.d.ts +4 -0
  60. package/dist/errors/illegal-asset-transformation.js +3 -0
  61. package/dist/errors/index.d.ts +28 -0
  62. package/dist/errors/index.js +28 -0
  63. package/dist/errors/invalid-credentials.d.ts +1 -0
  64. package/dist/errors/invalid-credentials.js +3 -0
  65. package/dist/errors/invalid-foreign-key.d.ts +6 -0
  66. package/dist/errors/invalid-foreign-key.js +14 -0
  67. package/dist/errors/invalid-ip.d.ts +1 -0
  68. package/dist/errors/invalid-ip.js +3 -0
  69. package/dist/errors/invalid-otp.d.ts +1 -0
  70. package/dist/errors/invalid-otp.js +3 -0
  71. package/dist/errors/invalid-payload.d.ts +5 -0
  72. package/dist/errors/invalid-payload.js +4 -0
  73. package/dist/errors/invalid-provider-config.d.ts +5 -0
  74. package/dist/errors/invalid-provider-config.js +3 -0
  75. package/dist/errors/invalid-provider.d.ts +1 -0
  76. package/dist/errors/invalid-provider.js +3 -0
  77. package/dist/errors/invalid-query.d.ts +5 -0
  78. package/dist/errors/invalid-query.js +4 -0
  79. package/dist/errors/invalid-token.d.ts +1 -0
  80. package/dist/errors/invalid-token.js +3 -0
  81. package/dist/errors/method-not-allowed.d.ts +6 -0
  82. package/dist/errors/method-not-allowed.js +6 -0
  83. package/dist/errors/not-null-violation.d.ts +6 -0
  84. package/dist/errors/not-null-violation.js +14 -0
  85. package/dist/errors/range-not-satisfiable.d.ts +7 -0
  86. package/dist/errors/range-not-satisfiable.js +7 -0
  87. package/dist/errors/record-not-unique.d.ts +6 -0
  88. package/dist/errors/record-not-unique.js +14 -0
  89. package/dist/errors/route-not-found.d.ts +5 -0
  90. package/dist/errors/route-not-found.js +4 -0
  91. package/dist/errors/service-unavailable.d.ts +7 -0
  92. package/dist/errors/service-unavailable.js +4 -0
  93. package/dist/errors/token-expired.d.ts +1 -0
  94. package/dist/errors/token-expired.js +3 -0
  95. package/dist/errors/unexpected-response.d.ts +1 -0
  96. package/dist/errors/unexpected-response.js +3 -0
  97. package/dist/errors/unprocessable-content.d.ts +5 -0
  98. package/dist/errors/unprocessable-content.js +4 -0
  99. package/dist/errors/unsupported-media-type.d.ts +6 -0
  100. package/dist/errors/unsupported-media-type.js +4 -0
  101. package/dist/errors/user-suspended.d.ts +1 -0
  102. package/dist/errors/user-suspended.js +3 -0
  103. package/dist/errors/value-out-of-range.d.ts +6 -0
  104. package/dist/errors/value-out-of-range.js +14 -0
  105. package/dist/errors/value-too-long.d.ts +6 -0
  106. package/dist/errors/value-too-long.js +14 -0
  107. package/dist/extensions.js +0 -4
  108. package/dist/flows.js +6 -8
  109. package/dist/index.d.ts +0 -2
  110. package/dist/index.js +0 -2
  111. package/dist/messenger.d.ts +3 -3
  112. package/dist/messenger.js +18 -9
  113. package/dist/middleware/authenticate.js +2 -38
  114. package/dist/middleware/check-ip.js +2 -2
  115. package/dist/middleware/collection-exists.js +2 -2
  116. package/dist/middleware/error-handler.js +7 -7
  117. package/dist/middleware/graphql.js +11 -9
  118. package/dist/middleware/rate-limiter-global.d.ts +2 -2
  119. package/dist/middleware/rate-limiter-global.js +2 -3
  120. package/dist/middleware/rate-limiter-ip.d.ts +2 -2
  121. package/dist/middleware/rate-limiter-ip.js +2 -3
  122. package/dist/middleware/validate-batch.js +3 -4
  123. package/dist/rate-limiter.js +2 -9
  124. package/dist/server.js +10 -0
  125. package/dist/services/activity.js +3 -2
  126. package/dist/services/assets.js +9 -10
  127. package/dist/services/authentication.js +12 -11
  128. package/dist/services/authorization.d.ts +1 -1
  129. package/dist/services/authorization.js +16 -16
  130. package/dist/services/collections.js +17 -16
  131. package/dist/services/fields.js +16 -14
  132. package/dist/services/files.js +7 -6
  133. package/dist/services/graphql/errors/execution.d.ts +6 -0
  134. package/dist/services/graphql/errors/execution.js +2 -0
  135. package/dist/services/graphql/errors/index.d.ts +2 -0
  136. package/dist/services/graphql/errors/index.js +2 -0
  137. package/dist/services/graphql/errors/validation.d.ts +6 -0
  138. package/dist/services/graphql/errors/validation.js +2 -0
  139. package/dist/services/graphql/index.d.ts +2 -8
  140. package/dist/services/graphql/index.js +125 -66
  141. package/dist/services/graphql/subscription.d.ts +16 -0
  142. package/dist/services/graphql/subscription.js +77 -0
  143. package/dist/services/graphql/utils/process-error.js +3 -3
  144. package/dist/services/import-export.js +7 -7
  145. package/dist/services/index.d.ts +1 -0
  146. package/dist/services/index.js +1 -0
  147. package/dist/services/items.js +14 -13
  148. package/dist/services/mail/index.js +3 -3
  149. package/dist/services/meta.js +3 -3
  150. package/dist/services/payload.js +11 -7
  151. package/dist/services/relations.js +32 -22
  152. package/dist/services/revisions.js +3 -3
  153. package/dist/services/roles.js +10 -9
  154. package/dist/services/schema.js +5 -5
  155. package/dist/services/server.js +24 -0
  156. package/dist/services/shares.js +4 -4
  157. package/dist/services/tfa.js +6 -6
  158. package/dist/services/translations.d.ts +2 -2
  159. package/dist/services/translations.js +4 -4
  160. package/dist/services/users.js +26 -29
  161. package/dist/services/utils.js +4 -4
  162. package/dist/services/websocket.d.ts +14 -0
  163. package/dist/services/websocket.js +26 -0
  164. package/dist/synchronization.js +3 -3
  165. package/dist/types/items.d.ts +2 -2
  166. package/dist/utils/apply-diff.js +13 -4
  167. package/dist/utils/apply-query.js +22 -13
  168. package/dist/utils/get-accountability-for-role.js +1 -2
  169. package/dist/utils/get-accountability-for-token.d.ts +2 -0
  170. package/dist/utils/get-accountability-for-token.js +50 -0
  171. package/dist/utils/get-column-path.js +5 -3
  172. package/dist/utils/get-column.js +3 -3
  173. package/dist/utils/get-service.d.ts +7 -0
  174. package/dist/utils/get-service.js +49 -0
  175. package/dist/utils/jwt.js +5 -5
  176. package/dist/utils/redact.d.ts +4 -0
  177. package/dist/utils/redact.js +15 -1
  178. package/dist/utils/to-boolean.d.ts +4 -0
  179. package/dist/utils/to-boolean.js +6 -0
  180. package/dist/utils/validate-diff.js +23 -9
  181. package/dist/utils/validate-keys.js +3 -3
  182. package/dist/utils/validate-query.d.ts +2 -0
  183. package/dist/utils/validate-query.js +27 -21
  184. package/dist/utils/validate-snapshot.js +11 -5
  185. package/dist/websocket/authenticate.d.ts +6 -0
  186. package/dist/websocket/authenticate.js +59 -0
  187. package/dist/websocket/controllers/base.d.ts +42 -0
  188. package/dist/websocket/controllers/base.js +279 -0
  189. package/dist/websocket/controllers/graphql.d.ts +12 -0
  190. package/dist/websocket/controllers/graphql.js +102 -0
  191. package/dist/websocket/controllers/hooks.d.ts +1 -0
  192. package/dist/websocket/controllers/hooks.js +122 -0
  193. package/dist/websocket/controllers/index.d.ts +10 -0
  194. package/dist/websocket/controllers/index.js +31 -0
  195. package/dist/websocket/controllers/rest.d.ts +9 -0
  196. package/dist/websocket/controllers/rest.js +47 -0
  197. package/dist/websocket/errors.d.ts +16 -0
  198. package/dist/websocket/errors.js +55 -0
  199. package/dist/websocket/handlers/heartbeat.d.ts +11 -0
  200. package/dist/websocket/handlers/heartbeat.js +72 -0
  201. package/dist/websocket/handlers/index.d.ts +4 -0
  202. package/dist/websocket/handlers/index.js +11 -0
  203. package/dist/websocket/handlers/items.d.ts +6 -0
  204. package/dist/websocket/handlers/items.js +103 -0
  205. package/dist/websocket/handlers/subscribe.d.ts +43 -0
  206. package/dist/websocket/handlers/subscribe.js +278 -0
  207. package/dist/websocket/messages.d.ts +311 -0
  208. package/dist/websocket/messages.js +96 -0
  209. package/dist/websocket/types.d.ts +34 -0
  210. package/dist/websocket/types.js +1 -0
  211. package/dist/websocket/utils/get-expires-at-for-token.d.ts +1 -0
  212. package/dist/websocket/utils/get-expires-at-for-token.js +8 -0
  213. package/dist/websocket/utils/message.d.ts +4 -0
  214. package/dist/websocket/utils/message.js +27 -0
  215. package/dist/websocket/utils/wait-for-message.d.ts +4 -0
  216. package/dist/websocket/utils/wait-for-message.js +45 -0
  217. package/package.json +21 -16
  218. package/dist/exceptions/content-too-large.d.ts +0 -4
  219. package/dist/exceptions/content-too-large.js +0 -6
  220. package/dist/exceptions/database/contains-null-values.d.ts +0 -9
  221. package/dist/exceptions/database/contains-null-values.js +0 -6
  222. package/dist/exceptions/database/invalid-foreign-key.d.ts +0 -10
  223. package/dist/exceptions/database/invalid-foreign-key.js +0 -11
  224. package/dist/exceptions/database/not-null-violation.d.ts +0 -9
  225. package/dist/exceptions/database/not-null-violation.js +0 -6
  226. package/dist/exceptions/database/record-not-unique.d.ts +0 -10
  227. package/dist/exceptions/database/record-not-unique.js +0 -11
  228. package/dist/exceptions/database/value-out-of-range.d.ts +0 -10
  229. package/dist/exceptions/database/value-out-of-range.js +0 -11
  230. package/dist/exceptions/database/value-too-long.d.ts +0 -9
  231. package/dist/exceptions/database/value-too-long.js +0 -11
  232. package/dist/exceptions/forbidden.d.ts +0 -6
  233. package/dist/exceptions/forbidden.js +0 -13
  234. package/dist/exceptions/graphql-validation.d.ts +0 -4
  235. package/dist/exceptions/graphql-validation.js +0 -6
  236. package/dist/exceptions/hit-rate-limit.d.ts +0 -9
  237. package/dist/exceptions/hit-rate-limit.js +0 -6
  238. package/dist/exceptions/illegal-asset-transformation.d.ts +0 -4
  239. package/dist/exceptions/illegal-asset-transformation.js +0 -6
  240. package/dist/exceptions/index.d.ts +0 -21
  241. package/dist/exceptions/index.js +0 -21
  242. package/dist/exceptions/invalid-config.d.ts +0 -4
  243. package/dist/exceptions/invalid-config.js +0 -6
  244. package/dist/exceptions/invalid-credentials.d.ts +0 -4
  245. package/dist/exceptions/invalid-credentials.js +0 -6
  246. package/dist/exceptions/invalid-ip.d.ts +0 -4
  247. package/dist/exceptions/invalid-ip.js +0 -6
  248. package/dist/exceptions/invalid-otp.d.ts +0 -4
  249. package/dist/exceptions/invalid-otp.js +0 -6
  250. package/dist/exceptions/invalid-payload.d.ts +0 -4
  251. package/dist/exceptions/invalid-payload.js +0 -6
  252. package/dist/exceptions/invalid-provider.d.ts +0 -4
  253. package/dist/exceptions/invalid-provider.js +0 -6
  254. package/dist/exceptions/invalid-query.d.ts +0 -4
  255. package/dist/exceptions/invalid-query.js +0 -6
  256. package/dist/exceptions/invalid-token.d.ts +0 -4
  257. package/dist/exceptions/invalid-token.js +0 -6
  258. package/dist/exceptions/method-not-allowed.d.ts +0 -8
  259. package/dist/exceptions/method-not-allowed.js +0 -6
  260. package/dist/exceptions/range-not-satisfiable.d.ts +0 -5
  261. package/dist/exceptions/range-not-satisfiable.js +0 -9
  262. package/dist/exceptions/route-not-found.d.ts +0 -4
  263. package/dist/exceptions/route-not-found.js +0 -6
  264. package/dist/exceptions/service-unavailable.d.ts +0 -9
  265. package/dist/exceptions/service-unavailable.js +0 -6
  266. package/dist/exceptions/token-expired.d.ts +0 -4
  267. package/dist/exceptions/token-expired.js +0 -6
  268. package/dist/exceptions/unexpected-response.d.ts +0 -4
  269. package/dist/exceptions/unexpected-response.js +0 -6
  270. package/dist/exceptions/unprocessable-entity.d.ts +0 -4
  271. package/dist/exceptions/unprocessable-entity.js +0 -6
  272. package/dist/exceptions/unsupported-media-type.d.ts +0 -4
  273. package/dist/exceptions/unsupported-media-type.js +0 -6
  274. package/dist/exceptions/user-suspended.d.ts +0 -4
  275. package/dist/exceptions/user-suspended.js +0 -6
  276. /package/dist/{exceptions/database → database/errors}/dialects/mssql.d.ts +0 -0
  277. /package/dist/{exceptions/database → database/errors}/dialects/mysql.d.ts +0 -0
  278. /package/dist/{exceptions/database → database/errors}/dialects/oracle.d.ts +0 -0
  279. /package/dist/{exceptions/database → database/errors}/dialects/postgres.d.ts +0 -0
  280. /package/dist/{exceptions/database → database/errors}/dialects/sqlite.d.ts +0 -0
  281. /package/dist/{exceptions/database → database/errors}/dialects/types.d.ts +0 -0
  282. /package/dist/{exceptions/database → database/errors}/dialects/types.js +0 -0
  283. /package/dist/{exceptions/database → database/errors}/translate.d.ts +0 -0
@@ -1,12 +1,12 @@
1
- import { FailedValidationException } from '@directus/exceptions';
2
1
  import { getSimpleHash, toArray } from '@directus/utils';
2
+ import { FailedValidationError, joiValidationErrorItemToErrorExtensions } from '@directus/validation';
3
3
  import jwt from 'jsonwebtoken';
4
4
  import { cloneDeep, isEmpty } from 'lodash-es';
5
5
  import { performance } from 'perf_hooks';
6
6
  import getDatabase from '../database/index.js';
7
7
  import env from '../env.js';
8
- import { RecordNotUniqueException } from '../exceptions/database/record-not-unique.js';
9
- import { ForbiddenException, InvalidPayloadException, UnprocessableEntityException } from '../exceptions/index.js';
8
+ import { ForbiddenError } from '../errors/forbidden.js';
9
+ import { InvalidPayloadError, RecordNotUniqueError, UnprocessableContentError } from '../errors/index.js';
10
10
  import isUrlAllowed from '../utils/is-url-allowed.js';
11
11
  import { verifyJWT } from '../utils/jwt.js';
12
12
  import { stall } from '../utils/stall.js';
@@ -29,10 +29,9 @@ export class UsersService extends ItemsService {
29
29
  emails = emails.map((email) => email.toLowerCase());
30
30
  const duplicates = emails.filter((value, index, array) => array.indexOf(value) !== index);
31
31
  if (duplicates.length) {
32
- throw new RecordNotUniqueException('email', {
32
+ throw new RecordNotUniqueError({
33
33
  collection: 'directus_users',
34
34
  field: 'email',
35
- invalid: duplicates[0],
36
35
  });
37
36
  }
38
37
  const query = this.knex
@@ -44,10 +43,9 @@ export class UsersService extends ItemsService {
44
43
  }
45
44
  const results = await query;
46
45
  if (results.length) {
47
- throw new RecordNotUniqueException('email', {
46
+ throw new RecordNotUniqueError({
48
47
  collection: 'directus_users',
49
48
  field: 'email',
50
- invalid: results[0].email,
51
49
  });
52
50
  }
53
51
  }
@@ -70,14 +68,14 @@ export class UsersService extends ItemsService {
70
68
  const regex = new RegExp(wrapped ? policyRegExString.slice(1, -1) : policyRegExString);
71
69
  for (const password of passwords) {
72
70
  if (!regex.test(password)) {
73
- throw new FailedValidationException({
71
+ throw new FailedValidationError(joiValidationErrorItemToErrorExtensions({
74
72
  message: `Provided password doesn't match password policy`,
75
73
  path: ['password'],
76
74
  type: 'custom.pattern.base',
77
75
  context: {
78
76
  value: password,
79
77
  },
80
- });
78
+ }));
81
79
  }
82
80
  }
83
81
  }
@@ -92,7 +90,7 @@ export class UsersService extends ItemsService {
92
90
  .first();
93
91
  const otherAdminUsersCount = +(otherAdminUsers?.count || 0);
94
92
  if (otherAdminUsersCount === 0) {
95
- throw new UnprocessableEntityException(`You can't remove the last admin user from the role.`);
93
+ throw new UnprocessableContentError({ reason: `You can't remove the last admin user from the role` });
96
94
  }
97
95
  }
98
96
  /**
@@ -109,7 +107,7 @@ export class UsersService extends ItemsService {
109
107
  .first();
110
108
  const otherAdminUsersCount = +(otherAdminUsers?.count || 0);
111
109
  if (otherAdminUsersCount === 0) {
112
- throw new UnprocessableEntityException(`You can't change the active status of the last admin user.`);
110
+ throw new UnprocessableContentError({ reason: `You can't change the active status of the last admin user` });
113
111
  }
114
112
  }
115
113
  /**
@@ -154,7 +152,7 @@ export class UsersService extends ItemsService {
154
152
  }
155
153
  }
156
154
  catch (err) {
157
- (opts || (opts = {})).preMutationException = err;
155
+ (opts || (opts = {})).preMutationError = err;
158
156
  }
159
157
  return await super.createMany(data, opts);
160
158
  }
@@ -185,7 +183,7 @@ export class UsersService extends ItemsService {
185
183
  });
186
184
  for (const item of data) {
187
185
  if (!item[primaryKeyField])
188
- throw new InvalidPayloadException(`User in update misses primary key.`);
186
+ throw new InvalidPayloadError({ reason: `User in update misses primary key` });
189
187
  keys.push(await service.updateOne(item[primaryKeyField], item, opts));
190
188
  }
191
189
  });
@@ -209,10 +207,9 @@ export class UsersService extends ItemsService {
209
207
  }
210
208
  if (data['email']) {
211
209
  if (keys.length > 1) {
212
- throw new RecordNotUniqueException('email', {
210
+ throw new RecordNotUniqueError({
213
211
  collection: 'directus_users',
214
212
  field: 'email',
215
- invalid: data['email'],
216
213
  });
217
214
  }
218
215
  await this.checkUniqueEmails([data['email']], keys[0]);
@@ -221,23 +218,23 @@ export class UsersService extends ItemsService {
221
218
  await this.checkPasswordPolicy([data['password']]);
222
219
  }
223
220
  if (data['tfa_secret'] !== undefined) {
224
- throw new InvalidPayloadException(`You can't change the "tfa_secret" value manually.`);
221
+ throw new InvalidPayloadError({ reason: `You can't change the "tfa_secret" value manually` });
225
222
  }
226
223
  if (data['provider'] !== undefined) {
227
224
  if (this.accountability && this.accountability.admin !== true) {
228
- throw new InvalidPayloadException(`You can't change the "provider" value manually.`);
225
+ throw new InvalidPayloadError({ reason: `You can't change the "provider" value manually` });
229
226
  }
230
227
  data['auth_data'] = null;
231
228
  }
232
229
  if (data['external_identifier'] !== undefined) {
233
230
  if (this.accountability && this.accountability.admin !== true) {
234
- throw new InvalidPayloadException(`You can't change the "external_identifier" value manually.`);
231
+ throw new InvalidPayloadError({ reason: `You can't change the "external_identifier" value manually` });
235
232
  }
236
233
  data['auth_data'] = null;
237
234
  }
238
235
  }
239
236
  catch (err) {
240
- (opts || (opts = {})).preMutationException = err;
237
+ (opts || (opts = {})).preMutationError = err;
241
238
  }
242
239
  return await super.updateMany(keys, data, opts);
243
240
  }
@@ -256,7 +253,7 @@ export class UsersService extends ItemsService {
256
253
  await this.checkRemainingAdminExistence(keys);
257
254
  }
258
255
  catch (err) {
259
- (opts || (opts = {})).preMutationException = err;
256
+ (opts || (opts = {})).preMutationError = err;
260
257
  }
261
258
  await this.knex('directus_notifications').update({ sender: null }).whereIn('sender', keys);
262
259
  await super.deleteMany(keys, opts);
@@ -281,11 +278,11 @@ export class UsersService extends ItemsService {
281
278
  const opts = {};
282
279
  try {
283
280
  if (url && isUrlAllowed(url, env['USER_INVITE_URL_ALLOW_LIST']) === false) {
284
- throw new InvalidPayloadException(`Url "${url}" can't be used to invite users.`);
281
+ throw new InvalidPayloadError({ reason: `Url "${url}" can't be used to invite users` });
285
282
  }
286
283
  }
287
284
  catch (err) {
288
- opts.preMutationException = err;
285
+ opts.preMutationError = err;
289
286
  }
290
287
  const emails = toArray(email);
291
288
  const mailService = new MailService({
@@ -323,10 +320,10 @@ export class UsersService extends ItemsService {
323
320
  async acceptInvite(token, password) {
324
321
  const { email, scope } = verifyJWT(token, env['SECRET']);
325
322
  if (scope !== 'invite')
326
- throw new ForbiddenException();
323
+ throw new ForbiddenError();
327
324
  const user = await this.getUserByEmail(email);
328
325
  if (user?.status !== 'invited') {
329
- throw new InvalidPayloadException(`Email address ${email} hasn't been invited.`);
326
+ throw new InvalidPayloadError({ reason: `Email address ${email} hasn't been invited` });
330
327
  }
331
328
  // Allow unauthenticated update
332
329
  const service = new UsersService({
@@ -341,10 +338,10 @@ export class UsersService extends ItemsService {
341
338
  const user = await this.getUserByEmail(email);
342
339
  if (user?.status !== 'active') {
343
340
  await stall(STALL_TIME, timeStart);
344
- throw new ForbiddenException();
341
+ throw new ForbiddenError();
345
342
  }
346
343
  if (url && isUrlAllowed(url, env['PASSWORD_RESET_URL_ALLOW_LIST']) === false) {
347
- throw new InvalidPayloadException(`Url "${url}" can't be used to reset passwords.`);
344
+ throw new InvalidPayloadError({ reason: `Url "${url}" can't be used to reset passwords` });
348
345
  }
349
346
  const mailService = new MailService({
350
347
  schema: this.schema,
@@ -373,17 +370,17 @@ export class UsersService extends ItemsService {
373
370
  async resetPassword(token, password) {
374
371
  const { email, scope, hash } = jwt.verify(token, env['SECRET'], { issuer: 'directus' });
375
372
  if (scope !== 'password-reset' || !hash)
376
- throw new ForbiddenException();
373
+ throw new ForbiddenError();
377
374
  const opts = {};
378
375
  try {
379
376
  await this.checkPasswordPolicy([password]);
380
377
  }
381
378
  catch (err) {
382
- opts.preMutationException = err;
379
+ opts.preMutationError = err;
383
380
  }
384
381
  const user = await this.getUserByEmail(email);
385
382
  if (user?.status !== 'active' || hash !== getSimpleHash('' + user.password)) {
386
- throw new ForbiddenException();
383
+ throw new ForbiddenError();
387
384
  }
388
385
  // Allow unauthenticated update
389
386
  const service = new UsersService({
@@ -1,7 +1,7 @@
1
1
  import getDatabase from '../database/index.js';
2
2
  import { systemCollectionRows } from '../database/system-data/collections/index.js';
3
3
  import emitter from '../emitter.js';
4
- import { ForbiddenException, InvalidPayloadException } from '../exceptions/index.js';
4
+ import { ForbiddenError, InvalidPayloadError } from '../errors/index.js';
5
5
  export class UtilsService {
6
6
  knex;
7
7
  accountability;
@@ -16,18 +16,18 @@ export class UtilsService {
16
16
  systemCollectionRows;
17
17
  const sortField = sortFieldResponse?.sort_field;
18
18
  if (!sortField) {
19
- throw new InvalidPayloadException(`Collection "${collection}" doesn't have a sort field.`);
19
+ throw new InvalidPayloadError({ reason: `Collection "${collection}" doesn't have a sort field` });
20
20
  }
21
21
  if (this.accountability?.admin !== true) {
22
22
  const permissions = this.accountability?.permissions?.find((permission) => {
23
23
  return permission.collection === collection && permission.action === 'update';
24
24
  });
25
25
  if (!permissions) {
26
- throw new ForbiddenException();
26
+ throw new ForbiddenError();
27
27
  }
28
28
  const allowedFields = permissions.fields ?? [];
29
29
  if (allowedFields[0] !== '*' && allowedFields.includes(sortField) === false) {
30
- throw new ForbiddenException();
30
+ throw new ForbiddenError();
31
31
  }
32
32
  }
33
33
  const primaryKeyField = this.schema.collections[collection].primary;
@@ -0,0 +1,14 @@
1
+ import type { ActionHandler } from '@directus/types';
2
+ import type { WebSocketClient } from '../websocket/types.js';
3
+ import type { WebSocketMessage } from '../websocket/messages.js';
4
+ export declare class WebSocketService {
5
+ private controller;
6
+ constructor();
7
+ on(event: 'connect' | 'message' | 'error' | 'close', callback: ActionHandler): void;
8
+ off(event: 'connect' | 'message' | 'error' | 'close', callback: ActionHandler): void;
9
+ broadcast(message: string | WebSocketMessage, filter?: {
10
+ user?: string;
11
+ role?: string;
12
+ }): void;
13
+ clients(): Set<WebSocketClient>;
14
+ }
@@ -0,0 +1,26 @@
1
+ import { getWebSocketController } from '../websocket/controllers/index.js';
2
+ import emitter from '../emitter.js';
3
+ export class WebSocketService {
4
+ controller;
5
+ constructor() {
6
+ this.controller = getWebSocketController();
7
+ }
8
+ on(event, callback) {
9
+ emitter.onAction('websocket.' + event, callback);
10
+ }
11
+ off(event, callback) {
12
+ emitter.offAction('websocket.' + event, callback);
13
+ }
14
+ broadcast(message, filter) {
15
+ this.controller.clients.forEach((client) => {
16
+ if (filter && filter.user && filter.user !== client.accountability?.user)
17
+ return;
18
+ if (filter && filter.role && filter.role !== client.accountability?.role)
19
+ return;
20
+ client.send(typeof message === 'string' ? message : JSON.stringify(message));
21
+ });
22
+ }
23
+ clients() {
24
+ return this.controller.clients;
25
+ }
26
+ }
@@ -73,9 +73,9 @@ class SynchronizationManagerRedis {
73
73
  namespace;
74
74
  client;
75
75
  constructor() {
76
- const config = getConfigFromEnv('SYNCHRONIZATION_REDIS');
77
- this.client = new Redis(env['SYNCHRONIZATION_REDIS'] ?? config);
78
- this.namespace = env['SYNCHRONIZATION_NAMESPACE'] ?? 'directus';
76
+ const config = getConfigFromEnv('REDIS');
77
+ this.client = new Redis(env['REDIS'] ?? config);
78
+ this.namespace = env['SYNCHRONIZATION_NAMESPACE'] ?? 'directus-sync';
79
79
  this.client.defineCommand('setGreaterThan', {
80
80
  numberOfKeys: 1,
81
81
  lua: SET_GREATER_THAN_SCRIPT,
@@ -2,7 +2,7 @@
2
2
  * I know this looks a little silly, but it allows us to explicitly differentiate between when we're
3
3
  * expecting an item vs any other generic object.
4
4
  */
5
- import type { BaseException } from '@directus/exceptions';
5
+ import type { DirectusError } from '@directus/errors';
6
6
  import type { EventContext } from '@directus/types';
7
7
  import type { MutationTracker } from '../services/items.js';
8
8
  export type Item = Record<string, any>;
@@ -46,7 +46,7 @@ export type MutationOptions = {
46
46
  * To keep track of mutation limits
47
47
  */
48
48
  mutationTracker?: MutationTracker | undefined;
49
- preMutationException?: BaseException | undefined;
49
+ preMutationError?: DirectusError | undefined;
50
50
  };
51
51
  export type ActionEventParams = {
52
52
  event: string | string[];
@@ -1,6 +1,6 @@
1
1
  import deepDiff from 'deep-diff';
2
2
  import { cloneDeep, merge, set } from 'lodash-es';
3
- import { clearSystemCache } from '../cache.js';
3
+ import { flushCaches } from '../cache.js';
4
4
  import { getHelpers } from '../database/helpers/index.js';
5
5
  import getDatabase from '../database/index.js';
6
6
  import emitter from '../emitter.js';
@@ -121,7 +121,12 @@ export async function applyDiff(currentSnapshot, snapshotDiff, options) {
121
121
  // then continue with nested collections recursively
122
122
  await createCollections(snapshotDiff.collections.filter(filterCollectionsForCreation));
123
123
  // delete top level collections (no group) first, then continue with nested collections recursively
124
- await deleteCollections(snapshotDiff.collections.filter(({ diff }) => diff[0]?.kind === DiffKind.DELETE && diff[0].lhs.meta?.group === null));
124
+ await deleteCollections(snapshotDiff.collections.filter(({ diff }) => {
125
+ if (diff.length === 0 || diff[0] === undefined)
126
+ return false;
127
+ const collectionDiff = diff[0];
128
+ return collectionDiff.kind === DiffKind.DELETE && collectionDiff.lhs?.meta?.group === null;
129
+ }));
125
130
  for (const { collection, diff } of snapshotDiff.collections) {
126
131
  if (diff?.[0]?.kind === DiffKind.EDIT || diff?.[0]?.kind === DiffKind.ARRAY) {
127
132
  const currentCollection = currentSnapshot.collections.find((field) => {
@@ -198,7 +203,11 @@ export async function applyDiff(currentSnapshot, snapshotDiff, options) {
198
203
  }
199
204
  if (diff?.[0]?.kind === DiffKind.NEW) {
200
205
  try {
201
- await relationsService.createOne(diff[0].rhs, mutationOptions);
206
+ await relationsService.createOne({
207
+ ...diff[0].rhs,
208
+ collection,
209
+ field,
210
+ }, mutationOptions);
202
211
  }
203
212
  catch (err) {
204
213
  logger.error(`Failed to create relation "${collection}.${field}"`);
@@ -237,7 +246,7 @@ export async function applyDiff(currentSnapshot, snapshotDiff, options) {
237
246
  if (runPostColumnChange) {
238
247
  await helpers.schema.postColumnChange();
239
248
  }
240
- await clearSystemCache();
249
+ await flushCaches();
241
250
  if (nestedActionEvents.length > 0) {
242
251
  const updatedSchema = await getSchema({ database, bypassCache: true });
243
252
  for (const nestedActionEvent of nestedActionEvents) {
@@ -1,14 +1,13 @@
1
1
  import { getFilterOperatorsForType, getOutputTypeForFunction } from '@directus/utils';
2
2
  import { clone, isPlainObject } from 'lodash-es';
3
+ import { customAlphabet } from 'nanoid/non-secure';
3
4
  import validate from 'uuid-validate';
4
5
  import { getHelpers } from '../database/helpers/index.js';
5
- import { InvalidQueryException } from '../exceptions/invalid-query.js';
6
+ import { InvalidQueryError } from '../errors/index.js';
6
7
  import { getColumnPath } from './get-column-path.js';
7
8
  import { getColumn } from './get-column.js';
8
9
  import { getRelationInfo } from './get-relation-info.js';
9
10
  import { stripFunction } from './strip-function.js';
10
- // @ts-ignore
11
- import { customAlphabet } from 'nanoid/non-secure';
12
11
  export const generateAlias = customAlphabet('abcdefghijklmnopqrstuvwxyz', 5);
13
12
  /**
14
13
  * Apply the Query to a given Knex query builder instance
@@ -79,7 +78,9 @@ function addJoin({ path, collection, aliasMap, rootQuery, schema, relations, kne
79
78
  else if (relationType === 'a2o') {
80
79
  const pathScope = pathParts[0].split(':')[1];
81
80
  if (!pathScope) {
82
- throw new InvalidQueryException(`You have to provide a collection scope when sorting or filtering on a many-to-any item`);
81
+ throw new InvalidQueryError({
82
+ reason: `You have to provide a collection scope when sorting or filtering on a many-to-any item`,
83
+ });
83
84
  }
84
85
  rootQuery.leftJoin({ [alias]: pathScope }, (joinClause) => {
85
86
  joinClause
@@ -113,7 +114,9 @@ function addJoin({ path, collection, aliasMap, rootQuery, schema, relations, kne
113
114
  else if (relationType === 'a2o') {
114
115
  const pathScope = pathParts[0].split(':')[1];
115
116
  if (!pathScope) {
116
- throw new InvalidQueryException(`You have to provide a collection scope when sorting or filtering on a many-to-any item`);
117
+ throw new InvalidQueryError({
118
+ reason: `You have to provide a collection scope when sorting or filtering on a many-to-any item`,
119
+ });
117
120
  }
118
121
  parent = pathScope;
119
122
  }
@@ -288,7 +291,9 @@ export function applyFilter(knex, schema, rootQuery, rootFilter, collection, ali
288
291
  }
289
292
  }
290
293
  if (filterPath.includes('_none') || filterPath.includes('_some')) {
291
- throw new InvalidQueryException(`"${filterPath.includes('_none') ? '_none' : '_some'}" can only be used with top level relational alias field`);
294
+ throw new InvalidQueryError({
295
+ reason: `"${filterPath.includes('_none') ? '_none' : '_some'}" can only be used with top level relational alias field`,
296
+ });
292
297
  }
293
298
  const { columnPath, targetCollection, addNestedPkField } = getColumnPath({
294
299
  path: filterPath,
@@ -314,7 +319,7 @@ export function applyFilter(knex, schema, rootQuery, rootFilter, collection, ali
314
319
  }
315
320
  function validateFilterField(fields, key, collection = 'unknown') {
316
321
  if (fields[key] === undefined) {
317
- throw new InvalidQueryException(`Invalid filter key "${key}" on "${collection}"`);
322
+ throw new InvalidQueryError({ reason: `Invalid filter key "${key}" on "${collection}"` });
318
323
  }
319
324
  return fields[key];
320
325
  }
@@ -323,11 +328,15 @@ export function applyFilter(knex, schema, rootQuery, rootFilter, collection, ali
323
328
  filterOperator = filterOperator.slice(1);
324
329
  }
325
330
  if (!getFilterOperatorsForType(type).includes(filterOperator)) {
326
- throw new InvalidQueryException(`"${type}" field type does not contain the "_${filterOperator}" filter operator`);
331
+ throw new InvalidQueryError({
332
+ reason: `"${type}" field type does not contain the "_${filterOperator}" filter operator`,
333
+ });
327
334
  }
328
335
  if (special.includes('conceal') &&
329
336
  !getFilterOperatorsForType('hash').includes(filterOperator)) {
330
- throw new InvalidQueryException(`Field with "conceal" special does not allow the "_${filterOperator}" filter operator`);
337
+ throw new InvalidQueryError({
338
+ reason: `Field with "conceal" special does not allow the "_${filterOperator}" filter operator`,
339
+ });
331
340
  }
332
341
  }
333
342
  function applyFilterToQuery(key, operator, compareValue, logical = 'and', originalCollectionName) {
@@ -337,18 +346,18 @@ export function applyFilter(knex, schema, rootQuery, rootFilter, collection, ali
337
346
  // Knex supports "raw" in the columnName parameter, but isn't typed as such. Too bad..
338
347
  // See https://github.com/knex/knex/issues/4518 @TODO remove as any once knex is updated
339
348
  // These operators don't rely on a value, and can thus be used without one (eg `?filter[field][_null]`)
340
- if (operator === '_null' || (operator === '_nnull' && compareValue === false)) {
349
+ if ((operator === '_null' && compareValue !== false) || (operator === '_nnull' && compareValue === false)) {
341
350
  dbQuery[logical].whereNull(selectionRaw);
342
351
  }
343
- if (operator === '_nnull' || (operator === '_null' && compareValue === false)) {
352
+ if ((operator === '_nnull' && compareValue !== false) || (operator === '_null' && compareValue === false)) {
344
353
  dbQuery[logical].whereNotNull(selectionRaw);
345
354
  }
346
- if (operator === '_empty' || (operator === '_nempty' && compareValue === false)) {
355
+ if ((operator === '_empty' && compareValue !== false) || (operator === '_nempty' && compareValue === false)) {
347
356
  dbQuery[logical].andWhere((query) => {
348
357
  query.whereNull(key).orWhere(key, '=', '');
349
358
  });
350
359
  }
351
- if (operator === '_nempty' || (operator === '_empty' && compareValue === false)) {
360
+ if ((operator === '_nempty' && compareValue !== false) || (operator === '_empty' && compareValue === false)) {
352
361
  dbQuery[logical].andWhere((query) => {
353
362
  query.whereNotNull(key).andWhere(key, '!=', '');
354
363
  });
@@ -1,4 +1,3 @@
1
- import { InvalidConfigException } from '../exceptions/index.js';
2
1
  import { getPermissions } from './get-permissions.js';
3
2
  export async function getAccountabilityForRole(role, context) {
4
3
  let generatedAccountability = context.accountability;
@@ -27,7 +26,7 @@ export async function getAccountabilityForRole(role, context) {
27
26
  .where({ id: role })
28
27
  .first();
29
28
  if (!roleInfo) {
30
- throw new InvalidConfigException(`Configured role "${role}" isn't a valid role ID or doesn't exist.`);
29
+ throw new Error(`Configured role "${role}" isn't a valid role ID or doesn't exist.`);
31
30
  }
32
31
  generatedAccountability = {
33
32
  role,
@@ -0,0 +1,2 @@
1
+ import type { Accountability } from '@directus/types';
2
+ export declare function getAccountabilityForToken(token?: string | null, accountability?: Accountability): Promise<Accountability>;
@@ -0,0 +1,50 @@
1
+ import getDatabase from '../database/index.js';
2
+ import env from '../env.js';
3
+ import { InvalidCredentialsError } from '../errors/index.js';
4
+ import isDirectusJWT from './is-directus-jwt.js';
5
+ import { verifyAccessJWT } from './jwt.js';
6
+ export async function getAccountabilityForToken(token, accountability) {
7
+ if (!accountability) {
8
+ accountability = {
9
+ user: null,
10
+ role: null,
11
+ admin: false,
12
+ app: false,
13
+ };
14
+ }
15
+ if (token) {
16
+ if (isDirectusJWT(token)) {
17
+ const payload = verifyAccessJWT(token, env['SECRET']);
18
+ accountability.role = payload.role;
19
+ accountability.admin = payload.admin_access === true || payload.admin_access == 1;
20
+ accountability.app = payload.app_access === true || payload.app_access == 1;
21
+ if (payload.share)
22
+ accountability.share = payload.share;
23
+ if (payload.share_scope)
24
+ accountability.share_scope = payload.share_scope;
25
+ if (payload.id)
26
+ accountability.user = payload.id;
27
+ }
28
+ else {
29
+ // Try finding the user with the provided token
30
+ const database = getDatabase();
31
+ const user = await database
32
+ .select('directus_users.id', 'directus_users.role', 'directus_roles.admin_access', 'directus_roles.app_access')
33
+ .from('directus_users')
34
+ .leftJoin('directus_roles', 'directus_users.role', 'directus_roles.id')
35
+ .where({
36
+ 'directus_users.token': token,
37
+ status: 'active',
38
+ })
39
+ .first();
40
+ if (!user) {
41
+ throw new InvalidCredentialsError();
42
+ }
43
+ accountability.user = user.id;
44
+ accountability.role = user.role;
45
+ accountability.admin = user.admin_access === true || user.admin_access == 1;
46
+ accountability.app = user.app_access === true || user.app_access == 1;
47
+ }
48
+ }
49
+ return accountability;
50
+ }
@@ -1,4 +1,4 @@
1
- import { InvalidQueryException } from '../exceptions/index.js';
1
+ import { InvalidQueryError } from '../errors/index.js';
2
2
  import { getRelationInfo } from './get-relation-info.js';
3
3
  /**
4
4
  * Converts a Directus field list path to the correct SQL names based on the constructed alias map.
@@ -15,7 +15,7 @@ export function getColumnPath({ path, collection, aliasMap, relations, schema })
15
15
  const pathRoot = pathParts[0].split(':')[0];
16
16
  const { relation, relationType } = getRelationInfo(relations, parentCollection, pathRoot);
17
17
  if (!relation) {
18
- throw new InvalidQueryException(`"${parentCollection}.${pathRoot}" is not a relational field`);
18
+ throw new InvalidQueryError({ reason: `"${parentCollection}.${pathRoot}" is not a relational field` });
19
19
  }
20
20
  const alias = parentFields ? aliasMap[`${parentFields}.${pathParts[0]}`]?.alias : aliasMap[pathParts[0]]?.alias;
21
21
  const remainingParts = pathParts.slice(1);
@@ -23,7 +23,9 @@ export function getColumnPath({ path, collection, aliasMap, relations, schema })
23
23
  if (relationType === 'a2o') {
24
24
  const pathScope = pathParts[0].split(':')[1];
25
25
  if (!pathScope) {
26
- throw new InvalidQueryException(`You have to provide a collection scope when sorting on a many-to-any item`);
26
+ throw new InvalidQueryError({
27
+ reason: `You have to provide a collection scope when sorting on a many-to-any item`,
28
+ });
27
29
  }
28
30
  parent = pathScope;
29
31
  }
@@ -1,7 +1,7 @@
1
1
  import { REGEX_BETWEEN_PARENS } from '@directus/constants';
2
2
  import { getFunctionsForType } from '@directus/utils';
3
3
  import { getFunctions } from '../database/helpers/index.js';
4
- import { InvalidQueryException } from '../exceptions/index.js';
4
+ import { InvalidQueryError } from '../errors/index.js';
5
5
  import { applyFunctionToColumnName } from './apply-function-to-column-name.js';
6
6
  /**
7
7
  * Return column prefixed by table. If column includes functions (like `year(date_created)`), the
@@ -25,7 +25,7 @@ export function getColumn(knex, table, column, alias = applyFunctionToColumnName
25
25
  const type = schema?.collections[collectionName]?.fields?.[columnName]?.type ?? 'unknown';
26
26
  const allowedFunctions = getFunctionsForType(type);
27
27
  if (allowedFunctions.includes(functionName) === false) {
28
- throw new InvalidQueryException(`Invalid function specified "${functionName}"`);
28
+ throw new InvalidQueryError({ reason: `Invalid function specified "${functionName}"` });
29
29
  }
30
30
  const result = fn[functionName](table, columnName, {
31
31
  type,
@@ -38,7 +38,7 @@ export function getColumn(knex, table, column, alias = applyFunctionToColumnName
38
38
  return result;
39
39
  }
40
40
  else {
41
- throw new InvalidQueryException(`Invalid function specified "${functionName}"`);
41
+ throw new InvalidQueryError({ reason: `Invalid function specified "${functionName}"` });
42
42
  }
43
43
  }
44
44
  if (alias && column !== alias) {
@@ -0,0 +1,7 @@
1
+ import { ItemsService } from '../services/index.js';
2
+ import type { AbstractServiceOptions } from '../types/services.js';
3
+ /**
4
+ * Select the correct service for the given collection. This allows the individual services to run
5
+ * their custom checks (f.e. it allows UsersService to prevent updating TFA secret from outside)
6
+ */
7
+ export declare function getService(collection: string, opts: AbstractServiceOptions): ItemsService;
@@ -0,0 +1,49 @@
1
+ import { ActivityService, DashboardsService, FilesService, FlowsService, FoldersService, ItemsService, NotificationsService, OperationsService, PanelsService, PermissionsService, PresetsService, RevisionsService, RolesService, SettingsService, SharesService, UsersService, WebhooksService, } from '../services/index.js';
2
+ /**
3
+ * Select the correct service for the given collection. This allows the individual services to run
4
+ * their custom checks (f.e. it allows UsersService to prevent updating TFA secret from outside)
5
+ */
6
+ export function getService(collection, opts) {
7
+ switch (collection) {
8
+ case 'directus_activity':
9
+ return new ActivityService(opts);
10
+ // case 'directus_collections':
11
+ // return new CollectionsService(opts);
12
+ case 'directus_dashboards':
13
+ return new DashboardsService(opts);
14
+ // case 'directus_fields':
15
+ // return new FieldsService(opts);
16
+ case 'directus_files':
17
+ return new FilesService(opts);
18
+ case 'directus_flows':
19
+ return new FlowsService(opts);
20
+ case 'directus_folders':
21
+ return new FoldersService(opts);
22
+ case 'directus_notifications':
23
+ return new NotificationsService(opts);
24
+ case 'directus_operations':
25
+ return new OperationsService(opts);
26
+ case 'directus_panels':
27
+ return new PanelsService(opts);
28
+ case 'directus_permissions':
29
+ return new PermissionsService(opts);
30
+ case 'directus_presets':
31
+ return new PresetsService(opts);
32
+ // case 'directus_relations':
33
+ // return new RelationsService(opts);
34
+ case 'directus_revisions':
35
+ return new RevisionsService(opts);
36
+ case 'directus_roles':
37
+ return new RolesService(opts);
38
+ case 'directus_settings':
39
+ return new SettingsService(opts);
40
+ case 'directus_shares':
41
+ return new SharesService(opts);
42
+ case 'directus_users':
43
+ return new UsersService(opts);
44
+ case 'directus_webhooks':
45
+ return new WebhooksService(opts);
46
+ default:
47
+ return new ItemsService(collection, opts);
48
+ }
49
+ }