From c657134b4651e9604d89cf1f9873a5110b08be2c Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Mon, 22 Aug 2022 10:35:21 +0200 Subject: [PATCH 01/14] types --- app/components/SocketProvider.tsx | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/app/components/SocketProvider.tsx b/app/components/SocketProvider.tsx index 26f2d7bd2..3fa53d73e 100644 --- a/app/components/SocketProvider.tsx +++ b/app/components/SocketProvider.tsx @@ -121,7 +121,8 @@ class SocketProvider extends React.Component { if (event.documentIds) { for (const documentDescriptor of event.documentIds) { const documentId = documentDescriptor.id; - let document = documents.get(documentId) || {}; + let document = documents.get(documentId); + const previousTitle = document?.title; if (event.event === "documents.delete") { const document = documents.get(documentId); @@ -136,10 +137,7 @@ class SocketProvider extends React.Component { // if we already have the latest version (it was us that performed // the change) then we don't need to update anything either. - // @ts-expect-error ts-migrate(2339) FIXME: Property 'title' does not exist on type '{}'. - const { title, updatedAt } = document; - - if (updatedAt === documentDescriptor.updatedAt) { + if (document?.updatedAt === documentDescriptor.updatedAt) { continue; } @@ -159,20 +157,17 @@ class SocketProvider extends React.Component { } // if the title changed then we need to update the collection also - // @ts-expect-error ts-migrate(2339) FIXME: Property 'title' does not exist on type '{}'. - if (title !== document.title) { + if (document && previousTitle !== document.title) { if (!event.collectionIds) { event.collectionIds = []; } const existing = find(event.collectionIds, { - // @ts-expect-error ts-migrate(2339) FIXME: Property 'collectionId' does not exist on type '{}... Remove this comment to see the full error message id: document.collectionId, }); if (!existing) { event.collectionIds.push({ - // @ts-expect-error ts-migrate(2339) FIXME: Property 'collectionId' does not exist on type '{}... Remove this comment to see the full error message id: document.collectionId, }); } @@ -231,13 +226,11 @@ class SocketProvider extends React.Component { if (event.groupIds) { for (const groupDescriptor of event.groupIds) { const groupId = groupDescriptor.id; - const group = groups.get(groupId) || {}; + const group = groups.get(groupId); + // if we already have the latest version (it was us that performed // the change) then we don't need to update anything either. - // @ts-expect-error ts-migrate(2339) FIXME: Property 'updatedAt' does not exist on type '{}'. - const { updatedAt } = group; - - if (updatedAt === groupDescriptor.updatedAt) { + if (group?.updatedAt === groupDescriptor.updatedAt) { continue; } From 138bc367dd420037d9cef853211623fb6f88597a Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Mon, 22 Aug 2022 11:45:39 +0200 Subject: [PATCH 02/14] types --- app/components/SocketProvider.tsx | 108 +++++++++++------- app/stores/BaseStore.ts | 4 +- app/types.ts | 38 +++++- .../queues/processors/WebsocketsProcessor.ts | 2 +- 4 files changed, 104 insertions(+), 48 deletions(-) diff --git a/app/components/SocketProvider.tsx b/app/components/SocketProvider.tsx index 3fa53d73e..1f6b15796 100644 --- a/app/components/SocketProvider.tsx +++ b/app/components/SocketProvider.tsx @@ -5,7 +5,17 @@ import { observer } from "mobx-react"; import * as React from "react"; import { io, Socket } from "socket.io-client"; import RootStore from "~/stores/RootStore"; +import FileOperation from "~/models/FileOperation"; +import Pin from "~/models/Pin"; +import Star from "~/models/Star"; import withStores from "~/components/withStores"; +import { + PartialWithId, + WebsocketCollectionUpdateIndexEvent, + WebsocketCollectionUserEvent, + WebsocketEntitiesEvent, + WebsocketEntityDeletedEvent, +} from "~/types"; import { AuthorizationError, NotFoundError } from "~/utils/errors"; import { getVisibilityListener, getPageVisible } from "~/utils/pageVisibility"; @@ -117,7 +127,7 @@ class SocketProvider extends React.Component { throw err; }); - this.socket.on("entities", async (event: any) => { + this.socket.on("entities", async (event: WebsocketEntitiesEvent) => { if (event.documentIds) { for (const documentDescriptor of event.documentIds) { const documentId = documentDescriptor.id; @@ -254,83 +264,95 @@ class SocketProvider extends React.Component { } }); - this.socket.on("pins.create", (event: any) => { + this.socket.on("pins.create", (event: PartialWithId) => { pins.add(event); }); - this.socket.on("pins.update", (event: any) => { + this.socket.on("pins.update", (event: PartialWithId) => { pins.add(event); }); - this.socket.on("pins.delete", (event: any) => { + this.socket.on("pins.delete", (event: WebsocketEntityDeletedEvent) => { pins.remove(event.modelId); }); - this.socket.on("stars.create", (event: any) => { + this.socket.on("stars.create", (event: PartialWithId) => { stars.add(event); }); - this.socket.on("stars.update", (event: any) => { + this.socket.on("stars.update", (event: PartialWithId) => { stars.add(event); }); - this.socket.on("stars.delete", (event: any) => { + this.socket.on("stars.delete", (event: WebsocketEntityDeletedEvent) => { stars.remove(event.modelId); }); - this.socket.on("documents.permanent_delete", (event: any) => { - documents.remove(event.documentId); - }); + this.socket.on( + "documents.permanent_delete", + (event: WebsocketEntityDeletedEvent) => { + documents.remove(event.modelId); + } + ); // received when a user is given access to a collection // if the user is us then we go ahead and load the collection from API. - this.socket.on("collections.add_user", (event: any) => { - if (auth.user && event.userId === auth.user.id) { - collections.fetch(event.collectionId, { - force: true, + this.socket.on( + "collections.add_user", + (event: WebsocketCollectionUserEvent) => { + if (auth.user && event.userId === auth.user.id) { + collections.fetch(event.collectionId, { + force: true, + }); + } + + // Document policies might need updating as the permission changes + documents.inCollection(event.collectionId).forEach((document) => { + policies.remove(document.id); }); } - - // Document policies might need updating as the permission changes - documents.inCollection(event.collectionId).forEach((document) => { - policies.remove(document.id); - }); - }); + ); // received when a user is removed from having access to a collection // to keep state in sync we must update our UI if the user is us, // or otherwise just remove any membership state we have for that user. - this.socket.on("collections.remove_user", (event: any) => { - if (auth.user && event.userId === auth.user.id) { - collections.remove(event.collectionId); - memberships.removeCollectionMemberships(event.collectionId); - documents.removeCollectionDocuments(event.collectionId); - } else { - memberships.remove(`${event.userId}-${event.collectionId}`); + this.socket.on( + "collections.remove_user", + (event: WebsocketCollectionUserEvent) => { + if (auth.user && event.userId === auth.user.id) { + collections.remove(event.collectionId); + memberships.removeCollectionMemberships(event.collectionId); + documents.removeCollectionDocuments(event.collectionId); + } else { + memberships.remove(`${event.userId}-${event.collectionId}`); + } } - }); + ); - this.socket.on("collections.update_index", (event: any) => { - const collection = collections.get(event.collectionId); + this.socket.on( + "collections.update_index", + (event: WebsocketCollectionUpdateIndexEvent) => { + const collection = collections.get(event.collectionId); - if (collection) { - collection.updateIndex(event.index); + if (collection) { + collection.updateIndex(event.index); + } } - }); + ); - this.socket.on("fileOperations.create", async (event: any) => { - const user = auth.user; - if (user) { - fileOperations.add({ ...event, user }); + this.socket.on( + "fileOperations.create", + (event: PartialWithId) => { + fileOperations.add(event); } - }); + ); - this.socket.on("fileOperations.update", async (event: any) => { - const user = auth.user; - if (user) { - fileOperations.add({ ...event, user }); + this.socket.on( + "fileOperations.update", + (event: PartialWithId) => { + fileOperations.add(event); } - }); + ); // received a message from the API server that we should request // to join a specific room. Forward that to the ws server. diff --git a/app/stores/BaseStore.ts b/app/stores/BaseStore.ts index 965ef684f..bfefe81fd 100644 --- a/app/stores/BaseStore.ts +++ b/app/stores/BaseStore.ts @@ -5,12 +5,10 @@ import { Class } from "utility-types"; import RootStore from "~/stores/RootStore"; import BaseModel from "~/models/BaseModel"; import Policy from "~/models/Policy"; -import { PaginationParams } from "~/types"; +import { PaginationParams, PartialWithId } from "~/types"; import { client } from "~/utils/ApiClient"; import { AuthorizationError, NotFoundError } from "~/utils/errors"; -type PartialWithId = Partial & { id: string }; - export enum RPCAction { Info = "info", List = "list", diff --git a/app/types.ts b/app/types.ts index 5364527ed..76c5a7c35 100644 --- a/app/types.ts +++ b/app/types.ts @@ -1,7 +1,12 @@ import { Location, LocationDescriptor } from "history"; import { TFunction } from "react-i18next"; import RootStore from "~/stores/RootStore"; -import Document from "~/models/Document"; +import Document from "./models/Document"; +import FileOperation from "./models/FileOperation"; +import Pin from "./models/Pin"; +import Star from "./models/Star"; + +export type PartialWithId = Partial & { id: string }; export type MenuItemButton = { type: "button"; @@ -178,3 +183,34 @@ export type ToastOptions = { onClick: React.MouseEventHandler; }; }; + +export type WebsocketEntityDeletedEvent = { + modelId: string; +}; + +export type WebsocketEntitiesEvent = { + documentIds: { id: string; updatedAt?: string }[]; + collectionIds: { id: string; updatedAt?: string }[]; + groupIds: { id: string; updatedAt?: string }[]; + teamIds: string[]; + event: string; +}; + +export type WebsocketCollectionUserEvent = { + collectionId: string; + userId: string; +}; + +export type WebsocketCollectionUpdateIndexEvent = { + collectionId: string; + index: string; +}; + +export type WebsocketEvent = + | PartialWithId + | PartialWithId + | PartialWithId + | WebsocketCollectionUserEvent + | WebsocketCollectionUpdateIndexEvent + | WebsocketEntityDeletedEvent + | WebsocketEntitiesEvent; diff --git a/server/queues/processors/WebsocketsProcessor.ts b/server/queues/processors/WebsocketsProcessor.ts index ed7337d51..0e9234245 100644 --- a/server/queues/processors/WebsocketsProcessor.ts +++ b/server/queues/processors/WebsocketsProcessor.ts @@ -93,7 +93,7 @@ export default class WebsocketsProcessor { return socketio .to(`collection-${event.collectionId}`) .emit(event.name, { - documentId: event.documentId, + modelId: event.documentId, }); } From b172da6fdf10cbdedf887b5cee84acf51f6aa0fd Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Tue, 23 Aug 2022 23:54:04 +0200 Subject: [PATCH 03/14] Separate collections.delete event --- app/components/SocketProvider.tsx | 36 +++++++++---------- .../queues/processors/WebsocketsProcessor.ts | 11 ++++-- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/app/components/SocketProvider.tsx b/app/components/SocketProvider.tsx index 1f6b15796..76e3ca256 100644 --- a/app/components/SocketProvider.tsx +++ b/app/components/SocketProvider.tsx @@ -190,26 +190,8 @@ class SocketProvider extends React.Component { const collectionId = collectionDescriptor.id; const collection = collections.get(collectionId); - if (event.event === "collections.delete") { - if (collection) { - collection.deletedAt = collectionDescriptor.updatedAt; - } - - const deletedDocuments = documents.inCollection(collectionId); - deletedDocuments.forEach((doc) => { - doc.deletedAt = collectionDescriptor.updatedAt; - policies.remove(doc.id); - }); - documents.removeCollectionDocuments(collectionId); - memberships.removeCollectionMemberships(collectionId); - collections.remove(collectionId); - policies.remove(collectionId); - continue; - } - // if we already have the latest version (it was us that performed // the change) then we don't need to update anything either. - if (collection?.updatedAt === collectionDescriptor.updatedAt) { continue; } @@ -264,6 +246,24 @@ class SocketProvider extends React.Component { } }); + this.socket.on( + "collections.delete", + (event: WebsocketEntityDeletedEvent) => { + const collectionId = event.modelId; + const deletedAt = new Date().toISOString(); + + const deletedDocuments = documents.inCollection(collectionId); + deletedDocuments.forEach((doc) => { + doc.deletedAt = deletedAt; + policies.remove(doc.id); + }); + documents.removeCollectionDocuments(collectionId); + memberships.removeCollectionMemberships(collectionId); + collections.remove(collectionId); + policies.remove(collectionId); + } + ); + this.socket.on("pins.create", (event: PartialWithId) => { pins.add(event); }); diff --git a/server/queues/processors/WebsocketsProcessor.ts b/server/queues/processors/WebsocketsProcessor.ts index 0e9234245..bcf96325b 100644 --- a/server/queues/processors/WebsocketsProcessor.ts +++ b/server/queues/processors/WebsocketsProcessor.ts @@ -211,8 +211,7 @@ export default class WebsocketsProcessor { }); } - case "collections.update": - case "collections.delete": { + case "collections.update": { const collection = await Collection.findByPk(event.collectionId, { paranoid: false, }); @@ -230,6 +229,14 @@ export default class WebsocketsProcessor { }); } + case "collections.delete": { + return socketio + .to(`collection-${event.collectionId}`) + .emit(event.name, { + modelId: event.collectionId, + }); + } + case "collections.move": { return socketio .to(`collection-${event.collectionId}`) From 4f1277f912a6528f1283b8c47e1a0fd9a690f89f Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Wed, 24 Aug 2022 00:06:45 +0200 Subject: [PATCH 04/14] Separate groups.delete event --- app/components/SocketProvider.tsx | 4 ++++ .../queues/processors/WebsocketsProcessor.ts | 23 +++++-------------- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/app/components/SocketProvider.tsx b/app/components/SocketProvider.tsx index 76e3ca256..20feff2a2 100644 --- a/app/components/SocketProvider.tsx +++ b/app/components/SocketProvider.tsx @@ -246,6 +246,10 @@ class SocketProvider extends React.Component { } }); + this.socket.on("groups.delete", (event: WebsocketEntityDeletedEvent) => { + groups.remove(event.modelId); + }); + this.socket.on( "collections.delete", (event: WebsocketEntityDeletedEvent) => { diff --git a/server/queues/processors/WebsocketsProcessor.ts b/server/queues/processors/WebsocketsProcessor.ts index bcf96325b..1e2cf14aa 100644 --- a/server/queues/processors/WebsocketsProcessor.ts +++ b/server/queues/processors/WebsocketsProcessor.ts @@ -525,25 +525,14 @@ export default class WebsocketsProcessor { } case "groups.delete": { - const group = await Group.findByPk(event.modelId, { - paranoid: false, + socketio.to(`team-${event.teamId}`).emit(event.name, { + modelId: event.modelId, }); - if (!group) { - return; - } - socketio.to(`team-${group.teamId}`).emit("entities", { - event: event.name, - groupIds: [ - { - id: group.id, - updatedAt: group.updatedAt, - }, - ], - }); - // we the users and collection relations that were just severed as a result of the group deletion - // since there are cascading deletes, we approximate this by looking for the recently deleted - // items in the GroupUser and CollectionGroup tables + // we get users and collection relations that were just severed as a + // result of the group deletion since there are cascading deletes, we + // approximate this by looking for the recently deleted items in the + // GroupUser and CollectionGroup tables const groupUsers = await GroupUser.findAll({ paranoid: false, where: { From d17e6f343213a7fb2c33833eb6baa78404457e95 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Wed, 24 Aug 2022 00:21:19 +0200 Subject: [PATCH 05/14] Separate documents.delete event --- app/components/SocketProvider.tsx | 21 ++++++++-------- .../queues/processors/WebsocketsProcessor.ts | 24 +++++++------------ 2 files changed, 18 insertions(+), 27 deletions(-) diff --git a/app/components/SocketProvider.tsx b/app/components/SocketProvider.tsx index 20feff2a2..bb983ef38 100644 --- a/app/components/SocketProvider.tsx +++ b/app/components/SocketProvider.tsx @@ -134,17 +134,6 @@ class SocketProvider extends React.Component { let document = documents.get(documentId); const previousTitle = document?.title; - if (event.event === "documents.delete") { - const document = documents.get(documentId); - - if (document) { - document.deletedAt = documentDescriptor.updatedAt; - } - - policies.remove(documentId); - continue; - } - // if we already have the latest version (it was us that performed // the change) then we don't need to update anything either. if (document?.updatedAt === documentDescriptor.updatedAt) { @@ -246,6 +235,16 @@ class SocketProvider extends React.Component { } }); + this.socket.on("documents.delete", (event: WebsocketEntityDeletedEvent) => { + const document = documents.get(event.modelId); + + if (document) { + document.deletedAt = new Date().toISOString(); + } + + policies.remove(event.modelId); + }); + this.socket.on("groups.delete", (event: WebsocketEntityDeletedEvent) => { groups.remove(event.modelId); }); diff --git a/server/queues/processors/WebsocketsProcessor.ts b/server/queues/processors/WebsocketsProcessor.ts index 1e2cf14aa..726a80037 100644 --- a/server/queues/processors/WebsocketsProcessor.ts +++ b/server/queues/processors/WebsocketsProcessor.ts @@ -59,28 +59,20 @@ export default class WebsocketsProcessor { return; } - if (!document.publishedAt) { - return socketio.to(`user-${document.createdById}`).emit("entities", { - event: event.name, - documentIds: [ - { - id: document.id, - updatedAt: document.updatedAt, - }, - ], + socketio + .to( + document.publishedAt + ? `collection-${document.collectionId}` + : `user-${document.createdById}` + ) + .emit(event.name, { + modelId: event.documentId, }); - } return socketio .to(`collection-${document.collectionId}`) .emit("entities", { event: event.name, - documentIds: [ - { - id: document.id, - updatedAt: document.updatedAt, - }, - ], collectionIds: [ { id: document.collectionId, From 7804f33e0d888207d2fb76732dd85ddc20384860 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Wed, 24 Aug 2022 00:35:50 +0200 Subject: [PATCH 06/14] Separate teams.update event --- app/components/SocketProvider.tsx | 23 ++++++++++--------- .../queues/processors/WebsocketsProcessor.ts | 17 +++++++------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/app/components/SocketProvider.tsx b/app/components/SocketProvider.tsx index bb983ef38..8db606bfe 100644 --- a/app/components/SocketProvider.tsx +++ b/app/components/SocketProvider.tsx @@ -8,6 +8,7 @@ import RootStore from "~/stores/RootStore"; import FileOperation from "~/models/FileOperation"; import Pin from "~/models/Pin"; import Star from "~/models/Star"; +import Team from "~/models/Team"; import withStores from "~/components/withStores"; import { PartialWithId, @@ -229,10 +230,6 @@ class SocketProvider extends React.Component { } } } - - if (event.teamIds) { - await auth.fetch(); - } }); this.socket.on("documents.delete", (event: WebsocketEntityDeletedEvent) => { @@ -245,6 +242,13 @@ class SocketProvider extends React.Component { policies.remove(event.modelId); }); + this.socket.on( + "documents.permanent_delete", + (event: WebsocketEntityDeletedEvent) => { + documents.remove(event.modelId); + } + ); + this.socket.on("groups.delete", (event: WebsocketEntityDeletedEvent) => { groups.remove(event.modelId); }); @@ -267,6 +271,10 @@ class SocketProvider extends React.Component { } ); + this.socket.on("teams.update", (event: PartialWithId) => { + auth.updateTeam(event); + }); + this.socket.on("pins.create", (event: PartialWithId) => { pins.add(event); }); @@ -291,13 +299,6 @@ class SocketProvider extends React.Component { stars.remove(event.modelId); }); - this.socket.on( - "documents.permanent_delete", - (event: WebsocketEntityDeletedEvent) => { - documents.remove(event.modelId); - } - ); - // received when a user is given access to a collection // if the user is us then we go ahead and load the collection from API. this.socket.on( diff --git a/server/queues/processors/WebsocketsProcessor.ts b/server/queues/processors/WebsocketsProcessor.ts index 726a80037..71fbfb536 100644 --- a/server/queues/processors/WebsocketsProcessor.ts +++ b/server/queues/processors/WebsocketsProcessor.ts @@ -10,11 +10,13 @@ import { GroupUser, Pin, Star, + Team, } from "@server/models"; import { presentFileOperation, presentPin, presentStar, + presentTeam, } from "@server/presenters"; import { Event } from "../../types"; @@ -582,14 +584,13 @@ export default class WebsocketsProcessor { } case "teams.update": { - return socketio.to(`team-${event.teamId}`).emit("entities", { - event: event.name, - teamIds: [ - { - id: event.teamId, - }, - ], - }); + const team = await Team.scope("withDomains").findByPk(event.teamId); + if (!team) { + return; + } + return socketio + .to(`team-${event.teamId}`) + .emit(event.name, presentTeam(team)); } default: From c62bfc4a60fe920ce0f364518a2ab91059371c57 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Wed, 24 Aug 2022 11:00:03 +0200 Subject: [PATCH 07/14] Separate documents.update event --- app/components/SocketProvider.tsx | 14 ++++++++++++++ app/models/Collection.ts | 2 +- server/queues/processors/WebsocketsProcessor.ts | 13 ++++--------- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/app/components/SocketProvider.tsx b/app/components/SocketProvider.tsx index 8db606bfe..428b45123 100644 --- a/app/components/SocketProvider.tsx +++ b/app/components/SocketProvider.tsx @@ -5,6 +5,7 @@ import { observer } from "mobx-react"; import * as React from "react"; import { io, Socket } from "socket.io-client"; import RootStore from "~/stores/RootStore"; +import Document from "~/models/Document"; import FileOperation from "~/models/FileOperation"; import Pin from "~/models/Pin"; import Star from "~/models/Star"; @@ -232,6 +233,19 @@ class SocketProvider extends React.Component { } }); + this.socket.on( + "documents.update", + (event: PartialWithId & { title: string; url: string }) => { + const document = documents.get(event.id); + document?.updateFromJson(event); + + if (event.collectionId) { + const collection = collections.get(event.collectionId); + collection?.updateDocument(event); + } + } + ); + this.socket.on("documents.delete", (event: WebsocketEntityDeletedEvent) => { const document = documents.get(event.modelId); diff --git a/app/models/Collection.ts b/app/models/Collection.ts index 09b50c65d..0f4acc19e 100644 --- a/app/models/Collection.ts +++ b/app/models/Collection.ts @@ -102,7 +102,7 @@ export default class Collection extends ParanoidModel { } @action - updateDocument(document: Document) { + updateDocument(document: Pick) { const travelNodes = (nodes: NavigationNode[]) => nodes.forEach((node) => { if (node.id === document.id) { diff --git a/server/queues/processors/WebsocketsProcessor.ts b/server/queues/processors/WebsocketsProcessor.ts index 71fbfb536..7af1c5ae4 100644 --- a/server/queues/processors/WebsocketsProcessor.ts +++ b/server/queues/processors/WebsocketsProcessor.ts @@ -13,6 +13,7 @@ import { Team, } from "@server/models"; import { + presentDocument, presentFileOperation, presentPin, presentStar, @@ -101,15 +102,9 @@ export default class WebsocketsProcessor { const channel = document.publishedAt ? `collection-${document.collectionId}` : `user-${event.actorId}`; - return socketio.to(channel).emit("entities", { - event: event.name, - documentIds: [ - { - id: document.id, - updatedAt: document.updatedAt, - }, - ], - }); + + const data = await presentDocument(document); + return socketio.to(channel).emit(event.name, data); } case "documents.create": { From de5524d3667be20863dcecd61dcc859150379924 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Wed, 24 Aug 2022 12:20:26 +0200 Subject: [PATCH 08/14] Add tracing around websocket processor --- server/services/websockets.ts | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/server/services/websockets.ts b/server/services/websockets.ts index 901e3b2a0..e0c171fa3 100644 --- a/server/services/websockets.ts +++ b/server/services/websockets.ts @@ -6,6 +6,8 @@ import IO from "socket.io"; import { createAdapter } from "socket.io-redis"; import Logger from "@server/logging/Logger"; import Metrics from "@server/logging/metrics"; +import * as Tracing from "@server/logging/tracing"; +import { APM } from "@server/logging/tracing"; import { Document, Collection, View, User } from "@server/models"; import { can } from "@server/policies"; import { getUserForJWT } from "@server/utils/jwt"; @@ -135,14 +137,23 @@ export default function init( // Handle events from event queue that should be sent to the clients down ws const websockets = new WebsocketsProcessor(); - websocketQueue.process(async function websocketEventsProcessor(job) { - const event = job.data; - websockets.perform(event, io).catch((error) => { - Logger.error("Error processing websocket event", error, { - event, + websocketQueue.process( + APM.traceFunction({ + serviceName: "websockets", + spanName: "process", + isRoot: true, + })(async function (job) { + const event = job.data; + + Tracing.setResource(`Processor.WebsocketsProcessor`); + + websockets.perform(event, io).catch((error) => { + Logger.error("Error processing websocket event", error, { + event, + }); }); - }); - }); + }) + ); } async function authenticated(io: IO.Server, socket: SocketWithAuth) { From 983010b5d8059955bf7e901db007cbc7432ce61a Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Wed, 24 Aug 2022 22:49:44 +0200 Subject: [PATCH 09/14] fix: collections.create event not propagated when initialized with private permissions --- app/components/SocketProvider.tsx | 5 +++++ server/queues/processors/WebsocketsProcessor.ts | 16 +++++----------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/app/components/SocketProvider.tsx b/app/components/SocketProvider.tsx index 428b45123..98d561ddd 100644 --- a/app/components/SocketProvider.tsx +++ b/app/components/SocketProvider.tsx @@ -5,6 +5,7 @@ import { observer } from "mobx-react"; import * as React from "react"; import { io, Socket } from "socket.io-client"; import RootStore from "~/stores/RootStore"; +import Collection from "~/models/Collection"; import Document from "~/models/Document"; import FileOperation from "~/models/FileOperation"; import Pin from "~/models/Pin"; @@ -267,6 +268,10 @@ class SocketProvider extends React.Component { groups.remove(event.modelId); }); + this.socket.on("collections.create", (event: PartialWithId) => { + collections.add(event); + }); + this.socket.on( "collections.delete", (event: WebsocketEntityDeletedEvent) => { diff --git a/server/queues/processors/WebsocketsProcessor.ts b/server/queues/processors/WebsocketsProcessor.ts index 7af1c5ae4..5f1255563 100644 --- a/server/queues/processors/WebsocketsProcessor.ts +++ b/server/queues/processors/WebsocketsProcessor.ts @@ -13,6 +13,7 @@ import { Team, } from "@server/models"; import { + presentCollection, presentDocument, presentFileOperation, presentPin, @@ -177,22 +178,15 @@ export default class WebsocketsProcessor { .to( collection.permission ? `team-${collection.teamId}` - : `collection-${collection.id}` + : `user-${collection.createdById}` ) - .emit("entities", { - event: event.name, - collectionIds: [ - { - id: collection.id, - updatedAt: collection.updatedAt, - }, - ], - }); + .emit(event.name, presentCollection(collection)); + return socketio .to( collection.permission ? `team-${collection.teamId}` - : `collection-${collection.id}` + : `user-${collection.createdById}` ) .emit("join", { event: event.name, From 60309975e058c30fe4966be8a826273b18c97291 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Thu, 25 Aug 2022 10:06:44 +0200 Subject: [PATCH 10/14] Allow usePolicy to fetch missing policies --- app/components/CollectionDescription.tsx | 2 +- app/components/DocumentListItem.tsx | 8 ++--- app/components/EventListItem.tsx | 2 +- app/components/Sidebar/App.tsx | 2 +- .../Sidebar/components/CollectionLink.tsx | 2 +- .../components/CollectionLinkChildren.tsx | 2 +- .../components/DraggableCollectionLink.tsx | 2 +- app/components/SocketProvider.tsx | 16 +++++++--- app/hooks/useAuthorizedSettingsConfig.ts | 2 +- app/hooks/usePolicy.ts | 30 ++++++++++++++++--- app/menus/CollectionMenu.tsx | 4 +-- app/menus/DocumentMenu.tsx | 2 +- app/menus/GroupMenu.tsx | 2 +- app/menus/NewDocumentMenu.tsx | 2 +- app/menus/NewTemplateMenu.tsx | 2 +- app/routes/authenticated.tsx | 2 +- app/scenes/Collection/Actions.tsx | 2 +- app/scenes/Collection/Empty.tsx | 2 +- app/scenes/Document/components/DataLoader.tsx | 2 +- app/scenes/Document/components/Header.tsx | 2 +- .../Document/components/SharePopover.tsx | 2 +- app/scenes/GroupMembers/GroupMembers.tsx | 2 +- app/scenes/Home.tsx | 2 +- app/scenes/Invite.tsx | 2 +- app/scenes/Settings/Groups.tsx | 2 +- app/scenes/Settings/Members.tsx | 2 +- app/scenes/Settings/Shares.tsx | 2 +- app/scenes/Settings/Tokens.tsx | 2 +- app/scenes/Settings/Webhooks.tsx | 2 +- app/scenes/Templates.tsx | 2 +- app/stores/BaseStore.ts | 16 ++++++++-- .../queues/processors/WebsocketsProcessor.ts | 18 +++++------ 32 files changed, 91 insertions(+), 53 deletions(-) diff --git a/app/components/CollectionDescription.tsx b/app/components/CollectionDescription.tsx index 81e34b02e..968e8a72c 100644 --- a/app/components/CollectionDescription.tsx +++ b/app/components/CollectionDescription.tsx @@ -25,7 +25,7 @@ function CollectionDescription({ collection }: Props) { const [isExpanded, setExpanded] = React.useState(false); const [isEditing, setEditing] = React.useState(false); const [isDirty, setDirty] = React.useState(false); - const can = usePolicy(collection.id); + const can = usePolicy(collection); const handleStartEditing = React.useCallback(() => { setEditing(true); diff --git a/app/components/DocumentListItem.tsx b/app/components/DocumentListItem.tsx index c5a9e3960..b043e1213 100644 --- a/app/components/DocumentListItem.tsx +++ b/app/components/DocumentListItem.tsx @@ -49,8 +49,8 @@ function DocumentListItem( ref: React.RefObject ) { const { t } = useTranslation(); - const currentUser = useCurrentUser(); - const currentTeam = useCurrentTeam(); + const user = useCurrentUser(); + const team = useCurrentTeam(); const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean(); const { @@ -70,7 +70,7 @@ function DocumentListItem( !!document.title.toLowerCase().includes(highlight.toLowerCase()); const canStar = !document.isDraft && !document.isArchived && !document.isTemplate; - const can = usePolicy(currentTeam.id); + const can = usePolicy(team); const canCollection = usePolicy(document.collectionId); return ( @@ -96,7 +96,7 @@ function DocumentListItem( highlight={highlight} dir={document.dir} /> - {document.isBadgedNew && document.createdBy.id !== currentUser.id && ( + {document.isBadgedNew && document.createdBy.id !== user.id && ( {t("New")} )} {canStar && ( diff --git a/app/components/EventListItem.tsx b/app/components/EventListItem.tsx index 47e43716b..0627439c7 100644 --- a/app/components/EventListItem.tsx +++ b/app/components/EventListItem.tsx @@ -33,7 +33,7 @@ type Props = { const EventListItem = ({ event, latest, document, ...rest }: Props) => { const { t } = useTranslation(); const location = useLocation(); - const can = usePolicy(document.id); + const can = usePolicy(document); const opts = { userName: event.actor.name, }; diff --git a/app/components/Sidebar/App.tsx b/app/components/Sidebar/App.tsx index b92be2181..fad3164a3 100644 --- a/app/components/Sidebar/App.tsx +++ b/app/components/Sidebar/App.tsx @@ -36,7 +36,7 @@ function AppSidebar() { const { documents } = useStores(); const team = useCurrentTeam(); const user = useCurrentUser(); - const can = usePolicy(team.id); + const can = usePolicy(team); React.useEffect(() => { if (!user.isViewer) { diff --git a/app/components/Sidebar/components/CollectionLink.tsx b/app/components/Sidebar/components/CollectionLink.tsx index aecabab02..0cdcb04eb 100644 --- a/app/components/Sidebar/components/CollectionLink.tsx +++ b/app/components/Sidebar/components/CollectionLink.tsx @@ -44,7 +44,7 @@ const CollectionLink: React.FC = ({ const { dialogs, documents, collections } = useStores(); const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean(); const [isEditing, setIsEditing] = React.useState(false); - const canUpdate = usePolicy(collection.id).update; + const canUpdate = usePolicy(collection).update; const { t } = useTranslation(); const history = useHistory(); const inStarredSection = useStarredContext(); diff --git a/app/components/Sidebar/components/CollectionLinkChildren.tsx b/app/components/Sidebar/components/CollectionLinkChildren.tsx index 9357a1d6c..8495a628a 100644 --- a/app/components/Sidebar/components/CollectionLinkChildren.tsx +++ b/app/components/Sidebar/components/CollectionLinkChildren.tsx @@ -25,7 +25,7 @@ function CollectionLinkChildren({ expanded, prefetchDocument, }: Props) { - const can = usePolicy(collection.id); + const can = usePolicy(collection); const { showToast } = useToasts(); const manualSort = collection.sort.field === "index"; const { documents } = useStores(); diff --git a/app/components/Sidebar/components/DraggableCollectionLink.tsx b/app/components/Sidebar/components/DraggableCollectionLink.tsx index 3815129e8..b2f17ab4c 100644 --- a/app/components/Sidebar/components/DraggableCollectionLink.tsx +++ b/app/components/Sidebar/components/DraggableCollectionLink.tsx @@ -39,7 +39,7 @@ function DraggableCollectionLink({ const [expanded, setExpanded] = React.useState( collection.id === ui.activeCollectionId && !locationStateStarred ); - const can = usePolicy(collection.id); + const can = usePolicy(collection); const belowCollectionIndex = belowCollection ? belowCollection.index : null; // Drop to reorder collection diff --git a/app/components/SocketProvider.tsx b/app/components/SocketProvider.tsx index 98d561ddd..d95ad4647 100644 --- a/app/components/SocketProvider.tsx +++ b/app/components/SocketProvider.tsx @@ -8,6 +8,7 @@ import RootStore from "~/stores/RootStore"; import Collection from "~/models/Collection"; import Document from "~/models/Document"; import FileOperation from "~/models/FileOperation"; +import Group from "~/models/Group"; import Pin from "~/models/Pin"; import Star from "~/models/Star"; import Team from "~/models/Team"; @@ -237,8 +238,7 @@ class SocketProvider extends React.Component { this.socket.on( "documents.update", (event: PartialWithId & { title: string; url: string }) => { - const document = documents.get(event.id); - document?.updateFromJson(event); + documents.patch(event); if (event.collectionId) { const collection = collections.get(event.collectionId); @@ -264,6 +264,14 @@ class SocketProvider extends React.Component { } ); + this.socket.on("groups.create", (event: PartialWithId) => { + groups.add(event); + }); + + this.socket.on("groups.update", (event: PartialWithId) => { + groups.patch(event); + }); + this.socket.on("groups.delete", (event: WebsocketEntityDeletedEvent) => { groups.remove(event.modelId); }); @@ -299,7 +307,7 @@ class SocketProvider extends React.Component { }); this.socket.on("pins.update", (event: PartialWithId) => { - pins.add(event); + pins.patch(event); }); this.socket.on("pins.delete", (event: WebsocketEntityDeletedEvent) => { @@ -311,7 +319,7 @@ class SocketProvider extends React.Component { }); this.socket.on("stars.update", (event: PartialWithId) => { - stars.add(event); + stars.patch(event); }); this.socket.on("stars.delete", (event: WebsocketEntityDeletedEvent) => { diff --git a/app/hooks/useAuthorizedSettingsConfig.ts b/app/hooks/useAuthorizedSettingsConfig.ts index 47aafc572..5ab3c6d14 100644 --- a/app/hooks/useAuthorizedSettingsConfig.ts +++ b/app/hooks/useAuthorizedSettingsConfig.ts @@ -67,7 +67,7 @@ type ConfigType = { const useAuthorizedSettingsConfig = () => { const team = useCurrentTeam(); - const can = usePolicy(team.id); + const can = usePolicy(team); const { t } = useTranslation(); const config: ConfigType = React.useMemo( diff --git a/app/hooks/usePolicy.ts b/app/hooks/usePolicy.ts index 762884cc6..5e455ab57 100644 --- a/app/hooks/usePolicy.ts +++ b/app/hooks/usePolicy.ts @@ -1,12 +1,34 @@ +import * as React from "react"; +import BaseModel from "~/models/BaseModel"; import useStores from "./useStores"; /** - * Quick access to retrieve the abilities of a policy for a given entity + * Retrieve the abilities of a policy for a given entity, if the policy is not + * located in the store, it will be fetched from the server. * - * @param entityId The entity id - * @returns The available abilities + * @param entity The model or model id + * @returns The policy for the model */ -export default function usePolicy(entityId: string) { +export default function usePolicy(entity: string | BaseModel | undefined) { const { policies } = useStores(); + const triggered = React.useRef(false); + const entityId = entity + ? typeof entity === "string" + ? entity + : entity.id + : ""; + + React.useEffect(() => { + if (entity && typeof entity !== "string") { + // The policy for this model is missing and we haven't tried to fetch it + // yet, go ahead and do that now. The force flag is needed otherwise the + // network request will be skipped due to the model existing in the store + if (!policies.get(entity.id) && !triggered.current) { + triggered.current = true; + void entity.store.fetch(entity.id, { force: true }); + } + } + }, [policies, entity]); + return policies.abilities(entityId); } diff --git a/app/menus/CollectionMenu.tsx b/app/menus/CollectionMenu.tsx index 8591c033e..faca20f54 100644 --- a/app/menus/CollectionMenu.tsx +++ b/app/menus/CollectionMenu.tsx @@ -187,8 +187,8 @@ function CollectionMenu({ ); const alphabeticalSort = collection.sort.field === "title"; - const can = usePolicy(collection.id); - const canUserInTeam = usePolicy(team.id); + const can = usePolicy(collection); + const canUserInTeam = usePolicy(team); const items: MenuItem[] = React.useMemo( () => [ { diff --git a/app/menus/DocumentMenu.tsx b/app/menus/DocumentMenu.tsx index 2b84faa74..638f98667 100644 --- a/app/menus/DocumentMenu.tsx +++ b/app/menus/DocumentMenu.tsx @@ -123,7 +123,7 @@ function DocumentMenu({ }, [menu]); const collection = collections.get(document.collectionId); - const can = usePolicy(document.id); + const can = usePolicy(document); const canViewHistory = can.read && !can.restore; const restoreItems = React.useMemo( () => [ diff --git a/app/menus/GroupMenu.tsx b/app/menus/GroupMenu.tsx index 711bd0841..86e19e0e6 100644 --- a/app/menus/GroupMenu.tsx +++ b/app/menus/GroupMenu.tsx @@ -24,7 +24,7 @@ function GroupMenu({ group, onMembers }: Props) { }); const [editModalOpen, setEditModalOpen] = React.useState(false); const [deleteModalOpen, setDeleteModalOpen] = React.useState(false); - const can = usePolicy(group.id); + const can = usePolicy(group); return ( <> diff --git a/app/menus/NewDocumentMenu.tsx b/app/menus/NewDocumentMenu.tsx index 901531280..1e9aa7acb 100644 --- a/app/menus/NewDocumentMenu.tsx +++ b/app/menus/NewDocumentMenu.tsx @@ -27,7 +27,7 @@ function NewDocumentMenu() { const { t } = useTranslation(); const team = useCurrentTeam(); const { collections, policies } = useStores(); - const can = usePolicy(team.id); + const can = usePolicy(team); const items = React.useMemo( () => collections.orderedData.reduce((filtered, collection) => { diff --git a/app/menus/NewTemplateMenu.tsx b/app/menus/NewTemplateMenu.tsx index 40ea0f916..e812bc575 100644 --- a/app/menus/NewTemplateMenu.tsx +++ b/app/menus/NewTemplateMenu.tsx @@ -22,7 +22,7 @@ function NewTemplateMenu() { const { t } = useTranslation(); const team = useCurrentTeam(); const { collections, policies } = useStores(); - const can = usePolicy(team.id); + const can = usePolicy(team); const items = React.useMemo( () => diff --git a/app/routes/authenticated.tsx b/app/routes/authenticated.tsx index 8bce9bb62..53e68e81a 100644 --- a/app/routes/authenticated.tsx +++ b/app/routes/authenticated.tsx @@ -64,7 +64,7 @@ const RedirectDocument = ({ function AuthenticatedRoutes() { const team = useCurrentTeam(); - const can = usePolicy(team.id); + const can = usePolicy(team); return ( diff --git a/app/scenes/Collection/Actions.tsx b/app/scenes/Collection/Actions.tsx index fe22705b7..ef98abe59 100644 --- a/app/scenes/Collection/Actions.tsx +++ b/app/scenes/Collection/Actions.tsx @@ -18,7 +18,7 @@ type Props = { function Actions({ collection }: Props) { const { t } = useTranslation(); - const can = usePolicy(collection.id); + const can = usePolicy(collection); return ( <> diff --git a/app/scenes/Collection/Empty.tsx b/app/scenes/Collection/Empty.tsx index 7d7136364..883c4b5fb 100644 --- a/app/scenes/Collection/Empty.tsx +++ b/app/scenes/Collection/Empty.tsx @@ -20,7 +20,7 @@ type Props = { function EmptyCollection({ collection }: Props) { const { t } = useTranslation(); - const can = usePolicy(collection.id); + const can = usePolicy(collection); const collectionName = collection ? collection.name : ""; const [ diff --git a/app/scenes/Document/components/DataLoader.tsx b/app/scenes/Document/components/DataLoader.tsx index 3924274bd..0c8251316 100644 --- a/app/scenes/Document/components/DataLoader.tsx +++ b/app/scenes/Document/components/DataLoader.tsx @@ -57,7 +57,7 @@ function DataLoader({ match, children }: Props) { : undefined; const isEditRoute = match.path === matchDocumentEdit; const isEditing = isEditRoute || !!auth.team?.collaborativeEditing; - const can = usePolicy(document ? document.id : ""); + const can = usePolicy(document); const location = useLocation(); React.useEffect(() => { diff --git a/app/scenes/Document/components/Header.tsx b/app/scenes/Document/components/Header.tsx index d17eeb9c4..cad4b769e 100644 --- a/app/scenes/Document/components/Header.tsx +++ b/app/scenes/Document/components/Header.tsx @@ -100,7 +100,7 @@ function DocumentHeader({ }, [onSave]); const { isDeleted, isTemplate } = document; - const can = usePolicy(document.id); + const can = usePolicy(document); const canToggleEmbeds = team?.documentEmbeds; const canEdit = can.update && !isEditing; const toc = ( diff --git a/app/scenes/Document/components/SharePopover.tsx b/app/scenes/Document/components/SharePopover.tsx index 6490d0fd5..35bff4f1e 100644 --- a/app/scenes/Document/components/SharePopover.tsx +++ b/app/scenes/Document/components/SharePopover.tsx @@ -45,7 +45,7 @@ function SharePopover({ const timeout = React.useRef>(); const buttonRef = React.useRef(null); const can = usePolicy(share ? share.id : ""); - const documentAbilities = usePolicy(document.id); + const documentAbilities = usePolicy(document); const canPublish = can.update && !document.isTemplate && diff --git a/app/scenes/GroupMembers/GroupMembers.tsx b/app/scenes/GroupMembers/GroupMembers.tsx index 305a20ded..33a9b23ed 100644 --- a/app/scenes/GroupMembers/GroupMembers.tsx +++ b/app/scenes/GroupMembers/GroupMembers.tsx @@ -26,7 +26,7 @@ function GroupMembers({ group }: Props) { const { users, groupMemberships } = useStores(); const { showToast } = useToasts(); const { t } = useTranslation(); - const can = usePolicy(group.id); + const can = usePolicy(group); const handleAddModal = (state: boolean) => { setAddModalOpen(state); diff --git a/app/scenes/Home.tsx b/app/scenes/Home.tsx index 2e1841c6f..cf6445e64 100644 --- a/app/scenes/Home.tsx +++ b/app/scenes/Home.tsx @@ -30,7 +30,7 @@ function Home() { pins.fetchPage(); }, [pins]); - const canManageTeam = usePolicy(team.id).manage; + const canManageTeam = usePolicy(team).manage; return ( { diff --git a/app/scenes/Settings/Groups.tsx b/app/scenes/Settings/Groups.tsx index 4971db55d..14b83aacc 100644 --- a/app/scenes/Settings/Groups.tsx +++ b/app/scenes/Settings/Groups.tsx @@ -24,7 +24,7 @@ function Groups() { const { t } = useTranslation(); const { groups } = useStores(); const team = useCurrentTeam(); - const can = usePolicy(team.id); + const can = usePolicy(team); const [ newGroupModalOpen, handleNewGroupModalOpen, diff --git a/app/scenes/Settings/Members.tsx b/app/scenes/Settings/Members.tsx index 291d3909a..d4262b56b 100644 --- a/app/scenes/Settings/Members.tsx +++ b/app/scenes/Settings/Members.tsx @@ -40,7 +40,7 @@ function Members() { const [data, setData] = React.useState([]); const [totalPages, setTotalPages] = React.useState(0); const [userIds, setUserIds] = React.useState([]); - const can = usePolicy(team.id); + const can = usePolicy(team); const query = params.get("query") || ""; const filter = params.get("filter") || ""; const sort = params.get("sort") || "name"; diff --git a/app/scenes/Settings/Shares.tsx b/app/scenes/Settings/Shares.tsx index c9bae0811..f3be922de 100644 --- a/app/scenes/Settings/Shares.tsx +++ b/app/scenes/Settings/Shares.tsx @@ -21,7 +21,7 @@ function Shares() { const { t } = useTranslation(); const { shares, auth } = useStores(); const canShareDocuments = auth.team && auth.team.sharing; - const can = usePolicy(team.id); + const can = usePolicy(team); const [isLoading, setIsLoading] = React.useState(false); const [data, setData] = React.useState([]); const [totalPages, setTotalPages] = React.useState(0); diff --git a/app/scenes/Settings/Tokens.tsx b/app/scenes/Settings/Tokens.tsx index 5379b1fc1..d2a4878e5 100644 --- a/app/scenes/Settings/Tokens.tsx +++ b/app/scenes/Settings/Tokens.tsx @@ -23,7 +23,7 @@ function Tokens() { const { t } = useTranslation(); const { apiKeys } = useStores(); const [newModalOpen, handleNewModalOpen, handleNewModalClose] = useBoolean(); - const can = usePolicy(team.id); + const can = usePolicy(team); return ( ) { const team = useCurrentTeam(); const { fetchTemplates, templates, templatesAlphabetical } = documents; const { sort } = props.match.params; - const can = usePolicy(team.id); + const can = usePolicy(team); return ( { }; @action - remove(id: string): void { - this.data.delete(id); + patch = (item: PartialWithId | T): T | undefined => { + const existingModel = this.data.get(item.id); + + if (existingModel) { + existingModel.updateFromJson(item); + return existingModel; + } + + return; + }; + + @action + remove(id: string): boolean { + return this.data.delete(id); } save( diff --git a/server/queues/processors/WebsocketsProcessor.ts b/server/queues/processors/WebsocketsProcessor.ts index 5f1255563..96ac954ba 100644 --- a/server/queues/processors/WebsocketsProcessor.ts +++ b/server/queues/processors/WebsocketsProcessor.ts @@ -16,6 +16,7 @@ import { presentCollection, presentDocument, presentFileOperation, + presentGroup, presentPin, presentStar, presentTeam, @@ -356,8 +357,9 @@ export default class WebsocketsProcessor { if (!fileOperation) { return; } - const data = await presentFileOperation(fileOperation); - return socketio.to(`user-${event.actorId}`).emit(event.name, data); + return socketio + .to(`user-${event.actorId}`) + .emit(event.name, presentFileOperation(fileOperation)); } case "pins.create": @@ -412,15 +414,9 @@ export default class WebsocketsProcessor { if (!group) { return; } - return socketio.to(`team-${group.teamId}`).emit("entities", { - event: event.name, - groupIds: [ - { - id: group.id, - updatedAt: group.updatedAt, - }, - ], - }); + return socketio + .to(`team-${group.teamId}`) + .emit(event.name, presentGroup(group)); } case "groups.add_user": { From d2aea687f3bbb6176e9bf100ed345e1c74a254e8 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Thu, 25 Aug 2022 10:43:05 +0200 Subject: [PATCH 11/14] Remove collection fetch on document delete --- app/components/SocketProvider.tsx | 23 +++++++++++----- app/models/Collection.ts | 27 +++++++++++++++++++ app/scenes/Document/components/DataLoader.tsx | 2 +- app/stores/BaseStore.ts | 4 +-- app/types.ts | 5 ++++ .../queues/processors/WebsocketsProcessor.ts | 14 ++-------- 6 files changed, 53 insertions(+), 22 deletions(-) diff --git a/app/components/SocketProvider.tsx b/app/components/SocketProvider.tsx index d95ad4647..824b1380f 100644 --- a/app/components/SocketProvider.tsx +++ b/app/components/SocketProvider.tsx @@ -17,6 +17,7 @@ import { PartialWithId, WebsocketCollectionUpdateIndexEvent, WebsocketCollectionUserEvent, + WebsocketDocumentDeletedEvent, WebsocketEntitiesEvent, WebsocketEntityDeletedEvent, } from "~/types"; @@ -247,15 +248,23 @@ class SocketProvider extends React.Component { } ); - this.socket.on("documents.delete", (event: WebsocketEntityDeletedEvent) => { - const document = documents.get(event.modelId); + this.socket.on( + "documents.delete", + (event: WebsocketDocumentDeletedEvent) => { + const document = documents.get(event.modelId); + const collection = collections.get(event.collectionId); - if (document) { - document.deletedAt = new Date().toISOString(); + if (collection) { + collection.removeDocument(event.modelId); + } + + if (document) { + document.deletedAt = new Date().toISOString(); + } + + policies.remove(event.modelId); } - - policies.remove(event.modelId); - }); + ); this.socket.on( "documents.permanent_delete", diff --git a/app/models/Collection.ts b/app/models/Collection.ts index 0f4acc19e..c13d5efe9 100644 --- a/app/models/Collection.ts +++ b/app/models/Collection.ts @@ -101,6 +101,12 @@ export default class Collection extends ParanoidModel { return sortNavigationNodes(this.documents, this.sort); } + /** + * Updates the document identified by the given id in the collection in memory. + * Does not update the document in the database. + * + * @param document The document properties stored in the collection + */ @action updateDocument(document: Pick) { const travelNodes = (nodes: NavigationNode[]) => @@ -116,6 +122,27 @@ export default class Collection extends ParanoidModel { travelNodes(this.documents); } + /** + * Removes the document identified by the given id from the collection in + * memory. Does not remove the document from the database. + * + * @param documentId The id of the document to remove. + */ + @action + removeDocument(documentId: string) { + this.documents = this.documents.filter(function f(node): boolean { + if (node.id === documentId) { + return false; + } + + if (node.children) { + node.children = node.children.filter(f); + } + + return true; + }); + } + @action updateIndex(index: string) { this.index = index; diff --git a/app/scenes/Document/components/DataLoader.tsx b/app/scenes/Document/components/DataLoader.tsx index 0c8251316..b4d924304 100644 --- a/app/scenes/Document/components/DataLoader.tsx +++ b/app/scenes/Document/components/DataLoader.tsx @@ -57,7 +57,7 @@ function DataLoader({ match, children }: Props) { : undefined; const isEditRoute = match.path === matchDocumentEdit; const isEditing = isEditRoute || !!auth.team?.collaborativeEditing; - const can = usePolicy(document); + const can = usePolicy(document?.id); const location = useLocation(); React.useEffect(() => { diff --git a/app/stores/BaseStore.ts b/app/stores/BaseStore.ts index d35c35b29..a678270b5 100644 --- a/app/stores/BaseStore.ts +++ b/app/stores/BaseStore.ts @@ -113,8 +113,8 @@ export default abstract class BaseStore { }; @action - remove(id: string): boolean { - return this.data.delete(id); + remove(id: string): void { + this.data.delete(id); } save( diff --git a/app/types.ts b/app/types.ts index 76c5a7c35..fbf766772 100644 --- a/app/types.ts +++ b/app/types.ts @@ -188,6 +188,11 @@ export type WebsocketEntityDeletedEvent = { modelId: string; }; +export type WebsocketDocumentDeletedEvent = WebsocketEntityDeletedEvent & { + modelId: string; + collectionId: string; +}; + export type WebsocketEntitiesEvent = { documentIds: { id: string; updatedAt?: string }[]; collectionIds: { id: string; updatedAt?: string }[]; diff --git a/server/queues/processors/WebsocketsProcessor.ts b/server/queues/processors/WebsocketsProcessor.ts index 96ac954ba..b3d89f7fa 100644 --- a/server/queues/processors/WebsocketsProcessor.ts +++ b/server/queues/processors/WebsocketsProcessor.ts @@ -64,7 +64,7 @@ export default class WebsocketsProcessor { return; } - socketio + return socketio .to( document.publishedAt ? `collection-${document.collectionId}` @@ -72,17 +72,7 @@ export default class WebsocketsProcessor { ) .emit(event.name, { modelId: event.documentId, - }); - - return socketio - .to(`collection-${document.collectionId}`) - .emit("entities", { - event: event.name, - collectionIds: [ - { - id: document.collectionId, - }, - ], + collectionId: event.collectionId, }); } From debadcb7117220530bebf6a7841bfbe6e3dd15c4 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Thu, 25 Aug 2022 10:56:47 +0200 Subject: [PATCH 12/14] fix: Wrap websocket handlers in action Separate documents.archive --- app/components/SocketProvider.tsx | 210 +++++++++--------- app/stores/BaseStore.ts | 12 - .../queues/processors/WebsocketsProcessor.ts | 2 +- 3 files changed, 102 insertions(+), 122 deletions(-) diff --git a/app/components/SocketProvider.tsx b/app/components/SocketProvider.tsx index 824b1380f..0db57a77b 100644 --- a/app/components/SocketProvider.tsx +++ b/app/components/SocketProvider.tsx @@ -1,6 +1,6 @@ import invariant from "invariant"; import { find } from "lodash"; -import { observable } from "mobx"; +import { action, observable } from "mobx"; import { observer } from "mobx-react"; import * as React from "react"; import { io, Socket } from "socket.io-client"; @@ -132,130 +132,122 @@ class SocketProvider extends React.Component { throw err; }); - this.socket.on("entities", async (event: WebsocketEntitiesEvent) => { - if (event.documentIds) { - for (const documentDescriptor of event.documentIds) { - const documentId = documentDescriptor.id; - let document = documents.get(documentId); - const previousTitle = document?.title; + this.socket.on( + "entities", + action(async (event: WebsocketEntitiesEvent) => { + if (event.documentIds) { + for (const documentDescriptor of event.documentIds) { + const documentId = documentDescriptor.id; + let document = documents.get(documentId); + const previousTitle = document?.title; - // if we already have the latest version (it was us that performed - // the change) then we don't need to update anything either. - if (document?.updatedAt === documentDescriptor.updatedAt) { - continue; - } - - // otherwise, grab the latest version of the document - try { - document = await documents.fetch(documentId, { - force: true, - }); - } catch (err) { - if ( - err instanceof AuthorizationError || - err instanceof NotFoundError - ) { - documents.remove(documentId); - return; - } - } - - // if the title changed then we need to update the collection also - if (document && previousTitle !== document.title) { - if (!event.collectionIds) { - event.collectionIds = []; + // if we already have the latest version (it was us that performed + // the change) then we don't need to update anything either. + if (document?.updatedAt === documentDescriptor.updatedAt) { + continue; } - const existing = find(event.collectionIds, { - id: document.collectionId, - }); + // otherwise, grab the latest version of the document + try { + document = await documents.fetch(documentId, { + force: true, + }); + } catch (err) { + if ( + err instanceof AuthorizationError || + err instanceof NotFoundError + ) { + documents.remove(documentId); + return; + } + } - if (!existing) { - event.collectionIds.push({ + // if the title changed then we need to update the collection also + if (document && previousTitle !== document.title) { + if (!event.collectionIds) { + event.collectionIds = []; + } + + const existing = find(event.collectionIds, { id: document.collectionId, }); + + if (!existing) { + event.collectionIds.push({ + id: document.collectionId, + }); + } } } } - } - if (event.collectionIds) { - for (const collectionDescriptor of event.collectionIds) { - const collectionId = collectionDescriptor.id; - const collection = collections.get(collectionId); + if (event.collectionIds) { + for (const collectionDescriptor of event.collectionIds) { + const collectionId = collectionDescriptor.id; + const collection = collections.get(collectionId); - // if we already have the latest version (it was us that performed - // the change) then we don't need to update anything either. - if (collection?.updatedAt === collectionDescriptor.updatedAt) { - continue; - } + // if we already have the latest version (it was us that performed + // the change) then we don't need to update anything either. + if (collection?.updatedAt === collectionDescriptor.updatedAt) { + continue; + } - try { - await collections.fetch(collectionId, { - force: true, - }); - } catch (err) { - if ( - err instanceof AuthorizationError || - err instanceof NotFoundError - ) { - documents.removeCollectionDocuments(collectionId); - memberships.removeCollectionMemberships(collectionId); - collections.remove(collectionId); - policies.remove(collectionId); - return; + try { + await collections.fetch(collectionId, { + force: true, + }); + } catch (err) { + if ( + err instanceof AuthorizationError || + err instanceof NotFoundError + ) { + documents.removeCollectionDocuments(collectionId); + memberships.removeCollectionMemberships(collectionId); + collections.remove(collectionId); + policies.remove(collectionId); + return; + } } } } - } - - if (event.groupIds) { - for (const groupDescriptor of event.groupIds) { - const groupId = groupDescriptor.id; - const group = groups.get(groupId); - - // if we already have the latest version (it was us that performed - // the change) then we don't need to update anything either. - if (group?.updatedAt === groupDescriptor.updatedAt) { - continue; - } - - try { - await groups.fetch(groupId, { - force: true, - }); - } catch (err) { - if ( - err instanceof AuthorizationError || - err instanceof NotFoundError - ) { - groups.remove(groupId); - } - } - } - } - }); + }) + ); this.socket.on( "documents.update", - (event: PartialWithId & { title: string; url: string }) => { - documents.patch(event); + action( + (event: PartialWithId & { title: string; url: string }) => { + documents.add(event); + + if (event.collectionId) { + const collection = collections.get(event.collectionId); + collection?.updateDocument(event); + } + } + ) + ); + + this.socket.on( + "documents.archive", + action((event: PartialWithId) => { + documents.add(event); + policies.remove(event.id); if (event.collectionId) { const collection = collections.get(event.collectionId); - collection?.updateDocument(event); + collection?.removeDocument(event.id); } - } + }) ); this.socket.on( "documents.delete", - (event: WebsocketDocumentDeletedEvent) => { + action((event: WebsocketDocumentDeletedEvent) => { const document = documents.get(event.modelId); - const collection = collections.get(event.collectionId); - if (collection) { - collection.removeDocument(event.modelId); + if (event.collectionId) { + const collection = collections.get(event.collectionId); + collection?.removeDocument(event.modelId); } if (document) { @@ -263,7 +255,7 @@ class SocketProvider extends React.Component { } policies.remove(event.modelId); - } + }) ); this.socket.on( @@ -278,7 +270,7 @@ class SocketProvider extends React.Component { }); this.socket.on("groups.update", (event: PartialWithId) => { - groups.patch(event); + groups.add(event); }); this.socket.on("groups.delete", (event: WebsocketEntityDeletedEvent) => { @@ -291,7 +283,7 @@ class SocketProvider extends React.Component { this.socket.on( "collections.delete", - (event: WebsocketEntityDeletedEvent) => { + action((event: WebsocketEntityDeletedEvent) => { const collectionId = event.modelId; const deletedAt = new Date().toISOString(); @@ -304,7 +296,7 @@ class SocketProvider extends React.Component { memberships.removeCollectionMemberships(collectionId); collections.remove(collectionId); policies.remove(collectionId); - } + }) ); this.socket.on("teams.update", (event: PartialWithId) => { @@ -316,7 +308,7 @@ class SocketProvider extends React.Component { }); this.socket.on("pins.update", (event: PartialWithId) => { - pins.patch(event); + pins.add(event); }); this.socket.on("pins.delete", (event: WebsocketEntityDeletedEvent) => { @@ -328,7 +320,7 @@ class SocketProvider extends React.Component { }); this.socket.on("stars.update", (event: PartialWithId) => { - stars.patch(event); + stars.add(event); }); this.socket.on("stars.delete", (event: WebsocketEntityDeletedEvent) => { @@ -339,7 +331,7 @@ class SocketProvider extends React.Component { // if the user is us then we go ahead and load the collection from API. this.socket.on( "collections.add_user", - (event: WebsocketCollectionUserEvent) => { + action((event: WebsocketCollectionUserEvent) => { if (auth.user && event.userId === auth.user.id) { collections.fetch(event.collectionId, { force: true, @@ -350,7 +342,7 @@ class SocketProvider extends React.Component { documents.inCollection(event.collectionId).forEach((document) => { policies.remove(document.id); }); - } + }) ); // received when a user is removed from having access to a collection @@ -358,7 +350,7 @@ class SocketProvider extends React.Component { // or otherwise just remove any membership state we have for that user. this.socket.on( "collections.remove_user", - (event: WebsocketCollectionUserEvent) => { + action((event: WebsocketCollectionUserEvent) => { if (auth.user && event.userId === auth.user.id) { collections.remove(event.collectionId); memberships.removeCollectionMemberships(event.collectionId); @@ -366,18 +358,18 @@ class SocketProvider extends React.Component { } else { memberships.remove(`${event.userId}-${event.collectionId}`); } - } + }) ); this.socket.on( "collections.update_index", - (event: WebsocketCollectionUpdateIndexEvent) => { + action((event: WebsocketCollectionUpdateIndexEvent) => { const collection = collections.get(event.collectionId); if (collection) { collection.updateIndex(event.index); } - } + }) ); this.socket.on( diff --git a/app/stores/BaseStore.ts b/app/stores/BaseStore.ts index a678270b5..bfefe81fd 100644 --- a/app/stores/BaseStore.ts +++ b/app/stores/BaseStore.ts @@ -100,18 +100,6 @@ export default abstract class BaseStore { return item; }; - @action - patch = (item: PartialWithId | T): T | undefined => { - const existingModel = this.data.get(item.id); - - if (existingModel) { - existingModel.updateFromJson(item); - return existingModel; - } - - return; - }; - @action remove(id: string): void { this.data.delete(id); diff --git a/server/queues/processors/WebsocketsProcessor.ts b/server/queues/processors/WebsocketsProcessor.ts index b3d89f7fa..84c71b6fd 100644 --- a/server/queues/processors/WebsocketsProcessor.ts +++ b/server/queues/processors/WebsocketsProcessor.ts @@ -28,7 +28,6 @@ export default class WebsocketsProcessor { switch (event.name) { case "documents.publish": case "documents.restore": - case "documents.archive": case "documents.unarchive": { const document = await Document.findByPk(event.documentId, { paranoid: false, @@ -84,6 +83,7 @@ export default class WebsocketsProcessor { }); } + case "documents.archive": case "documents.update": { const document = await Document.findByPk(event.documentId, { paranoid: false, From bb12f1fabbe2814bada1776c4b5797da6caf3a28 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Thu, 25 Aug 2022 11:14:00 +0200 Subject: [PATCH 13/14] SocketProvider -> WebsocketProvider --- ...cketProvider.tsx => WebsocketProvider.tsx} | 24 +++++++------------ app/routes/authenticated.tsx | 6 ++--- .../Document/components/SocketPresence.ts | 6 ++--- app/types.ts | 5 ---- .../queues/processors/WebsocketsProcessor.ts | 21 +--------------- 5 files changed, 16 insertions(+), 46 deletions(-) rename app/components/{SocketProvider.tsx => WebsocketProvider.tsx} (95%) diff --git a/app/components/SocketProvider.tsx b/app/components/WebsocketProvider.tsx similarity index 95% rename from app/components/SocketProvider.tsx rename to app/components/WebsocketProvider.tsx index 0db57a77b..627c80284 100644 --- a/app/components/SocketProvider.tsx +++ b/app/components/WebsocketProvider.tsx @@ -17,7 +17,6 @@ import { PartialWithId, WebsocketCollectionUpdateIndexEvent, WebsocketCollectionUserEvent, - WebsocketDocumentDeletedEvent, WebsocketEntitiesEvent, WebsocketEntityDeletedEvent, } from "~/types"; @@ -28,14 +27,14 @@ type SocketWithAuthentication = Socket & { authenticated?: boolean; }; -export const SocketContext = React.createContext( +export const WebsocketContext = React.createContext( null ); type Props = RootStore; @observer -class SocketProvider extends React.Component { +class WebsocketProvider extends React.Component { @observable socket: SocketWithAuthentication | null; @@ -242,19 +241,14 @@ class SocketProvider extends React.Component { this.socket.on( "documents.delete", - action((event: WebsocketDocumentDeletedEvent) => { - const document = documents.get(event.modelId); + action((event: PartialWithId) => { + documents.add(event); + policies.remove(event.id); if (event.collectionId) { const collection = collections.get(event.collectionId); - collection?.removeDocument(event.modelId); + collection?.removeDocument(event.id); } - - if (document) { - document.deletedAt = new Date().toISOString(); - } - - policies.remove(event.modelId); }) ); @@ -428,11 +422,11 @@ class SocketProvider extends React.Component { render() { return ( - + {this.props.children} - + ); } } -export default withStores(SocketProvider); +export default withStores(WebsocketProvider); diff --git a/app/routes/authenticated.tsx b/app/routes/authenticated.tsx index 53e68e81a..043eea54e 100644 --- a/app/routes/authenticated.tsx +++ b/app/routes/authenticated.tsx @@ -11,7 +11,7 @@ import Layout from "~/components/AuthenticatedLayout"; import CenteredContent from "~/components/CenteredContent"; import PlaceholderDocument from "~/components/PlaceholderDocument"; import Route from "~/components/ProfiledRoute"; -import SocketProvider from "~/components/SocketProvider"; +import WebsocketProvider from "~/components/WebsocketProvider"; import useCurrentTeam from "~/hooks/useCurrentTeam"; import usePolicy from "~/hooks/usePolicy"; import { matchDocumentSlug as slug } from "~/utils/routeHelpers"; @@ -67,7 +67,7 @@ function AuthenticatedRoutes() { const can = usePolicy(team); return ( - + - + ); } diff --git a/app/scenes/Document/components/SocketPresence.ts b/app/scenes/Document/components/SocketPresence.ts index 9491c9376..b67fdab3b 100644 --- a/app/scenes/Document/components/SocketPresence.ts +++ b/app/scenes/Document/components/SocketPresence.ts @@ -1,6 +1,6 @@ import * as React from "react"; import { USER_PRESENCE_INTERVAL } from "@shared/constants"; -import { SocketContext } from "~/components/SocketProvider"; +import { WebsocketContext } from "~/components/WebsocketProvider"; type Props = { documentId: string; @@ -8,9 +8,9 @@ type Props = { }; export default class SocketPresence extends React.Component { - static contextType = SocketContext; + static contextType = WebsocketContext; - previousContext: typeof SocketContext; + previousContext: typeof WebsocketContext; editingInterval: ReturnType; diff --git a/app/types.ts b/app/types.ts index fbf766772..76c5a7c35 100644 --- a/app/types.ts +++ b/app/types.ts @@ -188,11 +188,6 @@ export type WebsocketEntityDeletedEvent = { modelId: string; }; -export type WebsocketDocumentDeletedEvent = WebsocketEntityDeletedEvent & { - modelId: string; - collectionId: string; -}; - export type WebsocketEntitiesEvent = { documentIds: { id: string; updatedAt?: string }[]; collectionIds: { id: string; updatedAt?: string }[]; diff --git a/server/queues/processors/WebsocketsProcessor.ts b/server/queues/processors/WebsocketsProcessor.ts index 84c71b6fd..4aeb13717 100644 --- a/server/queues/processors/WebsocketsProcessor.ts +++ b/server/queues/processors/WebsocketsProcessor.ts @@ -55,26 +55,6 @@ export default class WebsocketsProcessor { }); } - case "documents.delete": { - const document = await Document.findByPk(event.documentId, { - paranoid: false, - }); - if (!document) { - return; - } - - return socketio - .to( - document.publishedAt - ? `collection-${document.collectionId}` - : `user-${document.createdById}` - ) - .emit(event.name, { - modelId: event.documentId, - collectionId: event.collectionId, - }); - } - case "documents.permanent_delete": { return socketio .to(`collection-${event.collectionId}`) @@ -84,6 +64,7 @@ export default class WebsocketsProcessor { } case "documents.archive": + case "documents.delete": case "documents.update": { const document = await Document.findByPk(event.documentId, { paranoid: false, From 354a68a8b77e52b2792076e3fe8ebf0b30a7544d Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Thu, 25 Aug 2022 21:51:34 +0200 Subject: [PATCH 14/14] Remove long-deprecated documents.star/documents.unstar --- .../components/WebhookSubscriptionForm.tsx | 2 - .../queues/processors/WebsocketsProcessor.ts | 7 -- server/queues/tasks/DeliverWebhookTask.ts | 2 - .../api/__snapshots__/documents.test.ts.snap | 18 ----- server/routes/api/documents.test.ts | 74 ------------------- server/routes/api/documents.ts | 70 ------------------ server/types.ts | 4 +- 7 files changed, 1 insertion(+), 176 deletions(-) diff --git a/app/scenes/Settings/components/WebhookSubscriptionForm.tsx b/app/scenes/Settings/components/WebhookSubscriptionForm.tsx index af0cf5cc9..fc58ec498 100644 --- a/app/scenes/Settings/components/WebhookSubscriptionForm.tsx +++ b/app/scenes/Settings/components/WebhookSubscriptionForm.tsx @@ -31,8 +31,6 @@ const WEBHOOK_EVENTS = { "documents.archive", "documents.unarchive", "documents.restore", - "documents.star", - "documents.unstar", "documents.move", "documents.update", "documents.update.delayed", diff --git a/server/queues/processors/WebsocketsProcessor.ts b/server/queues/processors/WebsocketsProcessor.ts index 4aeb13717..2469201ce 100644 --- a/server/queues/processors/WebsocketsProcessor.ts +++ b/server/queues/processors/WebsocketsProcessor.ts @@ -101,13 +101,6 @@ export default class WebsocketsProcessor { }); } - case "documents.star": - case "documents.unstar": { - return socketio.to(`user-${event.actorId}`).emit(event.name, { - documentId: event.documentId, - }); - } - case "documents.move": { const documents = await Document.findAll({ where: { diff --git a/server/queues/tasks/DeliverWebhookTask.ts b/server/queues/tasks/DeliverWebhookTask.ts index b9bd85113..39b103aa0 100644 --- a/server/queues/tasks/DeliverWebhookTask.ts +++ b/server/queues/tasks/DeliverWebhookTask.ts @@ -120,8 +120,6 @@ export default class DeliverWebhookTask extends BaseTask { case "documents.archive": case "documents.unarchive": case "documents.restore": - case "documents.star": - case "documents.unstar": case "documents.move": case "documents.update": case "documents.title_change": diff --git a/server/routes/api/__snapshots__/documents.test.ts.snap b/server/routes/api/__snapshots__/documents.test.ts.snap index 2ac18507a..b9386a3cc 100644 --- a/server/routes/api/__snapshots__/documents.test.ts.snap +++ b/server/routes/api/__snapshots__/documents.test.ts.snap @@ -44,24 +44,6 @@ Object { } `; -exports[`#documents.star should require authentication 1`] = ` -Object { - "error": "authentication_required", - "message": "Authentication required", - "ok": false, - "status": 401, -} -`; - -exports[`#documents.unstar should require authentication 1`] = ` -Object { - "error": "authentication_required", - "message": "Authentication required", - "ok": false, - "status": 401, -} -`; - exports[`#documents.update should fail if document lastRevision does not match 1`] = ` Object { "error": "invalid_request", diff --git a/server/routes/api/documents.test.ts b/server/routes/api/documents.test.ts index 7a8e5717b..4dfe8648e 100644 --- a/server/routes/api/documents.test.ts +++ b/server/routes/api/documents.test.ts @@ -1,7 +1,6 @@ import { Document, View, - Star, Revision, Backlink, CollectionUser, @@ -1819,79 +1818,6 @@ describe("#documents.restore", () => { }); }); -describe("#documents.star", () => { - it("should star the document", async () => { - const { user, document } = await seed(); - const res = await server.post("/api/documents.star", { - body: { - token: user.getJwtToken(), - id: document.id, - }, - }); - const stars = await Star.findAll(); - expect(res.status).toEqual(200); - expect(stars.length).toEqual(1); - expect(stars[0].documentId).toEqual(document.id); - }); - - it("should require authentication", async () => { - const res = await server.post("/api/documents.star"); - const body = await res.json(); - expect(res.status).toEqual(401); - expect(body).toMatchSnapshot(); - }); - - it("should require authorization", async () => { - const { document } = await seed(); - const user = await buildUser(); - const res = await server.post("/api/documents.star", { - body: { - token: user.getJwtToken(), - id: document.id, - }, - }); - expect(res.status).toEqual(403); - }); -}); - -describe("#documents.unstar", () => { - it("should unstar the document", async () => { - const { user, document } = await seed(); - await Star.create({ - documentId: document.id, - userId: user.id, - }); - const res = await server.post("/api/documents.unstar", { - body: { - token: user.getJwtToken(), - id: document.id, - }, - }); - const stars = await Star.findAll(); - expect(res.status).toEqual(200); - expect(stars.length).toEqual(0); - }); - - it("should require authentication", async () => { - const res = await server.post("/api/documents.star"); - const body = await res.json(); - expect(res.status).toEqual(401); - expect(body).toMatchSnapshot(); - }); - - it("should require authorization", async () => { - const { document } = await seed(); - const user = await buildUser(); - const res = await server.post("/api/documents.unstar", { - body: { - token: user.getJwtToken(), - id: document.id, - }, - }); - expect(res.status).toEqual(403); - }); -}); - describe("#documents.import", () => { it("should error if no file is passed", async () => { const user = await buildUser(); diff --git a/server/routes/api/documents.ts b/server/routes/api/documents.ts index cb7abd660..8a11dadf1 100644 --- a/server/routes/api/documents.ts +++ b/server/routes/api/documents.ts @@ -23,7 +23,6 @@ import { Event, Revision, SearchQuery, - Star, User, View, } from "@server/models"; @@ -731,75 +730,6 @@ router.post( } ); -// Deprecated – use stars.create instead -router.post("documents.star", auth(), async (ctx) => { - const { id } = ctx.body; - assertPresent(id, "id is required"); - const { user } = ctx.state; - - const document = await Document.findByPk(id, { - userId: user.id, - }); - authorize(user, "read", document); - - await Star.findOrCreate({ - where: { - documentId: document.id, - userId: user.id, - }, - }); - - await Event.create({ - name: "documents.star", - documentId: document.id, - collectionId: document.collectionId, - teamId: document.teamId, - actorId: user.id, - data: { - title: document.title, - }, - ip: ctx.request.ip, - }); - - ctx.body = { - success: true, - }; -}); - -// Deprecated – use stars.delete instead -router.post("documents.unstar", auth(), async (ctx) => { - const { id } = ctx.body; - assertPresent(id, "id is required"); - const { user } = ctx.state; - - const document = await Document.findByPk(id, { - userId: user.id, - }); - authorize(user, "read", document); - - await Star.destroy({ - where: { - documentId: document.id, - userId: user.id, - }, - }); - await Event.create({ - name: "documents.unstar", - documentId: document.id, - collectionId: document.collectionId, - teamId: document.teamId, - actorId: user.id, - data: { - title: document.title, - }, - ip: ctx.request.ip, - }); - - ctx.body = { - success: true, - }; -}); - router.post("documents.templatize", auth({ member: true }), async (ctx) => { const { id } = ctx.body; assertPresent(id, "id is required"); diff --git a/server/types.ts b/server/types.ts index eb40f2f8c..2f3b7543e 100644 --- a/server/types.ts +++ b/server/types.ts @@ -95,9 +95,7 @@ export type DocumentEvent = BaseEvent & | "documents.permanent_delete" | "documents.archive" | "documents.unarchive" - | "documents.restore" - | "documents.star" - | "documents.unstar"; + | "documents.restore"; documentId: string; collectionId: string; data: {