♻️Removed unused websockets
This commit is contained in:
@@ -25,14 +25,6 @@
|
||||
"root": "STATIC_ROOT"
|
||||
}
|
||||
},
|
||||
"websocket": {
|
||||
"auth": {
|
||||
"jwt": {
|
||||
"secret": "AUTH_JWT_SECRET",
|
||||
"expiration": "AUTH_JWT_EXPIRATION"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tracker": {
|
||||
"segment": {
|
||||
"key": "SEGMENT_SECRET_KEY"
|
||||
|
||||
@@ -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"],
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
-116
@@ -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();
|
||||
|
||||
});
|
||||
});
|
||||
-116
@@ -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();
|
||||
|
||||
});
|
||||
});
|
||||
-118
@@ -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();
|
||||
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user