♻️Removed unused websockets

This commit is contained in:
Anton SHEPILOV
2024-05-24 00:19:31 +02:00
parent 757a4759f8
commit 6f0bfbcd09
51 changed files with 7 additions and 1620 deletions
@@ -25,14 +25,6 @@
"root": "STATIC_ROOT"
}
},
"websocket": {
"auth": {
"jwt": {
"secret": "AUTH_JWT_SECRET",
"expiration": "AUTH_JWT_EXPIRATION"
}
}
},
"tracker": {
"segment": {
"key": "SEGMENT_SECRET_KEY"
-17
View File
@@ -55,21 +55,6 @@
"root": "./public"
}
},
"websocket": {
"path": "/socket/",
"adapters": {
"types": [],
"redis": {
"host": "redis",
"port": 6379
}
},
"auth": {
"jwt": {
"secret": "supersecret"
}
}
},
"auth": {
"jwt": {
"secret": "supersecret",
@@ -244,12 +229,10 @@
"push",
"storage",
"webserver",
"websocket",
"database",
"cron",
"search",
"message-queue",
"realtime",
"tracker",
"general",
"user",
@@ -24,7 +24,6 @@ const services = [
"applications",
"statistics",
"auth",
"realtime",
"push",
"platform-services",
"user",
@@ -1,4 +1,3 @@
export * from "./consumes";
export * from "./prefix";
export * from "./service-name";
export * from "./realtime";
@@ -1,41 +0,0 @@
import { CreateResult } from "../../api/crud-service";
import { RealtimeEntityEvent, RealtimeEntityActionType } from "../../../services/realtime/types";
import { websocketEventBus } from "../../../services/realtime/bus";
import { getRealtimeRecipients, getRoom, RealtimeRecipients } from ".";
/**
*
* @param path the path to push the notification to
* @param resourcePath the path of the resource itself
*/
export function RealtimeCreated<T>(recipients: RealtimeRecipients<T>): MethodDecorator {
// eslint-disable-next-line @typescript-eslint/ban-types
return function (target: Object, propertyKey: string, descriptor: PropertyDescriptor): void {
const originalMethod = descriptor.value;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
descriptor.value = async function (...args: any[]) {
const result: CreateResult<T> = await originalMethod.apply(this, args);
// context should always be the last arg
const context = args && args[args.length - 1];
if (!(result instanceof CreateResult)) {
return result;
}
getRealtimeRecipients(recipients, result.entity, context).forEach(
({ room, path, resource }) => {
websocketEventBus.publish<T>(RealtimeEntityActionType.Created, {
type: result.type,
room: getRoom(room, result, context),
resourcePath: path || "/",
entity: resource,
result,
} as RealtimeEntityEvent<T>);
},
);
return result;
};
};
}
@@ -1,41 +0,0 @@
import { getRealtimeRecipients, getRoom, RealtimeRecipients } from "..";
import { DeleteResult } from "../../api/crud-service";
import { RealtimeEntityEvent, RealtimeEntityActionType } from "../../../services/realtime/types";
import { websocketEventBus } from "../../../services/realtime/bus";
/**
*
* @param path the path to push the notification to
* @param resourcePath the path of the resource itself
*/
export function RealtimeDeleted<T>(recipients: RealtimeRecipients<T>): MethodDecorator {
// eslint-disable-next-line @typescript-eslint/ban-types
return function (target: Object, propertyKey: string, descriptor: PropertyDescriptor): void {
const originalMethod = descriptor.value;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
descriptor.value = async function (...args: any[]) {
const result: DeleteResult<T> = await originalMethod.apply(this, args);
const context = args && args[args.length - 1];
if (!(result instanceof DeleteResult)) {
return result;
}
if (result.deleted)
getRealtimeRecipients(recipients, result.entity, context).forEach(
({ room, path, resource }) => {
websocketEventBus.publish<T>(RealtimeEntityActionType.Deleted, {
type: result.type,
room: getRoom(room, result, context),
resourcePath: path,
entity: resource,
result,
} as RealtimeEntityEvent<T>);
},
);
return result;
};
};
}
@@ -1,67 +0,0 @@
import { ResourcePath } from "../../../../../core/platform/services/realtime/types";
import { EntityTarget, ExecutionContext } from "../../api/crud-service";
export * from "./created";
export * from "./deleted";
export * from "./updated";
export * from "./saved";
export type RealtimeRecipients<T> =
| RealtimeRecipient<T>
| RealtimeRecipient<T>[]
| ((type: T, context?: ExecutionContext) => RealtimeRecipient<T>[] | RealtimeRecipient<T>);
export type RealtimeRecipient<T> = {
room: RealtimePath<T>;
path?: string;
resource?: any | T;
};
export function getRealtimeRecipients<T>(
recipients: RealtimeRecipients<T>,
type: T,
context?: ExecutionContext,
): RealtimeRecipient<T>[] {
if (typeof recipients === "function") recipients = recipients(type, context);
if (!recipients) return [];
if (!(recipients as RealtimeRecipient<T>[]).length)
recipients = [recipients as RealtimeRecipient<T>];
return (recipients as RealtimeRecipient<T>[]).map(recipient => {
return {
resource: recipient.resource || type,
path: recipient.path || "/",
room: recipient.room,
};
});
}
export type RealtimePath<T> = string | ResourcePath | ResourcePathResolver<T>;
export interface ResourcePathResolver<T> {
(type: T, context?: ExecutionContext): ResourcePath;
}
export interface PathResolver<T> {
(type: T, context?: ExecutionContext): string;
}
export type RealtimeEntity<T> = null | ((type: T, context?: ExecutionContext) => T);
export function getRoom<T>(
path: RealtimePath<T> = ResourcePath.default(),
entity: EntityTarget<T>,
context: ExecutionContext,
): ResourcePath {
if (typeof path === "string") {
return ResourcePath.get(path);
}
return typeof path === "function" ? path(entity.entity, context) : path;
}
export function getPath<T>(
path: string | PathResolver<T> = "/",
entity: EntityTarget<T>,
context: ExecutionContext,
): string {
return typeof path === "function" ? path(entity.entity, context) : path;
}
@@ -1,41 +0,0 @@
import { getRealtimeRecipients, getRoom, RealtimeRecipients } from "..";
import { SaveResult } from "../../api/crud-service";
import { RealtimeEntityEvent, RealtimeEntityActionType } from "../../../services/realtime/types";
import { websocketEventBus } from "../../../services/realtime/bus";
/**
*
* @param path the path to push the notification to
* @param resourcePath the path of the resource itself
*/
export function RealtimeSaved<T>(recipients: RealtimeRecipients<T>): MethodDecorator {
// eslint-disable-next-line @typescript-eslint/ban-types
return function (target: Object, propertyKey: string, descriptor: PropertyDescriptor): void {
const originalMethod = descriptor.value;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
descriptor.value = async function (...args: any[]) {
const result: SaveResult<T> = await originalMethod.apply(this, args);
// context should always be the last arg
const context = args && args[args.length - 1];
if (!(result instanceof SaveResult)) {
return result;
}
getRealtimeRecipients(recipients, result.entity, context).forEach(
({ room, path, resource }) => {
websocketEventBus.publish<T>(RealtimeEntityActionType.Saved, {
type: result.type,
room: getRoom(room, result, context),
resourcePath: path,
entity: resource,
result,
} as RealtimeEntityEvent<T>);
},
);
return result;
};
};
}
@@ -1,40 +0,0 @@
import { getRealtimeRecipients, getRoom, RealtimeRecipients } from "..";
import { UpdateResult } from "../../api/crud-service";
import { RealtimeEntityEvent, RealtimeEntityActionType } from "../../../services/realtime/types";
import { websocketEventBus } from "../../../services/realtime/bus";
/**
*
* @param path the path to push the notification to
* @param resourcePath the path of the resource itself
*/
export function RealtimeUpdated<T>(recipients: RealtimeRecipients<T>): MethodDecorator {
// eslint-disable-next-line @typescript-eslint/ban-types
return function (target: Object, propertyKey: string, descriptor: PropertyDescriptor): void {
const originalMethod = descriptor.value;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
descriptor.value = async function (...args: any[]) {
const result: UpdateResult<T> = await originalMethod.apply(this, args);
const context = args && args[args.length - 1];
if (!(result instanceof UpdateResult)) {
return result;
}
getRealtimeRecipients(recipients, result.entity, context).forEach(
({ room, path, resource }) => {
websocketEventBus.publish<T>(RealtimeEntityActionType.Updated, {
type: result.type,
room: getRoom(room, result, context),
resourcePath: path,
entity: resource,
result,
} as RealtimeEntityEvent<T>);
},
);
return result;
};
};
}
@@ -1,78 +0,0 @@
import { TdriveServiceProvider } from "../../framework";
import { WebSocketUser, WebSocket } from "../../services/websocket/types";
import RealtimeEntityManager from "./services/entity-manager";
import { RealtimeEntityEvent } from "./types";
export interface RealtimeServiceAPI extends TdriveServiceProvider {
/**
* Get the realtime event bus instance
*/
getBus(): RealtimeEventBus;
/**
* Get the room manager
*/
getRoomManager(): RealtimeRoomManager;
/**
* Get the entity manager
*/
getEntityManager(): RealtimeEntityManager;
/**
* Create a token for a room
*/
sign(items: { room: string }[], userId: string): { room: string; token: string }[];
}
export interface RealtimeRoomManager {
/**
* Get the number of connected users in the given room
* @param room name of the room
*/
getConnectedUsers(room: string): number;
/**
* Add the user to a room
*
* @param websocket
* @param room
* @param user
*/
join(websocket: WebSocket, room: string, user: WebSocketUser): void;
/**
* Remove the user from a room
*
* @param websocket
* @param room
* @param user
*/
leave(websocket: WebSocket, room: string, user: WebSocketUser): void;
/**
* Remove a user from all the room he joined
*
* @param websocket
* @param user
*/
leaveAll(websocket: WebSocket, user: WebSocketUser): void;
}
export interface RealtimeEventBus {
/**
* Subscribes to a topic
*
* @param topic Topic to subscribe to
* @param listener Listener to call when event is published in the given topic
*/
subscribe<Entity>(topic: string, listener: (event: RealtimeEntityEvent<Entity>) => void): this;
/**
* Publish event in a topic
*
* @param topic Topic to publish event to
* @param event Event to publish
*/
publish<Entity>(topic: string, event: RealtimeEntityEvent<Entity>): boolean;
}
@@ -1,22 +0,0 @@
/**
* EventBus is used internally to exchange events between decorators and realtime manager
*/
import { EventEmitter } from "events";
import { RealtimeEventBus } from "./api";
import { RealtimeEntityEvent } from "./types";
class WebsocketEventBus extends EventEmitter implements RealtimeEventBus {
constructor() {
super();
}
subscribe<Entity>(topic: string, listener: (event: RealtimeEntityEvent<Entity>) => void): this {
return this.on(topic, listener);
}
publish<Entity>(topic: string, event: RealtimeEntityEvent<Entity>): boolean {
return this.emit(topic, event);
}
}
export const websocketEventBus = new WebsocketEventBus();
@@ -1,76 +0,0 @@
import { Consumes, ServiceName, TdriveService } from "../../framework";
import { SkipCLI } from "../../framework/decorators/skip";
import { localEventBus } from "../../framework/event-bus";
import WebSocketAPI from "../../services/websocket/provider";
import AuthService from "../auth/provider";
import { RealtimeEventBus, RealtimeRoomManager, RealtimeServiceAPI } from "./api";
import { websocketEventBus } from "./bus";
import RealtimeEntityManager from "./services/entity-manager";
import RoomManagerImpl from "./services/room-manager";
import { RealtimeBaseBusEvent, RealtimeLocalBusEvent } from "./types";
@Consumes(["websocket", "auth"])
@ServiceName("realtime")
export default class RealtimeService
extends TdriveService<RealtimeServiceAPI>
implements RealtimeServiceAPI
{
private roomManager: RoomManagerImpl;
private entityManager: RealtimeEntityManager;
private auth: AuthService;
version = "1";
api(): RealtimeServiceAPI {
return this;
}
@SkipCLI()
async doStart(): Promise<this> {
const ws = this.context.getProvider<WebSocketAPI>("websocket");
this.auth = this.context.getProvider<AuthService>("auth");
this.roomManager = new RoomManagerImpl(ws, this.auth);
this.roomManager.init();
this.entityManager = new RealtimeEntityManager(ws);
this.entityManager.init();
localEventBus.subscribe("realtime:event", (event: RealtimeBaseBusEvent<any>) => {
event.data._type = event.type;
ws.getIo().to(event.room).emit("realtime:event", { name: event.room, data: event.data });
});
localEventBus.subscribe("realtime:publish", (data: RealtimeLocalBusEvent<any>) => {
this.getBus().publish(data.topic, data.event);
});
return this;
}
getBus(): RealtimeEventBus {
return websocketEventBus;
}
getRoomManager(): RealtimeRoomManager {
return this.roomManager;
}
getEntityManager(): RealtimeEntityManager {
return this.entityManager;
}
sign(items: { room: string }[], sub: string) {
return items.map(item => {
const token = this.auth.sign({
sub,
name: item.room,
//Fixme, reduce this number but also in frontend reload the websocket token after this ends
iat: Math.round(new Date().getTime() / 1000) + 60 * 60 * 24 * 31,
nbf: Math.round(new Date().getTime() / 1000) - 60,
});
return {
...item,
token,
};
});
}
}
@@ -1,54 +0,0 @@
import { logger } from "../../../framework/logger";
import { RealtimeEntityActionType, RealtimeEntityEvent } from "../types";
import WebSocketAPI from "../../../services/websocket/provider";
import { websocketEventBus } from "../bus";
const REALTIME_RESOURCE = "realtime:resource";
export default class RealtimeEntityManager {
constructor(private ws: WebSocketAPI) {}
init(): void {
websocketEventBus.subscribe(RealtimeEntityActionType.Created, event => {
this.pushResourceEvent(event, RealtimeEntityActionType.Created);
});
websocketEventBus.subscribe(RealtimeEntityActionType.Updated, event => {
this.pushResourceEvent(event, RealtimeEntityActionType.Updated);
});
websocketEventBus.subscribe(RealtimeEntityActionType.Deleted, event => {
this.pushResourceEvent(event, RealtimeEntityActionType.Deleted);
});
websocketEventBus.subscribe(RealtimeEntityActionType.Saved, event => {
this.pushResourceEvent(event, RealtimeEntityActionType.Saved);
});
websocketEventBus.subscribe(RealtimeEntityActionType.Event, event => {
this.pushResourceEvent(event, RealtimeEntityActionType.Event);
});
}
private pushResourceEvent(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
event: RealtimeEntityEvent<any>,
action: RealtimeEntityActionType,
): void {
event.room.path.forEach(path => {
logger.info(`Pushing ${action} entity to room ${path}`);
const message: unknown = {
action,
room: path,
type: event.type,
path: event.resourcePath,
resource: event.entity,
};
if (logger.isLevelEnabled("debug")) {
logger.debug(`Entity to push to room ${path}: %o`, message);
}
this.ws.getIo().to(path).emit(REALTIME_RESOURCE, message);
});
}
}
@@ -1,2 +0,0 @@
export * from "./entity-manager";
export * from "./room-manager";
@@ -1,164 +0,0 @@
import { logger } from "../../../framework/logger";
import {
JoinLeaveRoomError,
JoinLeaveRoomSuccess,
JoinRoomEvent,
LeaveRoomEvent,
ClientEvent,
WebsocketRoomSignature,
} from "../types";
import { RealtimeRoomManager } from "../api";
import WebSocketAPI from "../../../services/websocket/provider";
import { WebSocketUser, WebSocket } from "../../../services/websocket/types";
import AuthService from "../../auth/provider";
export default class RoomManager implements RealtimeRoomManager {
constructor(private ws: WebSocketAPI, private auth: AuthService) {}
init(): void {
this.ws.onUserConnected(event => {
logger.info(`User ${event.user.id} is connected`);
event.socket.on("realtime:join", async (joinEvent: JoinRoomEvent) => {
const canJoin =
joinEvent.name.indexOf("previous::") === 0 || //Compatibility with old collections
(await this.userCanJoinRoom(event.user, joinEvent));
if (canJoin) {
this.join(event.socket, joinEvent.name, event.user);
} else {
this.sendError("join", event.socket, {
name: joinEvent.name,
message: "User is not authorized to join room",
});
}
});
event.socket.on("realtime:leave", (leaveEvent: LeaveRoomEvent) => {
this.leave(event.socket, leaveEvent.name, event.user);
});
event.socket.on("realtime:event", async (clientEvent: ClientEvent) => {
const canEmit =
clientEvent.name.indexOf("previous::") === 0 || //Compatibility with old collections
(await this.userCanEmitInRoom(event.user, clientEvent));
if (canEmit) {
this.sendEvent(clientEvent.name, clientEvent.data);
} else {
this.sendError("event", event.socket, {
name: clientEvent.name,
message: "User is not authorized to emit in this room",
});
}
});
});
this.ws.onUserDisconnected(event => {
logger.info(`User ${event.user.id} is disconnected`);
this.leaveAll(event.socket, event.user);
});
}
getConnectedUsers(room: string): number {
if (this.ws.getIo().sockets.adapter.rooms.has(room)) {
return this.ws.getIo().sockets.adapter.rooms.get(room).entries.length;
}
return 0;
}
/**
* Check of the user can join room
*
* @param user
* @param joinEvent
* @return Promise<boolean> true if can join, false otherwise. Never rejects.
*/
async userCanJoinRoom(user: WebSocketUser, joinEvent: JoinRoomEvent): Promise<boolean> {
logger.info(`Checking if user ${user.id} can join room ${joinEvent.name} with token`);
try {
//Public rooms we just check the user is logged in
if (
joinEvent.name.startsWith("/users/") || //User update room
joinEvent.name === "/ping"
) {
return !!this.auth.verifyToken(joinEvent.token)?.sub;
}
//Retro-compatibility for mobile up to february 2021 (to remove after this date)
if (joinEvent.token === "tdrive") return true;
const signature = this.auth.verifyTokenObject<WebsocketRoomSignature>(joinEvent.token);
return (
signature &&
signature.name === joinEvent.name &&
signature.sub === user.id &&
signature.iat > Math.round(new Date().getTime() / 1000)
);
} catch (err) {
console.log(err);
return false;
}
}
userCanEmitInRoom = this.userCanJoinRoom;
async join(websocket: WebSocket, room: string, user: WebSocketUser): Promise<void> {
logger.info(`User ${user.id} is joining room ${room}`);
try {
await websocket.join(room);
} catch (err) {
if (err) {
logger.error(`Error while joining room ${room}`, err);
this.sendError("join", websocket, {
name: room,
message: "Error while joining room",
});
return;
}
}
this.sendSuccess("join", websocket, { name: room });
logger.info(`User ${user.id} joined room ${room}`);
}
async leave(websocket: WebSocket, room: string, user: WebSocketUser): Promise<void> {
logger.info(`User ${user.id} is leaving room ${room}`);
try {
await websocket.leave(room);
} catch (err) {
if (err) {
logger.error(`Error while leaving room ${room}`, err);
this.sendError("leave", websocket, {
name: room,
message: "Error while leaving room",
});
return;
}
}
this.sendSuccess("leave", websocket, { name: room });
logger.info(`User ${user.id} left room ${room}`);
}
leaveAll(websocket: WebSocket, user: WebSocketUser): void {
logger.info(`Leaving rooms for user ${user.id}`);
websocket.rooms.forEach(room => websocket.leave(room));
}
sendError(event: string, websocket: WebSocket, error: JoinLeaveRoomError): void {
websocket.emit(`realtime:${event}:error`, error);
}
sendSuccess(event: string, websocket: WebSocket, success: JoinLeaveRoomSuccess): void {
websocket.emit(`realtime:${event}:success`, success);
}
sendEvent(path: string, data: any): void {
this.ws.getIo().to(path).emit("realtime:event", { name: path, data: data });
}
}
@@ -1,83 +0,0 @@
import { EntityOperationResult } from "../../framework/api/crud-service";
export enum RealtimeEntityActionType {
Created = "created",
Saved = "saved",
Updated = "updated",
Deleted = "deleted",
Event = "event",
}
export interface WebsocketRoomSignature {
name: string; //Ws room name
sub: string; //User id
iat: number; //Deadline
}
export class ResourcePath {
constructor(readonly path: string[] = ["/"], readonly name: string = "default") {}
static default(): ResourcePath {
return new ResourcePath();
}
static get(path: string | string[]): ResourcePath {
return new ResourcePath(typeof path === "string" ? [path] : path);
}
}
export class RealtimeEntityEvent<Entity> {
// the type of the resource
type: string;
// the room to push the resource to
room: ResourcePath;
// the unique full path of the resource, ie where we can get it
resourcePath: string | null;
// the input resource
entity: Entity;
// the action result which fired this event
result: EntityOperationResult<Entity> | null;
}
export class RealtimeApplicationEvent {
action: "configure" | "close_configure";
application: unknown;
form: unknown;
hidden_data: unknown;
}
export class RealtimeBaseBusEvent<T> {
room: string;
type: string;
data: T;
}
export class RealtimeLocalBusEvent<Entity> {
event: RealtimeEntityEvent<Entity>;
topic: string;
}
export class JoinRoomEvent {
name: string;
token?: string;
}
export class LeaveRoomEvent {
name: string;
}
export class ClientEvent {
name: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: any;
}
export interface JoinLeaveRoomError {
name: string;
message?: string;
type?: string;
}
export interface JoinLeaveRoomSuccess {
name: string;
message?: string;
}
@@ -1,47 +0,0 @@
import { Consumes, ServiceName, TdriveService } from "../../framework";
import WebServerAPI from "../webserver/provider";
import WebSocketAPI from "./provider";
import { WebSocketService } from "./services";
import { AdaptersConfiguration } from "./types";
import FastifyIO from "fastify-socket.io";
@Consumes(["webserver"])
@ServiceName("websocket")
export default class WebSocket extends TdriveService<WebSocketAPI> {
private service: WebSocketService;
name = "websocket";
version = "1";
api(): WebSocketAPI {
return this.service;
}
async doInit(): Promise<this> {
const webserver = this.context.getProvider<WebServerAPI>("webserver");
const fastify = webserver.getServer();
const options = {
path: this.configuration.get<string>("path", "/socket"),
};
fastify.register(FastifyIO, {
...options,
allowEIO3: true,
cors: {
//Allow all origins
origin: (origin, callback) => callback(null, origin),
credentials: true,
},
});
this.service = new WebSocketService({
server: fastify,
options,
ready: webserver.onReady.bind(webserver),
adapters: this.configuration.get<AdaptersConfiguration>("adapters"),
auth: this.configuration.get<{ secret: string }>("auth.jwt"),
});
return this;
}
}
@@ -1,17 +0,0 @@
import { EventEmitter } from "events";
import socketIO from "socket.io";
import { TdriveServiceProvider } from "../../framework";
import { User } from "../../../../utils/types";
import { WebsocketUserEvent, WebSocket, WebSocketUser } from "./types";
export default interface WebSocketAPI extends TdriveServiceProvider, EventEmitter {
getIo(): socketIO.Server;
isConnected(user: User): boolean;
onUserConnected(listener: (event: WebsocketUserEvent) => void): this;
onUserDisconnected(listener: (event: WebsocketUserEvent) => void): this;
getUser(websocket: WebSocket): WebSocketUser;
}
@@ -1,105 +0,0 @@
import socketIO from "socket.io";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { createClient } = require("redis");
import SocketIORedis from "@socket.io/redis-adapter";
import jwt from "jsonwebtoken";
import WebSocketAPI from "../provider";
import {
WebSocket,
WebSocketServiceConfiguration,
WebSocketUser,
WebsocketUserEvent,
} from "../types";
import { EventEmitter } from "events";
import { User } from "../../../../../utils/types";
import { JwtType } from "../../types";
export class WebSocketService extends EventEmitter implements WebSocketAPI {
version: "1";
private io: socketIO.Server;
constructor(serviceConfiguration: WebSocketServiceConfiguration) {
super();
serviceConfiguration.ready(() => {
this.io = serviceConfiguration.server.io;
if (serviceConfiguration.adapters?.types?.includes("redis")) {
const pubClient = createClient(serviceConfiguration.adapters.redis);
const subClient = pubClient.duplicate();
this.io.adapter(SocketIORedis.createAdapter(pubClient, subClient));
}
this.io.on("connection", socket => {
socket.on("authenticate", message => {
if (message.token) {
jwt.verify(
message.token as string,
serviceConfiguration.auth.secret as string,
(err, decoded) => {
if (err) {
socket.emit("unauthorized", { err });
socket.disconnect();
return;
}
(socket as unknown as WebSocket).decoded_token = decoded as JwtType;
const user = this.getUser(socket as WebSocket);
socket.emit("authenticated");
socket.on("message", message => {
this.io.emit("message", message);
});
this.emit("user:connected", {
user,
socket,
event: "user:connected",
} as WebsocketUserEvent);
socket.on("disconnect", () =>
this.emit("user:disconnected", {
user,
socket,
event: "user:disconnected",
} as WebsocketUserEvent),
);
},
);
} else {
socket.emit("unauthorized", { err: "No token provided" });
socket.disconnect();
return;
}
});
});
});
}
onUserConnected(listener: (event: WebsocketUserEvent) => void): this {
return this.on("user:connected", listener);
}
onUserDisconnected(listener: (event: WebsocketUserEvent) => void): this {
return this.on("user:disconnected", listener);
}
getUser(socket: WebSocket): WebSocketUser {
return {
id: socket.decoded_token.sub,
identity_provider_id: socket.decoded_token.provider_id,
email: socket.decoded_token.email,
token: socket.decoded_token,
allow_tracking: socket.decoded_token.track,
};
}
getIo(): socketIO.Server {
return this.io;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
isConnected(user: User): boolean {
return false;
}
}
@@ -1,39 +0,0 @@
import socketIO, { Socket } from "socket.io";
import { RedisAdapterOptions } from "@socket.io/redis-adapter";
import { User } from "../../../../utils/types";
import { JwtType } from "../types";
import { FastifyInstance } from "fastify";
export interface AdaptersConfiguration {
types: Array<string>;
redis: RedisAdapterOptions;
}
export interface WebSocketServiceConfiguration {
server: FastifyInstance;
// eslint-disable-next-line @typescript-eslint/ban-types
ready: Function;
options?: socketIO.ServerOptions | { path: string };
adapters?: AdaptersConfiguration;
auth?: { secret: string };
}
export interface WebSocketUser extends User {
token?: DecodedToken;
}
export interface WebSockets {
[index: string]: WebSocket[];
}
export interface WebSocket extends Socket {
decoded_token: DecodedToken;
}
export interface WebsocketUserEvent {
event: string;
socket: WebSocket;
user: WebSocketUser;
}
type DecodedToken = JwtType;
@@ -1,12 +1,6 @@
import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import _ from "lodash";
import { v4 } from "uuid";
import { CrudException } from "../../../../core/platform/framework/api/crud-service";
import { localEventBus } from "../../../../core/platform/framework/event-bus";
import {
RealtimeApplicationEvent,
RealtimeBaseBusEvent,
} from "../../../../core/platform/services/realtime/types";
import { ResourceGetResponse } from "../../../../utils/types";
import {
ApplicationObject,
@@ -84,26 +78,6 @@ export class ApplicationsApiController {
throw CrudException.forbidden("Application not found");
}
const body = request.body;
const data = {
action: "configure",
application: {
id: app_id,
identity: application.identity,
},
form: body.form,
connection_id: body.connection_id,
hidden_data: {},
configurator_id: v4(),
};
localEventBus.publish("realtime:event", {
room: "/me/" + body.user_id,
type: "application",
data,
} as RealtimeBaseBusEvent<RealtimeApplicationEvent>);
return { status: "ok" };
}
@@ -1,13 +0,0 @@
import { WebsocketMetadata } from "../../utils/types";
export function getCompanyApplicationRooms(companyId: string): WebsocketMetadata[] {
return [
{
room: getCompanyApplicationRoom(companyId),
},
];
}
export function getCompanyApplicationRoom(companyApplicationId: string): string {
return `/company-application/${companyApplicationId}`;
}
@@ -10,7 +10,6 @@ import {
import { PublicApplicationObject } from "../../entities/application";
import { CompanyExecutionContext } from "../types";
import { CrudController } from "../../../../core/platform/services/webserver/types";
import { getCompanyApplicationRooms } from "../../realtime";
import gr from "../../../global-resolver";
export class CompanyApplicationController
@@ -55,11 +54,6 @@ export class CompanyApplicationController
return {
resources: resources.getEntities().map(ca => ca.application),
next_page_token: resources.nextPage?.page_token,
websockets:
gr.platformServices.realtime.sign(
getCompanyApplicationRooms(request.params.company_id),
context.user.id,
) || [],
};
}
}
@@ -53,13 +53,8 @@ import {
makeStandaloneAccessLevel,
getItemScope,
} from "./access-check";
import { websocketEventBus } from "../../../core/platform/services/realtime/bus";
import archiver from "archiver";
import internal from "stream";
import {
RealtimeEntityActionType,
ResourcePath,
} from "../../../core/platform/services/realtime/types";
import config from "config";
export class DocumentsService {
version: "1";
@@ -410,9 +405,6 @@ export class DocumentsService {
//TODO[ASH] update item size only for files, there is not need to do during direcotry creation
await updateItemSize(driveItem.parent_id, this.repository, context);
//TODO[ASH] there is no need to notify websocket, until we implement user notification
await this.notifyWebsocket(driveItem.parent_id, context);
await globalResolver.platformServices.messageQueue.publish<DocumentsMessageQueueRequest>(
"services:documents:process",
{
@@ -681,11 +673,7 @@ export class DocumentsService {
await this.update(item.id, item, context);
}
await updateItemSize(previousParentId, this.repository, context);
await this.notifyWebsocket(previousParentId, context);
}
await this.notifyWebsocket("trash", context);
};
/**
@@ -963,19 +951,6 @@ export class DocumentsService {
return archive;
};
notifyWebsocket = async (id: string, context: DriveExecutionContext) => {
websocketEventBus.publish(RealtimeEntityActionType.Event, {
type: "documents:updated",
room: ResourcePath.get(`/companies/${context.company.id}/documents/item/${id}`),
entity: {
companyId: context.company.id,
id: id,
},
resourcePath: null,
result: null,
});
};
/**
* Search for Drive items.
*
@@ -5,11 +5,7 @@ import { CrudException, ListResult } from "../../../../core/platform/framework/a
import { File } from "../../../../services/files/entities/file";
import { UploadOptions } from "../../../../services/files/types";
import globalResolver from "../../../../services/global-resolver";
import {
CompanyUserRole,
PaginationQueryParameters,
ResourceWebsocket,
} from "../../../../utils/types";
import { CompanyUserRole, PaginationQueryParameters } from "../../../../utils/types";
import { DriveFile } from "../../entities/drive-file";
import { FileVersion } from "../../entities/file-version";
import {
@@ -160,18 +156,12 @@ export class DocumentsController {
Params: ItemRequestParams;
Querystring: PaginationQueryParameters & { public_token?: string };
}>,
): Promise<DriveItemDetails & { websockets: ResourceWebsocket[] }> => {
): Promise<DriveItemDetails> => {
const context = getDriveExecutionContext(request);
const { id } = request.params;
return {
...(await globalResolver.services.documents.documents.get(id, context)),
websockets: request.currentUser?.id
? globalResolver.platformServices.realtime.sign(
[{ room: `/companies/${context.company.id}/documents/item/${id}` }],
request.currentUser?.id,
)
: [],
};
};
@@ -188,7 +178,7 @@ export class DocumentsController {
Body: SearchDocumentsBody;
Querystring: PaginationQueryParameters & { public_token?: string };
}>,
): Promise<DriveItemDetails & { websockets: ResourceWebsocket[] }> => {
): Promise<DriveItemDetails> => {
const context = getDriveExecutionContext(request);
const { id } = request.params;
@@ -202,7 +192,6 @@ export class DocumentsController {
return {
...(await globalResolver.services.documents.documents.browse(id, options, context)),
websockets: [],
};
};
@@ -9,12 +9,10 @@ import { DatabaseServiceAPI } from "../core/platform/services/database/api";
import EmailPusherAPI from "../core/platform/services/email-pusher/provider";
import { MessageQueueServiceAPI } from "../core/platform/services/message-queue/api";
import { PushServiceAPI } from "../core/platform/services/push/api";
import { RealtimeServiceAPI } from "../core/platform/services/realtime/api";
import { SearchServiceAPI } from "../core/platform/services/search/api";
import StorageAPI from "../core/platform/services/storage/provider";
import TrackerAPI from "../core/platform/services/tracker/provider";
import WebServerAPI from "../core/platform/services/webserver/provider";
import WebSocketAPI from "../core/platform/services/websocket/provider";
import assert from "assert";
import { logger } from "../core/platform/framework";
@@ -42,12 +40,10 @@ type PlatformServices = {
cron: CronAPI;
messageQueue: MessageQueueServiceAPI;
push: PushServiceAPI;
realtime: RealtimeServiceAPI;
search: SearchServiceAPI;
storage: StorageAPI;
tracker: TrackerAPI;
webserver: WebServerAPI;
websocket: WebSocketAPI;
emailPusher: EmailPusherAPI;
};
@@ -96,12 +92,10 @@ class GlobalResolver {
cron: platform.getProvider<CronAPI>("cron"),
messageQueue: platform.getProvider<MessageQueueServiceAPI>("message-queue"),
push: platform.getProvider<PushServiceAPI>("push"),
realtime: platform.getProvider<RealtimeServiceAPI>("realtime"),
search: platform.getProvider<SearchServiceAPI>("search"),
storage: platform.getProvider<StorageAPI>("storage"),
tracker: platform.getProvider<TrackerAPI>("tracker"),
webserver: platform.getProvider<WebServerAPI>("webserver"),
websocket: platform.getProvider<WebSocketAPI>("websocket"),
emailPusher: platform.getProvider<EmailPusherAPI>("email-pusher"),
};
@@ -1,45 +0,0 @@
import { WebsocketMetadata } from "../../utils/types";
import CompanyEntity from "./entities/company";
import UserEntity from "./entities/user";
/**
* User Rooms
*/
export function getUserRooms(user: UserEntity): WebsocketMetadata[] {
return [
{
room: getUserRoom(user.id),
},
];
}
export function getUserRoom(userId: string): string {
return `/me/${userId}`;
}
export function getPublicUserRoom(userId: string): string {
return `/users/${userId}`;
}
export function getUserName(userId: string): string {
return `user-room-${userId}`;
}
/**
* Company Rooms
*/
export function getCompanyRooms(company: CompanyEntity): WebsocketMetadata[] {
return [
{
room: getCompanyRoom(company.id),
},
];
}
export function getCompanyRoom(companyId: string): string {
return `/company/${companyId}`;
}
export function getCompanyName(companyId: string): string {
return `company-room-${companyId}`;
}
@@ -30,8 +30,7 @@ import ExternalGroup, {
ExternalGroupPrimaryKey,
getInstance as getExternalGroupInstance,
} from "../entities/external_company";
import { logger, RealtimeSaved } from "../../../core/platform/framework";
import { getCompanyRoom, getUserRoom } from "../realtime";
import { logger } from "../../../core/platform/framework";
import gr from "../../global-resolver";
import { localEventBus } from "../../../core/platform/framework/event-bus";
@@ -59,15 +58,6 @@ export class CompanyServiceImpl {
return this.externalCompanyRepository.findOne(pk, {}, context);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@RealtimeSaved<Company>((company, _context) => {
return [
{
room: getCompanyRoom(company.id),
resource: company,
},
];
})
async updateCompany<SaveOptions>(
company: Company,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -186,14 +176,6 @@ export class CompanyServiceImpl {
);
}
@RealtimeSaved<CompanyUser>((companyUser, _) => {
return [
{
room: getUserRoom(companyUser?.user_id),
resource: companyUser,
},
];
})
async removeUserFromCompany(
companyPk: CompanyPrimaryKey,
userPk: UserPrimaryKey,
@@ -249,14 +231,6 @@ export class CompanyServiceImpl {
return new DeleteResult<Company>("company", instance, !!instance);
}
@RealtimeSaved<CompanyUser>((companyUser, _) => {
return [
{
room: getUserRoom(companyUser?.user_id),
resource: companyUser,
},
];
})
async setUserRole(
companyId: uuid,
userId: uuid,
@@ -28,11 +28,8 @@ import assert from "assert";
import { localEventBus } from "../../../../core/platform/framework/event-bus";
import { ResourceEventsPayload } from "../../../../utils/types";
import { isNumber, isString } from "lodash";
import { RealtimeSaved } from "../../../../core/platform/framework";
import { getPublicUserRoom, getUserRoom } from "../../realtime";
import NodeCache from "node-cache";
import gr from "../../../global-resolver";
import { formatUser } from "../../../../utils/users";
import { TYPE as DriveFileType, DriveFile } from "../../../documents/entities/drive-file";
export class UserServiceImpl {
@@ -99,34 +96,11 @@ export class UserServiceImpl {
throw new Error("Method not implemented.");
}
@RealtimeSaved<User>((user, _context) => {
return [
{
room: getPublicUserRoom(user.id),
resource: user,
},
];
})
async publishPublicUserRealtime(userId: string): Promise<void> {
const user = await this.get({ id: userId });
new SaveResult("user", formatUser(user, { includeCompanies: true }), OperationType.UPDATE);
}
@RealtimeSaved<User>((user, _context) => {
return [
{
room: getUserRoom(user.id),
resource: formatUser(user), // FIX ME we should formatUser here
},
];
})
async save(user: User, context?: ExecutionContext): Promise<SaveResult<User>> {
this.assignDefaults(user);
await this.repository.save(user, context);
await this.updateExtRepository(user);
await this.publishPublicUserRealtime(user.id);
return new SaveResult("user", user, OperationType.UPDATE);
}
@@ -20,19 +20,18 @@ import {
CompanyParameters,
CompanyStatsObject,
CompanyUserObject,
CompanyUsersParameters,
DeregisterDeviceParams,
RegisterDeviceBody,
RegisterDeviceParams,
UserListQueryParameters,
UserObject,
UserParameters,
CompanyUsersParameters,
UserQuota,
} from "./types";
import Company from "../entities/company";
import CompanyUser from "../entities/company_user";
import coalesce from "../../../utils/coalesce";
import { getCompanyRooms, getUserRooms } from "../realtime";
import { formatCompany, getCompanyStats } from "../utils";
import { formatUser } from "../../../utils/users";
import gr from "../../global-resolver";
@@ -70,9 +69,6 @@ export class UsersCrudController
return {
resource: userObject,
websocket: context.user.id
? gr.platformServices.realtime.sign(getUserRooms(user), context.user.id)[0]
: undefined,
};
}
@@ -100,11 +96,7 @@ export class UsersCrudController
async setPreferences(
request: FastifyRequest<{ Body: User["preferences"] }>,
): Promise<User["preferences"]> {
const preferences = await gr.services.users.setPreferences(
{ id: request.currentUser.id },
request.body,
);
return preferences;
return await gr.services.users.setPreferences({ id: request.currentUser.id }, request.body);
}
async list(
@@ -143,7 +135,6 @@ export class UsersCrudController
// return users;
return {
resources: resUsers,
websockets: gr.platformServices.realtime.sign([], context.user.id),
};
}
@@ -220,7 +211,6 @@ export class UsersCrudController
return {
resources: combos.map(combo => formatCompany(...combo)),
websockets: gr.platformServices.realtime.sign([], context.user.id),
};
}
@@ -253,9 +243,6 @@ export class UsersCrudController
companyUserObj,
getCompanyStats(company, await gr.services.statistics.get(company.id, "messages")),
),
websocket: context.user?.id
? gr.platformServices.realtime.sign(getCompanyRooms(company), context.user.id)[0]
: undefined,
};
}
@@ -1,4 +1,3 @@
import { webSocketSchema } from "../../../utils/types";
import { CompanyFeaturesEnum, CompanyLimitsEnum } from "./types";
export const userObjectSchema = {
@@ -126,7 +125,6 @@ export const getUserSchema = {
type: "object",
properties: {
resource: userObjectSchema,
websocket: webSocketSchema,
},
required: ["resource"],
},
@@ -231,7 +229,6 @@ export const getCompanySchema = {
type: "object",
properties: {
resource: companyObjectSchema,
websocket: webSocketSchema,
},
required: ["resource"],
},
@@ -1,30 +0,0 @@
import { WebsocketMetadata } from "../../utils/types";
import Workspace from "./entities/workspace";
import { WorkspaceExecutionContext } from "./types";
export function getWebsocketInformation(workspace: Workspace): WebsocketMetadata {
return {
name: workspace.name,
room: getWorkspacePath(workspace),
};
}
export function getWorkspaceRooms(context: WorkspaceExecutionContext): WebsocketMetadata[] {
return [
{
name: "workspaces",
room: getRoomName(context),
},
];
}
export function getRoomName(context: Pick<Workspace, "company_id">): string {
return `/companies/${context.company_id}/workspaces`;
}
export function getWorkspacePath(
workspace: Pick<Workspace, "id">,
context?: WorkspaceExecutionContext,
): string {
return `/companies/${context?.company_id}/workspaces/${workspace.id}`;
}
@@ -41,13 +41,7 @@ import {
WorkspaceCounterPrimaryKey,
} from "../entities/workspace_counters";
import { countRepositoryItems } from "../../../utils/counters";
import {
Initializable,
RealtimeSaved,
TdriveServiceProvider,
} from "../../../core/platform/framework";
import { ResourcePath } from "../../../core/platform/services/realtime/types";
import { getRoomName, getWorkspacePath } from "../realtime";
import { Initializable, TdriveServiceProvider } from "../../../core/platform/framework";
import { InviteTokenObject, WorkspaceInviteTokenObject } from "../web/types";
import WorkspaceInviteTokens, {
getInstance as getWorkspaceInviteTokensInstance,
@@ -149,15 +143,6 @@ export class WorkspaceServiceImpl implements TdriveServiceProvider, Initializabl
return new CreateResult<Workspace>(TYPE, created.entity);
}
// TODO: remove logic from context
@RealtimeSaved<Workspace>((workspace, context) => [
{
// FIXME: For now the room is defined at the company level
// It meay be good to have a special room where just some users are receiving this event
room: ResourcePath.get(getRoomName(workspace)),
path: getWorkspacePath(workspace, context as WorkspaceExecutionContext),
},
])
async save<SaveOptions>(
item: Partial<Workspace>,
options?: SaveOptions & { logo_b64?: string },
@@ -19,7 +19,6 @@ import { WorkspaceExecutionContext } from "../../types";
import { plainToClass } from "class-transformer";
import { hasCompanyAdminLevel, hasCompanyMemberLevel } from "../../../../utils/company";
import { hasWorkspaceAdminLevel } from "../../../../utils/workspace";
import { getWorkspaceRooms } from "../../realtime";
import {
CrudException,
ExecutionContext,
@@ -166,7 +165,6 @@ export class WorkspacesCrudController
this.formatWorkspace(ws, await this.getWorkspaceUsersCount(ws.id), context.user.id),
),
),
websockets: gr.platformServices.realtime.sign(getWorkspaceRooms(context), context.user.id),
};
}
@@ -1,4 +1,3 @@
import { webSocketSchema } from "../../../utils/types";
import { companyObjectSchema, userObjectSchema } from "../../user/web/schemas";
const workspaceObjectSchema = {
@@ -35,7 +34,6 @@ export const getWorkspacesSchema = {
type: "object",
properties: {
resources: { type: "array", items: workspaceObjectSchema },
websockets: { type: "array", items: webSocketSchema },
},
required: ["resources"],
},
-42
View File
@@ -32,51 +32,28 @@ export interface User {
public_token_document_id?: string;
}
export const webSocketSchema = {
type: "object",
properties: {
name: { type: "string" },
room: { type: "string" },
token: { type: "string" },
},
};
export interface Channel extends Workspace {
id: string;
}
export enum ChannelType {
DIRECT = "direct",
}
export interface Workspace {
company_id: string;
workspace_id: string;
}
export interface WebsocketMetadata {
room: string;
name?: string;
token?: string;
}
export class ResourceListResponse<T> {
resources: T[];
websockets?: ResourceWebsocket[];
next_page_token?: string;
total?: number;
}
export class ResourceGetResponse<T> {
websocket?: ResourceWebsocket;
resource: T;
}
export class ResourceUpdateResponse<T> {
websocket?: ResourceWebsocket;
resource: T;
}
export class ResourceCreateResponse<T> {
websocket?: ResourceWebsocket;
resource: T;
}
@@ -84,16 +61,7 @@ export class ResourceDeleteResponse {
status: DeleteStatus;
}
export interface ResourceListQueryParameters extends PaginationQueryParameters {
search_query?: string;
mine?: boolean;
}
export declare type DeleteStatus = "success" | "error";
export interface ResourceWebsocket {
room: string;
token?: string;
}
export interface ResourceEventsPayload {
user: User;
@@ -104,21 +72,11 @@ export interface ResourceEventsPayload {
workspace?: WorkspacePrimaryKey;
}
export interface DocumentEventsPayload {
user: User;
actor?: User;
document?: {
sender: string;
};
company?: { id: string };
}
export interface PaginationQueryParameters {
// It is offset for MongoDB and pointer to the next page for ScyllaDB
page_token?: string;
// Page size
limit?: string;
websockets?: boolean;
}
export interface AccessToken {
@@ -19,7 +19,6 @@ describe("The Documents Browser Window and API", () => {
"user",
"files",
"auth",
"realtime",
"statistics",
"platform-services",
"documents",
@@ -28,10 +28,8 @@ describe("the Drive feature", () => {
"user",
"search",
"files",
"websocket",
"messages",
"auth",
"realtime",
"channels",
"counter",
"statistics",
@@ -45,10 +45,8 @@ describe("the Drive feature", () => {
"user",
"search",
"files",
"websocket",
"messages",
"auth",
"realtime",
"channels",
"counter",
"statistics",
@@ -25,10 +25,8 @@ describe("the Drive Search feature", () => {
"user",
"search",
"files",
"websocket",
"messages",
"auth",
"realtime",
"channels",
"statistics",
"platform-services",
@@ -39,10 +39,8 @@ describe("the Drive Tdrive tabs feature", () => {
"user",
"search",
"files",
"websocket",
"messages",
"auth",
"realtime",
"channels",
"counter",
"statistics",
@@ -32,10 +32,8 @@ describe("the Drive feature", () => {
"user",
"search",
"files",
"websocket",
"messages",
"auth",
"realtime",
"channels",
"counter",
"statistics",
@@ -44,10 +44,8 @@ describe("the My Drive feature", () => {
"user",
"search",
"files",
"websocket",
"messages",
"auth",
"realtime",
"channels",
"counter",
"statistics",
@@ -44,10 +44,8 @@ describe("the public links feature", () => {
"user",
"search",
"files",
"websocket",
"messages",
"auth",
"realtime",
"channels",
"counter",
"statistics",
@@ -12,16 +12,6 @@
"endpoint": "http://localhost:9200"
}
},
"websocket": {
"path": "/socket",
"adapters": {
"types": ["redis"],
"redis": {
"host": "localhost",
"port": 6379
}
}
},
"auth": {
"jwt": {
"secret": "supersecret"
@@ -22,7 +22,6 @@ describe("The /users/quota API", () => {
"database",
"search",
"message-queue",
"websocket",
"applications",
"webserver",
"user",
@@ -13,7 +13,6 @@ describe("The /users API", () => {
"database",
"search",
"message-queue",
"websocket",
"webserver",
"user",
"auth",
@@ -27,7 +27,6 @@ describe("The /users API", () => {
"database",
"search",
"message-queue",
"websocket",
"applications",
"webserver",
"user",
@@ -1,116 +0,0 @@
import { describe, expect, it, jest } from "@jest/globals";
import { CreateResult } from "../../../../../../../src/core/platform/framework/api/crud-service";
import { RealtimeCreated } from "../../../../../../../src/core/platform/framework/decorators";
import { websocketEventBus } from "../../../../../../../src/core/platform/services/realtime/bus";
import { ResourcePath } from "../../../../../../../src/core/platform/services/realtime/types";
describe("The RealtimeCreated decorator", () => {
it("should call the original method send back original result but do not emit event if result type is wrong", async () => {
const emitSpy = jest.spyOn(websocketEventBus, "emit");
class TestMe {
@RealtimeCreated({ room: "/foo/bar" })
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
reverseMeBaby(input: string): Promise<string> {
return Promise.resolve(input.split("").reverse().join(""));
}
}
const test = new TestMe();
const originalSpy = jest.spyOn(test, "reverseMeBaby");
const result = await test.reverseMeBaby("yolo");
expect(result).toEqual("oloy");
expect(originalSpy).toHaveBeenCalledTimes(1);
expect(originalSpy).toHaveBeenCalledWith("yolo");
expect(emitSpy).toHaveBeenCalledTimes(0);
emitSpy.mockRestore();
});
it("should call the original method send back original result and emit event", async () => {
const emitSpy = jest.spyOn(websocketEventBus, "emit");
class TestMe {
@RealtimeCreated({ room: "/foo/bar", path: "/foo/bar/baz" })
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
async reverseMeBaby(input: string): Promise<CreateResult<string>> {
return new CreateResult<string>("string", input.split("").reverse().join(""));
}
}
const test = new TestMe();
const originalSpy = jest.spyOn(test, "reverseMeBaby");
const result = await test.reverseMeBaby("yolo");
expect(result.entity).toEqual("oloy");
expect(originalSpy).toHaveBeenCalledTimes(1);
expect(originalSpy).toHaveBeenCalledWith("yolo");
expect(emitSpy).toHaveBeenCalledTimes(1);
expect(emitSpy).toHaveBeenCalledWith("created", {
room: {
name: "default",
path: ["/foo/bar"],
} as ResourcePath,
resourcePath: "/foo/bar/baz",
entity: "oloy",
type: "string",
result: {
entity: "oloy",
type: "string",
context: undefined,
operation: "create",
raw: undefined,
} as CreateResult<string>,
});
emitSpy.mockRestore();
});
it("should emit event with path computed from function", async () => {
const emitSpy = jest.spyOn(websocketEventBus, "emit");
class TestMe {
@RealtimeCreated<string>(input => [
{ room: ResourcePath.get(`/foo/bar/${input}`), path: "/foo/bar/baz" },
])
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
async reverseMeBaby(input: string): Promise<CreateResult<string>> {
return new CreateResult<string>("string", input.split("").reverse().join(""));
}
}
const test = new TestMe();
const originalSpy = jest.spyOn(test, "reverseMeBaby");
const result = await test.reverseMeBaby("yolo");
expect(result.entity).toEqual("oloy");
expect(originalSpy).toHaveBeenCalledTimes(1);
expect(originalSpy).toHaveBeenCalledWith("yolo");
expect(emitSpy).toHaveBeenCalledTimes(1);
expect(emitSpy).toHaveBeenCalledWith("created", {
room: {
name: "default",
path: ["/foo/bar/oloy"],
} as ResourcePath,
resourcePath: "/foo/bar/baz",
entity: "oloy",
type: "string",
result: {
entity: "oloy",
context: undefined,
operation: "create",
type: "string",
raw: undefined,
} as CreateResult<string>,
});
emitSpy.mockRestore();
});
});
@@ -1,116 +0,0 @@
import { describe, expect, it, jest } from "@jest/globals";
import { DeleteResult } from "../../../../../../../src/core/platform/framework/api/crud-service";
import { RealtimeDeleted } from "../../../../../../../src/core/platform/framework/decorators";
import { websocketEventBus } from "../../../../../../../src/core/platform/services/realtime/bus";
import { ResourcePath } from "../../../../../../../src/core/platform/services/realtime/types";
describe("The RealtimeDeleted decorator", () => {
it("should call the original method send back original result but do not emit event if result type is wrong", async () => {
const emitSpy = jest.spyOn(websocketEventBus, "emit");
class TestMe {
@RealtimeDeleted({ room: "/foo/bar" })
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
reverseMeBaby(input: string): Promise<string> {
return Promise.resolve(input.split("").reverse().join(""));
}
}
const test = new TestMe();
const originalSpy = jest.spyOn(test, "reverseMeBaby");
const result = await test.reverseMeBaby("yolo");
expect(result).toEqual("oloy");
expect(originalSpy).toHaveBeenCalledTimes(1);
expect(originalSpy).toHaveBeenCalledWith("yolo");
expect(emitSpy).toHaveBeenCalledTimes(0);
emitSpy.mockRestore();
});
it("should call the original method send back original result and emit event", async () => {
const emitSpy = jest.spyOn(websocketEventBus, "emit");
class TestMe {
@RealtimeDeleted({ room: "/foo/bar", path: "/foo/bar/baz" })
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
async reverseMeBaby(input: string): Promise<DeleteResult<string>> {
return new DeleteResult<string>("string", input.split("").reverse().join(""), true);
}
}
const test = new TestMe();
const originalSpy = jest.spyOn(test, "reverseMeBaby");
const result = await test.reverseMeBaby("yolo");
expect(result.entity).toEqual("oloy");
expect(originalSpy).toHaveBeenCalledTimes(1);
expect(originalSpy).toHaveBeenCalledWith("yolo");
expect(emitSpy).toHaveBeenCalledTimes(1);
expect(emitSpy).toHaveBeenCalledWith("deleted", {
room: {
name: "default",
path: ["/foo/bar"],
} as ResourcePath,
resourcePath: "/foo/bar/baz",
entity: "oloy",
type: "string",
result: {
entity: "oloy",
context: undefined,
operation: "delete",
deleted: true,
type: "string",
} as DeleteResult<string>,
});
emitSpy.mockRestore();
});
it("should emit event with path computed from function", async () => {
const emitSpy = jest.spyOn(websocketEventBus, "emit");
class TestMe {
@RealtimeDeleted(result => [
{ room: ResourcePath.get(`/foo/bar/${result}`), path: "/foo/bar/baz" },
])
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
async reverseMeBaby(input: string): Promise<DeleteResult<string>> {
return new DeleteResult<string>("string", input.split("").reverse().join(""), true);
}
}
const test = new TestMe();
const originalSpy = jest.spyOn(test, "reverseMeBaby");
const result = await test.reverseMeBaby("yolo");
expect(result.entity).toEqual("oloy");
expect(originalSpy).toHaveBeenCalledTimes(1);
expect(originalSpy).toHaveBeenCalledWith("yolo");
expect(emitSpy).toHaveBeenCalledTimes(1);
expect(emitSpy).toHaveBeenCalledWith("deleted", {
room: {
name: "default",
path: ["/foo/bar/oloy"],
} as ResourcePath,
resourcePath: "/foo/bar/baz",
entity: "oloy",
type: "string",
result: {
entity: "oloy",
context: undefined,
operation: "delete",
deleted: true,
type: "string",
} as DeleteResult<string>,
});
emitSpy.mockRestore();
});
});
@@ -1,118 +0,0 @@
import { describe, expect, it, jest } from "@jest/globals";
import { UpdateResult } from "../../../../../../../src/core/platform/framework/api/crud-service";
import { RealtimeUpdated } from "../../../../../../../src/core/platform/framework/decorators";
import { websocketEventBus } from "../../../../../../../src/core/platform/services/realtime/bus";
import { ResourcePath } from "../../../../../../../src/core/platform/services/realtime/types";
describe("The RealtimeUpdated decorator", () => {
it("should call the original method send back original result but do not emit event if result type is wrong", async () => {
const emitSpy = jest.spyOn(websocketEventBus, "emit");
class TestMe {
@RealtimeUpdated({ room: "/foo/bar" })
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
reverseMeBaby(input: string): Promise<string> {
return Promise.resolve(input.split("").reverse().join(""));
}
}
const test = new TestMe();
const originalSpy = jest.spyOn(test, "reverseMeBaby");
const result = await test.reverseMeBaby("yolo");
expect(result).toEqual("oloy");
expect(originalSpy).toHaveBeenCalledTimes(1);
expect(originalSpy).toHaveBeenCalledWith("yolo");
expect(emitSpy).toHaveBeenCalledTimes(0);
emitSpy.mockRestore();
});
it("should call the original method send back original result and emit event", async () => {
const emitSpy = jest.spyOn(websocketEventBus, "emit");
class TestMe {
@RealtimeUpdated({ room: "/foo/bar", path: "/foo/bar/baz" })
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
async reverseMeBaby(input: string): Promise<UpdateResult<string>> {
return new UpdateResult<string>("string", input.split("").reverse().join(""));
}
}
const test = new TestMe();
const originalSpy = jest.spyOn(test, "reverseMeBaby");
const result = await test.reverseMeBaby("yolo");
expect(result.entity).toEqual("oloy");
expect(originalSpy).toHaveBeenCalledTimes(1);
expect(originalSpy).toHaveBeenCalledWith("yolo");
expect(emitSpy).toHaveBeenCalledTimes(1);
expect(emitSpy).toHaveBeenCalledWith("updated", {
entity: "oloy",
room: {
name: "default",
path: ["/foo/bar"],
} as ResourcePath,
resourcePath: "/foo/bar/baz",
type: "string",
result: {
type: "string",
entity: "oloy",
affected: undefined,
context: undefined,
operation: "update",
raw: undefined,
} as UpdateResult<string>,
});
emitSpy.mockRestore();
});
it("should emit event with path computed from function", async () => {
const emitSpy = jest.spyOn(websocketEventBus, "emit");
class TestMe {
@RealtimeUpdated(result => [
{ room: ResourcePath.get(`/foo/bar/${result}`), path: "/foo/bar/baz" },
])
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
async reverseMeBaby(input: string): Promise<UpdateResult<string>> {
return new UpdateResult<string>("string", input.split("").reverse().join(""));
}
}
const test = new TestMe();
const originalSpy = jest.spyOn(test, "reverseMeBaby");
const result = await test.reverseMeBaby("yolo");
expect(result.entity).toEqual("oloy");
expect(originalSpy).toHaveBeenCalledTimes(1);
expect(originalSpy).toHaveBeenCalledWith("yolo");
expect(emitSpy).toHaveBeenCalledTimes(1);
expect(emitSpy).toHaveBeenCalledWith("updated", {
entity: "oloy",
room: {
name: "default",
path: ["/foo/bar/oloy"],
} as ResourcePath,
resourcePath: "/foo/bar/baz",
type: "string",
result: {
type: "string",
context: undefined,
operation: "update",
entity: "oloy",
affected: undefined,
raw: undefined,
} as UpdateResult<string>,
});
emitSpy.mockRestore();
});
});