@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
@@ -0,0 +1,55 @@
1
+ import { isDirectusError } from '@directus/errors';
2
+ import { ZodError } from 'zod';
3
+ import { fromZodError } from 'zod-validation-error';
4
+ import logger from '../logger.js';
5
+ export class WebSocketError extends Error {
6
+ type;
7
+ code;
8
+ uid;
9
+ constructor(type, code, message, uid) {
10
+ super(message);
11
+ this.type = type;
12
+ this.code = code;
13
+ this.uid = uid;
14
+ }
15
+ toJSON() {
16
+ const message = {
17
+ type: this.type,
18
+ status: 'error',
19
+ error: {
20
+ code: this.code,
21
+ message: this.message,
22
+ },
23
+ };
24
+ if (this.uid !== undefined) {
25
+ message.uid = this.uid;
26
+ }
27
+ return message;
28
+ }
29
+ toMessage() {
30
+ return JSON.stringify(this.toJSON());
31
+ }
32
+ static fromError(error, type = 'unknown') {
33
+ return new WebSocketError(type, error.code, error.message);
34
+ }
35
+ static fromZodError(error, type = 'unknown') {
36
+ const zError = fromZodError(error);
37
+ return new WebSocketError(type, 'INVALID_PAYLOAD', zError.message);
38
+ }
39
+ }
40
+ export function handleWebSocketError(client, error, type) {
41
+ if (isDirectusError(error)) {
42
+ client.send(WebSocketError.fromError(error, type).toMessage());
43
+ return;
44
+ }
45
+ if (error instanceof WebSocketError) {
46
+ client.send(error.toMessage());
47
+ return;
48
+ }
49
+ if (error instanceof ZodError) {
50
+ client.send(WebSocketError.fromZodError(error, type).toMessage());
51
+ return;
52
+ }
53
+ // unhandled exceptions
54
+ logger.error(`WebSocket unhandled exception ${JSON.stringify({ type, error })}`);
55
+ }
@@ -0,0 +1,11 @@
1
+ import { WebSocketController } from '../controllers/index.js';
2
+ import { WebSocketMessage } from '../messages.js';
3
+ import type { WebSocketClient } from '../types.js';
4
+ export declare class HeartbeatHandler {
5
+ private pulse;
6
+ private controller;
7
+ constructor(controller?: WebSocketController);
8
+ private checkClients;
9
+ onMessage(client: WebSocketClient, message: WebSocketMessage): void;
10
+ pingClients(): void;
11
+ }
@@ -0,0 +1,72 @@
1
+ import emitter from '../../emitter.js';
2
+ import env from '../../env.js';
3
+ import { toBoolean } from '../../utils/to-boolean.js';
4
+ import { WebSocketController, getWebSocketController } from '../controllers/index.js';
5
+ import { WebSocketMessage } from '../messages.js';
6
+ import { fmtMessage, getMessageType } from '../utils/message.js';
7
+ const HEARTBEAT_FREQUENCY = Number(env['WEBSOCKETS_HEARTBEAT_PERIOD']) * 1000;
8
+ export class HeartbeatHandler {
9
+ pulse;
10
+ controller;
11
+ constructor(controller) {
12
+ this.controller = controller ?? getWebSocketController();
13
+ emitter.onAction('websocket.message', ({ client, message }) => {
14
+ try {
15
+ this.onMessage(client, WebSocketMessage.parse(message));
16
+ }
17
+ catch {
18
+ /* ignore errors */
19
+ }
20
+ });
21
+ if (toBoolean(env['WEBSOCKETS_HEARTBEAT_ENABLED']) === true) {
22
+ emitter.onAction('websocket.connect', () => this.checkClients());
23
+ emitter.onAction('websocket.error', () => this.checkClients());
24
+ emitter.onAction('websocket.close', () => this.checkClients());
25
+ }
26
+ }
27
+ checkClients() {
28
+ const hasClients = this.controller.clients.size > 0;
29
+ if (hasClients && !this.pulse) {
30
+ this.pulse = setInterval(() => {
31
+ this.pingClients();
32
+ }, HEARTBEAT_FREQUENCY);
33
+ }
34
+ if (!hasClients && this.pulse) {
35
+ clearInterval(this.pulse);
36
+ this.pulse = undefined;
37
+ }
38
+ }
39
+ onMessage(client, message) {
40
+ if (getMessageType(message) !== 'ping')
41
+ return;
42
+ // send pong message back as acknowledgement
43
+ const data = 'uid' in message ? { uid: message.uid } : {};
44
+ client.send(fmtMessage('pong', data));
45
+ }
46
+ pingClients() {
47
+ const pendingClients = new Set(this.controller.clients);
48
+ const activeClients = new Set();
49
+ const timeout = setTimeout(() => {
50
+ // close connections that haven't responded
51
+ for (const client of pendingClients) {
52
+ client.close();
53
+ }
54
+ }, HEARTBEAT_FREQUENCY);
55
+ const messageWatcher = ({ client }) => {
56
+ // any message means this connection is still open
57
+ if (!activeClients.has(client)) {
58
+ pendingClients.delete(client);
59
+ activeClients.add(client);
60
+ }
61
+ if (pendingClients.size === 0) {
62
+ clearTimeout(timeout);
63
+ emitter.offAction('websocket.message', messageWatcher);
64
+ }
65
+ };
66
+ emitter.onAction('websocket.message', messageWatcher);
67
+ // ping all the clients
68
+ for (const client of pendingClients) {
69
+ client.send(fmtMessage('ping'));
70
+ }
71
+ }
72
+ }
@@ -0,0 +1,4 @@
1
+ export declare function startWebSocketHandlers(): void;
2
+ export * from './heartbeat.js';
3
+ export * from './items.js';
4
+ export * from './subscribe.js';
@@ -0,0 +1,11 @@
1
+ import { HeartbeatHandler } from './heartbeat.js';
2
+ import { ItemsHandler } from './items.js';
3
+ import { SubscribeHandler } from './subscribe.js';
4
+ export function startWebSocketHandlers() {
5
+ new HeartbeatHandler();
6
+ new ItemsHandler();
7
+ new SubscribeHandler();
8
+ }
9
+ export * from './heartbeat.js';
10
+ export * from './items.js';
11
+ export * from './subscribe.js';
@@ -0,0 +1,6 @@
1
+ import { WebSocketItemsMessage } from '../messages.js';
2
+ import type { WebSocketClient } from '../types.js';
3
+ export declare class ItemsHandler {
4
+ constructor();
5
+ onMessage(client: WebSocketClient, message: WebSocketItemsMessage): Promise<void>;
6
+ }
@@ -0,0 +1,103 @@
1
+ import emitter from '../../emitter.js';
2
+ import { ItemsService, MetaService } from '../../services/index.js';
3
+ import { getSchema } from '../../utils/get-schema.js';
4
+ import { sanitizeQuery } from '../../utils/sanitize-query.js';
5
+ import { WebSocketError, handleWebSocketError } from '../errors.js';
6
+ import { WebSocketItemsMessage } from '../messages.js';
7
+ import { fmtMessage, getMessageType } from '../utils/message.js';
8
+ export class ItemsHandler {
9
+ constructor() {
10
+ emitter.onAction('websocket.message', ({ client, message }) => {
11
+ if (getMessageType(message) !== 'items')
12
+ return;
13
+ try {
14
+ const parsedMessage = WebSocketItemsMessage.parse(message);
15
+ this.onMessage(client, parsedMessage).catch((err) => {
16
+ // this catch is required because the async onMessage function is not awaited
17
+ handleWebSocketError(client, err, 'items');
18
+ });
19
+ }
20
+ catch (err) {
21
+ handleWebSocketError(client, err, 'items');
22
+ }
23
+ });
24
+ }
25
+ async onMessage(client, message) {
26
+ const uid = message.uid;
27
+ const accountability = client.accountability;
28
+ const schema = await getSchema();
29
+ if (!schema.collections[message.collection] || message.collection.startsWith('directus_')) {
30
+ throw new WebSocketError('items', 'INVALID_COLLECTION', 'The provided collection does not exists or is not accessible.', uid);
31
+ }
32
+ const isSingleton = !!schema.collections[message.collection]?.singleton;
33
+ const service = new ItemsService(message.collection, { schema, accountability });
34
+ const metaService = new MetaService({ schema, accountability });
35
+ let result, meta;
36
+ if (message.action === 'create') {
37
+ const query = sanitizeQuery(message?.query ?? {}, accountability);
38
+ if (Array.isArray(message.data)) {
39
+ const keys = await service.createMany(message.data);
40
+ result = await service.readMany(keys, query);
41
+ }
42
+ else {
43
+ const key = await service.createOne(message.data);
44
+ result = await service.readOne(key, query);
45
+ }
46
+ }
47
+ if (message.action === 'read') {
48
+ const query = sanitizeQuery(message.query ?? {}, accountability);
49
+ if (message.id) {
50
+ result = await service.readOne(message.id, query);
51
+ }
52
+ else if (message.ids) {
53
+ result = await service.readMany(message.ids, query);
54
+ }
55
+ else if (isSingleton) {
56
+ result = await service.readSingleton(query);
57
+ }
58
+ else {
59
+ result = await service.readByQuery(query);
60
+ }
61
+ meta = await metaService.getMetaForQuery(message.collection, query);
62
+ }
63
+ if (message.action === 'update') {
64
+ const query = sanitizeQuery(message.query ?? {}, accountability);
65
+ if (message.id) {
66
+ const key = await service.updateOne(message.id, message.data);
67
+ result = await service.readOne(key);
68
+ }
69
+ else if (message.ids) {
70
+ const keys = await service.updateMany(message.ids, message.data);
71
+ meta = await metaService.getMetaForQuery(message.collection, query);
72
+ result = await service.readMany(keys, query);
73
+ }
74
+ else if (isSingleton) {
75
+ await service.upsertSingleton(message.data);
76
+ result = await service.readSingleton(query);
77
+ }
78
+ else {
79
+ const keys = await service.updateByQuery(query, message.data);
80
+ meta = await metaService.getMetaForQuery(message.collection, query);
81
+ result = await service.readMany(keys, query);
82
+ }
83
+ }
84
+ if (message.action === 'delete') {
85
+ if (message.id) {
86
+ await service.deleteOne(message.id);
87
+ result = message.id;
88
+ }
89
+ else if (message.ids) {
90
+ await service.deleteMany(message.ids);
91
+ result = message.ids;
92
+ }
93
+ else if (message.query) {
94
+ const query = sanitizeQuery(message.query, accountability);
95
+ result = await service.deleteByQuery(query);
96
+ }
97
+ else {
98
+ throw new WebSocketError('items', 'INVALID_PAYLOAD', "Either 'ids', 'id' or 'query' is required for a DELETE request.", uid);
99
+ }
100
+ }
101
+ client.send(fmtMessage('items', { data: result, ...(meta ? { meta } : {}) }, uid));
102
+ }
103
+ }
@@ -0,0 +1,43 @@
1
+ import type { Messenger } from '../../messenger.js';
2
+ import type { WebSocketEvent } from '../messages.js';
3
+ import { WebSocketSubscribeMessage } from '../messages.js';
4
+ import type { Subscription, WebSocketClient } from '../types.js';
5
+ /**
6
+ * Handler responsible for subscriptions
7
+ */
8
+ export declare class SubscribeHandler {
9
+ subscriptions: Record<string, Set<Subscription>>;
10
+ protected messenger: Messenger;
11
+ /**
12
+ * Initialize the handler
13
+ */
14
+ constructor();
15
+ /**
16
+ * Hook into websocket client lifecycle events
17
+ */
18
+ bindWebSocket(): void;
19
+ /**
20
+ * Register a subscription
21
+ * @param subscription
22
+ */
23
+ subscribe(subscription: Subscription): void;
24
+ /**
25
+ * Remove a subscription
26
+ * @param subscription
27
+ */
28
+ unsubscribe(client: WebSocketClient, uid?: string | number): void;
29
+ /**
30
+ * Dispatch event to subscriptions
31
+ */
32
+ dispatch(event: WebSocketEvent): Promise<void>;
33
+ /**
34
+ * Handle incoming (un)subscribe requests
35
+ */
36
+ onMessage(client: WebSocketClient, message: WebSocketSubscribeMessage): Promise<void>;
37
+ private getSinglePayload;
38
+ private getMultiPayload;
39
+ private getCollectionPayload;
40
+ private getFieldsPayload;
41
+ private getItemsPayload;
42
+ private getSubscription;
43
+ }
@@ -0,0 +1,278 @@
1
+ import emitter from '../../emitter.js';
2
+ import { InvalidPayloadError } from '../../errors/index.js';
3
+ import { getMessenger } from '../../messenger.js';
4
+ import { CollectionsService, FieldsService, MetaService } from '../../services/index.js';
5
+ import { getSchema } from '../../utils/get-schema.js';
6
+ import { getService } from '../../utils/get-service.js';
7
+ import { sanitizeQuery } from '../../utils/sanitize-query.js';
8
+ import { refreshAccountability } from '../authenticate.js';
9
+ import { WebSocketError, handleWebSocketError } from '../errors.js';
10
+ import { WebSocketSubscribeMessage } from '../messages.js';
11
+ import { fmtMessage, getMessageType } from '../utils/message.js';
12
+ /**
13
+ * Handler responsible for subscriptions
14
+ */
15
+ export class SubscribeHandler {
16
+ // storage of subscriptions per collection
17
+ subscriptions;
18
+ // internal message bus
19
+ messenger;
20
+ /**
21
+ * Initialize the handler
22
+ */
23
+ constructor() {
24
+ this.subscriptions = {};
25
+ this.messenger = getMessenger();
26
+ this.bindWebSocket();
27
+ // listen to the Redis pub/sub and dispatch
28
+ this.messenger.subscribe('websocket.event', (message) => {
29
+ try {
30
+ this.dispatch(message);
31
+ }
32
+ catch {
33
+ // don't error on an invalid event from the messenger
34
+ }
35
+ });
36
+ }
37
+ /**
38
+ * Hook into websocket client lifecycle events
39
+ */
40
+ bindWebSocket() {
41
+ // listen to incoming messages on the connected websockets
42
+ emitter.onAction('websocket.message', ({ client, message }) => {
43
+ if (!['subscribe', 'unsubscribe'].includes(getMessageType(message)))
44
+ return;
45
+ try {
46
+ this.onMessage(client, WebSocketSubscribeMessage.parse(message));
47
+ }
48
+ catch (error) {
49
+ handleWebSocketError(client, error, 'subscribe');
50
+ }
51
+ });
52
+ // unsubscribe when a connection drops
53
+ emitter.onAction('websocket.error', ({ client }) => this.unsubscribe(client));
54
+ emitter.onAction('websocket.close', ({ client }) => this.unsubscribe(client));
55
+ }
56
+ /**
57
+ * Register a subscription
58
+ * @param subscription
59
+ */
60
+ subscribe(subscription) {
61
+ const { collection } = subscription;
62
+ if ('item' in subscription && ['directus_fields', 'directus_relations'].includes(collection)) {
63
+ throw new InvalidPayloadError({ reason: `Cannot subscribe to a specific item in the ${collection} collection.` });
64
+ }
65
+ if (!this.subscriptions[collection]) {
66
+ this.subscriptions[collection] = new Set();
67
+ }
68
+ this.subscriptions[collection]?.add(subscription);
69
+ }
70
+ /**
71
+ * Remove a subscription
72
+ * @param subscription
73
+ */
74
+ unsubscribe(client, uid) {
75
+ if (uid !== undefined) {
76
+ const subscription = this.getSubscription(client, String(uid));
77
+ if (subscription) {
78
+ this.subscriptions[subscription.collection]?.delete(subscription);
79
+ }
80
+ }
81
+ else {
82
+ for (const key of Object.keys(this.subscriptions)) {
83
+ const subscriptions = Array.from(this.subscriptions[key] || []);
84
+ for (let i = subscriptions.length - 1; i >= 0; i--) {
85
+ const subscription = subscriptions[i];
86
+ if (!subscription)
87
+ continue;
88
+ if (subscription.client === client && (!uid || subscription.uid === uid)) {
89
+ this.subscriptions[key]?.delete(subscription);
90
+ }
91
+ }
92
+ }
93
+ }
94
+ }
95
+ /**
96
+ * Dispatch event to subscriptions
97
+ */
98
+ async dispatch(event) {
99
+ const subscriptions = this.subscriptions[event.collection];
100
+ if (!subscriptions || subscriptions.size === 0)
101
+ return;
102
+ const schema = await getSchema();
103
+ for (const subscription of subscriptions) {
104
+ const { client } = subscription;
105
+ if (subscription.event !== undefined && event.action !== subscription.event) {
106
+ continue; // skip filtered events
107
+ }
108
+ try {
109
+ client.accountability = await refreshAccountability(client.accountability);
110
+ const result = 'item' in subscription
111
+ ? await this.getSinglePayload(subscription, client.accountability, schema, event)
112
+ : await this.getMultiPayload(subscription, client.accountability, schema, event);
113
+ if (Array.isArray(result?.['data']) && result?.['data']?.length === 0)
114
+ return;
115
+ client.send(fmtMessage('subscription', result, subscription.uid));
116
+ }
117
+ catch (err) {
118
+ handleWebSocketError(client, err, 'subscribe');
119
+ }
120
+ }
121
+ }
122
+ /**
123
+ * Handle incoming (un)subscribe requests
124
+ */
125
+ async onMessage(client, message) {
126
+ if (getMessageType(message) === 'subscribe') {
127
+ try {
128
+ const collection = String(message.collection);
129
+ const accountability = client.accountability;
130
+ const schema = await getSchema();
131
+ if (!accountability?.admin && !schema.collections[collection]) {
132
+ throw new WebSocketError('subscribe', 'INVALID_COLLECTION', 'The provided collection does not exists or is not accessible.', message.uid);
133
+ }
134
+ const subscription = {
135
+ client,
136
+ collection,
137
+ };
138
+ if ('event' in message) {
139
+ subscription.event = message.event;
140
+ }
141
+ if ('query' in message) {
142
+ subscription.query = sanitizeQuery(message.query, accountability);
143
+ }
144
+ if ('item' in message)
145
+ subscription.item = String(message.item);
146
+ if ('uid' in message) {
147
+ subscription.uid = String(message.uid);
148
+ // remove the subscription if it already exists
149
+ this.unsubscribe(client, subscription.uid);
150
+ }
151
+ let data;
152
+ if (subscription.event === undefined) {
153
+ data =
154
+ 'item' in subscription
155
+ ? await this.getSinglePayload(subscription, accountability, schema)
156
+ : await this.getMultiPayload(subscription, accountability, schema);
157
+ }
158
+ else {
159
+ data = { event: 'init' };
160
+ }
161
+ // if no errors were thrown register the subscription
162
+ this.subscribe(subscription);
163
+ // send an initial response
164
+ client.send(fmtMessage('subscription', data, subscription.uid));
165
+ }
166
+ catch (err) {
167
+ handleWebSocketError(client, err, 'subscribe');
168
+ }
169
+ }
170
+ if (getMessageType(message) === 'unsubscribe') {
171
+ try {
172
+ this.unsubscribe(client, message.uid);
173
+ client.send(fmtMessage('subscription', { event: 'unsubscribe' }, message.uid));
174
+ }
175
+ catch (err) {
176
+ handleWebSocketError(client, err, 'unsubscribe');
177
+ }
178
+ }
179
+ }
180
+ async getSinglePayload(subscription, accountability, schema, event) {
181
+ const metaService = new MetaService({ schema, accountability });
182
+ const query = subscription.query ?? {};
183
+ const id = subscription.item;
184
+ const result = {
185
+ event: event?.action ?? 'init',
186
+ };
187
+ if (subscription.collection === 'directus_collections') {
188
+ const service = new CollectionsService({ schema, accountability });
189
+ result['data'] = await service.readOne(String(id));
190
+ }
191
+ else {
192
+ const service = getService(subscription.collection, { schema, accountability });
193
+ result['data'] = await service.readOne(id, query);
194
+ }
195
+ if ('meta' in query) {
196
+ result['meta'] = await metaService.getMetaForQuery(subscription.collection, query);
197
+ }
198
+ return result;
199
+ }
200
+ async getMultiPayload(subscription, accountability, schema, event) {
201
+ const metaService = new MetaService({ schema, accountability });
202
+ const result = {
203
+ event: event?.action ?? 'init',
204
+ };
205
+ switch (subscription.collection) {
206
+ case 'directus_collections':
207
+ result['data'] = await this.getCollectionPayload(accountability, schema, event);
208
+ break;
209
+ case 'directus_fields':
210
+ result['data'] = await this.getFieldsPayload(accountability, schema, event);
211
+ break;
212
+ case 'directus_relations':
213
+ result['data'] = event?.payload;
214
+ break;
215
+ default:
216
+ result['data'] = await this.getItemsPayload(subscription, accountability, schema, event);
217
+ break;
218
+ }
219
+ const query = subscription.query ?? {};
220
+ if ('meta' in query) {
221
+ result['meta'] = await metaService.getMetaForQuery(subscription.collection, query);
222
+ }
223
+ return result;
224
+ }
225
+ async getCollectionPayload(accountability, schema, event) {
226
+ const service = new CollectionsService({ schema, accountability });
227
+ if (!event?.action) {
228
+ return await service.readByQuery();
229
+ }
230
+ else if (event.action === 'create') {
231
+ return await service.readMany([String(event.key)]);
232
+ }
233
+ else if (event.action === 'delete') {
234
+ return event.keys;
235
+ }
236
+ else {
237
+ return await service.readMany(event.keys.map((key) => String(key)));
238
+ }
239
+ }
240
+ async getFieldsPayload(accountability, schema, event) {
241
+ const service = new FieldsService({ schema, accountability });
242
+ if (!event?.action) {
243
+ return await service.readAll();
244
+ }
245
+ else if (event.action === 'delete') {
246
+ return event.keys;
247
+ }
248
+ else {
249
+ return await service.readOne(event.payload?.['collection'], event.payload?.['field']);
250
+ }
251
+ }
252
+ async getItemsPayload(subscription, accountability, schema, event) {
253
+ const query = subscription.query ?? {};
254
+ const service = getService(subscription.collection, { schema, accountability });
255
+ if (!event?.action) {
256
+ return await service.readByQuery(query);
257
+ }
258
+ else if (event.action === 'create') {
259
+ return await service.readMany([event.key], query);
260
+ }
261
+ else if (event.action === 'delete') {
262
+ return event.keys;
263
+ }
264
+ else {
265
+ return await service.readMany(event.keys, query);
266
+ }
267
+ }
268
+ getSubscription(client, uid) {
269
+ for (const userSubscriptions of Object.values(this.subscriptions)) {
270
+ for (const subscription of userSubscriptions) {
271
+ if (subscription.client === client && subscription.uid === uid) {
272
+ return subscription;
273
+ }
274
+ }
275
+ }
276
+ return undefined;
277
+ }
278
+ }