@directus/api 11.0.1 → 11.1.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 (57) hide show
  1. package/dist/emitter.d.ts +3 -2
  2. package/dist/emitter.js +12 -4
  3. package/dist/env.js +15 -4
  4. package/dist/messenger.d.ts +3 -3
  5. package/dist/messenger.js +14 -5
  6. package/dist/middleware/authenticate.js +2 -38
  7. package/dist/server.js +10 -0
  8. package/dist/services/graphql/index.d.ts +0 -6
  9. package/dist/services/graphql/index.js +98 -57
  10. package/dist/services/graphql/subscription.d.ts +16 -0
  11. package/dist/services/graphql/subscription.js +77 -0
  12. package/dist/services/server.js +24 -0
  13. package/dist/services/websocket.d.ts +14 -0
  14. package/dist/services/websocket.js +26 -0
  15. package/dist/utils/apply-diff.js +11 -2
  16. package/dist/utils/apply-query.js +5 -6
  17. package/dist/utils/get-accountability-for-token.d.ts +2 -0
  18. package/dist/utils/get-accountability-for-token.js +50 -0
  19. package/dist/utils/get-service.d.ts +7 -0
  20. package/dist/utils/get-service.js +49 -0
  21. package/dist/utils/redact.d.ts +4 -0
  22. package/dist/utils/redact.js +15 -1
  23. package/dist/utils/to-boolean.d.ts +4 -0
  24. package/dist/utils/to-boolean.js +6 -0
  25. package/dist/websocket/authenticate.d.ts +6 -0
  26. package/dist/websocket/authenticate.js +62 -0
  27. package/dist/websocket/controllers/base.d.ts +42 -0
  28. package/dist/websocket/controllers/base.js +276 -0
  29. package/dist/websocket/controllers/graphql.d.ts +12 -0
  30. package/dist/websocket/controllers/graphql.js +102 -0
  31. package/dist/websocket/controllers/hooks.d.ts +1 -0
  32. package/dist/websocket/controllers/hooks.js +122 -0
  33. package/dist/websocket/controllers/index.d.ts +10 -0
  34. package/dist/websocket/controllers/index.js +35 -0
  35. package/dist/websocket/controllers/rest.d.ts +9 -0
  36. package/dist/websocket/controllers/rest.js +47 -0
  37. package/dist/websocket/exceptions.d.ts +16 -0
  38. package/dist/websocket/exceptions.js +55 -0
  39. package/dist/websocket/handlers/heartbeat.d.ts +11 -0
  40. package/dist/websocket/handlers/heartbeat.js +72 -0
  41. package/dist/websocket/handlers/index.d.ts +4 -0
  42. package/dist/websocket/handlers/index.js +11 -0
  43. package/dist/websocket/handlers/items.d.ts +6 -0
  44. package/dist/websocket/handlers/items.js +103 -0
  45. package/dist/websocket/handlers/subscribe.d.ts +43 -0
  46. package/dist/websocket/handlers/subscribe.js +278 -0
  47. package/dist/websocket/messages.d.ts +311 -0
  48. package/dist/websocket/messages.js +96 -0
  49. package/dist/websocket/types.d.ts +34 -0
  50. package/dist/websocket/types.js +1 -0
  51. package/dist/websocket/utils/get-expires-at-for-token.d.ts +1 -0
  52. package/dist/websocket/utils/get-expires-at-for-token.js +8 -0
  53. package/dist/websocket/utils/message.d.ts +4 -0
  54. package/dist/websocket/utils/message.js +27 -0
  55. package/dist/websocket/utils/wait-for-message.d.ts +4 -0
  56. package/dist/websocket/utils/wait-for-message.js +45 -0
  57. package/package.json +19 -14
@@ -0,0 +1,102 @@
1
+ import { CloseCode, MessageType, makeServer } from 'graphql-ws';
2
+ import env from '../../env.js';
3
+ import logger from '../../logger.js';
4
+ import { bindPubSub } from '../../services/graphql/subscription.js';
5
+ import { GraphQLService } from '../../services/index.js';
6
+ import { getSchema } from '../../utils/get-schema.js';
7
+ import { authenticateConnection, refreshAccountability } from '../authenticate.js';
8
+ import { handleWebSocketException } from '../exceptions.js';
9
+ import { ConnectionParams, WebSocketMessage } from '../messages.js';
10
+ import { getMessageType } from '../utils/message.js';
11
+ import SocketController from './base.js';
12
+ export class GraphQLSubscriptionController extends SocketController {
13
+ gql;
14
+ constructor(httpServer) {
15
+ super(httpServer, 'WEBSOCKETS_GRAPHQL');
16
+ this.server.on('connection', (ws, auth) => {
17
+ this.bindEvents(this.createClient(ws, auth));
18
+ });
19
+ this.gql = makeServer({
20
+ schema: async (ctx) => {
21
+ const accountability = ctx.extra.client.accountability;
22
+ // for now only the items will be watched, system events tbd
23
+ const service = new GraphQLService({
24
+ schema: await getSchema(),
25
+ scope: 'items',
26
+ accountability,
27
+ });
28
+ return service.getSchema();
29
+ },
30
+ });
31
+ bindPubSub();
32
+ logger.info(`GraphQL Subscriptions started at ws://${env['HOST']}:${env['PORT']}${this.endpoint}`);
33
+ }
34
+ bindEvents(client) {
35
+ const closedHandler = this.gql.opened({
36
+ protocol: client.protocol,
37
+ send: (data) => new Promise((resolve, reject) => {
38
+ client.send(data, (err) => (err ? reject(err) : resolve()));
39
+ }),
40
+ close: (code, reason) => client.close(code, reason),
41
+ onMessage: (cb) => {
42
+ client.on('parsed-message', async (message) => {
43
+ try {
44
+ if (getMessageType(message) === 'connection_init' && this.authentication.mode !== 'strict') {
45
+ const params = ConnectionParams.parse(message['payload'] ?? {});
46
+ if (this.authentication.mode === 'handshake') {
47
+ if (typeof params.access_token === 'string') {
48
+ const { accountability, expires_at } = await authenticateConnection({
49
+ access_token: params.access_token,
50
+ });
51
+ client.accountability = accountability;
52
+ client.expires_at = expires_at;
53
+ }
54
+ else {
55
+ client.close(CloseCode.Forbidden, 'Forbidden');
56
+ return;
57
+ }
58
+ }
59
+ }
60
+ else if (this.authentication.mode === 'handshake' && !client.accountability?.user) {
61
+ // the first message should authenticate successfully in this mode
62
+ client.close(CloseCode.Forbidden, 'Forbidden');
63
+ return;
64
+ }
65
+ else {
66
+ client.accountability = await refreshAccountability(client.accountability);
67
+ }
68
+ await cb(JSON.stringify(message));
69
+ }
70
+ catch (error) {
71
+ handleWebSocketException(client, error, MessageType.Error);
72
+ }
73
+ });
74
+ },
75
+ }, { client });
76
+ // notify server that the socket closed
77
+ client.once('close', (code, reason) => closedHandler(code, reason.toString()));
78
+ // check strict authentication status
79
+ if (this.authentication.mode === 'strict' && !client.accountability?.user) {
80
+ client.close(CloseCode.Forbidden, 'Forbidden');
81
+ }
82
+ }
83
+ setTokenExpireTimer(client) {
84
+ if (client.auth_timer !== null) {
85
+ clearTimeout(client.auth_timer);
86
+ client.auth_timer = null;
87
+ }
88
+ if (this.authentication.mode !== 'handshake')
89
+ return;
90
+ client.auth_timer = setTimeout(() => {
91
+ if (!client.accountability?.user) {
92
+ client.close(CloseCode.Forbidden, 'Forbidden');
93
+ }
94
+ }, this.authentication.timeout);
95
+ }
96
+ async handleHandshakeUpgrade({ request, socket, head }) {
97
+ this.server.handleUpgrade(request, socket, head, async (ws) => {
98
+ this.server.emit('connection', ws, { accountability: null, expires_at: null });
99
+ // actual enforcement is handled by the setTokenExpireTimer function
100
+ });
101
+ }
102
+ }
@@ -0,0 +1 @@
1
+ export declare function registerWebSocketEvents(): void;
@@ -0,0 +1,122 @@
1
+ import emitter from '../../emitter.js';
2
+ import { getMessenger } from '../../messenger.js';
3
+ let actionsRegistered = false;
4
+ export function registerWebSocketEvents() {
5
+ if (actionsRegistered)
6
+ return;
7
+ actionsRegistered = true;
8
+ registerActionHooks([
9
+ 'items',
10
+ 'activity',
11
+ 'collections',
12
+ 'folders',
13
+ 'permissions',
14
+ 'presets',
15
+ 'revisions',
16
+ 'roles',
17
+ 'settings',
18
+ 'users',
19
+ 'webhooks',
20
+ ]);
21
+ registerFieldsHooks();
22
+ registerFilesHooks();
23
+ registerRelationsHooks();
24
+ }
25
+ function registerActionHooks(modules) {
26
+ // register event hooks that can be handled in an uniform manner
27
+ for (const module of modules) {
28
+ registerAction(module + '.create', ({ key, collection, payload = {} }) => ({
29
+ collection,
30
+ action: 'create',
31
+ key,
32
+ payload,
33
+ }));
34
+ registerAction(module + '.update', ({ keys, collection, payload = {} }) => ({
35
+ collection,
36
+ action: 'update',
37
+ keys,
38
+ payload,
39
+ }));
40
+ registerAction(module + '.delete', ({ keys, collection, payload = [] }) => ({
41
+ collection,
42
+ action: 'delete',
43
+ keys,
44
+ payload,
45
+ }));
46
+ }
47
+ }
48
+ function registerFieldsHooks() {
49
+ // exception for field hooks that don't report `directus_fields` as being the collection
50
+ registerAction('fields.create', ({ key, payload = {} }) => ({
51
+ collection: 'directus_fields',
52
+ action: 'create',
53
+ key,
54
+ payload,
55
+ }));
56
+ registerAction('fields.update', ({ keys, payload = {} }) => ({
57
+ collection: 'directus_fields',
58
+ action: 'update',
59
+ keys,
60
+ payload,
61
+ }));
62
+ registerAction('fields.delete', ({ keys, payload = [] }) => ({
63
+ collection: 'directus_fields',
64
+ action: 'delete',
65
+ keys,
66
+ payload,
67
+ }));
68
+ }
69
+ function registerFilesHooks() {
70
+ // extra event for file uploads that doubles as create event
71
+ registerAction('files.upload', ({ key, collection, payload = {} }) => ({
72
+ collection,
73
+ action: 'create',
74
+ key,
75
+ payload,
76
+ }));
77
+ registerAction('files.update', ({ keys, collection, payload = {} }) => ({
78
+ collection,
79
+ action: 'update',
80
+ keys,
81
+ payload,
82
+ }));
83
+ registerAction('files.delete', ({ keys, collection, payload = [] }) => ({
84
+ collection,
85
+ action: 'delete',
86
+ keys,
87
+ payload,
88
+ }));
89
+ }
90
+ function registerRelationsHooks() {
91
+ // exception for relation hooks that don't report `directus_relations` as being the collection
92
+ registerAction('relations.create', ({ key, payload = {} }) => ({
93
+ collection: 'directus_relations',
94
+ action: 'create',
95
+ key,
96
+ payload: { ...payload, key },
97
+ }));
98
+ registerAction('relations.update', ({ keys, payload = {} }) => ({
99
+ collection: 'directus_relations',
100
+ action: 'update',
101
+ keys,
102
+ payload,
103
+ }));
104
+ registerAction('relations.delete', ({ collection, payload = [] }) => ({
105
+ collection: 'directus_relations',
106
+ action: 'delete',
107
+ keys: payload,
108
+ payload: { collection, fields: payload },
109
+ }));
110
+ }
111
+ /**
112
+ * Wrapper for emitter.onAction to hook into system events
113
+ * @param event The action event to watch
114
+ * @param transform Transformer function
115
+ */
116
+ function registerAction(event, transform) {
117
+ const messenger = getMessenger();
118
+ emitter.onAction(event, async (data) => {
119
+ // push the event through the Redis pub/sub
120
+ messenger.publish('websocket.event', transform(data));
121
+ });
122
+ }
@@ -0,0 +1,10 @@
1
+ /// <reference types="node" resolution-mode="require"/>
2
+ import type { Server as httpServer } from 'http';
3
+ import { GraphQLSubscriptionController } from './graphql.js';
4
+ import { WebSocketController } from './rest.js';
5
+ export declare function createWebSocketController(server: httpServer): void;
6
+ export declare function getWebSocketController(): WebSocketController;
7
+ export declare function createSubscriptionController(server: httpServer): void;
8
+ export declare function getSubscriptionController(): GraphQLSubscriptionController | undefined;
9
+ export * from './graphql.js';
10
+ export * from './rest.js';
@@ -0,0 +1,35 @@
1
+ import env from '../../env.js';
2
+ import { ServiceUnavailableException } from '../../index.js';
3
+ import { toBoolean } from '../../utils/to-boolean.js';
4
+ import { GraphQLSubscriptionController } from './graphql.js';
5
+ import { WebSocketController } from './rest.js';
6
+ let websocketController;
7
+ let subscriptionController;
8
+ export function createWebSocketController(server) {
9
+ if (toBoolean(env['WEBSOCKETS_REST_ENABLED'])) {
10
+ websocketController = new WebSocketController(server);
11
+ }
12
+ }
13
+ export function getWebSocketController() {
14
+ if (!toBoolean(env['WEBSOCKETS_ENABLED']) || !toBoolean(env['WEBSOCKETS_REST_ENABLED'])) {
15
+ throw new ServiceUnavailableException('WebSocket server is disabled', {
16
+ service: 'get-websocket-controller',
17
+ });
18
+ }
19
+ if (!websocketController) {
20
+ throw new ServiceUnavailableException('WebSocket server is not initialized', {
21
+ service: 'get-websocket-controller',
22
+ });
23
+ }
24
+ return websocketController;
25
+ }
26
+ export function createSubscriptionController(server) {
27
+ if (toBoolean(env['WEBSOCKETS_GRAPHQL_ENABLED'])) {
28
+ subscriptionController = new GraphQLSubscriptionController(server);
29
+ }
30
+ }
31
+ export function getSubscriptionController() {
32
+ return subscriptionController;
33
+ }
34
+ export * from './graphql.js';
35
+ export * from './rest.js';
@@ -0,0 +1,9 @@
1
+ /// <reference types="node" resolution-mode="require"/>
2
+ import type { Server as httpServer } from 'http';
3
+ import { WebSocketMessage } from '../messages.js';
4
+ import SocketController from './base.js';
5
+ export declare class WebSocketController extends SocketController {
6
+ constructor(httpServer: httpServer);
7
+ private bindEvents;
8
+ protected parseMessage(data: string): WebSocketMessage;
9
+ }
@@ -0,0 +1,47 @@
1
+ import { parseJSON } from '@directus/utils';
2
+ import emitter from '../../emitter.js';
3
+ import env from '../../env.js';
4
+ import logger from '../../logger.js';
5
+ import { refreshAccountability } from '../authenticate.js';
6
+ import { WebSocketException, handleWebSocketException } from '../exceptions.js';
7
+ import { WebSocketMessage } from '../messages.js';
8
+ import SocketController from './base.js';
9
+ export class WebSocketController extends SocketController {
10
+ constructor(httpServer) {
11
+ super(httpServer, 'WEBSOCKETS_REST');
12
+ this.server.on('connection', (ws, auth) => {
13
+ this.bindEvents(this.createClient(ws, auth));
14
+ });
15
+ logger.info(`WebSocket Server started at ws://${env['HOST']}:${env['PORT']}${this.endpoint}`);
16
+ }
17
+ bindEvents(client) {
18
+ client.on('parsed-message', async (message) => {
19
+ try {
20
+ message = WebSocketMessage.parse(await emitter.emitFilter('websocket.message', message, { client }));
21
+ client.accountability = await refreshAccountability(client.accountability);
22
+ emitter.emitAction('websocket.message', { message, client });
23
+ }
24
+ catch (error) {
25
+ handleWebSocketException(client, error, 'server');
26
+ return;
27
+ }
28
+ });
29
+ client.on('error', (event) => {
30
+ emitter.emitAction('websocket.error', { client, event });
31
+ });
32
+ client.on('close', (event) => {
33
+ emitter.emitAction('websocket.close', { client, event });
34
+ });
35
+ emitter.emitAction('websocket.connect', { client });
36
+ }
37
+ parseMessage(data) {
38
+ let message;
39
+ try {
40
+ message = parseJSON(data);
41
+ }
42
+ catch (err) {
43
+ throw new WebSocketException('server', 'INVALID_PAYLOAD', 'Unable to parse the incoming message.');
44
+ }
45
+ return message;
46
+ }
47
+ }
@@ -0,0 +1,16 @@
1
+ import { BaseException } from '@directus/exceptions';
2
+ import type { WebSocket } from 'ws';
3
+ import { ZodError } from 'zod';
4
+ import type { WebSocketResponse } from './messages.js';
5
+ import type { WebSocketClient } from './types.js';
6
+ export declare class WebSocketException extends Error {
7
+ type: string;
8
+ code: string;
9
+ uid: string | number | undefined;
10
+ constructor(type: string, code: string, message: string, uid?: string | number);
11
+ toJSON(): WebSocketResponse;
12
+ toMessage(): string;
13
+ static fromException(error: BaseException, type?: string): WebSocketException;
14
+ static fromZodError(error: ZodError, type?: string): WebSocketException;
15
+ }
16
+ export declare function handleWebSocketException(client: WebSocketClient | WebSocket, error: unknown, type?: string): void;
@@ -0,0 +1,55 @@
1
+ import { BaseException } from '@directus/exceptions';
2
+ import { ZodError } from 'zod';
3
+ import { fromZodError } from 'zod-validation-error';
4
+ import logger from '../logger.js';
5
+ export class WebSocketException 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 fromException(error, type = 'unknown') {
33
+ return new WebSocketException(type, error.code, error.message);
34
+ }
35
+ static fromZodError(error, type = 'unknown') {
36
+ const zError = fromZodError(error);
37
+ return new WebSocketException(type, 'INVALID_PAYLOAD', zError.message);
38
+ }
39
+ }
40
+ export function handleWebSocketException(client, error, type) {
41
+ if (error instanceof BaseException) {
42
+ client.send(WebSocketException.fromException(error, type).toMessage());
43
+ return;
44
+ }
45
+ if (error instanceof WebSocketException) {
46
+ client.send(error.toMessage());
47
+ return;
48
+ }
49
+ if (error instanceof ZodError) {
50
+ client.send(WebSocketException.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 { WebSocketException, handleWebSocketException } from '../exceptions.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
+ handleWebSocketException(client, err, 'items');
18
+ });
19
+ }
20
+ catch (err) {
21
+ handleWebSocketException(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 WebSocketException('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 WebSocketException('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
+ }