@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.
- package/dist/app.js +3 -4
- package/dist/auth/auth.d.ts +4 -4
- package/dist/auth/auth.js +2 -2
- package/dist/auth/drivers/ldap.js +20 -17
- package/dist/auth/drivers/local.js +5 -5
- package/dist/auth/drivers/oauth2.js +16 -16
- package/dist/auth/drivers/openid.js +18 -17
- package/dist/auth/drivers/saml.js +6 -7
- package/dist/auth.js +3 -2
- package/dist/cache.js +3 -13
- package/dist/cli/utils/create-env/env-stub.liquid +5 -7
- package/dist/controllers/activity.js +7 -6
- package/dist/controllers/assets.js +25 -12
- package/dist/controllers/auth.js +8 -7
- package/dist/controllers/collections.js +4 -3
- package/dist/controllers/dashboards.js +5 -4
- package/dist/controllers/extensions.js +3 -3
- package/dist/controllers/fields.js +9 -8
- package/dist/controllers/files.js +11 -11
- package/dist/controllers/flows.js +5 -4
- package/dist/controllers/folders.js +5 -4
- package/dist/controllers/items.js +14 -13
- package/dist/controllers/not-found.js +2 -2
- package/dist/controllers/notifications.js +5 -4
- package/dist/controllers/operations.js +5 -4
- package/dist/controllers/panels.js +5 -4
- package/dist/controllers/permissions.js +5 -4
- package/dist/controllers/presets.js +5 -4
- package/dist/controllers/relations.js +6 -5
- package/dist/controllers/roles.js +5 -4
- package/dist/controllers/schema.js +8 -8
- package/dist/controllers/server.js +2 -2
- package/dist/controllers/settings.js +3 -2
- package/dist/controllers/shares.js +7 -6
- package/dist/controllers/translations.js +6 -5
- package/dist/controllers/users.js +22 -21
- package/dist/controllers/utils.js +10 -10
- package/dist/controllers/webhooks.js +5 -4
- package/dist/{exceptions/database → database/errors}/dialects/mssql.js +8 -18
- package/dist/{exceptions/database → database/errors}/dialects/mysql.js +9 -19
- package/dist/{exceptions/database → database/errors}/dialects/oracle.js +2 -2
- package/dist/{exceptions/database → database/errors}/dialects/postgres.js +7 -18
- package/dist/{exceptions/database → database/errors}/dialects/sqlite.js +7 -10
- package/dist/{exceptions/database → database/errors}/translate.js +1 -1
- package/dist/database/migrations/run.js +10 -1
- package/dist/emitter.d.ts +3 -2
- package/dist/emitter.js +12 -4
- package/dist/env.js +21 -17
- package/dist/errors/codes.d.ts +29 -0
- package/dist/errors/codes.js +30 -0
- package/dist/errors/contains-null-values.d.ts +7 -0
- package/dist/errors/contains-null-values.js +4 -0
- package/dist/errors/content-too-large.d.ts +1 -0
- package/dist/errors/content-too-large.js +3 -0
- package/dist/errors/forbidden.d.ts +1 -0
- package/dist/errors/forbidden.js +3 -0
- package/dist/errors/hit-rate-limit.d.ts +6 -0
- package/dist/errors/hit-rate-limit.js +8 -0
- package/dist/errors/illegal-asset-transformation.d.ts +4 -0
- package/dist/errors/illegal-asset-transformation.js +3 -0
- package/dist/errors/index.d.ts +28 -0
- package/dist/errors/index.js +28 -0
- package/dist/errors/invalid-credentials.d.ts +1 -0
- package/dist/errors/invalid-credentials.js +3 -0
- package/dist/errors/invalid-foreign-key.d.ts +6 -0
- package/dist/errors/invalid-foreign-key.js +14 -0
- package/dist/errors/invalid-ip.d.ts +1 -0
- package/dist/errors/invalid-ip.js +3 -0
- package/dist/errors/invalid-otp.d.ts +1 -0
- package/dist/errors/invalid-otp.js +3 -0
- package/dist/errors/invalid-payload.d.ts +5 -0
- package/dist/errors/invalid-payload.js +4 -0
- package/dist/errors/invalid-provider-config.d.ts +5 -0
- package/dist/errors/invalid-provider-config.js +3 -0
- package/dist/errors/invalid-provider.d.ts +1 -0
- package/dist/errors/invalid-provider.js +3 -0
- package/dist/errors/invalid-query.d.ts +5 -0
- package/dist/errors/invalid-query.js +4 -0
- package/dist/errors/invalid-token.d.ts +1 -0
- package/dist/errors/invalid-token.js +3 -0
- package/dist/errors/method-not-allowed.d.ts +6 -0
- package/dist/errors/method-not-allowed.js +6 -0
- package/dist/errors/not-null-violation.d.ts +6 -0
- package/dist/errors/not-null-violation.js +14 -0
- package/dist/errors/range-not-satisfiable.d.ts +7 -0
- package/dist/errors/range-not-satisfiable.js +7 -0
- package/dist/errors/record-not-unique.d.ts +6 -0
- package/dist/errors/record-not-unique.js +14 -0
- package/dist/errors/route-not-found.d.ts +5 -0
- package/dist/errors/route-not-found.js +4 -0
- package/dist/errors/service-unavailable.d.ts +7 -0
- package/dist/errors/service-unavailable.js +4 -0
- package/dist/errors/token-expired.d.ts +1 -0
- package/dist/errors/token-expired.js +3 -0
- package/dist/errors/unexpected-response.d.ts +1 -0
- package/dist/errors/unexpected-response.js +3 -0
- package/dist/errors/unprocessable-content.d.ts +5 -0
- package/dist/errors/unprocessable-content.js +4 -0
- package/dist/errors/unsupported-media-type.d.ts +6 -0
- package/dist/errors/unsupported-media-type.js +4 -0
- package/dist/errors/user-suspended.d.ts +1 -0
- package/dist/errors/user-suspended.js +3 -0
- package/dist/errors/value-out-of-range.d.ts +6 -0
- package/dist/errors/value-out-of-range.js +14 -0
- package/dist/errors/value-too-long.d.ts +6 -0
- package/dist/errors/value-too-long.js +14 -0
- package/dist/extensions.js +0 -4
- package/dist/flows.js +6 -8
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -2
- package/dist/messenger.d.ts +3 -3
- package/dist/messenger.js +18 -9
- package/dist/middleware/authenticate.js +2 -38
- package/dist/middleware/check-ip.js +2 -2
- package/dist/middleware/collection-exists.js +2 -2
- package/dist/middleware/error-handler.js +7 -7
- package/dist/middleware/graphql.js +11 -9
- package/dist/middleware/rate-limiter-global.d.ts +2 -2
- package/dist/middleware/rate-limiter-global.js +2 -3
- package/dist/middleware/rate-limiter-ip.d.ts +2 -2
- package/dist/middleware/rate-limiter-ip.js +2 -3
- package/dist/middleware/validate-batch.js +3 -4
- package/dist/rate-limiter.js +2 -9
- package/dist/server.js +10 -0
- package/dist/services/activity.js +3 -2
- package/dist/services/assets.js +9 -10
- package/dist/services/authentication.js +12 -11
- package/dist/services/authorization.d.ts +1 -1
- package/dist/services/authorization.js +16 -16
- package/dist/services/collections.js +17 -16
- package/dist/services/fields.js +16 -14
- package/dist/services/files.js +7 -6
- package/dist/services/graphql/errors/execution.d.ts +6 -0
- package/dist/services/graphql/errors/execution.js +2 -0
- package/dist/services/graphql/errors/index.d.ts +2 -0
- package/dist/services/graphql/errors/index.js +2 -0
- package/dist/services/graphql/errors/validation.d.ts +6 -0
- package/dist/services/graphql/errors/validation.js +2 -0
- package/dist/services/graphql/index.d.ts +2 -8
- package/dist/services/graphql/index.js +125 -66
- package/dist/services/graphql/subscription.d.ts +16 -0
- package/dist/services/graphql/subscription.js +77 -0
- package/dist/services/graphql/utils/process-error.js +3 -3
- package/dist/services/import-export.js +7 -7
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/items.js +14 -13
- package/dist/services/mail/index.js +3 -3
- package/dist/services/meta.js +3 -3
- package/dist/services/payload.js +11 -7
- package/dist/services/relations.js +32 -22
- package/dist/services/revisions.js +3 -3
- package/dist/services/roles.js +10 -9
- package/dist/services/schema.js +5 -5
- package/dist/services/server.js +24 -0
- package/dist/services/shares.js +4 -4
- package/dist/services/tfa.js +6 -6
- package/dist/services/translations.d.ts +2 -2
- package/dist/services/translations.js +4 -4
- package/dist/services/users.js +26 -29
- package/dist/services/utils.js +4 -4
- package/dist/services/websocket.d.ts +14 -0
- package/dist/services/websocket.js +26 -0
- package/dist/synchronization.js +3 -3
- package/dist/types/items.d.ts +2 -2
- package/dist/utils/apply-diff.js +13 -4
- package/dist/utils/apply-query.js +22 -13
- package/dist/utils/get-accountability-for-role.js +1 -2
- package/dist/utils/get-accountability-for-token.d.ts +2 -0
- package/dist/utils/get-accountability-for-token.js +50 -0
- package/dist/utils/get-column-path.js +5 -3
- package/dist/utils/get-column.js +3 -3
- package/dist/utils/get-service.d.ts +7 -0
- package/dist/utils/get-service.js +49 -0
- package/dist/utils/jwt.js +5 -5
- package/dist/utils/redact.d.ts +4 -0
- package/dist/utils/redact.js +15 -1
- package/dist/utils/to-boolean.d.ts +4 -0
- package/dist/utils/to-boolean.js +6 -0
- package/dist/utils/validate-diff.js +23 -9
- package/dist/utils/validate-keys.js +3 -3
- package/dist/utils/validate-query.d.ts +2 -0
- package/dist/utils/validate-query.js +27 -21
- package/dist/utils/validate-snapshot.js +11 -5
- package/dist/websocket/authenticate.d.ts +6 -0
- package/dist/websocket/authenticate.js +59 -0
- package/dist/websocket/controllers/base.d.ts +42 -0
- package/dist/websocket/controllers/base.js +279 -0
- package/dist/websocket/controllers/graphql.d.ts +12 -0
- package/dist/websocket/controllers/graphql.js +102 -0
- package/dist/websocket/controllers/hooks.d.ts +1 -0
- package/dist/websocket/controllers/hooks.js +122 -0
- package/dist/websocket/controllers/index.d.ts +10 -0
- package/dist/websocket/controllers/index.js +31 -0
- package/dist/websocket/controllers/rest.d.ts +9 -0
- package/dist/websocket/controllers/rest.js +47 -0
- package/dist/websocket/errors.d.ts +16 -0
- package/dist/websocket/errors.js +55 -0
- package/dist/websocket/handlers/heartbeat.d.ts +11 -0
- package/dist/websocket/handlers/heartbeat.js +72 -0
- package/dist/websocket/handlers/index.d.ts +4 -0
- package/dist/websocket/handlers/index.js +11 -0
- package/dist/websocket/handlers/items.d.ts +6 -0
- package/dist/websocket/handlers/items.js +103 -0
- package/dist/websocket/handlers/subscribe.d.ts +43 -0
- package/dist/websocket/handlers/subscribe.js +278 -0
- package/dist/websocket/messages.d.ts +311 -0
- package/dist/websocket/messages.js +96 -0
- package/dist/websocket/types.d.ts +34 -0
- package/dist/websocket/types.js +1 -0
- package/dist/websocket/utils/get-expires-at-for-token.d.ts +1 -0
- package/dist/websocket/utils/get-expires-at-for-token.js +8 -0
- package/dist/websocket/utils/message.d.ts +4 -0
- package/dist/websocket/utils/message.js +27 -0
- package/dist/websocket/utils/wait-for-message.d.ts +4 -0
- package/dist/websocket/utils/wait-for-message.js +45 -0
- package/package.json +21 -16
- package/dist/exceptions/content-too-large.d.ts +0 -4
- package/dist/exceptions/content-too-large.js +0 -6
- package/dist/exceptions/database/contains-null-values.d.ts +0 -9
- package/dist/exceptions/database/contains-null-values.js +0 -6
- package/dist/exceptions/database/invalid-foreign-key.d.ts +0 -10
- package/dist/exceptions/database/invalid-foreign-key.js +0 -11
- package/dist/exceptions/database/not-null-violation.d.ts +0 -9
- package/dist/exceptions/database/not-null-violation.js +0 -6
- package/dist/exceptions/database/record-not-unique.d.ts +0 -10
- package/dist/exceptions/database/record-not-unique.js +0 -11
- package/dist/exceptions/database/value-out-of-range.d.ts +0 -10
- package/dist/exceptions/database/value-out-of-range.js +0 -11
- package/dist/exceptions/database/value-too-long.d.ts +0 -9
- package/dist/exceptions/database/value-too-long.js +0 -11
- package/dist/exceptions/forbidden.d.ts +0 -6
- package/dist/exceptions/forbidden.js +0 -13
- package/dist/exceptions/graphql-validation.d.ts +0 -4
- package/dist/exceptions/graphql-validation.js +0 -6
- package/dist/exceptions/hit-rate-limit.d.ts +0 -9
- package/dist/exceptions/hit-rate-limit.js +0 -6
- package/dist/exceptions/illegal-asset-transformation.d.ts +0 -4
- package/dist/exceptions/illegal-asset-transformation.js +0 -6
- package/dist/exceptions/index.d.ts +0 -21
- package/dist/exceptions/index.js +0 -21
- package/dist/exceptions/invalid-config.d.ts +0 -4
- package/dist/exceptions/invalid-config.js +0 -6
- package/dist/exceptions/invalid-credentials.d.ts +0 -4
- package/dist/exceptions/invalid-credentials.js +0 -6
- package/dist/exceptions/invalid-ip.d.ts +0 -4
- package/dist/exceptions/invalid-ip.js +0 -6
- package/dist/exceptions/invalid-otp.d.ts +0 -4
- package/dist/exceptions/invalid-otp.js +0 -6
- package/dist/exceptions/invalid-payload.d.ts +0 -4
- package/dist/exceptions/invalid-payload.js +0 -6
- package/dist/exceptions/invalid-provider.d.ts +0 -4
- package/dist/exceptions/invalid-provider.js +0 -6
- package/dist/exceptions/invalid-query.d.ts +0 -4
- package/dist/exceptions/invalid-query.js +0 -6
- package/dist/exceptions/invalid-token.d.ts +0 -4
- package/dist/exceptions/invalid-token.js +0 -6
- package/dist/exceptions/method-not-allowed.d.ts +0 -8
- package/dist/exceptions/method-not-allowed.js +0 -6
- package/dist/exceptions/range-not-satisfiable.d.ts +0 -5
- package/dist/exceptions/range-not-satisfiable.js +0 -9
- package/dist/exceptions/route-not-found.d.ts +0 -4
- package/dist/exceptions/route-not-found.js +0 -6
- package/dist/exceptions/service-unavailable.d.ts +0 -9
- package/dist/exceptions/service-unavailable.js +0 -6
- package/dist/exceptions/token-expired.d.ts +0 -4
- package/dist/exceptions/token-expired.js +0 -6
- package/dist/exceptions/unexpected-response.d.ts +0 -4
- package/dist/exceptions/unexpected-response.js +0 -6
- package/dist/exceptions/unprocessable-entity.d.ts +0 -4
- package/dist/exceptions/unprocessable-entity.js +0 -6
- package/dist/exceptions/unsupported-media-type.d.ts +0 -4
- package/dist/exceptions/unsupported-media-type.js +0 -6
- package/dist/exceptions/user-suspended.d.ts +0 -4
- package/dist/exceptions/user-suspended.js +0 -6
- /package/dist/{exceptions/database → database/errors}/dialects/mssql.d.ts +0 -0
- /package/dist/{exceptions/database → database/errors}/dialects/mysql.d.ts +0 -0
- /package/dist/{exceptions/database → database/errors}/dialects/oracle.d.ts +0 -0
- /package/dist/{exceptions/database → database/errors}/dialects/postgres.d.ts +0 -0
- /package/dist/{exceptions/database → database/errors}/dialects/sqlite.d.ts +0 -0
- /package/dist/{exceptions/database → database/errors}/dialects/types.d.ts +0 -0
- /package/dist/{exceptions/database → database/errors}/dialects/types.js +0 -0
- /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,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,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
|
+
}
|