@directus/api 32.2.0 → 33.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.
- package/dist/ai/chat/controllers/chat.post.js +19 -4
- package/dist/ai/chat/lib/create-ui-stream.d.ts +8 -7
- package/dist/ai/chat/lib/create-ui-stream.js +28 -25
- package/dist/ai/chat/middleware/load-settings.js +31 -7
- package/dist/ai/chat/models/chat-request.d.ts +135 -2
- package/dist/ai/chat/models/chat-request.js +56 -2
- package/dist/ai/chat/models/providers.d.ts +16 -2
- package/dist/ai/chat/models/providers.js +16 -2
- package/dist/ai/chat/utils/chat-request-tool-to-ai-sdk-tool.js +3 -4
- package/dist/ai/chat/utils/format-context.d.ts +5 -0
- package/dist/ai/chat/utils/format-context.js +122 -0
- package/dist/ai/mcp/server.d.ts +27 -1
- package/dist/ai/providers/index.d.ts +3 -0
- package/dist/ai/providers/index.js +3 -0
- package/dist/ai/providers/options.d.ts +14 -0
- package/dist/ai/providers/options.js +26 -0
- package/dist/ai/providers/registry.d.ts +6 -0
- package/dist/ai/providers/registry.js +65 -0
- package/dist/ai/providers/types.d.ts +34 -0
- package/dist/ai/providers/types.js +1 -0
- package/dist/ai/tools/assets/index.js +1 -1
- package/dist/ai/tools/collections/index.js +2 -2
- package/dist/ai/tools/fields/index.js +2 -2
- package/dist/ai/tools/files/index.js +1 -1
- package/dist/ai/tools/flows/index.js +1 -1
- package/dist/ai/tools/folders/index.js +1 -1
- package/dist/ai/tools/items/index.js +6 -3
- package/dist/ai/tools/items/prompt.md +7 -9
- package/dist/ai/tools/relations/index.js +1 -1
- package/dist/ai/tools/schema.js +1 -1
- package/dist/ai/tools/trigger-flow/index.js +1 -1
- package/dist/app.js +12 -8
- package/dist/auth/drivers/ldap.d.ts +1 -1
- package/dist/auth/drivers/ldap.js +144 -139
- package/dist/auth/drivers/local.js +1 -1
- package/dist/auth/drivers/oauth2.d.ts +1 -2
- package/dist/auth/drivers/oauth2.js +22 -17
- package/dist/auth/drivers/openid.d.ts +1 -2
- package/dist/auth/drivers/openid.js +18 -13
- package/dist/auth/drivers/saml.js +3 -3
- package/dist/auth/utils/generate-callback-url.d.ts +11 -0
- package/dist/auth/utils/generate-callback-url.js +40 -0
- package/dist/auth/utils/is-login-redirect-allowed.d.ts +7 -0
- package/dist/{utils → auth/utils}/is-login-redirect-allowed.js +12 -9
- package/dist/cache.d.ts +12 -0
- package/dist/cache.js +27 -3
- package/dist/cli/commands/bootstrap/index.js +2 -2
- package/dist/cli/commands/database/install.js +1 -1
- package/dist/cli/commands/database/migrate.js +1 -1
- package/dist/cli/commands/init/index.js +2 -2
- package/dist/cli/commands/roles/create.js +4 -4
- package/dist/cli/commands/schema/apply.js +3 -3
- package/dist/cli/commands/schema/snapshot.js +1 -1
- package/dist/cli/utils/create-db-connection.d.ts +1 -1
- package/dist/cli/utils/create-db-connection.js +1 -1
- package/dist/cli/utils/create-env/env-stub.liquid +3 -0
- package/dist/cli/utils/create-env/index.js +1 -1
- package/dist/constants.d.ts +7 -3
- package/dist/constants.js +7 -3
- package/dist/controllers/access.js +1 -1
- package/dist/controllers/assets.js +1 -1
- package/dist/controllers/deployment.js +481 -0
- package/dist/controllers/extensions.js +1 -1
- package/dist/controllers/fields.js +8 -6
- package/dist/controllers/files.js +1 -1
- package/dist/controllers/items.js +1 -1
- package/dist/controllers/not-found.js +1 -1
- package/dist/controllers/relations.js +1 -1
- package/dist/database/errors/dialects/mysql.d.ts +1 -1
- package/dist/database/errors/dialects/postgres.d.ts +1 -1
- package/dist/database/errors/dialects/sqlite.d.ts +1 -1
- package/dist/database/errors/translate.d.ts +1 -1
- package/dist/database/errors/translate.js +1 -1
- package/dist/database/get-ast-from-query/lib/parse-fields.js +2 -2
- package/dist/database/helpers/date/dialects/mssql.js +1 -1
- package/dist/database/helpers/date/dialects/mysql.js +1 -1
- package/dist/database/helpers/date/types.js +1 -1
- package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +1 -0
- package/dist/database/helpers/schema/dialects/cockroachdb.js +24 -1
- package/dist/database/helpers/schema/dialects/mssql.d.ts +1 -1
- package/dist/database/helpers/schema/dialects/mysql.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/mysql.js +16 -3
- package/dist/database/helpers/schema/dialects/postgres.d.ts +1 -1
- package/dist/database/helpers/schema/types.d.ts +13 -0
- package/dist/database/helpers/schema/types.js +24 -0
- package/dist/database/index.js +4 -4
- package/dist/database/migrations/20220429A-add-flows.js +1 -1
- package/dist/database/migrations/20230526A-migrate-translation-strings.js +1 -1
- package/dist/database/migrations/20231009A-update-csv-fields-to-text.js +1 -1
- package/dist/database/migrations/20240204A-marketplace.js +9 -7
- package/dist/database/migrations/20240311A-deprecate-webhooks.d.ts +15 -0
- package/dist/database/migrations/20240311A-deprecate-webhooks.js +1 -1
- package/dist/database/migrations/20240806A-permissions-policies.js +2 -2
- package/dist/database/migrations/20240924A-migrate-legacy-comments.js +1 -1
- package/dist/database/migrations/20251014A-add-project-owner.js +1 -1
- package/dist/database/migrations/20251224A-remove-webhooks.d.ts +3 -0
- package/dist/database/migrations/20251224A-remove-webhooks.js +19 -0
- package/dist/database/migrations/20260110A-add-ai-provider-settings.d.ts +3 -0
- package/dist/database/migrations/20260110A-add-ai-provider-settings.js +35 -0
- package/dist/database/migrations/20260113A-add-revisions-index.d.ts +3 -0
- package/dist/database/migrations/20260113A-add-revisions-index.js +41 -0
- package/dist/database/migrations/20260128A-add-collaborative-editing.d.ts +3 -0
- package/dist/database/migrations/20260128A-add-collaborative-editing.js +10 -0
- package/dist/database/migrations/20260204A-add-deployment.d.ts +3 -0
- package/dist/database/migrations/20260204A-add-deployment.js +32 -0
- package/dist/database/migrations/run.js +3 -3
- package/dist/database/run-ast/lib/apply-query/add-join.js +1 -1
- package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.d.ts +2 -2
- package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.js +1 -1
- package/dist/database/run-ast/lib/apply-query/filter/index.js +1 -1
- package/dist/database/run-ast/lib/apply-query/filter/operator.js +1 -1
- package/dist/database/run-ast/lib/apply-query/sort.js +1 -1
- package/dist/database/run-ast/utils/get-column-pre-processor.js +2 -2
- package/dist/database/run-ast/utils/get-column.js +1 -1
- package/dist/database/seeds/run.js +3 -3
- package/dist/deployment/deployment.d.ts +94 -0
- package/dist/deployment/deployment.js +29 -0
- package/dist/deployment/drivers/index.d.ts +1 -0
- package/dist/deployment/drivers/index.js +1 -0
- package/dist/deployment/drivers/vercel.d.ts +32 -0
- package/dist/deployment/drivers/vercel.js +208 -0
- package/dist/deployment/index.d.ts +2 -0
- package/dist/deployment/index.js +2 -0
- package/dist/deployment.d.ts +24 -0
- package/dist/deployment.js +39 -0
- package/dist/extensions/lib/get-extensions-path.js +1 -1
- package/dist/extensions/lib/get-extensions-settings.js +1 -1
- package/dist/extensions/lib/get-extensions.js +1 -1
- package/dist/extensions/lib/get-shared-deps-mapping.js +3 -3
- package/dist/extensions/lib/installation/manager.js +3 -3
- package/dist/extensions/lib/sandbox/register/route.d.ts +1 -1
- package/dist/extensions/lib/sync/status.js +1 -1
- package/dist/extensions/lib/sync/sync.js +7 -7
- package/dist/extensions/lib/sync/utils.js +2 -2
- package/dist/extensions/manager.d.ts +1 -1
- package/dist/extensions/manager.js +8 -8
- package/dist/flows.d.ts +1 -1
- package/dist/logger/index.js +1 -1
- package/dist/logger/logs-stream.d.ts +1 -1
- package/dist/logger/logs-stream.js +1 -1
- package/dist/mailer.js +1 -1
- package/dist/metrics/lib/create-metrics.js +2 -2
- package/dist/middleware/authenticate.js +3 -3
- package/dist/middleware/collection-exists.js +1 -1
- package/dist/middleware/extract-token.js +1 -1
- package/dist/middleware/graphql.js +2 -2
- package/dist/middleware/respond.js +27 -14
- package/dist/middleware/validate-batch.js +1 -1
- package/dist/operations/exec/index.js +2 -1
- package/dist/operations/mail/index.js +1 -1
- package/dist/operations/mail/rate-limiter.js +2 -2
- package/dist/permissions/cache.js +5 -0
- package/dist/permissions/modules/fetch-allowed-collections/fetch-allowed-collections.js +1 -1
- package/dist/permissions/modules/fetch-allowed-field-map/fetch-allowed-field-map.js +1 -1
- package/dist/permissions/modules/fetch-inconsistent-field-map/fetch-inconsistent-field-map.js +2 -2
- package/dist/permissions/modules/process-ast/lib/inject-cases.js +1 -1
- package/dist/permissions/modules/process-ast/process-ast.js +1 -1
- package/dist/permissions/modules/process-ast/utils/find-related-collection.js +1 -1
- package/dist/permissions/modules/process-payload/process-payload.js +1 -1
- package/dist/permissions/modules/validate-access/lib/validate-item-access.d.ts +14 -2
- package/dist/permissions/modules/validate-access/lib/validate-item-access.js +72 -13
- package/dist/permissions/modules/validate-access/validate-access.js +3 -2
- package/dist/rate-limiter.js +1 -1
- package/dist/request/is-denied-ip.js +1 -1
- package/dist/schedules/project.js +1 -1
- package/dist/schedules/telemetry.js +1 -1
- package/dist/schedules/tus.js +1 -1
- package/dist/server.js +6 -5
- package/dist/services/assets.d.ts +2 -1
- package/dist/services/assets.js +35 -8
- package/dist/services/authentication.js +2 -2
- package/dist/services/collections.js +1 -1
- package/dist/services/deployment-projects.d.ts +20 -0
- package/dist/services/deployment-projects.js +34 -0
- package/dist/services/deployment-runs.d.ts +13 -0
- package/dist/services/deployment-runs.js +6 -0
- package/dist/services/deployment.d.ts +40 -0
- package/dist/services/deployment.js +202 -0
- package/dist/services/extensions.d.ts +1 -1
- package/dist/services/files/utils/get-metadata.d.ts +1 -1
- package/dist/services/files/utils/get-metadata.js +1 -1
- package/dist/services/files.d.ts +1 -1
- package/dist/services/files.js +4 -4
- package/dist/services/graphql/index.d.ts +1 -1
- package/dist/services/graphql/index.js +1 -1
- package/dist/services/graphql/resolvers/mutation.js +1 -1
- package/dist/services/graphql/resolvers/system-admin.js +2 -3
- package/dist/services/graphql/schema/get-types.d.ts +1 -1
- package/dist/services/graphql/schema/read.js +1 -1
- package/dist/services/graphql/subscription.d.ts +1 -1
- package/dist/services/graphql/types/date.js +1 -1
- package/dist/services/graphql/types/hash.js +1 -1
- package/dist/services/graphql/utils/add-path-to-validation-error.js +1 -1
- package/dist/services/graphql/utils/filter-replace-m2a.js +3 -4
- package/dist/services/import-export.d.ts +1 -1
- package/dist/services/import-export.js +2 -2
- package/dist/services/index.d.ts +3 -1
- package/dist/services/index.js +3 -1
- package/dist/services/mail/index.js +2 -2
- package/dist/services/mail/rate-limiter.js +2 -2
- package/dist/services/payload.js +2 -2
- package/dist/services/schema.js +1 -1
- package/dist/services/server.js +13 -4
- package/dist/services/settings.js +2 -2
- package/dist/services/specifications.js +2 -2
- package/dist/services/tfa.js +1 -1
- package/dist/services/translations.js +1 -1
- package/dist/services/tus/data-store.d.ts +1 -3
- package/dist/services/tus/data-store.js +2 -5
- package/dist/services/tus/server.js +6 -6
- package/dist/services/users.js +4 -4
- package/dist/services/versions.js +1 -1
- package/dist/telemetry/lib/get-report.js +2 -0
- package/dist/telemetry/lib/send-report.d.ts +1 -1
- package/dist/telemetry/lib/send-report.js +1 -1
- package/dist/telemetry/lib/track.js +1 -1
- package/dist/telemetry/types/report.d.ts +8 -0
- package/dist/telemetry/utils/get-settings.d.ts +2 -0
- package/dist/telemetry/utils/get-settings.js +5 -0
- package/dist/test-utils/knex.js +1 -1
- package/dist/types/collection.d.ts +1 -1
- package/dist/utils/async-handler.d.ts +1 -1
- package/dist/utils/calculate-field-depth.js +1 -1
- package/dist/utils/compress.js +1 -1
- package/dist/utils/deep-map-response.d.ts +1 -1
- package/dist/utils/deep-map-response.js +2 -2
- package/dist/utils/get-cache-key.js +1 -1
- package/dist/utils/get-column-path.js +1 -1
- package/dist/utils/get-field-system-rows.js +1 -1
- package/dist/utils/get-ip-from-req.d.ts +1 -1
- package/dist/utils/get-ip-from-req.js +1 -1
- package/dist/utils/get-local-type.js +7 -3
- package/dist/utils/get-service.js +7 -3
- package/dist/utils/get-snapshot-diff.js +1 -1
- package/dist/utils/is-field-allowed.d.ts +4 -0
- package/dist/utils/is-field-allowed.js +9 -0
- package/dist/utils/is-url-allowed.js +1 -1
- package/dist/utils/jwt.js +1 -1
- package/dist/utils/sanitize-schema.d.ts +1 -1
- package/dist/utils/should-clear-cache.d.ts +1 -1
- package/dist/utils/should-skip-cache.js +2 -2
- package/dist/utils/validate-diff.js +1 -1
- package/dist/utils/validate-snapshot.js +3 -3
- package/dist/utils/validate-storage.js +2 -2
- package/dist/utils/verify-session-jwt.js +1 -1
- package/dist/utils/versioning/handle-version.js +1 -1
- package/dist/websocket/collab/calculate-cache-metadata.d.ts +9 -0
- package/dist/websocket/collab/calculate-cache-metadata.js +121 -0
- package/dist/websocket/collab/collab.d.ts +63 -0
- package/dist/websocket/collab/collab.js +481 -0
- package/dist/websocket/collab/constants.d.ts +1 -0
- package/dist/websocket/collab/constants.js +13 -0
- package/dist/websocket/collab/filter-to-fields.d.ts +2 -0
- package/dist/websocket/collab/filter-to-fields.js +11 -0
- package/dist/websocket/collab/messenger.d.ts +43 -0
- package/dist/websocket/collab/messenger.js +225 -0
- package/dist/websocket/collab/payload-permissions.d.ts +18 -0
- package/dist/websocket/collab/payload-permissions.js +158 -0
- package/dist/websocket/collab/permissions-cache.d.ts +52 -0
- package/dist/websocket/collab/permissions-cache.js +204 -0
- package/dist/websocket/collab/room.d.ts +125 -0
- package/dist/websocket/collab/room.js +593 -0
- package/dist/websocket/collab/store.d.ts +7 -0
- package/dist/websocket/collab/store.js +33 -0
- package/dist/websocket/collab/types.d.ts +21 -0
- package/dist/websocket/collab/types.js +1 -0
- package/dist/websocket/collab/verify-permissions.d.ts +11 -0
- package/dist/websocket/collab/verify-permissions.js +100 -0
- package/dist/websocket/controllers/base.d.ts +2 -2
- package/dist/websocket/controllers/base.js +3 -3
- package/dist/websocket/controllers/graphql.d.ts +1 -1
- package/dist/websocket/controllers/graphql.js +1 -1
- package/dist/websocket/controllers/logs.d.ts +1 -1
- package/dist/websocket/controllers/rest.d.ts +1 -1
- package/dist/websocket/controllers/rest.js +2 -2
- package/dist/websocket/handlers/heartbeat.js +1 -1
- package/dist/websocket/handlers/index.d.ts +2 -0
- package/dist/websocket/handlers/index.js +9 -0
- package/dist/websocket/handlers/items.js +2 -2
- package/dist/websocket/handlers/subscribe.js +1 -1
- package/dist/websocket/types.d.ts +1 -1
- package/dist/websocket/utils/items.d.ts +2 -2
- package/dist/websocket/utils/message.d.ts +1 -1
- package/dist/websocket/utils/message.js +2 -2
- package/dist/websocket/utils/wait-for-message.js +1 -1
- package/package.json +35 -33
- package/dist/controllers/webhooks.js +0 -74
- package/dist/services/webhooks.d.ts +0 -14
- package/dist/services/webhooks.js +0 -32
- package/dist/utils/get-relation-info.d.ts +0 -6
- package/dist/utils/get-relation-info.js +0 -43
- package/dist/utils/get-relation-type.d.ts +0 -6
- package/dist/utils/get-relation-type.js +0 -18
- package/dist/utils/is-login-redirect-allowed.d.ts +0 -4
- package/dist/utils/versioning/deep-map-with-schema.d.ts +0 -23
- package/dist/utils/versioning/deep-map-with-schema.js +0 -81
- /package/dist/controllers/{webhooks.d.ts → deployment.d.ts} +0 -0
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import { ErrorCode } from '@directus/errors';
|
|
3
|
+
import { WS_TYPE } from '@directus/types';
|
|
4
|
+
import { ACTION, COLORS, } from '@directus/types/collab';
|
|
5
|
+
import { isDetailedUpdateSyntax, isObject } from '@directus/utils';
|
|
6
|
+
import { isEqual, random, uniq } from 'lodash-es';
|
|
7
|
+
import getDatabase from '../../database/index.js';
|
|
8
|
+
import { useLogger } from '../../logger/index.js';
|
|
9
|
+
import { getSchema } from '../../utils/get-schema.js';
|
|
10
|
+
import { getService } from '../../utils/get-service.js';
|
|
11
|
+
import { isFieldAllowed } from '../../utils/is-field-allowed.js';
|
|
12
|
+
import { Messenger } from './messenger.js';
|
|
13
|
+
import { sanitizePayload } from './payload-permissions.js';
|
|
14
|
+
import { useStore } from './store.js';
|
|
15
|
+
import { verifyPermissions } from './verify-permissions.js';
|
|
16
|
+
/**
|
|
17
|
+
* Store and manage all active collaborative editing rooms
|
|
18
|
+
*/
|
|
19
|
+
export class RoomManager {
|
|
20
|
+
rooms = {};
|
|
21
|
+
messenger;
|
|
22
|
+
constructor(messenger = new Messenger()) {
|
|
23
|
+
this.messenger = messenger;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Create a new collaborative editing room or return an existing one matching collection, item and version.
|
|
27
|
+
*/
|
|
28
|
+
async createRoom(collection, item, version, initialChanges) {
|
|
29
|
+
// Deterministic UID ensures clients on same resource join same room
|
|
30
|
+
const uid = getRoomHash(collection, item, version);
|
|
31
|
+
if (!(uid in this.rooms)) {
|
|
32
|
+
const room = new Room(uid, collection, item, version, initialChanges, this.messenger);
|
|
33
|
+
this.rooms[uid] = room;
|
|
34
|
+
await room.ensureInitialized();
|
|
35
|
+
await this.messenger.registerRoom(uid);
|
|
36
|
+
}
|
|
37
|
+
this.messenger.setRoomListener(uid, (message) => {
|
|
38
|
+
if (message.action === 'close') {
|
|
39
|
+
this.rooms[uid]?.dispose();
|
|
40
|
+
delete this.rooms[uid];
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
return this.rooms[uid];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Remove a room from local memory
|
|
47
|
+
*/
|
|
48
|
+
removeRoom(uid) {
|
|
49
|
+
if (this.rooms[uid]) {
|
|
50
|
+
delete this.rooms[uid];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Get an existing room by UID.
|
|
55
|
+
* If the room is not part of the local rooms, it will be loaded from shared memory.
|
|
56
|
+
* The room will not be persisted in local memory.
|
|
57
|
+
*/
|
|
58
|
+
async getRoom(uid) {
|
|
59
|
+
let room = this.rooms[uid];
|
|
60
|
+
if (!room) {
|
|
61
|
+
const store = useStore(uid);
|
|
62
|
+
// Loads room from shared memory
|
|
63
|
+
room = await store(async (store) => {
|
|
64
|
+
if (!(await store.has('uid')))
|
|
65
|
+
return;
|
|
66
|
+
const collection = await store.get('collection');
|
|
67
|
+
const item = await store.get('item');
|
|
68
|
+
const version = await store.get('version');
|
|
69
|
+
const changes = await store.get('changes');
|
|
70
|
+
const room = new Room(uid, collection, item, version, changes, this.messenger);
|
|
71
|
+
this.rooms[uid] = room;
|
|
72
|
+
return room;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
await room?.ensureInitialized();
|
|
76
|
+
return room;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Get all rooms a client is currently in from local memory
|
|
80
|
+
*/
|
|
81
|
+
async getClientRooms(uid) {
|
|
82
|
+
const rooms = [];
|
|
83
|
+
for (const room of Object.values(this.rooms)) {
|
|
84
|
+
if (await room.hasClient(uid)) {
|
|
85
|
+
rooms.push(room);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return rooms;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Returns all clients that are part of a room in the local memory
|
|
92
|
+
*/
|
|
93
|
+
async getLocalRoomClients() {
|
|
94
|
+
return (await Promise.all(Object.values(this.rooms).map((room) => room.getClients()))).flat();
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Remove empty rooms from local memory
|
|
98
|
+
*/
|
|
99
|
+
async cleanupRooms(uids) {
|
|
100
|
+
const rooms = uids ? uids.map((uid) => this.rooms[uid]).filter((room) => !!room) : Object.values(this.rooms);
|
|
101
|
+
for (const room of rooms) {
|
|
102
|
+
if (await room.close()) {
|
|
103
|
+
delete this.rooms[room.uid];
|
|
104
|
+
useLogger().debug(`[Collab] Closed inactive room ${room.getDisplayName()}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Forcefully close all local rooms and notify clients.
|
|
110
|
+
*/
|
|
111
|
+
async terminateAll() {
|
|
112
|
+
const rooms = Object.values(this.rooms);
|
|
113
|
+
for (const room of rooms) {
|
|
114
|
+
await room.close({
|
|
115
|
+
force: true,
|
|
116
|
+
reason: {
|
|
117
|
+
type: WS_TYPE.COLLAB,
|
|
118
|
+
action: ACTION.SERVER.ERROR,
|
|
119
|
+
code: ErrorCode.ServiceUnavailable,
|
|
120
|
+
message: 'Collaborative editing is disabled',
|
|
121
|
+
},
|
|
122
|
+
terminate: true,
|
|
123
|
+
});
|
|
124
|
+
delete this.rooms[room.uid];
|
|
125
|
+
}
|
|
126
|
+
useLogger().debug(`[Collab] Forcefully closed all ${rooms.length} active rooms`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const roomDefaults = {
|
|
130
|
+
changes: {},
|
|
131
|
+
clients: [],
|
|
132
|
+
focuses: {},
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* Represents a single collaborative editing room for a specific item
|
|
136
|
+
*/
|
|
137
|
+
export class Room {
|
|
138
|
+
uid;
|
|
139
|
+
collection;
|
|
140
|
+
item;
|
|
141
|
+
version;
|
|
142
|
+
initialChanges;
|
|
143
|
+
messenger;
|
|
144
|
+
store;
|
|
145
|
+
onUpdateHandler;
|
|
146
|
+
onDeleteHandler;
|
|
147
|
+
constructor(uid, collection, item, version, initialChanges, messenger = new Messenger()) {
|
|
148
|
+
this.uid = uid;
|
|
149
|
+
this.collection = collection;
|
|
150
|
+
this.item = item;
|
|
151
|
+
this.version = version;
|
|
152
|
+
this.initialChanges = initialChanges;
|
|
153
|
+
this.messenger = messenger;
|
|
154
|
+
this.store = useStore(uid, roomDefaults);
|
|
155
|
+
this.onUpdateHandler = async (meta) => {
|
|
156
|
+
const { keys } = meta;
|
|
157
|
+
const target = this.version ?? this.item;
|
|
158
|
+
// Skip updates for different items (singletons have item=null)
|
|
159
|
+
if (target !== null && !keys.some((key) => String(key) === String(target)))
|
|
160
|
+
return;
|
|
161
|
+
try {
|
|
162
|
+
const schema = await getSchema();
|
|
163
|
+
const result = await (async () => {
|
|
164
|
+
if (this.version) {
|
|
165
|
+
const service = getService('directus_versions', { schema });
|
|
166
|
+
const versionData = await service.readOne(this.version);
|
|
167
|
+
return versionData['delta'] ?? {};
|
|
168
|
+
}
|
|
169
|
+
const service = getService(collection, { schema });
|
|
170
|
+
return item ? await service.readOne(item) : await service.readSingleton({});
|
|
171
|
+
})();
|
|
172
|
+
const clients = await this.store(async (store) => {
|
|
173
|
+
let changes = await store.get('changes');
|
|
174
|
+
changes = Object.fromEntries(Object.entries(changes).filter(([key, value]) => {
|
|
175
|
+
// Always clear relational fields after save to prevent duplicate creation
|
|
176
|
+
if (isDetailedUpdateSyntax(value))
|
|
177
|
+
return false;
|
|
178
|
+
// Partial delta for versions and full record for regular items
|
|
179
|
+
if (!(key in result))
|
|
180
|
+
return !!this.version;
|
|
181
|
+
// For primitives, only clear if saved value matches pending change
|
|
182
|
+
if (isEqual(value, result[key]))
|
|
183
|
+
return false;
|
|
184
|
+
// Reconcile M2O objects with the PK in result
|
|
185
|
+
if (isObject(value)) {
|
|
186
|
+
const relation = schema.relations.find((r) => r.collection === collection && r.field === key);
|
|
187
|
+
if (relation) {
|
|
188
|
+
const pkField = schema.collections[relation.related_collection]?.primary;
|
|
189
|
+
if (pkField && isEqual(value[pkField], result[key])) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return true;
|
|
195
|
+
}));
|
|
196
|
+
await store.set('changes', changes);
|
|
197
|
+
return await store.get('clients');
|
|
198
|
+
});
|
|
199
|
+
for (const client of clients) {
|
|
200
|
+
this.send(client.uid, {
|
|
201
|
+
action: ACTION.SERVER.SAVE,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
useLogger().error(err, `[Collab] External update handler failed for ${collection}/${item ?? 'singleton'}`);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
this.onDeleteHandler = async (meta) => {
|
|
210
|
+
try {
|
|
211
|
+
const { keys, collection: eventCollection } = meta;
|
|
212
|
+
// Skip deletions for different versions
|
|
213
|
+
const isVersionMatch = this.version && eventCollection === 'directus_versions' && keys.some((key) => String(key) === this.version);
|
|
214
|
+
// Skip deletions for different items (singletons have item=null)
|
|
215
|
+
const isItemMatch = eventCollection === collection && (item === null || keys.some((key) => String(key) === String(item)));
|
|
216
|
+
if (!isVersionMatch && !isItemMatch)
|
|
217
|
+
return;
|
|
218
|
+
await this.sendAll({
|
|
219
|
+
action: ACTION.SERVER.DELETE,
|
|
220
|
+
});
|
|
221
|
+
await this.close({ force: true });
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
useLogger().error(err, `[Collab] External delete handler failed for ${collection}/${item ?? 'singleton'}`);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Ensures that foundational room state (metadata) exists in shared memory even after restarts
|
|
230
|
+
*/
|
|
231
|
+
async ensureInitialized() {
|
|
232
|
+
await this.store(async (store) => {
|
|
233
|
+
if (await store.has('uid'))
|
|
234
|
+
return;
|
|
235
|
+
await store.set('uid', this.uid);
|
|
236
|
+
await store.set('collection', this.collection);
|
|
237
|
+
await store.set('item', this.item);
|
|
238
|
+
await store.set('version', this.version);
|
|
239
|
+
await store.set('changes', this.initialChanges ?? {});
|
|
240
|
+
await store.set('clients', []);
|
|
241
|
+
await store.set('focuses', {});
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
getDisplayName() {
|
|
245
|
+
return [this.collection, this.item, this.version].filter(Boolean).join(':');
|
|
246
|
+
}
|
|
247
|
+
async getClients() {
|
|
248
|
+
return this.store((store) => store.get('clients'));
|
|
249
|
+
}
|
|
250
|
+
async getFocuses() {
|
|
251
|
+
return this.store((store) => store.get('focuses'));
|
|
252
|
+
}
|
|
253
|
+
async getChanges() {
|
|
254
|
+
return this.store((store) => store.get('changes'));
|
|
255
|
+
}
|
|
256
|
+
async hasClient(id) {
|
|
257
|
+
return this.store(async (store) => {
|
|
258
|
+
const clients = await store.get('clients');
|
|
259
|
+
return clients.findIndex((c) => c.uid === id) !== -1;
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
async getFocusByUser(id) {
|
|
263
|
+
return this.store(async (store) => (await store.get('focuses'))[id]);
|
|
264
|
+
}
|
|
265
|
+
async getFocusByField(field) {
|
|
266
|
+
return this.store(async (store) => {
|
|
267
|
+
const focuses = await store.get('focuses');
|
|
268
|
+
return Object.entries(focuses).find(([_, f]) => f === field)?.[0];
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Client requesting to join a room. If the client hasn't entered the room already, add a new client.
|
|
273
|
+
* Otherwise all users just will be informed again that the user has joined.
|
|
274
|
+
*/
|
|
275
|
+
async join(client, color) {
|
|
276
|
+
this.messenger.addClient(client);
|
|
277
|
+
let added = false;
|
|
278
|
+
let clientColor;
|
|
279
|
+
if (!(await this.hasClient(client.uid))) {
|
|
280
|
+
await this.store(async (store) => {
|
|
281
|
+
const clients = await store.get('clients');
|
|
282
|
+
added = true;
|
|
283
|
+
const existingColors = clients.map((c) => c.color);
|
|
284
|
+
const colorsAvailable = COLORS.filter((color) => !existingColors.includes(color));
|
|
285
|
+
if (colorsAvailable.length === 0) {
|
|
286
|
+
colorsAvailable.push(...COLORS);
|
|
287
|
+
}
|
|
288
|
+
if (color && colorsAvailable.includes(color)) {
|
|
289
|
+
clientColor = color;
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
clientColor = colorsAvailable[random(colorsAvailable.length - 1)];
|
|
293
|
+
}
|
|
294
|
+
clients.push({
|
|
295
|
+
uid: client.uid,
|
|
296
|
+
accountability: client.accountability,
|
|
297
|
+
color: clientColor,
|
|
298
|
+
});
|
|
299
|
+
await store.set('clients', clients);
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
if (added && clientColor) {
|
|
303
|
+
await this.sendExcluding({
|
|
304
|
+
action: ACTION.SERVER.JOIN,
|
|
305
|
+
user: client.accountability.user,
|
|
306
|
+
connection: client.uid,
|
|
307
|
+
color: clientColor,
|
|
308
|
+
}, client.uid);
|
|
309
|
+
}
|
|
310
|
+
const { changes, focuses, clients } = await this.store(async (store) => {
|
|
311
|
+
return {
|
|
312
|
+
changes: await store.get('changes'),
|
|
313
|
+
focuses: await store.get('focuses'),
|
|
314
|
+
clients: await store.get('clients'),
|
|
315
|
+
};
|
|
316
|
+
});
|
|
317
|
+
const schema = await getSchema();
|
|
318
|
+
const knex = getDatabase();
|
|
319
|
+
const allowedFields = await verifyPermissions(client.accountability, this.collection, this.item, 'read', {
|
|
320
|
+
schema,
|
|
321
|
+
knex,
|
|
322
|
+
});
|
|
323
|
+
this.send(client.uid, {
|
|
324
|
+
action: ACTION.SERVER.INIT,
|
|
325
|
+
collection: this.collection,
|
|
326
|
+
item: this.item,
|
|
327
|
+
version: this.version,
|
|
328
|
+
changes: (await sanitizePayload(changes, this.collection, {
|
|
329
|
+
accountability: client.accountability,
|
|
330
|
+
schema,
|
|
331
|
+
knex,
|
|
332
|
+
itemId: this.item,
|
|
333
|
+
})),
|
|
334
|
+
focuses: Object.fromEntries(Object.entries(focuses).filter(([_, field]) => allowedFields === null || isFieldAllowed(allowedFields, field))),
|
|
335
|
+
connection: client.uid,
|
|
336
|
+
users: Array.from(clients).map((client) => ({
|
|
337
|
+
user: client.accountability.user,
|
|
338
|
+
connection: client.uid,
|
|
339
|
+
color: client.color,
|
|
340
|
+
})),
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Leave the room
|
|
345
|
+
*/
|
|
346
|
+
async leave(uid) {
|
|
347
|
+
await this.store(async (store) => {
|
|
348
|
+
const clients = (await store.get('clients')).filter((c) => c.uid !== uid);
|
|
349
|
+
await store.set('clients', clients);
|
|
350
|
+
const focuses = await store.get('focuses');
|
|
351
|
+
if (uid in focuses) {
|
|
352
|
+
delete focuses[uid];
|
|
353
|
+
await store.set('focuses', focuses);
|
|
354
|
+
}
|
|
355
|
+
if (clients.length === 0) {
|
|
356
|
+
await store.set('changes', {});
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
this.sendAll({
|
|
360
|
+
action: ACTION.SERVER.LEAVE,
|
|
361
|
+
connection: uid,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Propagate an update to other clients
|
|
366
|
+
*/
|
|
367
|
+
async update(sender, changes) {
|
|
368
|
+
const { clients } = await this.store(async (store) => {
|
|
369
|
+
const existing_changes = await store.get('changes');
|
|
370
|
+
Object.assign(existing_changes, changes);
|
|
371
|
+
await store.set('changes', existing_changes);
|
|
372
|
+
return {
|
|
373
|
+
clients: await store.get('clients'),
|
|
374
|
+
};
|
|
375
|
+
});
|
|
376
|
+
const schema = await getSchema();
|
|
377
|
+
const knex = getDatabase();
|
|
378
|
+
for (const client of clients) {
|
|
379
|
+
if (client.uid === sender.uid)
|
|
380
|
+
continue;
|
|
381
|
+
const sanitizedChanges = (await sanitizePayload(changes, this.collection, {
|
|
382
|
+
accountability: client.accountability,
|
|
383
|
+
schema,
|
|
384
|
+
knex,
|
|
385
|
+
itemId: this.item,
|
|
386
|
+
})) || {};
|
|
387
|
+
for (const field of Object.keys(changes)) {
|
|
388
|
+
if (field in sanitizedChanges) {
|
|
389
|
+
this.send(client.uid, {
|
|
390
|
+
action: ACTION.SERVER.UPDATE,
|
|
391
|
+
field,
|
|
392
|
+
changes: sanitizedChanges[field],
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Propagate an unset to other clients
|
|
400
|
+
*/
|
|
401
|
+
async unset(sender, field) {
|
|
402
|
+
const clients = await this.store(async (store) => {
|
|
403
|
+
const changes = await store.get('changes');
|
|
404
|
+
delete changes[field];
|
|
405
|
+
await store.set('changes', changes);
|
|
406
|
+
return await store.get('clients');
|
|
407
|
+
});
|
|
408
|
+
const schema = await getSchema();
|
|
409
|
+
const knex = getDatabase();
|
|
410
|
+
for (const client of clients) {
|
|
411
|
+
if (client.uid === sender.uid)
|
|
412
|
+
continue;
|
|
413
|
+
const allowedFields = await verifyPermissions(client.accountability, this.collection, this.item, 'read', {
|
|
414
|
+
schema,
|
|
415
|
+
knex,
|
|
416
|
+
});
|
|
417
|
+
if (field && allowedFields !== null && !isFieldAllowed(allowedFields, field))
|
|
418
|
+
continue;
|
|
419
|
+
this.send(client.uid, {
|
|
420
|
+
action: ACTION.SERVER.DISCARD,
|
|
421
|
+
fields: [field],
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Discard specified changes in the room and propagate to other clients
|
|
427
|
+
*/
|
|
428
|
+
async discard(fields) {
|
|
429
|
+
if (fields.length === 0)
|
|
430
|
+
return;
|
|
431
|
+
const clients = await this.store(async (store) => {
|
|
432
|
+
let changes = await store.get('changes');
|
|
433
|
+
if (fields.includes('*')) {
|
|
434
|
+
changes = {};
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
for (const field of fields) {
|
|
438
|
+
delete changes[field];
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
await store.set('changes', changes);
|
|
442
|
+
return await store.get('clients');
|
|
443
|
+
});
|
|
444
|
+
const schema = await getSchema();
|
|
445
|
+
const knex = getDatabase();
|
|
446
|
+
for (const client of clients) {
|
|
447
|
+
const allowedFields = await verifyPermissions(client.accountability, this.collection, this.item, 'read', {
|
|
448
|
+
schema,
|
|
449
|
+
knex,
|
|
450
|
+
});
|
|
451
|
+
const sendFields = [];
|
|
452
|
+
// Send "*" when discarding all fields and recipient has full permissions
|
|
453
|
+
if (fields.includes('*') && allowedFields?.includes('*')) {
|
|
454
|
+
sendFields.push('*');
|
|
455
|
+
}
|
|
456
|
+
else if (fields.includes('*')) {
|
|
457
|
+
sendFields.push(...(allowedFields ?? []));
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
for (const field of fields) {
|
|
461
|
+
if (allowedFields?.includes('*') || allowedFields?.includes(field)) {
|
|
462
|
+
sendFields.push(field);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
this.send(client.uid, {
|
|
467
|
+
action: ACTION.SERVER.DISCARD,
|
|
468
|
+
fields: uniq(sendFields),
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Atomically acquire or release focus and propagate focus state to other clients
|
|
474
|
+
*/
|
|
475
|
+
async focus(sender, field) {
|
|
476
|
+
const result = await this.store(async (store) => {
|
|
477
|
+
const focuses = await store.get('focuses');
|
|
478
|
+
const clients = await store.get('clients');
|
|
479
|
+
if (field === null) {
|
|
480
|
+
const focusedField = focuses[sender.uid];
|
|
481
|
+
delete focuses[sender.uid];
|
|
482
|
+
await store.set('focuses', focuses);
|
|
483
|
+
return { success: true, clients, focusedField };
|
|
484
|
+
}
|
|
485
|
+
const currentFocuser = Object.entries(focuses).find(([_, f]) => f === field)?.[0];
|
|
486
|
+
if (currentFocuser && currentFocuser !== sender.uid) {
|
|
487
|
+
return { success: false };
|
|
488
|
+
}
|
|
489
|
+
focuses[sender.uid] = field;
|
|
490
|
+
await store.set('focuses', focuses);
|
|
491
|
+
return { success: true, clients, focusedField: field };
|
|
492
|
+
});
|
|
493
|
+
if (!result.success)
|
|
494
|
+
return false;
|
|
495
|
+
const schema = await getSchema();
|
|
496
|
+
const knex = getDatabase();
|
|
497
|
+
for (const client of result.clients) {
|
|
498
|
+
if (client.uid === sender.uid)
|
|
499
|
+
continue;
|
|
500
|
+
const allowedFields = await verifyPermissions(client.accountability, this.collection, this.item, 'read', {
|
|
501
|
+
schema,
|
|
502
|
+
knex,
|
|
503
|
+
});
|
|
504
|
+
if (result.focusedField && allowedFields !== null && !isFieldAllowed(allowedFields, result.focusedField))
|
|
505
|
+
continue;
|
|
506
|
+
this.send(client.uid, {
|
|
507
|
+
action: ACTION.SERVER.FOCUS,
|
|
508
|
+
connection: sender.uid,
|
|
509
|
+
field,
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
return true;
|
|
513
|
+
}
|
|
514
|
+
async sendAll(message) {
|
|
515
|
+
for (const client of await this.getClients()) {
|
|
516
|
+
this.send(client.uid, message);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
async sendExcluding(message, exclude) {
|
|
520
|
+
for (const client of await this.getClients()) {
|
|
521
|
+
if (client.uid !== exclude) {
|
|
522
|
+
this.send(client.uid, message);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
send(client, message) {
|
|
527
|
+
// Route via Messenger for multi-instance scaling
|
|
528
|
+
this.messenger.sendClient(client, { ...message, type: WS_TYPE.COLLAB, room: this.uid });
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Close the room and clean up shared state
|
|
532
|
+
*
|
|
533
|
+
* @param options.force If true, close the room even if active clients are present
|
|
534
|
+
* @param options.reason Optional reason to be sent to clients
|
|
535
|
+
* @param options.terminate If true, forcefully terminate the client connection after closing
|
|
536
|
+
*/
|
|
537
|
+
async close(options = {}) {
|
|
538
|
+
const { force = false, reason, terminate = false } = options;
|
|
539
|
+
let roomClients = [];
|
|
540
|
+
if (force) {
|
|
541
|
+
roomClients = await this.getClients();
|
|
542
|
+
for (const client of roomClients) {
|
|
543
|
+
if (this.messenger.hasClient(client.uid)) {
|
|
544
|
+
if (reason)
|
|
545
|
+
this.messenger.sendError(client.uid, reason);
|
|
546
|
+
if (terminate)
|
|
547
|
+
this.messenger.terminateClient(client.uid);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
const closed = await this.store(async (store) => {
|
|
552
|
+
if (!force) {
|
|
553
|
+
const clients = await store.get('clients');
|
|
554
|
+
if (clients.length > 0)
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
if (!(await store.has('uid')))
|
|
558
|
+
return false;
|
|
559
|
+
await store.delete('uid');
|
|
560
|
+
await store.delete('collection');
|
|
561
|
+
await store.delete('item');
|
|
562
|
+
await store.delete('version');
|
|
563
|
+
await store.delete('changes');
|
|
564
|
+
await store.delete('clients');
|
|
565
|
+
await store.delete('focuses');
|
|
566
|
+
return true;
|
|
567
|
+
});
|
|
568
|
+
if (closed) {
|
|
569
|
+
await this.messenger.unregisterRoom(this.uid);
|
|
570
|
+
this.messenger.sendRoom(this.uid, { action: 'close' });
|
|
571
|
+
if (force) {
|
|
572
|
+
for (const client of roomClients) {
|
|
573
|
+
if (!this.messenger.hasClient(client.uid)) {
|
|
574
|
+
if (reason)
|
|
575
|
+
this.messenger.sendError(client.uid, reason);
|
|
576
|
+
if (terminate)
|
|
577
|
+
this.messenger.terminateClient(client.uid);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if (closed || force) {
|
|
583
|
+
this.dispose();
|
|
584
|
+
}
|
|
585
|
+
return closed;
|
|
586
|
+
}
|
|
587
|
+
dispose() {
|
|
588
|
+
this.messenger.removeRoomListener(this.uid);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
export function getRoomHash(collection, item, version) {
|
|
592
|
+
return createHash('sha256').update([collection, item, version].join('-')).digest('hex');
|
|
593
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type RedisStore<T> = {
|
|
2
|
+
has(key: keyof T): Promise<boolean>;
|
|
3
|
+
get<K extends keyof T>(key: K): Promise<T[K]>;
|
|
4
|
+
set<K extends keyof T>(key: K, value: T[K]): Promise<void>;
|
|
5
|
+
delete(key: keyof T): Promise<void>;
|
|
6
|
+
};
|
|
7
|
+
export declare function useStore<Type extends object>(uid: string, defaults?: Partial<Type>): <T>(callback: (store: RedisStore<Type>) => Promise<T>) => Promise<T>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useEnv } from '@directus/env';
|
|
2
|
+
import { createCache } from '@directus/memory';
|
|
3
|
+
import { redisConfigAvailable, useRedis } from '../../redis/index.js';
|
|
4
|
+
const env = useEnv();
|
|
5
|
+
const localOnly = redisConfigAvailable() === false;
|
|
6
|
+
const config = localOnly
|
|
7
|
+
? {
|
|
8
|
+
type: 'local',
|
|
9
|
+
}
|
|
10
|
+
: {
|
|
11
|
+
type: 'redis',
|
|
12
|
+
namespace: env['WEBSOCKETS_COLLAB_STORE_NAMESPACE'] ?? 'collab',
|
|
13
|
+
redis: useRedis(),
|
|
14
|
+
};
|
|
15
|
+
const store = createCache(config);
|
|
16
|
+
export function useStore(uid, defaults) {
|
|
17
|
+
return (callback) => store.usingLock(`lock:${uid}`, async () => {
|
|
18
|
+
return await callback({
|
|
19
|
+
has(key) {
|
|
20
|
+
return store.has(`${uid}:${String(key)}`);
|
|
21
|
+
},
|
|
22
|
+
async get(key) {
|
|
23
|
+
return ((await store.get(`${uid}:${String(key)}`)) ?? defaults?.[key]);
|
|
24
|
+
},
|
|
25
|
+
set(key, value) {
|
|
26
|
+
return store.set(`${uid}:${String(key)}`, value);
|
|
27
|
+
},
|
|
28
|
+
delete(key) {
|
|
29
|
+
return store.delete(`${uid}:${String(key)}`);
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { WebSocketClient } from '@directus/types';
|
|
2
|
+
import type { ACTION, ClientMessage } from '@directus/types/collab';
|
|
3
|
+
export type PermissionClient = Pick<WebSocketClient, 'uid' | 'accountability'>;
|
|
4
|
+
export type JoinMessage = Extract<ClientMessage, {
|
|
5
|
+
action: typeof ACTION.CLIENT.JOIN;
|
|
6
|
+
}>;
|
|
7
|
+
export type LeaveMessage = Extract<ClientMessage, {
|
|
8
|
+
action: typeof ACTION.CLIENT.LEAVE;
|
|
9
|
+
}>;
|
|
10
|
+
export type UpdateMessage = Extract<ClientMessage, {
|
|
11
|
+
action: typeof ACTION.CLIENT.UPDATE;
|
|
12
|
+
}>;
|
|
13
|
+
export type UpdateAllMessage = Extract<ClientMessage, {
|
|
14
|
+
action: typeof ACTION.CLIENT.UPDATE_ALL;
|
|
15
|
+
}>;
|
|
16
|
+
export type FocusMessage = Extract<ClientMessage, {
|
|
17
|
+
action: typeof ACTION.CLIENT.FOCUS;
|
|
18
|
+
}>;
|
|
19
|
+
export type DiscardMessage = Extract<ClientMessage, {
|
|
20
|
+
action: typeof ACTION.CLIENT.DISCARD;
|
|
21
|
+
}>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Accountability, PrimaryKey, SchemaOverview } from '@directus/types';
|
|
2
|
+
import type { Knex } from 'knex';
|
|
3
|
+
/**
|
|
4
|
+
* Verify if a client has permissions to perform an action on the item.
|
|
5
|
+
* - `string[]`: List of fields the client has access to, empty if item exists but access is restricted.
|
|
6
|
+
* - `null`: Indicates the item doesn't exist.
|
|
7
|
+
*/
|
|
8
|
+
export declare function verifyPermissions(accountability: Accountability | null, collection: string, item: PrimaryKey | null, action: "create" | "read" | "update" | "delete" | undefined, options: {
|
|
9
|
+
knex: Knex;
|
|
10
|
+
schema: SchemaOverview;
|
|
11
|
+
}): Promise<string[] | null>;
|