From 3d17dd5f8d05d1142cd211cb6bedc2279ecae50e Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Fri, 19 Jun 2026 15:47:32 -0300 Subject: [PATCH 01/11] fix(deno): lint problem --- .../lib/tests/secureFields.test.ts | 6 +- .../node-runtime/src/AppObjectRegistry.ts | 25 + .../apps/node-runtime/src/error-handlers.ts | 10 + packages/apps/node-runtime/src/globals.d.ts | 45 ++ .../node-runtime/src/handlers/api-handler.ts | 51 ++ .../src/handlers/app/construct.ts | 137 ++++ .../src/handlers/app/handleGetStatus.ts | 16 + .../src/handlers/app/handleInitialize.ts | 24 + .../src/handlers/app/handleOnDisable.ts | 20 + .../src/handlers/app/handleOnEnable.ts | 22 + .../src/handlers/app/handleOnInstall.ts | 34 + .../handlers/app/handleOnPreSettingUpdate.ts | 31 + .../handlers/app/handleOnSettingUpdated.ts | 33 + .../src/handlers/app/handleOnUninstall.ts | 34 + .../src/handlers/app/handleOnUpdate.ts | 34 + .../src/handlers/app/handleSetStatus.ts | 32 + .../src/handlers/app/handleUploadEvents.ts | 81 +++ .../node-runtime/src/handlers/app/handler.ts | 116 ++++ .../src/handlers/lib/assertions.ts | 51 ++ .../src/handlers/listener/handler.ts | 157 +++++ .../src/handlers/outboundcomms-handler.ts | 38 ++ .../src/handlers/scheduler-handler.ts | 66 ++ .../src/handlers/slashcommand-handler.ts | 123 ++++ .../src/handlers/tests/api-handler.test.ts | 118 ++++ .../src/handlers/tests/helpers/mod.ts | 29 + .../handlers/tests/listener-handler.test.ts | 234 +++++++ .../handlers/tests/scheduler-handler.test.ts | 41 ++ .../tests/slashcommand-handler.test.ts | 169 +++++ .../src/handlers/tests/uikit-handler.test.ts | 105 +++ .../tests/upload-event-handler.test.ts | 109 +++ .../tests/videoconference-handler.test.ts | 125 ++++ .../src/handlers/uikit/handler.ts | 110 ++++ .../src/handlers/videoconference-handler.ts | 53 ++ .../lib/accessors/builders/BlockBuilder.ts | 20 + .../accessors/builders/DiscussionBuilder.ts | 56 ++ .../builders/LivechatMessageBuilder.ts | 202 ++++++ .../lib/accessors/builders/MessageBuilder.ts | 271 ++++++++ .../src/lib/accessors/builders/RoomBuilder.ts | 196 ++++++ .../src/lib/accessors/builders/UserBuilder.ts | 80 +++ .../builders/VideoConferenceBuilder.ts | 92 +++ .../lib/accessors/extenders/HttpExtender.ts | 58 ++ .../accessors/extenders/MessageExtender.ts | 65 ++ .../lib/accessors/extenders/RoomExtender.ts | 60 ++ .../extenders/VideoConferenceExtend.ts | 68 ++ .../accessors/formatResponseErrorHandler.ts | 14 + .../node-runtime/src/lib/accessors/http.ts | 95 +++ .../node-runtime/src/lib/accessors/mod.ts | 356 ++++++++++ .../src/lib/accessors/modify/ModifyCreator.ts | 385 +++++++++++ .../lib/accessors/modify/ModifyExtender.ts | 105 +++ .../src/lib/accessors/modify/ModifyUpdater.ts | 169 +++++ .../src/lib/accessors/notifier.ts | 83 +++ .../lib/accessors/tests/AppAccessors.test.ts | 122 ++++ .../lib/accessors/tests/ModifyCreator.test.ts | 259 ++++++++ .../accessors/tests/ModifyExtender.test.ts | 244 +++++++ .../lib/accessors/tests/ModifyUpdater.test.ts | 243 +++++++ .../tests/formatResponseErrorHandler.test.ts | 211 ++++++ .../src/lib/accessors/tests/http.test.ts | 164 +++++ .../node-runtime/src/lib/ast/acorn-walk.d.ts | 16 + .../apps/node-runtime/src/lib/ast/acorn.d.ts | 622 ++++++++++++++++++ packages/apps/node-runtime/src/lib/ast/mod.ts | 69 ++ .../node-runtime/src/lib/ast/operations.ts | 246 +++++++ .../src/lib/ast/tests/data/ast_blocks.ts | 436 ++++++++++++ .../src/lib/ast/tests/operations.test.ts | 261 ++++++++ packages/apps/node-runtime/src/lib/codec.ts | 54 ++ .../apps/node-runtime/src/lib/loader-hook.ts | 21 + packages/apps/node-runtime/src/lib/logger.ts | 164 +++++ .../apps/node-runtime/src/lib/messenger.ts | 204 ++++++ .../node-runtime/src/lib/metricsCollector.ts | 21 + .../apps/node-runtime/src/lib/parseArgs.ts | 25 + .../node-runtime/src/lib/requestContext.ts | 10 + packages/apps/node-runtime/src/lib/room.ts | 118 ++++ .../apps/node-runtime/src/lib/roomFactory.ts | 29 + .../src/lib/sanitizeDeprecatedUsage.ts | 20 + .../apps/node-runtime/src/lib/secureFields.ts | 27 + .../node-runtime/src/lib/tests/logger.test.ts | 111 ++++ .../src/lib/tests/messenger.test.ts | 99 +++ .../src/lib/tests/secureFields.test.ts | 59 ++ .../node-runtime/src/lib/wrapAppForRequest.ts | 60 ++ packages/apps/node-runtime/src/main.ts | 132 ++++ packages/apps/node-runtime/tsconfig.json | 15 + 80 files changed, 8713 insertions(+), 3 deletions(-) create mode 100644 packages/apps/node-runtime/src/AppObjectRegistry.ts create mode 100644 packages/apps/node-runtime/src/error-handlers.ts create mode 100644 packages/apps/node-runtime/src/globals.d.ts create mode 100644 packages/apps/node-runtime/src/handlers/api-handler.ts create mode 100644 packages/apps/node-runtime/src/handlers/app/construct.ts create mode 100644 packages/apps/node-runtime/src/handlers/app/handleGetStatus.ts create mode 100644 packages/apps/node-runtime/src/handlers/app/handleInitialize.ts create mode 100644 packages/apps/node-runtime/src/handlers/app/handleOnDisable.ts create mode 100644 packages/apps/node-runtime/src/handlers/app/handleOnEnable.ts create mode 100644 packages/apps/node-runtime/src/handlers/app/handleOnInstall.ts create mode 100644 packages/apps/node-runtime/src/handlers/app/handleOnPreSettingUpdate.ts create mode 100644 packages/apps/node-runtime/src/handlers/app/handleOnSettingUpdated.ts create mode 100644 packages/apps/node-runtime/src/handlers/app/handleOnUninstall.ts create mode 100644 packages/apps/node-runtime/src/handlers/app/handleOnUpdate.ts create mode 100644 packages/apps/node-runtime/src/handlers/app/handleSetStatus.ts create mode 100644 packages/apps/node-runtime/src/handlers/app/handleUploadEvents.ts create mode 100644 packages/apps/node-runtime/src/handlers/app/handler.ts create mode 100644 packages/apps/node-runtime/src/handlers/lib/assertions.ts create mode 100644 packages/apps/node-runtime/src/handlers/listener/handler.ts create mode 100644 packages/apps/node-runtime/src/handlers/outboundcomms-handler.ts create mode 100644 packages/apps/node-runtime/src/handlers/scheduler-handler.ts create mode 100644 packages/apps/node-runtime/src/handlers/slashcommand-handler.ts create mode 100644 packages/apps/node-runtime/src/handlers/tests/api-handler.test.ts create mode 100644 packages/apps/node-runtime/src/handlers/tests/helpers/mod.ts create mode 100644 packages/apps/node-runtime/src/handlers/tests/listener-handler.test.ts create mode 100644 packages/apps/node-runtime/src/handlers/tests/scheduler-handler.test.ts create mode 100644 packages/apps/node-runtime/src/handlers/tests/slashcommand-handler.test.ts create mode 100644 packages/apps/node-runtime/src/handlers/tests/uikit-handler.test.ts create mode 100644 packages/apps/node-runtime/src/handlers/tests/upload-event-handler.test.ts create mode 100644 packages/apps/node-runtime/src/handlers/tests/videoconference-handler.test.ts create mode 100644 packages/apps/node-runtime/src/handlers/uikit/handler.ts create mode 100644 packages/apps/node-runtime/src/handlers/videoconference-handler.ts create mode 100644 packages/apps/node-runtime/src/lib/accessors/builders/BlockBuilder.ts create mode 100644 packages/apps/node-runtime/src/lib/accessors/builders/DiscussionBuilder.ts create mode 100644 packages/apps/node-runtime/src/lib/accessors/builders/LivechatMessageBuilder.ts create mode 100644 packages/apps/node-runtime/src/lib/accessors/builders/MessageBuilder.ts create mode 100644 packages/apps/node-runtime/src/lib/accessors/builders/RoomBuilder.ts create mode 100644 packages/apps/node-runtime/src/lib/accessors/builders/UserBuilder.ts create mode 100644 packages/apps/node-runtime/src/lib/accessors/builders/VideoConferenceBuilder.ts create mode 100644 packages/apps/node-runtime/src/lib/accessors/extenders/HttpExtender.ts create mode 100644 packages/apps/node-runtime/src/lib/accessors/extenders/MessageExtender.ts create mode 100644 packages/apps/node-runtime/src/lib/accessors/extenders/RoomExtender.ts create mode 100644 packages/apps/node-runtime/src/lib/accessors/extenders/VideoConferenceExtend.ts create mode 100644 packages/apps/node-runtime/src/lib/accessors/formatResponseErrorHandler.ts create mode 100644 packages/apps/node-runtime/src/lib/accessors/http.ts create mode 100644 packages/apps/node-runtime/src/lib/accessors/mod.ts create mode 100644 packages/apps/node-runtime/src/lib/accessors/modify/ModifyCreator.ts create mode 100644 packages/apps/node-runtime/src/lib/accessors/modify/ModifyExtender.ts create mode 100644 packages/apps/node-runtime/src/lib/accessors/modify/ModifyUpdater.ts create mode 100644 packages/apps/node-runtime/src/lib/accessors/notifier.ts create mode 100644 packages/apps/node-runtime/src/lib/accessors/tests/AppAccessors.test.ts create mode 100644 packages/apps/node-runtime/src/lib/accessors/tests/ModifyCreator.test.ts create mode 100644 packages/apps/node-runtime/src/lib/accessors/tests/ModifyExtender.test.ts create mode 100644 packages/apps/node-runtime/src/lib/accessors/tests/ModifyUpdater.test.ts create mode 100644 packages/apps/node-runtime/src/lib/accessors/tests/formatResponseErrorHandler.test.ts create mode 100644 packages/apps/node-runtime/src/lib/accessors/tests/http.test.ts create mode 100644 packages/apps/node-runtime/src/lib/ast/acorn-walk.d.ts create mode 100644 packages/apps/node-runtime/src/lib/ast/acorn.d.ts create mode 100644 packages/apps/node-runtime/src/lib/ast/mod.ts create mode 100644 packages/apps/node-runtime/src/lib/ast/operations.ts create mode 100644 packages/apps/node-runtime/src/lib/ast/tests/data/ast_blocks.ts create mode 100644 packages/apps/node-runtime/src/lib/ast/tests/operations.test.ts create mode 100644 packages/apps/node-runtime/src/lib/codec.ts create mode 100644 packages/apps/node-runtime/src/lib/loader-hook.ts create mode 100644 packages/apps/node-runtime/src/lib/logger.ts create mode 100644 packages/apps/node-runtime/src/lib/messenger.ts create mode 100644 packages/apps/node-runtime/src/lib/metricsCollector.ts create mode 100644 packages/apps/node-runtime/src/lib/parseArgs.ts create mode 100644 packages/apps/node-runtime/src/lib/requestContext.ts create mode 100644 packages/apps/node-runtime/src/lib/room.ts create mode 100644 packages/apps/node-runtime/src/lib/roomFactory.ts create mode 100644 packages/apps/node-runtime/src/lib/sanitizeDeprecatedUsage.ts create mode 100644 packages/apps/node-runtime/src/lib/secureFields.ts create mode 100644 packages/apps/node-runtime/src/lib/tests/logger.test.ts create mode 100644 packages/apps/node-runtime/src/lib/tests/messenger.test.ts create mode 100644 packages/apps/node-runtime/src/lib/tests/secureFields.test.ts create mode 100644 packages/apps/node-runtime/src/lib/wrapAppForRequest.ts create mode 100644 packages/apps/node-runtime/src/main.ts create mode 100644 packages/apps/node-runtime/tsconfig.json diff --git a/packages/apps/deno-runtime/lib/tests/secureFields.test.ts b/packages/apps/deno-runtime/lib/tests/secureFields.test.ts index a27332eb35fdc..f48a0af8d6f6a 100644 --- a/packages/apps/deno-runtime/lib/tests/secureFields.test.ts +++ b/packages/apps/deno-runtime/lib/tests/secureFields.test.ts @@ -13,7 +13,7 @@ describe('applySecureFields', () => { it('throws when app is unavailable', () => { assertThrows( - () => applySecureFields({ foo: 'bar', [SECURE_FIELDS_KEY]: [] } as any), + () => applySecureFields({ foo: 'bar', [SECURE_FIELDS_KEY]: [] }), Error, "App unavailable, can't parse object with secure fields", ); @@ -32,7 +32,7 @@ describe('applySecureFields', () => { { permission: 'abac.read', name: 'abacAttributes', value: { department: 'support' } }, { permission: 'api.read', name: 'apiToken', value: 'secret' }, ], - } as any); + }); assertEquals(parsed, { foo: 'bar', @@ -50,7 +50,7 @@ describe('applySecureFields', () => { const parsed = applySecureFields({ abacAttributes: null, [SECURE_FIELDS_KEY]: [{ permission: 'abac.read', name: 'abacAttributes', value: { tenant: 'alpha' } }], - } as any); + }); assertEquals(parsed, { abacAttributes: { tenant: 'alpha' }, diff --git a/packages/apps/node-runtime/src/AppObjectRegistry.ts b/packages/apps/node-runtime/src/AppObjectRegistry.ts new file mode 100644 index 0000000000000..fd7c8adb23f68 --- /dev/null +++ b/packages/apps/node-runtime/src/AppObjectRegistry.ts @@ -0,0 +1,25 @@ +export type Maybe = T | null | undefined; + +export const AppObjectRegistry = new (class { + registry: Record = {}; + + public get(key: string): Maybe { + return this.registry[key] as Maybe; + } + + public set(key: string, value: unknown): void { + this.registry[key] = value; + } + + public has(key: string): boolean { + return key in this.registry; + } + + public delete(key: string): void { + delete this.registry[key]; + } + + public clear(): void { + this.registry = {}; + } +})(); diff --git a/packages/apps/node-runtime/src/error-handlers.ts b/packages/apps/node-runtime/src/error-handlers.ts new file mode 100644 index 0000000000000..f30bfa5df535d --- /dev/null +++ b/packages/apps/node-runtime/src/error-handlers.ts @@ -0,0 +1,10 @@ +import * as Messenger from './lib/messenger'; + +export default function registerErrorListeners() { + process.on('uncaughtException', (error: Error, origin: 'uncaughtException' | 'unhandledRejection') => { + Messenger.sendNotification({ + method: origin, + params: [error.stack || error], + }); + }); +} diff --git a/packages/apps/node-runtime/src/globals.d.ts b/packages/apps/node-runtime/src/globals.d.ts new file mode 100644 index 0000000000000..9f149b4c9e76f --- /dev/null +++ b/packages/apps/node-runtime/src/globals.d.ts @@ -0,0 +1,45 @@ +/** + * Ambient declarations for Web-compatible globals that Node.js 18+ provides + * but are not included in @types/node. + */ + +interface IPromiseRejectionEventInit extends EventInit { + promise: Promise; + reason?: unknown; +} + +declare class PromiseRejectionEvent extends Event { + readonly promise: Promise; + + readonly reason: unknown; + + constructor(type: string, eventInitDict: IPromiseRejectionEventInit); +} + +interface IErrorEventInit extends EventInit { + message?: string; + filename?: string; + lineno?: number; + colno?: number; + error?: unknown; +} + +declare class ErrorEvent extends Event { + readonly message: string; + + readonly filename: string; + + readonly lineno: number; + + readonly colno: number; + + readonly error: unknown; + + constructor(type: string, eventInitDict?: IErrorEventInit); +} + +declare function addEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions, +): void; diff --git a/packages/apps/node-runtime/src/handlers/api-handler.ts b/packages/apps/node-runtime/src/handlers/api-handler.ts new file mode 100644 index 0000000000000..aabc4e12dc09e --- /dev/null +++ b/packages/apps/node-runtime/src/handlers/api-handler.ts @@ -0,0 +1,51 @@ +import type { IApiEndpoint } from '@rocket.chat/apps-engine/definition/api/IApiEndpoint'; +import type { Defined } from 'jsonrpc-lite'; +import { JsonRpcError } from 'jsonrpc-lite'; + +import { AppObjectRegistry } from '../AppObjectRegistry'; +import { AppAccessorsInstance } from '../lib/accessors/mod'; +import type { RequestContext } from '../lib/requestContext'; +import { wrapComposedApp } from '../lib/wrapAppForRequest'; + +export default async function apiHandler(request: RequestContext): Promise { + const { method: call, params } = request; + const [, /* always "api" */ ...parts] = call.split(':'); + const httpMethod = parts.pop(); + const path = parts.join(':'); + + const endpoint = AppObjectRegistry.get(`api:${path}`); + const { logger } = request.context; + + if (!endpoint) { + return new JsonRpcError(`Endpoint ${path} not found`, -32000); + } + + const method = endpoint[httpMethod as keyof IApiEndpoint]; + + if (typeof method !== 'function') { + return new JsonRpcError(`${path}'s ${httpMethod} not exists`, -32000); + } + + const [requestData, endpointInfo] = params as Array; + + logger.debug(`${path}'s ${call} is being executed...`, requestData); + + try { + // deno-lint-ignore ban-types + const result = await (method as Function).apply(wrapComposedApp(endpoint, request), [ + requestData, + endpointInfo, + AppAccessorsInstance.getReader(), + AppAccessorsInstance.getModifier(), + AppAccessorsInstance.getHttp(), + AppAccessorsInstance.getPersistence(), + ]); + + logger.debug(`${path}'s ${call} was successfully executed.`); + + return result; + } catch (e) { + logger.debug(`${path}'s ${call} was unsuccessful.`); + return new JsonRpcError(e.message || 'Internal server error', -32000); + } +} diff --git a/packages/apps/node-runtime/src/handlers/app/construct.ts b/packages/apps/node-runtime/src/handlers/app/construct.ts new file mode 100644 index 0000000000000..15c0d6ba43ef5 --- /dev/null +++ b/packages/apps/node-runtime/src/handlers/app/construct.ts @@ -0,0 +1,137 @@ +/* eslint-disable @typescript-eslint/no-require-imports, import/no-dynamic-require -- We need to build a require for apps */ + +import type { IParseAppPackageResult } from '@rocket.chat/apps/dist/server/compiler/IParseAppPackageResult'; +import { App } from '@rocket.chat/apps-engine/definition/App'; + +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import { AppAccessorsInstance } from '../../lib/accessors/mod'; +import type { RequestContext } from '../../lib/requestContext'; +import { sanitizeDeprecatedUsage } from '../../lib/sanitizeDeprecatedUsage'; + +const ALLOWED_NATIVE_MODULES = [ + 'path', + 'url', + 'crypto', + 'buffer', + 'stream', + 'net', + 'http', + 'https', + 'zlib', + 'util', + 'punycode', + 'os', + 'querystring', + 'fs', +]; +const ALLOWED_EXTERNAL_MODULES = ['uuid']; + +// As the apps are bundled, the only times they will call require are +// 1. To require native modules +// 2. To require external npm packages we may provide +// 3. To require apps-engine files +function buildRequire(): (module: string) => unknown { + return (module: string): unknown => { + // Normalize Node built-in specifiers: accept both 'crypto' and 'node:crypto' + const normalized = module.replace('node:', ''); + + if (ALLOWED_NATIVE_MODULES.includes(normalized)) { + return require(`node:${normalized}`); + } + + if (ALLOWED_EXTERNAL_MODULES.includes(module)) { + return require(`npm:${module}`); + } + + if (module.startsWith('@rocket.chat/apps-engine')) { + // Our `require` function knows how to handle these + return require(module); + } + + throw new Error(`Module ${module} is not allowed`); + }; +} + +function wrapAppCode(code: string): (require: (module: string) => unknown) => Promise> { + // eslint-disable-next-line @typescript-eslint/no-implied-eval -- This is the reason we run in a separate process + return new Function( + 'require', + ` + const exports = {}; + const module = { exports }; + const _error = console.error.bind(console); + const _console = { + log: _error, + error: _error, + debug: _error, + info: _error, + warn: _error, + }; + + const result = (async (exports,module,require,console,globalThis) => { + ${code}; + })(exports,module,require,Buffer,_console,undefined,undefined); + + return result.then(() => module.exports);`, + ) as (require: (module: string) => unknown) => Promise>; +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars -- We need this type to identify an instance of App class, since the original App is abstract +class _ConstructedApp extends App {} + +type ConstructedApp = typeof _ConstructedApp; + +export default async function handleConstructApp(request: RequestContext): Promise { + const { params } = request; + + if (!Array.isArray(params)) { + throw new Error('Invalid params', { cause: 'invalid_param_type' }); + } + + const [appPackage] = params as [IParseAppPackageResult]; + + if (!appPackage?.info?.id || !appPackage?.info?.classFile || !appPackage?.files) { + throw new Error('Invalid params', { cause: 'invalid_param_type' }); + } + + AppObjectRegistry.set('id', appPackage.info.id); + const source = sanitizeDeprecatedUsage(appPackage.files[appPackage.info.classFile]); + + const require = buildRequire(); + const exports = await wrapAppCode(source)(require); + + // This is the same naive logic we've been using in the App Compiler + // Applying the correct type here is quite difficult because of the dynamic nature of the code + // deno-lint-ignore no-explicit-any + const appClass = Object.values(exports)[0] as ConstructedApp; + + const app = new appClass(appPackage.info, request.context.logger, AppAccessorsInstance.getDefaultAppAccessors()); + + if (typeof app.getName !== 'function') { + throw new Error('App must contain a getName function'); + } + + if (typeof app.getNameSlug !== 'function') { + throw new Error('App must contain a getNameSlug function'); + } + + if (typeof app.getVersion !== 'function') { + throw new Error('App must contain a getVersion function'); + } + + if (typeof app.getID !== 'function') { + throw new Error('App must contain a getID function'); + } + + if (typeof app.getDescription !== 'function') { + throw new Error('App must contain a getDescription function'); + } + + if (typeof app.getRequiredApiVersion !== 'function') { + throw new Error('App must contain a getRequiredApiVersion function'); + } + + AppObjectRegistry.set('app', app); + + return true; +} diff --git a/packages/apps/node-runtime/src/handlers/app/handleGetStatus.ts b/packages/apps/node-runtime/src/handlers/app/handleGetStatus.ts new file mode 100644 index 0000000000000..402285f6c743b --- /dev/null +++ b/packages/apps/node-runtime/src/handlers/app/handleGetStatus.ts @@ -0,0 +1,16 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App'; +import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; + +import { AppObjectRegistry } from '../../AppObjectRegistry'; + +export default function handleGetStatus(): Promise { + const app = AppObjectRegistry.get('app'); + + if (typeof app?.getStatus !== 'function') { + throw new Error('App must contain a getStatus function', { + cause: 'invalid_app', + }); + } + + return app.getStatus(); +} diff --git a/packages/apps/node-runtime/src/handlers/app/handleInitialize.ts b/packages/apps/node-runtime/src/handlers/app/handleInitialize.ts new file mode 100644 index 0000000000000..ae09f1a442608 --- /dev/null +++ b/packages/apps/node-runtime/src/handlers/app/handleInitialize.ts @@ -0,0 +1,24 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App'; + +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import { AppAccessorsInstance } from '../../lib/accessors/mod'; +import type { RequestContext } from '../../lib/requestContext'; +import { wrapAppForRequest } from '../../lib/wrapAppForRequest'; + +export default async function handleInitialize(request: RequestContext): Promise { + const app = AppObjectRegistry.get('app'); + + if (typeof app?.initialize !== 'function') { + throw new Error('App must contain an initialize function', { + cause: 'invalid_app', + }); + } + + await app.initialize.call( + wrapAppForRequest(app, request), + AppAccessorsInstance.getConfigurationExtend(), + AppAccessorsInstance.getEnvironmentRead(), + ); + + return true; +} diff --git a/packages/apps/node-runtime/src/handlers/app/handleOnDisable.ts b/packages/apps/node-runtime/src/handlers/app/handleOnDisable.ts new file mode 100644 index 0000000000000..57c4365212866 --- /dev/null +++ b/packages/apps/node-runtime/src/handlers/app/handleOnDisable.ts @@ -0,0 +1,20 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App'; + +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import { AppAccessorsInstance } from '../../lib/accessors/mod'; +import type { RequestContext } from '../../lib/requestContext'; +import { wrapAppForRequest } from '../../lib/wrapAppForRequest'; + +export default async function handleOnDisable(request: RequestContext): Promise { + const app = AppObjectRegistry.get('app'); + + if (typeof app?.onDisable !== 'function') { + throw new Error('App must contain an onDisable function', { + cause: 'invalid_app', + }); + } + + await app.onDisable.call(wrapAppForRequest(app, request), AppAccessorsInstance.getConfigurationModify()); + + return true; +} diff --git a/packages/apps/node-runtime/src/handlers/app/handleOnEnable.ts b/packages/apps/node-runtime/src/handlers/app/handleOnEnable.ts new file mode 100644 index 0000000000000..0e9f42d16ba9b --- /dev/null +++ b/packages/apps/node-runtime/src/handlers/app/handleOnEnable.ts @@ -0,0 +1,22 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App'; + +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import { AppAccessorsInstance } from '../../lib/accessors/mod'; +import type { RequestContext } from '../../lib/requestContext'; +import { wrapAppForRequest } from '../../lib/wrapAppForRequest'; + +export default function handleOnEnable(request: RequestContext): Promise { + const app = AppObjectRegistry.get('app'); + + if (typeof app?.onEnable !== 'function') { + throw new Error('App must contain an onEnable function', { + cause: 'invalid_app', + }); + } + + return app.onEnable.call( + wrapAppForRequest(app, request), + AppAccessorsInstance.getEnvironmentRead(), + AppAccessorsInstance.getConfigurationModify(), + ); +} diff --git a/packages/apps/node-runtime/src/handlers/app/handleOnInstall.ts b/packages/apps/node-runtime/src/handlers/app/handleOnInstall.ts new file mode 100644 index 0000000000000..1ec8d91a58769 --- /dev/null +++ b/packages/apps/node-runtime/src/handlers/app/handleOnInstall.ts @@ -0,0 +1,34 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App'; + +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import { AppAccessorsInstance } from '../../lib/accessors/mod'; +import type { RequestContext } from '../../lib/requestContext'; +import { wrapAppForRequest } from '../../lib/wrapAppForRequest'; + +export default async function handleOnInstall(request: RequestContext): Promise { + const { params } = request; + const app = AppObjectRegistry.get('app'); + + if (typeof app?.onInstall !== 'function') { + throw new Error('App must contain an onInstall function', { + cause: 'invalid_app', + }); + } + + if (!Array.isArray(params)) { + throw new Error('Invalid params', { cause: 'invalid_param_type' }); + } + + const [context] = params as [Record]; + + await app.onInstall.call( + wrapAppForRequest(app, request), + context, + AppAccessorsInstance.getReader(), + AppAccessorsInstance.getHttp(), + AppAccessorsInstance.getPersistence(), + AppAccessorsInstance.getModifier(), + ); + + return true; +} diff --git a/packages/apps/node-runtime/src/handlers/app/handleOnPreSettingUpdate.ts b/packages/apps/node-runtime/src/handlers/app/handleOnPreSettingUpdate.ts new file mode 100644 index 0000000000000..8ce303c8aed13 --- /dev/null +++ b/packages/apps/node-runtime/src/handlers/app/handleOnPreSettingUpdate.ts @@ -0,0 +1,31 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App'; + +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import { AppAccessorsInstance } from '../../lib/accessors/mod'; +import type { RequestContext } from '../../lib/requestContext'; +import { wrapAppForRequest } from '../../lib/wrapAppForRequest'; + +export default function handleOnPreSettingUpdate(request: RequestContext): Promise { + const { params } = request; + const app = AppObjectRegistry.get('app'); + + if (typeof app?.onPreSettingUpdate !== 'function') { + throw new Error('App must contain an onPreSettingUpdate function', { + cause: 'invalid_app', + }); + } + + if (!Array.isArray(params)) { + throw new Error('Invalid params', { cause: 'invalid_param_type' }); + } + + const [setting] = params as [Record]; + + return app.onPreSettingUpdate.call( + wrapAppForRequest(app, request), + setting, + AppAccessorsInstance.getConfigurationModify(), + AppAccessorsInstance.getReader(), + AppAccessorsInstance.getHttp(), + ); +} diff --git a/packages/apps/node-runtime/src/handlers/app/handleOnSettingUpdated.ts b/packages/apps/node-runtime/src/handlers/app/handleOnSettingUpdated.ts new file mode 100644 index 0000000000000..caed4617c1920 --- /dev/null +++ b/packages/apps/node-runtime/src/handlers/app/handleOnSettingUpdated.ts @@ -0,0 +1,33 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App'; + +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import { AppAccessorsInstance } from '../../lib/accessors/mod'; +import type { RequestContext } from '../../lib/requestContext'; +import { wrapAppForRequest } from '../../lib/wrapAppForRequest'; + +export default async function handleOnSettingUpdated(request: RequestContext): Promise { + const { params } = request; + const app = AppObjectRegistry.get('app'); + + if (typeof app?.onSettingUpdated !== 'function') { + throw new Error('App must contain an onSettingUpdated function', { + cause: 'invalid_app', + }); + } + + if (!Array.isArray(params)) { + throw new Error('Invalid params', { cause: 'invalid_param_type' }); + } + + const [setting] = params as [Record]; + + await app.onSettingUpdated.call( + wrapAppForRequest(app, request), + setting, + AppAccessorsInstance.getConfigurationModify(), + AppAccessorsInstance.getReader(), + AppAccessorsInstance.getHttp(), + ); + + return true; +} diff --git a/packages/apps/node-runtime/src/handlers/app/handleOnUninstall.ts b/packages/apps/node-runtime/src/handlers/app/handleOnUninstall.ts new file mode 100644 index 0000000000000..158eaa8ac31ec --- /dev/null +++ b/packages/apps/node-runtime/src/handlers/app/handleOnUninstall.ts @@ -0,0 +1,34 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App'; + +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import { AppAccessorsInstance } from '../../lib/accessors/mod'; +import type { RequestContext } from '../../lib/requestContext'; +import { wrapAppForRequest } from '../../lib/wrapAppForRequest'; + +export default async function handleOnUninstall(request: RequestContext): Promise { + const { params } = request; + const app = AppObjectRegistry.get('app'); + + if (typeof app?.onUninstall !== 'function') { + throw new Error('App must contain an onUninstall function', { + cause: 'invalid_app', + }); + } + + if (!Array.isArray(params)) { + throw new Error('Invalid params', { cause: 'invalid_param_type' }); + } + + const [context] = params as [Record]; + + await app.onUninstall.call( + wrapAppForRequest(app, request), + context, + AppAccessorsInstance.getReader(), + AppAccessorsInstance.getHttp(), + AppAccessorsInstance.getPersistence(), + AppAccessorsInstance.getModifier(), + ); + + return true; +} diff --git a/packages/apps/node-runtime/src/handlers/app/handleOnUpdate.ts b/packages/apps/node-runtime/src/handlers/app/handleOnUpdate.ts new file mode 100644 index 0000000000000..06d80245ed3b5 --- /dev/null +++ b/packages/apps/node-runtime/src/handlers/app/handleOnUpdate.ts @@ -0,0 +1,34 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App'; + +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import { AppAccessorsInstance } from '../../lib/accessors/mod'; +import type { RequestContext } from '../../lib/requestContext'; +import { wrapAppForRequest } from '../../lib/wrapAppForRequest'; + +export default async function handleOnUpdate(request: RequestContext): Promise { + const { params } = request; + const app = AppObjectRegistry.get('app'); + + if (typeof app?.onUpdate !== 'function') { + throw new Error('App must contain an onUpdate function', { + cause: 'invalid_app', + }); + } + + if (!Array.isArray(params)) { + throw new Error('Invalid params', { cause: 'invalid_param_type' }); + } + + const [context] = params as [Record]; + + await app.onUpdate.call( + wrapAppForRequest(app, request), + context, + AppAccessorsInstance.getReader(), + AppAccessorsInstance.getHttp(), + AppAccessorsInstance.getPersistence(), + AppAccessorsInstance.getModifier(), + ); + + return true; +} diff --git a/packages/apps/node-runtime/src/handlers/app/handleSetStatus.ts b/packages/apps/node-runtime/src/handlers/app/handleSetStatus.ts new file mode 100644 index 0000000000000..042bb5369044f --- /dev/null +++ b/packages/apps/node-runtime/src/handlers/app/handleSetStatus.ts @@ -0,0 +1,32 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App'; +import type { AppStatus as _AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; + +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import type { RequestContext } from '../../lib/requestContext'; +import { wrapAppForRequest } from '../../lib/wrapAppForRequest'; + +const { AppStatus } = require('@rocket.chat/apps-engine/definition/AppStatus.js') as { + AppStatus: typeof _AppStatus; +}; + +export default async function handleSetStatus(request: RequestContext): Promise { + const { params } = request; + + if (!Array.isArray(params) || !Object.values(AppStatus).includes(params[0])) { + throw new Error('Invalid params', { cause: 'invalid_param_type' }); + } + + const [status] = params as [typeof AppStatus]; + + const app = AppObjectRegistry.get('app'); + + if (!app || typeof (app as unknown as Record).setStatus !== 'function') { + throw new Error('App must contain a setStatus function', { + cause: 'invalid_app', + }); + } + + await (app as unknown as { setStatus(status: typeof AppStatus): Promise }).setStatus.call(wrapAppForRequest(app, request), status); + + return null; +} diff --git a/packages/apps/node-runtime/src/handlers/app/handleUploadEvents.ts b/packages/apps/node-runtime/src/handlers/app/handleUploadEvents.ts new file mode 100644 index 0000000000000..4cd6bb69fec24 --- /dev/null +++ b/packages/apps/node-runtime/src/handlers/app/handleUploadEvents.ts @@ -0,0 +1,81 @@ +import { Buffer } from 'node:buffer'; +import { readFile } from 'node:fs/promises'; + +import type { App } from '@rocket.chat/apps-engine/definition/App'; +import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions/AppsEngineException'; +import type { IFileUploadContext } from '@rocket.chat/apps-engine/definition/uploads/IFileUploadContext'; +import type { IUploadDetails } from '@rocket.chat/apps-engine/definition/uploads/IUploadDetails'; +import type { Defined } from 'jsonrpc-lite'; +import { JsonRpcError } from 'jsonrpc-lite'; + +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import { AppAccessorsInstance } from '../../lib/accessors/mod'; +import type { RequestContext } from '../../lib/requestContext'; +import { wrapAppForRequest } from '../../lib/wrapAppForRequest'; +import { assertAppAvailable, assertHandlerFunction, isPlainObject } from '../lib/assertions'; + +export const uploadEvents = ['executePreFileUpload'] as const; + +function assertIsUpload(v: unknown): asserts v is IUploadDetails { + if (isPlainObject(v) && !!v.rid && (!!v.userId || !!v.visitorToken)) return; + + throw JsonRpcError.invalidParams({ err: `Invalid 'file' parameter. Expected IUploadDetails, got`, value: v }); +} + +function assertString(v: unknown): asserts v is string { + if (v && typeof v === 'string') return; + + throw JsonRpcError.invalidParams({ err: `Invalid 'path' parameter. Expected string, got`, value: v }); +} + +export default async function handleUploadEvents(request: RequestContext): Promise { + const { method: rawMethod, params } = request as { + method: `app:${(typeof uploadEvents)[number]}`; + params: [{ file?: IUploadDetails; path?: string }]; + }; + const [, method] = rawMethod.split(':') as ['app', (typeof uploadEvents)[number]]; + + try { + const [{ file, path }] = params; + + const app = AppObjectRegistry.get('app'); + const handlerFunction = app?.[method as keyof App] as unknown; + + assertAppAvailable(app); + assertHandlerFunction(handlerFunction); + assertIsUpload(file); + assertString(path); + + let context: IFileUploadContext; + + switch (method) { + case 'executePreFileUpload': { + const fileContents = await readFile(path); + context = { file, content: Buffer.from(fileContents) }; + break; + } + } + + return await handlerFunction.call( + wrapAppForRequest(app, request), + context, + AppAccessorsInstance.getReader(), + AppAccessorsInstance.getHttp(), + AppAccessorsInstance.getPersistence(), + AppAccessorsInstance.getModifier(), + ); + } catch (e) { + if (e?.name === AppsEngineException.name) { + return new JsonRpcError(e.message, AppsEngineException.JSONRPC_ERROR_CODE, { name: e.name }); + } + + if (e instanceof JsonRpcError) { + return e; + } + + return JsonRpcError.internalError({ + err: e.message, + ...(e.code && { code: e.code }), + }); + } +} diff --git a/packages/apps/node-runtime/src/handlers/app/handler.ts b/packages/apps/node-runtime/src/handlers/app/handler.ts new file mode 100644 index 0000000000000..991d2e3a6b5b4 --- /dev/null +++ b/packages/apps/node-runtime/src/handlers/app/handler.ts @@ -0,0 +1,116 @@ +import type { Defined } from 'jsonrpc-lite'; +import { JsonRpcError } from 'jsonrpc-lite'; + +import handleConstructApp from './construct'; +import handleGetStatus from './handleGetStatus'; +import handleInitialize from './handleInitialize'; +import handleOnDisable from './handleOnDisable'; +import handleOnEnable from './handleOnEnable'; +import handleOnInstall from './handleOnInstall'; +import handleOnPreSettingUpdate from './handleOnPreSettingUpdate'; +import handleOnSettingUpdated from './handleOnSettingUpdated'; +import handleOnUninstall from './handleOnUninstall'; +import handleOnUpdate from './handleOnUpdate'; +import handleSetStatus from './handleSetStatus'; +import handleUploadEvents, { uploadEvents } from './handleUploadEvents'; +import type { RequestContext } from '../../lib/requestContext'; +import { isOneOf } from '../lib/assertions'; +import handleListener from '../listener/handler'; +import handleUIKitInteraction, { uikitInteractions } from '../uikit/handler'; + +export default async function handleApp(request: RequestContext): Promise { + const { method } = request; + const { logger } = request.context; + const [, appMethod] = method.split(':'); + + try { + // We don't want the getStatus method to generate logs, so we handle it separately + if (appMethod === 'getStatus') { + return await handleGetStatus(); + } + + logger.debug({ msg: `A method is being called...`, appMethod }); + + const formatResult = (result: Defined | JsonRpcError): Defined | JsonRpcError => { + if (result instanceof JsonRpcError) { + logger.debug({ + msg: `'${appMethod}' was unsuccessful.`, + appMethod, + err: result, + errorMessage: result.message, + }); + } else { + logger.debug({ + msg: `'${appMethod}' was successfully called! The result is:`, + appMethod, + result, + }); + } + + return result; + }; + + let result: Promise | undefined = undefined; + + if (isOneOf(appMethod, uploadEvents)) { + result = handleUploadEvents(request); + } else if (isOneOf(appMethod, uikitInteractions)) { + result = handleUIKitInteraction(request); + } else if (appMethod.startsWith('check') || appMethod.startsWith('execute')) { + result = handleListener(request); + } + + switch (appMethod) { + case 'construct': + result = handleConstructApp(request); + break; + case 'initialize': + result = handleInitialize(request); + break; + case 'setStatus': + result = handleSetStatus(request); + break; + case 'onEnable': + result = handleOnEnable(request); + break; + case 'onDisable': + result = handleOnDisable(request); + break; + case 'onInstall': + result = handleOnInstall(request); + break; + case 'onUninstall': + result = handleOnUninstall(request); + break; + case 'onPreSettingUpdate': + result = handleOnPreSettingUpdate(request); + break; + case 'onSettingUpdated': + result = handleOnSettingUpdated(request); + break; + case 'onUpdate': + result = handleOnUpdate(request); + break; + } + + if (typeof result === 'undefined') { + throw new JsonRpcError(`Unknown method "${appMethod}"`, -32601); + } + + return await result.then(formatResult); + } catch (e: unknown) { + if (!(e instanceof Error)) { + return new JsonRpcError('Unknown error', -32000, e); + } + + if ((e.cause as string)?.includes('invalid_param_type')) { + return JsonRpcError.invalidParams(null); + } + + if ((e.cause as string)?.includes('invalid_app')) { + return JsonRpcError.internalError({ message: 'App unavailable' }); + } + + return new JsonRpcError(e.message, -32000, e); + } +} diff --git a/packages/apps/node-runtime/src/handlers/lib/assertions.ts b/packages/apps/node-runtime/src/handlers/lib/assertions.ts new file mode 100644 index 0000000000000..65d0ad8885473 --- /dev/null +++ b/packages/apps/node-runtime/src/handlers/lib/assertions.ts @@ -0,0 +1,51 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App'; +import { JsonRpcError } from 'jsonrpc-lite'; + +/** + * Known failures that can happen in the runtime. + * + * DRT = Deno RunTime + */ +export const Errors = { + DRT_APP_NOT_AVAILABLE: 'DRT_APP_NOT_AVAILABLE', + DRT_EVENT_HANDLER_FUNCTION_MISSING: 'DRT_EVENT_HANDLER_FUNCTION_MISSING', +}; + +export function isRecord(v: unknown): v is Record { + return !!v && typeof v === 'object' && !Array.isArray(v); +} + +export function isPlainObject(v: unknown): v is Record { + if (!isRecord(v)) { + return false; + } + + const prototype = Object.getPrototypeOf(v); + + return prototype === null || prototype.constructor === Object; +} + +/** + * Type guard function to check if a value is included in a readonly array + * and narrow its type accordingly. + */ +export function isOneOf(value: unknown, array: readonly T[]): value is T { + return array.includes(value as T); +} + +export function isApp(v: unknown): v is App { + return !!v && typeof (v as unknown as Record).extendConfiguration === 'function'; +} + +export function assertAppAvailable(v: unknown): asserts v is App { + if (isApp(v)) return; + + throw JsonRpcError.internalError({ err: 'App object not available', code: Errors.DRT_APP_NOT_AVAILABLE }); +} + +// deno-lint-ignore ban-types -- Function is the best we can do at this time +export function assertHandlerFunction(v: unknown): asserts v is Function { + if (v instanceof Function) return; + + throw JsonRpcError.internalError({ err: `Expected handler function, got ${v}`, code: Errors.DRT_EVENT_HANDLER_FUNCTION_MISSING }); +} diff --git a/packages/apps/node-runtime/src/handlers/listener/handler.ts b/packages/apps/node-runtime/src/handlers/listener/handler.ts new file mode 100644 index 0000000000000..cdbaa766d63fb --- /dev/null +++ b/packages/apps/node-runtime/src/handlers/listener/handler.ts @@ -0,0 +1,157 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App'; +import type { AppsEngineException as _AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions/AppsEngineException'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; +import type { Defined } from 'jsonrpc-lite'; +import { JsonRpcError } from 'jsonrpc-lite'; + +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import { MessageBuilder } from '../../lib/accessors/builders/MessageBuilder'; +import { RoomBuilder } from '../../lib/accessors/builders/RoomBuilder'; +import { MessageExtender } from '../../lib/accessors/extenders/MessageExtender'; +import { RoomExtender } from '../../lib/accessors/extenders/RoomExtender'; +import type { AppAccessors } from '../../lib/accessors/mod'; +import { AppAccessorsInstance } from '../../lib/accessors/mod'; +import type { RequestContext } from '../../lib/requestContext'; +import { Room } from '../../lib/room'; +import createRoom from '../../lib/roomFactory'; +import { wrapAppForRequest } from '../../lib/wrapAppForRequest'; + +const { AppsEngineException } = require('@rocket.chat/apps-engine/definition/exceptions/AppsEngineException.js') as { + AppsEngineException: typeof _AppsEngineException; +}; + +export default async function handleListener(request: RequestContext): Promise { + const { method, params } = request; + const [, evtInterface] = method.split(':'); + const app = AppObjectRegistry.get('app'); + + const eventExecutor = app?.[evtInterface as keyof App]; + + if (!app || typeof eventExecutor !== 'function') { + return JsonRpcError.methodNotFound({ + message: 'Invalid event interface called on app', + }); + } + + if (!Array.isArray(params) || params.length < 1 || params.length > 2) { + return JsonRpcError.invalidParams(null); + } + + try { + const args = parseArgs({ AppAccessorsInstance }, evtInterface, params); + return await (eventExecutor as (...args: unknown[]) => Promise).apply(wrapAppForRequest(app, request), args); + } catch (e) { + if (e instanceof JsonRpcError) { + return e; + } + + if (e instanceof AppsEngineException) { + return new JsonRpcError(e.message, AppsEngineException.JSONRPC_ERROR_CODE, { name: e.name }); + } + + return JsonRpcError.internalError({ message: e.message }); + } +} + +export function parseArgs(deps: { AppAccessorsInstance: AppAccessors }, evtMethod: string, params: unknown[]): unknown[] { + const { AppAccessorsInstance } = deps; + /** + * param1 is the context for the event handler execution + * param2 is an optional extra content that some hanlers require + */ + const [param1, param2] = params as [unknown, unknown]; + + if (!param1) { + throw JsonRpcError.invalidParams(null); + } + + let context = param1; + + if (evtMethod.includes('Message')) { + context = hydrateMessageObjects(context) as Record; + } else if (evtMethod.endsWith('RoomUserJoined') || evtMethod.endsWith('RoomUserLeave')) { + (context as Record).room = createRoom( + (context as Record).room as IRoom, + AppAccessorsInstance.getSenderFn(), + ); + } else if (evtMethod.includes('PreRoom')) { + context = createRoom(context as IRoom, AppAccessorsInstance.getSenderFn()); + } + + const args: unknown[] = [context, AppAccessorsInstance.getReader(), AppAccessorsInstance.getHttp()]; + + // "check" events will only go this far - (context, reader, http) + if (evtMethod.startsWith('check')) { + // "checkPostMessageDeleted" has an extra param - (context, reader, http, extraContext) + if (param2) { + args.push(hydrateMessageObjects(param2)); + } + + return args; + } + + // From this point on, all events will require (reader, http, persistence) injected + args.push(AppAccessorsInstance.getPersistence()); + + // "extend" events have an additional "Extender" param - (context, extender, reader, http, persistence) + if (evtMethod.endsWith('Extend')) { + if (evtMethod.includes('Message')) { + args.splice(1, 0, new MessageExtender(param1 as IMessage)); + } else if (evtMethod.includes('Room')) { + args.splice(1, 0, new RoomExtender(param1 as IRoom)); + } + + return args; + } + + // "Modify" events have an additional "Builder" param - (context, builder, reader, http, persistence) + if (evtMethod.endsWith('Modify')) { + if (evtMethod.includes('Message')) { + args.splice(1, 0, new MessageBuilder(param1 as IMessage)); + } else if (evtMethod.includes('Room')) { + args.splice(1, 0, new RoomBuilder(param1 as IRoom)); + } + + return args; + } + + // From this point on, all events will require (reader, http, persistence, modifier) injected + args.push(AppAccessorsInstance.getModifier()); + + // This guy gets an extra one + if (evtMethod === 'executePostMessageDeleted') { + if (!param2) { + throw JsonRpcError.invalidParams(null); + } + + args.push(hydrateMessageObjects(param2)); + } + + return args; +} + +/** + * Hydrate the context object with the correct IMessage + * + * Some information is lost upon serializing the data from listeners through the pipes, + * so here we hydrate the complete object as necessary + */ +function hydrateMessageObjects(context: unknown): unknown { + if (objectIsRawMessage(context)) { + context.room = createRoom(context.room as IRoom, AppAccessorsInstance.getSenderFn()) as unknown as IRoom; + } else if ((context as Record)?.message) { + (context as Record).message = hydrateMessageObjects((context as Record).message); + } + + return context; +} + +function objectIsRawMessage(value: unknown): value is IMessage { + if (!value) return false; + + const { id, room, sender, createdAt } = value as Record; + + // Check if we have the fields of a message and the room hasn't already been hydrated + return !!(id && room && sender && createdAt) && !(room instanceof Room); +} diff --git a/packages/apps/node-runtime/src/handlers/outboundcomms-handler.ts b/packages/apps/node-runtime/src/handlers/outboundcomms-handler.ts new file mode 100644 index 0000000000000..500c4700fb49f --- /dev/null +++ b/packages/apps/node-runtime/src/handlers/outboundcomms-handler.ts @@ -0,0 +1,38 @@ +import type { IOutboundMessageProviders } from '@rocket.chat/apps-engine/definition/outboundCommunication/IOutboundCommsProvider'; +import type { Defined } from 'jsonrpc-lite'; +import { JsonRpcError } from 'jsonrpc-lite'; + +import { AppObjectRegistry } from '../AppObjectRegistry'; +import { AppAccessorsInstance } from '../lib/accessors/mod'; +import type { RequestContext } from '../lib/requestContext'; +import { wrapComposedApp } from '../lib/wrapAppForRequest'; + +export default async function outboundMessageHandler(request: RequestContext): Promise { + const { method: call, params } = request; + const [, providerName, methodName] = call.split(':'); + + const provider = AppObjectRegistry.get(`outboundCommunication:${providerName}`); + + if (!provider) { + return new JsonRpcError('error-invalid-provider', -32000); + } + + const method = provider[methodName as keyof IOutboundMessageProviders]; + const { logger } = request.context; + const args = (params as Array) ?? []; + + try { + logger.debug(`Executing ${methodName} on outbound communication provider...`); + + // deno-lint-ignore ban-types + return await (method as Function).apply(wrapComposedApp(provider, request), [ + ...args, + AppAccessorsInstance.getReader(), + AppAccessorsInstance.getModifier(), + AppAccessorsInstance.getHttp(), + AppAccessorsInstance.getPersistence(), + ]); + } catch (e) { + return new JsonRpcError(e.message, -32000); + } +} diff --git a/packages/apps/node-runtime/src/handlers/scheduler-handler.ts b/packages/apps/node-runtime/src/handlers/scheduler-handler.ts new file mode 100644 index 0000000000000..a7730c8728806 --- /dev/null +++ b/packages/apps/node-runtime/src/handlers/scheduler-handler.ts @@ -0,0 +1,66 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App'; +import type { IProcessor } from '@rocket.chat/apps-engine/definition/scheduler/IProcessor'; +import type { Defined } from 'jsonrpc-lite'; +import { JsonRpcError } from 'jsonrpc-lite'; + +import { AppObjectRegistry } from '../AppObjectRegistry'; +import { AppAccessorsInstance } from '../lib/accessors/mod'; +import type { RequestContext } from '../lib/requestContext'; +import { wrapAppForRequest } from '../lib/wrapAppForRequest'; +import { assertAppAvailable } from './lib/assertions'; + +export default async function handleScheduler(request: RequestContext): Promise { + const { method, params } = request; + const { logger } = request.context; + + const [, processorId] = method.split(':'); + if (!Array.isArray(params)) { + return JsonRpcError.invalidParams({ message: 'Invalid params' }); + } + + const [context] = params as [Record]; + + // AppSchedulerManager will append the appId to the processor name to avoid conflicts + const processor = AppObjectRegistry.get(`scheduler:${processorId}`); + + if (!processor) { + return JsonRpcError.methodNotFound({ + message: `Could not find processor for method ${method}`, + }); + } + + logger.debug({ msg: 'Job processor is being executed...', processorId: processor.id }); + + const app = AppObjectRegistry.get('app'); + + try { + assertAppAvailable(app); + + await processor.processor.call( + // Processor registration doesn't require the App dev to instantiate a class passing + // a reference to an App object, so we don't have a good way of hijacking the Logger + // we need. + // The only way we have to provide a durable Logger instance for the processor is by + // binding its execution to the proxied App reference itself. Unfortunately, the API + // ends up being opaque, but there isn't much we can do for now. + wrapAppForRequest(app, request), + context, + AppAccessorsInstance.getReader(), + AppAccessorsInstance.getModifier(), + AppAccessorsInstance.getHttp(), + AppAccessorsInstance.getPersistence(), + ); + + logger.debug({ msg: 'Job processor was successfully executed', processorId: processor.id }); + + return null; + } catch (err) { + logger.error({ err, msg: 'Job processor was unsuccessful', processorId: processor.id }); + + if (err instanceof JsonRpcError) { + return err; + } + + return JsonRpcError.internalError({ message: err.message }); + } +} diff --git a/packages/apps/node-runtime/src/handlers/slashcommand-handler.ts b/packages/apps/node-runtime/src/handlers/slashcommand-handler.ts new file mode 100644 index 0000000000000..3dbba0c07caf5 --- /dev/null +++ b/packages/apps/node-runtime/src/handlers/slashcommand-handler.ts @@ -0,0 +1,123 @@ +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; +import type { ISlashCommand } from '@rocket.chat/apps-engine/definition/slashcommands/ISlashCommand'; +import { SlashCommandContext } from '@rocket.chat/apps-engine/definition/slashcommands/SlashCommandContext'; +import type { Defined } from 'jsonrpc-lite'; +import { JsonRpcError } from 'jsonrpc-lite'; + +import { AppObjectRegistry } from '../AppObjectRegistry'; +import type { AppAccessors } from '../lib/accessors/mod'; +import { AppAccessorsInstance } from '../lib/accessors/mod'; +import type { RequestContext } from '../lib/requestContext'; +import createRoom from '../lib/roomFactory'; +import { wrapComposedApp } from '../lib/wrapAppForRequest'; + +export default async function slashCommandHandler(request: RequestContext): Promise { + const { method: call, params } = request; + const { logger } = request.context; + + const [, commandName, method] = call.split(':'); + + const command = AppObjectRegistry.get(`slashcommand:${commandName}`); + + if (!command) { + return new JsonRpcError(`Slashcommand ${commandName} not found`, -32000); + } + + let result: Awaited>; + + logger.debug({ msg: `Command is being executed...`, commandName, method, params }); + + try { + if (method === 'executor' || method === 'previewer') { + result = await handleExecutor({ AppAccessorsInstance, request }, command, method, params); + } else if (method === 'executePreviewItem') { + result = await handlePreviewItem({ AppAccessorsInstance, request }, command, params); + } else { + return new JsonRpcError(`Method ${method} not found on slashcommand ${commandName}`, -32000); + } + + logger.debug({ msg: `Command was successfully executed.`, commandName, method }); + } catch (error) { + logger.debug({ msg: `Command was unsuccessful.`, commandName, method, err: error }); + + return new JsonRpcError(error.message, -32000); + } + + return result; +} + +type Deps = { + AppAccessorsInstance: AppAccessors; + request: RequestContext; +}; + +/** + * @param deps Dependencies that need to be injected into the slashcommand + * @param command The slashcommand that is being executed + * @param method The method that is being executed + * @param params The parameters that are being passed to the method + */ +export function handleExecutor(deps: Deps, command: ISlashCommand, method: 'executor' | 'previewer', params: unknown) { + const executor = command[method]; + + if (typeof executor !== 'function') { + throw new Error(`Method ${method} not found on slashcommand ${command.command}`); + } + + if (!Array.isArray(params) || typeof params[0] !== 'object' || !params[0]) { + throw new Error(`First parameter must be an object`); + } + + const { sender, room, params: args, threadId, triggerId } = params[0] as Record; + + const context = new SlashCommandContext( + sender as SlashCommandContext['sender'], + createRoom(room as IRoom, deps.AppAccessorsInstance.getSenderFn()) as unknown as IRoom, + args as SlashCommandContext['params'], + threadId as SlashCommandContext['threadId'], + triggerId as SlashCommandContext['triggerId'], + ); + + return executor.apply(wrapComposedApp(command, deps.request), [ + context, + deps.AppAccessorsInstance.getReader(), + deps.AppAccessorsInstance.getModifier(), + deps.AppAccessorsInstance.getHttp(), + deps.AppAccessorsInstance.getPersistence(), + ]); +} + +/** + * @param deps Dependencies that need to be injected into the slashcommand + * @param command The slashcommand that is being executed + * @param params The parameters that are being passed to the method + */ +export function handlePreviewItem(deps: Deps, command: ISlashCommand, params: unknown) { + if (typeof command.executePreviewItem !== 'function') { + throw new Error(`Method not found on slashcommand ${command.command}`); + } + + if (!Array.isArray(params) || typeof params[0] !== 'object' || !params[0]) { + throw new Error(`First parameter must be an object`); + } + + const [previewItem, { sender, room, params: args, threadId, triggerId }] = params as [Record, Record]; + + const context = new SlashCommandContext( + sender as SlashCommandContext['sender'], + createRoom(room as IRoom, deps.AppAccessorsInstance.getSenderFn()) as unknown as IRoom, + args as SlashCommandContext['params'], + threadId as SlashCommandContext['threadId'], + triggerId as SlashCommandContext['triggerId'], + ); + + return command.executePreviewItem.call( + wrapComposedApp(command, deps.request), + previewItem, + context, + deps.AppAccessorsInstance.getReader(), + deps.AppAccessorsInstance.getModifier(), + deps.AppAccessorsInstance.getHttp(), + deps.AppAccessorsInstance.getPersistence(), + ); +} diff --git a/packages/apps/node-runtime/src/handlers/tests/api-handler.test.ts b/packages/apps/node-runtime/src/handlers/tests/api-handler.test.ts new file mode 100644 index 0000000000000..280469e3905ff --- /dev/null +++ b/packages/apps/node-runtime/src/handlers/tests/api-handler.test.ts @@ -0,0 +1,118 @@ +// deno-lint-ignore-file no-explicit-any +import type { IApiEndpoint } from '@rocket.chat/apps-engine/definition/api/IApiEndpoint'; +import { assertInstanceOf } from 'https://deno.land/std@0.203.0/assert/assert_instance_of'; +import { assertEquals, assertObjectMatch } from 'https://deno.land/std@0.203.0/assert/mod'; +import { beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd'; +import { spy } from 'https://deno.land/std@0.203.0/testing/mock'; +import { JsonRpcError } from 'jsonrpc-lite'; + +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import apiHandler from '../api-handler'; +import { createMockRequest } from './helpers/mod'; + +describe('handlers > api', () => { + const mockEndpoint: IApiEndpoint = { + path: '/test', + // deno-lint-ignore no-unused-vars + get: (request: any, endpoint: any, read: any, modify: any, http: any, persis: any) => Promise.resolve('ok'), + // deno-lint-ignore no-unused-vars + post: (request: any, endpoint: any, read: any, modify: any, http: any, persis: any) => Promise.resolve('ok'), + // deno-lint-ignore no-unused-vars + put: (request: any, endpoint: any, read: any, modify: any, http: any, persis: any) => { + throw new Error('Method execution error example'); + }, + }; + + beforeEach(() => { + AppObjectRegistry.clear(); + AppObjectRegistry.set('api:/test', mockEndpoint); + }); + + it('correctly handles execution of an api endpoint method GET', async () => { + const _spy = spy(mockEndpoint, 'get'); + + const result = await apiHandler(createMockRequest({ method: 'api:/test:get', params: ['request', 'endpointInfo'] })); + + assertEquals(result, 'ok'); + assertEquals(_spy.calls[0].args.length, 6); + assertEquals(_spy.calls[0].args[0], 'request'); + assertEquals(_spy.calls[0].args[1], 'endpointInfo'); + }); + + it('correctly handles execution of an api endpoint method POST', async () => { + const _spy = spy(mockEndpoint, 'post'); + + const result = await apiHandler(createMockRequest({ method: 'api:/test:post', params: ['request', 'endpointInfo'] })); + + assertEquals(result, 'ok'); + assertEquals(_spy.calls[0].args.length, 6); + assertEquals(_spy.calls[0].args[0], 'request'); + assertEquals(_spy.calls[0].args[1], 'endpointInfo'); + }); + + it('correctly handles an error if the method not exists for the selected endpoint', async () => { + const result = await apiHandler(createMockRequest({ method: `api:/test:delete`, params: ['request', 'endpointInfo'] })); + + assertInstanceOf(result, JsonRpcError); + assertObjectMatch(result, { + message: `/test's delete not exists`, + code: -32000, + }); + }); + + it('correctly handles an error if endpoint not exists', async () => { + const result = await apiHandler(createMockRequest({ method: `api:/error:get`, params: ['request', 'endpointInfo'] })); + + assertInstanceOf(result, JsonRpcError); + assertObjectMatch(result, { + message: `Endpoint /error not found`, + code: -32000, + }); + }); + + it('correctly handles an error if the method execution fails', async () => { + const result = await apiHandler(createMockRequest({ method: `api:/test:put`, params: ['request', 'endpointInfo'] })); + + assertInstanceOf(result, JsonRpcError); + assertObjectMatch(result, { + message: `Method execution error example`, + code: -32000, + }); + }); + + it('correctly handles dynamic paths with parameters (e.g., webhook/:event)', async () => { + const mockDynamicEndpoint: IApiEndpoint = { + path: 'webhook/:event', + // deno-lint-ignore no-unused-vars + post: (request: any, endpoint: any, read: any, modify: any, http: any, persis: any) => Promise.resolve('webhook handled'), + }; + + AppObjectRegistry.set('api:webhook/:event', mockDynamicEndpoint); + + const _spy = spy(mockDynamicEndpoint, 'post'); + + const result = await apiHandler(createMockRequest({ method: 'api:webhook/:event:post', params: ['request', 'endpointInfo'] })); + + assertEquals(result, 'webhook handled'); + assertEquals(_spy.calls[0].args.length, 6); + assertEquals(_spy.calls[0].args[0], 'request'); + assertEquals(_spy.calls[0].args[1], 'endpointInfo'); + }); + + it('correctly handles paths with multiple segments and colons', async () => { + const mockComplexEndpoint: IApiEndpoint = { + path: 'api/v1/:resource/:id', + // deno-lint-ignore no-unused-vars + get: (request: any, endpoint: any, read: any, modify: any, http: any, persis: any) => Promise.resolve('complex path'), + }; + + AppObjectRegistry.set('api:api/v1/:resource/:id', mockComplexEndpoint); + + const _spy = spy(mockComplexEndpoint, 'get'); + + const result = await apiHandler(createMockRequest({ method: 'api:api/v1/:resource/:id:get', params: ['request', 'endpointInfo'] })); + + assertEquals(result, 'complex path'); + assertEquals(_spy.calls[0].args.length, 6); + }); +}); diff --git a/packages/apps/node-runtime/src/handlers/tests/helpers/mod.ts b/packages/apps/node-runtime/src/handlers/tests/helpers/mod.ts new file mode 100644 index 0000000000000..739c944359484 --- /dev/null +++ b/packages/apps/node-runtime/src/handlers/tests/helpers/mod.ts @@ -0,0 +1,29 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App'; + +import { Logger } from '../../../lib/logger'; +import type { RequestDescriptor } from '../../../lib/messenger'; +import type { RequestContext } from '../../../lib/requestContext'; + +export function createMockRequest({ method, params }: RequestDescriptor): RequestContext { + return { + jsonrpc: '2.0', + id: 1, + method, + params, + context: { + logger: new Logger(method), + }, + serialize: () => '', + }; +} + +export function createMockApp(): App { + return { + extendConfiguration: () => {}, + getID: () => 'mockApp', + getLogger: () => ({ + debug: () => {}, + error: () => {}, + }), + } as unknown as App; +} diff --git a/packages/apps/node-runtime/src/handlers/tests/listener-handler.test.ts b/packages/apps/node-runtime/src/handlers/tests/listener-handler.test.ts new file mode 100644 index 0000000000000..f5aebbcbb56d3 --- /dev/null +++ b/packages/apps/node-runtime/src/handlers/tests/listener-handler.test.ts @@ -0,0 +1,234 @@ +// deno-lint-ignore-file no-explicit-any +import { assertEquals, assertInstanceOf, assertObjectMatch } from 'https://deno.land/std@0.203.0/assert/mod'; +import { describe, it } from 'https://deno.land/std@0.203.0/testing/bdd'; + +import { MessageBuilder } from '../../lib/accessors/builders/MessageBuilder'; +import { RoomBuilder } from '../../lib/accessors/builders/RoomBuilder'; +import { MessageExtender } from '../../lib/accessors/extenders/MessageExtender'; +import { RoomExtender } from '../../lib/accessors/extenders/RoomExtender'; +import type { AppAccessors } from '../../lib/accessors/mod'; +import { Room } from '../../lib/room'; +import { parseArgs } from '../listener/handler'; + +describe('handlers > listeners', () => { + const mockAppAccessors = { + getReader: () => ({ __type: 'reader' }), + getHttp: () => ({ __type: 'http' }), + getModifier: () => ({ __type: 'modifier' }), + getPersistence: () => ({ __type: 'persistence' }), + getSenderFn: () => (id: string) => Promise.resolve([{ __type: 'bridgeCall' }, { id }]), + } as unknown as AppAccessors; + + it('correctly parses the arguments for a request to trigger the "checkPreMessageSentPrevent" method', () => { + const evtMethod = 'checkPreMessageSentPrevent'; + // For the 'checkPreMessageSentPrevent' method, the context will be a message in a real scenario + const evtArgs = [{ __type: 'context' }]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 3); + assertEquals(params[0], { __type: 'context' }); + assertEquals(params[1], { __type: 'reader' }); + assertEquals(params[2], { __type: 'http' }); + }); + + it('correctly parses the arguments for a request to trigger the "checkPostMessageDeleted" method', () => { + const evtMethod = 'checkPostMessageDeleted'; + // For the 'checkPostMessageDeleted' method, the context will be a message in a real scenario, + // and the extraContext will provide further information such the user who deleted the message + const evtArgs = [{ __type: 'context' }, { __type: 'extraContext' }]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 4); + assertEquals(params[0], { __type: 'context' }); + assertEquals(params[1], { __type: 'reader' }); + assertEquals(params[2], { __type: 'http' }); + assertEquals(params[3], { __type: 'extraContext' }); + }); + + it('correctly parses the arguments for a request to trigger the "checkPreRoomCreateExtend" method', () => { + const evtMethod = 'checkPreRoomCreateExtend'; + // For the 'checkPreRoomCreateExtend' method, the context will be a room in a real scenario + const evtArgs = [ + { + id: 'fake', + type: 'fake', + slugifiedName: 'fake', + creator: 'fake', + createdAt: Date.now(), + }, + ]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 3); + + assertInstanceOf(params[0], Room); + assertEquals(params[1], { __type: 'reader' }); + assertEquals(params[2], { __type: 'http' }); + }); + + it('correctly parses the arguments for a request to trigger the "executePreMessageSentExtend" method', () => { + const evtMethod = 'executePreMessageSentExtend'; + // For the 'executePreMessageSentExtend' method, the context will be a message in a real scenario + const evtArgs = [{ __type: 'context' }]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 5); + // Instantiating the MessageExtender might modify the original object, so we need to assert it matches instead of equals + assertObjectMatch(params[0] as Record, { + __type: 'context', + }); + assertInstanceOf(params[1], MessageExtender); + assertEquals(params[2], { __type: 'reader' }); + assertEquals(params[3], { __type: 'http' }); + assertEquals(params[4], { __type: 'persistence' }); + }); + + it('correctly parses the arguments for a request to trigger the "executePreRoomCreateExtend" method', () => { + const evtMethod = 'executePreRoomCreateExtend'; + // For the 'executePreRoomCreateExtend' method, the context will be a room in a real scenario + const evtArgs = [{ __type: 'context' }]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 5); + // Instantiating the RoomExtender might modify the original object, so we need to assert it matches instead of equals + assertObjectMatch(params[0] as Record, { + __type: 'context', + }); + assertInstanceOf(params[1], RoomExtender); + assertEquals(params[2], { __type: 'reader' }); + assertEquals(params[3], { __type: 'http' }); + assertEquals(params[4], { __type: 'persistence' }); + }); + + it('correctly parses the arguments for a request to trigger the "executePreMessageSentModify" method', () => { + const evtMethod = 'executePreMessageSentModify'; + // For the 'executePreMessageSentModify' method, the context will be a message in a real scenario + const evtArgs = [{ __type: 'context' }]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 5); + // Instantiating the MessageBuilder might modify the original object, so we need to assert it matches instead of equals + assertObjectMatch(params[0] as Record, { + __type: 'context', + }); + assertInstanceOf(params[1], MessageBuilder); + assertEquals(params[2], { __type: 'reader' }); + assertEquals(params[3], { __type: 'http' }); + assertEquals(params[4], { __type: 'persistence' }); + }); + + it('correctly parses the arguments for a request to trigger the "executePreRoomCreateModify" method', () => { + const evtMethod = 'executePreRoomCreateModify'; + // For the 'executePreRoomCreateModify' method, the context will be a room in a real scenario + const evtArgs = [{ __type: 'context' }]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 5); + // Instantiating the RoomBuilder might modify the original object, so we need to assert it matches instead of equals + assertObjectMatch(params[0] as Record, { + __type: 'context', + }); + assertInstanceOf(params[1], RoomBuilder); + assertEquals(params[2], { __type: 'reader' }); + assertEquals(params[3], { __type: 'http' }); + assertEquals(params[4], { __type: 'persistence' }); + }); + + it('correctly parses the arguments for a request to trigger the "executePostRoomUserJoined" method', () => { + const evtMethod = 'executePostRoomUserJoined'; + // For the 'executePostRoomUserJoined' method, the context will be a room in a real scenario + const room = { + id: 'fake', + type: 'fake', + slugifiedName: 'fake', + creator: 'fake', + createdAt: Date.now(), + }; + + const evtArgs = [{ __type: 'context', room }]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 5); + assertInstanceOf((params[0] as any).room, Room); + assertEquals(params[1], { __type: 'reader' }); + assertEquals(params[2], { __type: 'http' }); + assertEquals(params[3], { __type: 'persistence' }); + assertEquals(params[4], { __type: 'modifier' }); + }); + + it('correctly parses the arguments for a request to trigger the "executePostRoomUserLeave" method', () => { + const evtMethod = 'executePostRoomUserLeave'; + // For the 'executePostRoomUserLeave' method, the context will be a room in a real scenario + const room = { + id: 'fake', + type: 'fake', + slugifiedName: 'fake', + creator: 'fake', + createdAt: Date.now(), + }; + + const evtArgs = [{ __type: 'context', room }]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 5); + assertInstanceOf((params[0] as any).room, Room); + assertEquals(params[1], { __type: 'reader' }); + assertEquals(params[2], { __type: 'http' }); + assertEquals(params[3], { __type: 'persistence' }); + assertEquals(params[4], { __type: 'modifier' }); + }); + + it('correctly parses the arguments for a request to trigger the "executePostMessageDeleted" method', () => { + const evtMethod = 'executePostMessageDeleted'; + // For the 'executePostMessageDeleted' method, the context will be a message in a real scenario + const evtArgs = [{ __type: 'context' }, { __type: 'extraContext' }]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 6); + assertEquals(params[0], { __type: 'context' }); + assertEquals(params[1], { __type: 'reader' }); + assertEquals(params[2], { __type: 'http' }); + assertEquals(params[3], { __type: 'persistence' }); + assertEquals(params[4], { __type: 'modifier' }); + assertEquals(params[5], { __type: 'extraContext' }); + }); + + it('correctly parses the arguments for a request to trigger the "executePostMessageSent" method', () => { + const evtMethod = 'executePostMessageSent'; + // For the 'executePostMessageDeleted' method, the context will be a message in a real scenario + const evtArgs = [ + { + id: 'fake', + sender: 'fake', + createdAt: Date.now(), + room: { + id: 'fake-room', + type: 'fake', + slugifiedName: 'fake', + creator: 'fake', + createdAt: Date.now(), + }, + }, + ]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 5); + assertObjectMatch(params[0] as Record, { id: 'fake' }); + assertInstanceOf((params[0] as any).room, Room); + assertEquals(params[1], { __type: 'reader' }); + assertEquals(params[2], { __type: 'http' }); + assertEquals(params[3], { __type: 'persistence' }); + assertEquals(params[4], { __type: 'modifier' }); + }); +}); diff --git a/packages/apps/node-runtime/src/handlers/tests/scheduler-handler.test.ts b/packages/apps/node-runtime/src/handlers/tests/scheduler-handler.test.ts new file mode 100644 index 0000000000000..322a95b8aa89c --- /dev/null +++ b/packages/apps/node-runtime/src/handlers/tests/scheduler-handler.test.ts @@ -0,0 +1,41 @@ +import { assertEquals } from 'https://deno.land/std@0.203.0/assert/mod'; +import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd'; + +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import { AppAccessors } from '../../lib/accessors/mod'; +import handleScheduler from '../scheduler-handler'; +import { createMockApp, createMockRequest } from './helpers/mod'; + +describe('handlers > scheduler', () => { + const mockAppAccessors = new AppAccessors(() => + Promise.resolve({ + id: 'mockId', + result: {}, + jsonrpc: '2.0', + serialize: () => '', + }), + ); + + const mockApp = createMockApp(); + + beforeEach(() => { + AppObjectRegistry.clear(); + AppObjectRegistry.set('app', mockApp); + mockAppAccessors.getConfigurationExtend().scheduler.registerProcessors([ + { + id: 'mockId', + processor: () => Promise.resolve('it works!'), + }, + ]); + }); + + afterAll(() => { + AppObjectRegistry.clear(); + }); + + it('correctly executes a request to a processor', async () => { + const result = await handleScheduler(createMockRequest({ method: 'scheduler:mockId', params: [{}] })); + + assertEquals(result, null); + }); +}); diff --git a/packages/apps/node-runtime/src/handlers/tests/slashcommand-handler.test.ts b/packages/apps/node-runtime/src/handlers/tests/slashcommand-handler.test.ts new file mode 100644 index 0000000000000..5007ef1b45060 --- /dev/null +++ b/packages/apps/node-runtime/src/handlers/tests/slashcommand-handler.test.ts @@ -0,0 +1,169 @@ +// deno-lint-ignore-file no-explicit-any +import { assertEquals, assertInstanceOf } from 'https://deno.land/std@0.203.0/assert/mod'; +import { beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd'; +import { spy } from 'https://deno.land/std@0.203.0/testing/mock'; + +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import { createMockRequest } from './helpers/mod'; +import type { AppAccessors } from '../../lib/accessors/mod'; +import { Room } from '../../lib/room'; +import { handleExecutor, handlePreviewItem } from '../slashcommand-handler'; + +describe('handlers > slashcommand', () => { + const mockAppAccessors = { + getReader: () => ({ __type: 'reader' }), + getHttp: () => ({ __type: 'http' }), + getModifier: () => ({ __type: 'modifier' }), + getPersistence: () => ({ __type: 'persistence' }), + getSenderFn: () => (id: string) => Promise.resolve([{ __type: 'bridgeCall' }, { id }]), + } as unknown as AppAccessors; + + const mockCommandExecutorOnly = { + command: 'executor-only', + i18nParamsExample: 'test', + i18nDescription: 'test', + providesPreview: false, + // deno-lint-ignore no-unused-vars + async executor(context: any, read: any, modify: any, http: any, persis: any): Promise {}, + }; + + const mockCommandExecutorAndPreview = { + command: 'executor-and-preview', + i18nParamsExample: 'test', + i18nDescription: 'test', + providesPreview: true, + // deno-lint-ignore no-unused-vars + async executor(context: any, read: any, modify: any, http: any, persis: any): Promise {}, + // deno-lint-ignore no-unused-vars + async previewer(context: any, read: any, modify: any, http: any, persis: any): Promise {}, + // deno-lint-ignore no-unused-vars + async executePreviewItem(previewItem: any, context: any, read: any, modify: any, http: any, persis: any): Promise {}, + }; + + const mockCommandPreviewWithNoExecutor = { + command: 'preview-with-no-executor', + i18nParamsExample: 'test', + i18nDescription: 'test', + providesPreview: true, + // deno-lint-ignore no-unused-vars + async previewer(context: any, read: any, modify: any, http: any, persis: any): Promise {}, + // deno-lint-ignore no-unused-vars + async executePreviewItem(previewItem: any, context: any, read: any, modify: any, http: any, persis: any): Promise {}, + }; + + beforeEach(() => { + AppObjectRegistry.clear(); + AppObjectRegistry.set('slashcommand:executor-only', mockCommandExecutorOnly); + AppObjectRegistry.set('slashcommand:executor-and-preview', mockCommandExecutorAndPreview); + AppObjectRegistry.set('slashcommand:preview-with-no-executor', mockCommandPreviewWithNoExecutor); + }); + + it('correctly handles execution of a slash command', async () => { + const mockContext = { + sender: { __type: 'sender' }, + room: { __type: 'room' }, + params: { __type: 'params' }, + threadId: 'threadId', + triggerId: 'triggerId', + }; + + const _spy = spy(mockCommandExecutorOnly, 'executor'); + + const mockRequest = createMockRequest({ method: 'slashcommand:executor-only:executor', params: [mockContext] }); + + await handleExecutor({ AppAccessorsInstance: mockAppAccessors, request: mockRequest }, mockCommandExecutorOnly, 'executor', [ + mockContext, + ]); + + const context = _spy.calls[0].args[0]; + + assertInstanceOf(context.getRoom(), Room); + assertEquals(context.getSender(), { __type: 'sender' }); + assertEquals(context.getArguments(), { __type: 'params' }); + assertEquals(context.getThreadId(), 'threadId'); + assertEquals(context.getTriggerId(), 'triggerId'); + + assertEquals(_spy.calls[0].args[1], mockAppAccessors.getReader()); + assertEquals(_spy.calls[0].args[2], mockAppAccessors.getModifier()); + assertEquals(_spy.calls[0].args[3], mockAppAccessors.getHttp()); + assertEquals(_spy.calls[0].args[4], mockAppAccessors.getPersistence()); + + _spy.restore(); + }); + + it('correctly handles execution of a slash command previewer', async () => { + const mockContext = { + sender: { __type: 'sender' }, + room: { __type: 'room' }, + params: { __type: 'params' }, + threadId: 'threadId', + triggerId: 'triggerId', + }; + + const _spy = spy(mockCommandExecutorAndPreview, 'previewer'); + + const mockRequest = createMockRequest({ method: 'slashcommand:executor-and-preview:previewer', params: [mockContext] }); + + await handleExecutor({ AppAccessorsInstance: mockAppAccessors, request: mockRequest }, mockCommandExecutorAndPreview, 'previewer', [ + mockContext, + ]); + + const context = _spy.calls[0].args[0]; + + assertInstanceOf(context.getRoom(), Room); + assertEquals(context.getSender(), { __type: 'sender' }); + assertEquals(context.getArguments(), { __type: 'params' }); + assertEquals(context.getThreadId(), 'threadId'); + assertEquals(context.getTriggerId(), 'triggerId'); + + assertEquals(_spy.calls[0].args[1], mockAppAccessors.getReader()); + assertEquals(_spy.calls[0].args[2], mockAppAccessors.getModifier()); + assertEquals(_spy.calls[0].args[3], mockAppAccessors.getHttp()); + assertEquals(_spy.calls[0].args[4], mockAppAccessors.getPersistence()); + + _spy.restore(); + }); + + it('correctly handles execution of a slash command preview item executor', async () => { + const mockContext = { + sender: { __type: 'sender' }, + room: { __type: 'room' }, + params: { __type: 'params' }, + threadId: 'threadId', + triggerId: 'triggerId', + }; + + const mockPreviewItem = { + id: 'previewItemId', + type: 'image', + value: 'https://example.com/image.png', + }; + + const _spy = spy(mockCommandExecutorAndPreview, 'executePreviewItem'); + + const mockRequest = createMockRequest({ + method: 'slashcommand:executor-and-preview:executePreviewItem', + params: [mockPreviewItem, mockContext], + }); + + await handlePreviewItem({ AppAccessorsInstance: mockAppAccessors, request: mockRequest }, mockCommandExecutorAndPreview, [ + mockPreviewItem, + mockContext, + ]); + + const context = _spy.calls[0].args[1]; + + assertInstanceOf(context.getRoom(), Room); + assertEquals(context.getSender(), { __type: 'sender' }); + assertEquals(context.getArguments(), { __type: 'params' }); + assertEquals(context.getThreadId(), 'threadId'); + assertEquals(context.getTriggerId(), 'triggerId'); + + assertEquals(_spy.calls[0].args[2], mockAppAccessors.getReader()); + assertEquals(_spy.calls[0].args[3], mockAppAccessors.getModifier()); + assertEquals(_spy.calls[0].args[4], mockAppAccessors.getHttp()); + assertEquals(_spy.calls[0].args[5], mockAppAccessors.getPersistence()); + + _spy.restore(); + }); +}); diff --git a/packages/apps/node-runtime/src/handlers/tests/uikit-handler.test.ts b/packages/apps/node-runtime/src/handlers/tests/uikit-handler.test.ts new file mode 100644 index 0000000000000..e12f0bd3597b2 --- /dev/null +++ b/packages/apps/node-runtime/src/handlers/tests/uikit-handler.test.ts @@ -0,0 +1,105 @@ +// deno-lint-ignore-file no-explicit-any +import { assertInstanceOf } from 'https://deno.land/std@0.203.0/assert/mod'; +import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd'; +import jsonrpc from 'jsonrpc-lite'; + +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import handleUIKitInteraction, { + UIKitActionButtonInteractionContext, + UIKitBlockInteractionContext, + UIKitLivechatBlockInteractionContext, + UIKitViewCloseInteractionContext, + UIKitViewSubmitInteractionContext, +} from '../uikit/handler'; + +describe('handlers > uikit', () => { + const mockApp = { + getID: (): string => 'appId', + executeBlockActionHandler: (context: any): Promise => Promise.resolve(context), + executeViewSubmitHandler: (context: any): Promise => Promise.resolve(context), + executeViewClosedHandler: (context: any): Promise => Promise.resolve(context), + executeActionButtonHandler: (context: any): Promise => Promise.resolve(context), + executeLivechatBlockActionHandler: (context: any): Promise => Promise.resolve(context), + }; + + beforeEach(() => { + AppObjectRegistry.set('app', mockApp); + }); + + afterAll(() => { + AppObjectRegistry.clear(); + }); + + it('successfully handles a call for "executeBlockActionHandler"', async () => { + const request = jsonrpc.request(1, 'app:executeBlockActionHandler', [ + { + actionId: 'actionId', + blockId: 'blockId', + value: 'value', + viewId: 'viewId', + }, + ]); + + const result = await handleUIKitInteraction(request); + assertInstanceOf(result, UIKitBlockInteractionContext); + }); + + it('successfully handles a call for "executeViewSubmitHandler"', async () => { + const request = jsonrpc.request(1, 'app:executeViewSubmitHandler', [ + { + viewId: 'viewId', + appId: 'appId', + userId: 'userId', + isAppUser: true, + values: {}, + }, + ]); + + const result = await handleUIKitInteraction(request); + assertInstanceOf(result, UIKitViewSubmitInteractionContext); + }); + + it('successfully handles a call for "executeViewClosedHandler"', async () => { + const request = jsonrpc.request(1, 'app:executeViewClosedHandler', [ + { + viewId: 'viewId', + appId: 'appId', + userId: 'userId', + isAppUser: true, + }, + ]); + + const result = await handleUIKitInteraction(request); + assertInstanceOf(result, UIKitViewCloseInteractionContext); + }); + + it('successfully handles a call for "executeActionButtonHandler"', async () => { + const request = jsonrpc.request(1, 'app:executeActionButtonHandler', [ + { + actionId: 'actionId', + appId: 'appId', + userId: 'userId', + isAppUser: true, + }, + ]); + + const result = await handleUIKitInteraction(request); + assertInstanceOf(result, UIKitActionButtonInteractionContext); + }); + + it('successfully handles a call for "executeLivechatBlockActionHandler"', async () => { + const request = jsonrpc.request(1, 'app:executeLivechatBlockActionHandler', [ + { + actionId: 'actionId', + appId: 'appId', + userId: 'userId', + visitor: {}, + isAppUser: true, + room: {}, + }, + ]); + + const result = await handleUIKitInteraction(request); + assertInstanceOf(result, UIKitLivechatBlockInteractionContext); + }); +}); diff --git a/packages/apps/node-runtime/src/handlers/tests/upload-event-handler.test.ts b/packages/apps/node-runtime/src/handlers/tests/upload-event-handler.test.ts new file mode 100644 index 0000000000000..0b6bbf50aafd7 --- /dev/null +++ b/packages/apps/node-runtime/src/handlers/tests/upload-event-handler.test.ts @@ -0,0 +1,109 @@ +// deno-lint-ignore-file no-explicit-any +import { Buffer } from 'node:buffer'; + +import type { App } from '@rocket.chat/apps-engine/definition/App'; +import type { IPreFileUpload } from '@rocket.chat/apps-engine/definition/uploads/IPreFileUpload'; +import type { IUploadDetails } from '@rocket.chat/apps-engine/definition/uploads/IUploadDetails'; +import { assertInstanceOf, assertNotInstanceOf, assertEquals, assertStringIncludes } from 'https://deno.land/std@0.203.0/assert/mod'; +import { afterEach, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd'; +import { assertSpyCalls, spy } from 'https://deno.land/std@0.203.0/testing/mock'; +import { JsonRpcError } from 'jsonrpc-lite'; + +import { createMockRequest } from './helpers/mod'; +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import handleUploadEvents from '../app/handleUploadEvents'; +import { Errors } from '../lib/assertions'; + +describe('handlers > upload', () => { + let app: App & IPreFileUpload; + let path: string; + let file: IUploadDetails; + + beforeEach(async () => { + AppObjectRegistry.clear(); + + path = await Deno.makeTempFile(); + + app = { + extendConfiguration: () => {}, + executePreFileUpload: () => Promise.resolve(), + } as unknown as App; + + AppObjectRegistry.set('app', app); + + const content = 'Temp file for testing'; + + await Deno.writeTextFile(path, content); + + file = { + name: 'TempFile.txt', + size: content.length, + type: 'text/plain', + rid: 'RandomRoomId', + userId: 'RandomUserId', + }; + }); + + afterEach(async () => { + await Deno.remove(path).catch((e) => e?.code !== 'ENOENT' && console.warn(`Failed to remove temp file at ${path}`, e)); + }); + + it('correctly handles valid parameters', async () => { + const result = await handleUploadEvents(createMockRequest({ method: 'app:executePreFileUpload', params: [{ file, path }] })); + + assertNotInstanceOf(result, JsonRpcError, 'result is JsonRpcError'); + }); + + it('correctly loads the file contents for IPreFileUpload', async () => { + const _spy = spy(app as any, 'executePreFileUpload'); + + const result = await handleUploadEvents(createMockRequest({ method: 'app:executePreFileUpload', params: [{ file, path }] })); + + assertNotInstanceOf(result, JsonRpcError, 'result is JsonRpcError'); + assertSpyCalls(_spy, 1); + assertInstanceOf((_spy.calls[0].args[0] as any)?.content, Buffer); + }); + + it('fails when app object is not on registry', async () => { + AppObjectRegistry.clear(); + + const result = await handleUploadEvents(createMockRequest({ method: 'app:executePreFileUpload', params: [{ file, path }] })); + + assertInstanceOf(result, JsonRpcError); + assertEquals(result.data.code, Errors.DRT_APP_NOT_AVAILABLE); + }); + + it('fails when the app does not implement the IPreFileUpload event handler', async () => { + delete (app as any).executePreFileUpload; + + const result = await handleUploadEvents(createMockRequest({ method: 'app:executePreFileUpload', params: [{ file, path }] })); + + assertInstanceOf(result, JsonRpcError); + assertEquals(result.data.code, Errors.DRT_EVENT_HANDLER_FUNCTION_MISSING); + }); + + it('fails when "file" is not a proper IUploadDetails object', async () => { + const result = await handleUploadEvents( + createMockRequest({ method: 'app:executePreFileUpload', params: [{ file: { nope: 'bad' }, path }] }), + ); + + assertInstanceOf(result, JsonRpcError); + assertStringIncludes(result.data.err, 'Expected IUploadDetails'); + }); + + it('fails when "path" is not a proper string', async () => { + const result = await handleUploadEvents(createMockRequest({ method: 'app:executePreFileUpload', params: [{ file, path: {} }] })); + + assertInstanceOf(result, JsonRpcError); + assertStringIncludes(result.data.err, 'Expected string'); + }); + + it('fails when "path" is not a readable file path', async () => { + await Deno.remove(path); + + const result = await handleUploadEvents(createMockRequest({ method: 'app:executePreFileUpload', params: [{ file, path }] })); + + assertInstanceOf(result, JsonRpcError); + assertEquals(result.data.code, 'ENOENT'); + }); +}); diff --git a/packages/apps/node-runtime/src/handlers/tests/videoconference-handler.test.ts b/packages/apps/node-runtime/src/handlers/tests/videoconference-handler.test.ts new file mode 100644 index 0000000000000..20ca5d49a6688 --- /dev/null +++ b/packages/apps/node-runtime/src/handlers/tests/videoconference-handler.test.ts @@ -0,0 +1,125 @@ +// deno-lint-ignore-file no-explicit-any +import { assertEquals, assertObjectMatch, assertInstanceOf } from 'https://deno.land/std@0.203.0/assert/mod'; +import { beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd'; +import { spy } from 'https://deno.land/std@0.203.0/testing/mock'; +import { JsonRpcError } from 'jsonrpc-lite'; + +import videoconfHandler from '../videoconference-handler'; +import { createMockRequest } from './helpers/mod'; +import { AppObjectRegistry } from '../../AppObjectRegistry'; + +describe('handlers > videoconference', () => { + // deno-lint-ignore no-unused-vars + const mockMethodWithoutParam = (read: any, modify: any, http: any, persis: any): Promise => Promise.resolve('ok none'); + // deno-lint-ignore no-unused-vars + const mockMethodWithOneParam = (call: any, read: any, modify: any, http: any, persis: any): Promise => Promise.resolve('ok one'); + // deno-lint-ignore no-unused-vars + const mockMethodWithTwoParam = (call: any, user: any, read: any, modify: any, http: any, persis: any): Promise => + Promise.resolve('ok two'); + // deno-lint-ignore no-unused-vars + const mockMethodWithThreeParam = (call: any, user: any, options: any, read: any, modify: any, http: any, persis: any): Promise => + Promise.resolve('ok three'); + const mockProvider = { + empty: mockMethodWithoutParam, + one: mockMethodWithOneParam, + two: mockMethodWithTwoParam, + three: mockMethodWithThreeParam, + notAFunction: true, + error: () => { + throw new Error('Method execution error example'); + }, + }; + + beforeEach(() => { + AppObjectRegistry.clear(); + AppObjectRegistry.set('videoConfProvider:test-provider', mockProvider); + }); + + it('correctly handles execution of a videoconf method without additional params', async () => { + const _spy = spy(mockProvider, 'empty'); + + const result = await videoconfHandler(createMockRequest({ method: 'videoconference:test-provider:empty', params: [] })); + + assertEquals(result, 'ok none'); + assertEquals(_spy.calls[0].args.length, 4); + + _spy.restore(); + }); + + it('correctly handles execution of a videoconf method with one param', async () => { + const _spy = spy(mockProvider, 'one'); + + const result = await videoconfHandler(createMockRequest({ method: 'videoconference:test-provider:one', params: ['call'] })); + + assertEquals(result, 'ok one'); + assertEquals(_spy.calls[0].args.length, 5); + assertEquals(_spy.calls[0].args[0], 'call'); + + _spy.restore(); + }); + + it('correctly handles execution of a videoconf method with two params', async () => { + const _spy = spy(mockProvider, 'two'); + + const result = await videoconfHandler(createMockRequest({ method: 'videoconference:test-provider:two', params: ['call', 'user'] })); + + assertEquals(result, 'ok two'); + assertEquals(_spy.calls[0].args.length, 6); + assertEquals(_spy.calls[0].args[0], 'call'); + assertEquals(_spy.calls[0].args[1], 'user'); + + _spy.restore(); + }); + + it('correctly handles execution of a videoconf method with three params', async () => { + const _spy = spy(mockProvider, 'three'); + + const result = await videoconfHandler( + createMockRequest({ method: 'videoconference:test-provider:three', params: ['call', 'user', 'options'] }), + ); + + assertEquals(result, 'ok three'); + assertEquals(_spy.calls[0].args.length, 7); + assertEquals(_spy.calls[0].args[0], 'call'); + assertEquals(_spy.calls[0].args[1], 'user'); + assertEquals(_spy.calls[0].args[2], 'options'); + + _spy.restore(); + }); + + it('correctly handles an error on execution of a videoconf method', async () => { + const result = await videoconfHandler(createMockRequest({ method: 'videoconference:test-provider:error', params: [] })); + + assertInstanceOf(result, JsonRpcError); + assertObjectMatch(result, { + message: 'Method execution error example', + code: -32000, + }); + }); + + it('correctly handles an error when provider is not found', async () => { + const providerName = 'error-provider'; + const result = await videoconfHandler(createMockRequest({ method: `videoconference:${providerName}:method`, params: [] })); + + assertInstanceOf(result, JsonRpcError); + assertObjectMatch(result, { + message: `Provider ${providerName} not found`, + code: -32000, + }); + }); + + it('correctly handles an error if method is not a function of provider', async () => { + const methodName = 'notAFunction'; + const providerName = 'test-provider'; + const result = await videoconfHandler(createMockRequest({ method: `videoconference:${providerName}:${methodName}`, params: [] })); + + assertInstanceOf(result, JsonRpcError); + assertObjectMatch(result, { + message: 'Method not found', + code: -32601, + data: { + message: `Method ${methodName} not found on provider ${providerName}`, + }, + }); + }); +}); diff --git a/packages/apps/node-runtime/src/handlers/uikit/handler.ts b/packages/apps/node-runtime/src/handlers/uikit/handler.ts new file mode 100644 index 0000000000000..4d70db3ad8d8a --- /dev/null +++ b/packages/apps/node-runtime/src/handlers/uikit/handler.ts @@ -0,0 +1,110 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App'; +import type { + IUIKitBlockIncomingInteraction, + IUIKitViewSubmitIncomingInteraction, + IUIKitViewCloseIncomingInteraction, + IUIKitActionButtonIncomingInteraction, +} from '@rocket.chat/apps-engine/definition/uikit/UIKitIncomingInteractionTypes'; +import type { + UIKitBlockInteractionContext as _UIKitBlockInteractionContext, + UIKitViewSubmitInteractionContext as _UIKitViewSubmitInteractionContext, + UIKitViewCloseInteractionContext as _UIKitViewCloseInteractionContext, + UIKitActionButtonInteractionContext as _UIKitActionButtonInteractionContext, +} from '@rocket.chat/apps-engine/definition/uikit/UIKitInteractionContext'; +import type { IUIKitLivechatBlockIncomingInteraction } from '@rocket.chat/apps-engine/definition/uikit/livechat/UIKitLivechatIncomingInteractionType'; +import type { UIKitLivechatBlockInteractionContext as _UIKitLivechatBlockInteractionContext } from '@rocket.chat/apps-engine/definition/uikit/livechat/UIKitLivechatInteractionContext'; +import type { Defined } from 'jsonrpc-lite'; +import { JsonRpcError } from 'jsonrpc-lite'; + +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import { AppAccessorsInstance } from '../../lib/accessors/mod'; +import type { RequestContext } from '../../lib/requestContext'; +import { wrapAppForRequest } from '../../lib/wrapAppForRequest'; +import { isOneOf } from '../lib/assertions'; + +export const uikitInteractions = [ + 'executeBlockActionHandler', + 'executeViewSubmitHandler', + 'executeViewClosedHandler', + 'executeActionButtonHandler', + 'executeLivechatBlockActionHandler', +] as const; + +export const { + UIKitBlockInteractionContext, + UIKitViewSubmitInteractionContext, + UIKitViewCloseInteractionContext, + UIKitActionButtonInteractionContext, +} = require('@rocket.chat/apps-engine/definition/uikit/UIKitInteractionContext.js') as { + UIKitBlockInteractionContext: typeof _UIKitBlockInteractionContext; + UIKitViewSubmitInteractionContext: typeof _UIKitViewSubmitInteractionContext; + UIKitViewCloseInteractionContext: typeof _UIKitViewCloseInteractionContext; + UIKitActionButtonInteractionContext: typeof _UIKitActionButtonInteractionContext; +}; + +export const { UIKitLivechatBlockInteractionContext } = + require('@rocket.chat/apps-engine/definition/uikit/livechat/UIKitLivechatInteractionContext.js') as { + UIKitLivechatBlockInteractionContext: typeof _UIKitLivechatBlockInteractionContext; + }; + +export default async function handleUIKitInteraction(request: RequestContext): Promise { + const { method: reqMethod, params } = request; + const [, method] = reqMethod.split(':'); + + if (!isOneOf(method, uikitInteractions)) { + return JsonRpcError.methodNotFound(null); + } + + if (!Array.isArray(params)) { + return JsonRpcError.invalidParams(null); + } + + const app = AppObjectRegistry.get('app'); + + const interactionHandler = app?.[method as keyof App] as unknown; + + if (!app || typeof interactionHandler !== 'function') { + return JsonRpcError.methodNotFound({ + message: `App does not implement method "${method}"`, + }); + } + + const [payload] = params as [Record]; + + if (!payload) { + return JsonRpcError.invalidParams(null); + } + + let context; + + switch (method) { + case 'executeBlockActionHandler': + context = new UIKitBlockInteractionContext(payload as unknown as IUIKitBlockIncomingInteraction); + break; + case 'executeViewSubmitHandler': + context = new UIKitViewSubmitInteractionContext(payload as unknown as IUIKitViewSubmitIncomingInteraction); + break; + case 'executeViewClosedHandler': + context = new UIKitViewCloseInteractionContext(payload as unknown as IUIKitViewCloseIncomingInteraction); + break; + case 'executeActionButtonHandler': + context = new UIKitActionButtonInteractionContext(payload as unknown as IUIKitActionButtonIncomingInteraction); + break; + case 'executeLivechatBlockActionHandler': + context = new UIKitLivechatBlockInteractionContext(payload as unknown as IUIKitLivechatBlockIncomingInteraction); + break; + } + + try { + return await interactionHandler.call( + wrapAppForRequest(app, request), + context, + AppAccessorsInstance.getReader(), + AppAccessorsInstance.getHttp(), + AppAccessorsInstance.getPersistence(), + AppAccessorsInstance.getModifier(), + ); + } catch (e) { + return JsonRpcError.internalError({ message: e.message }); + } +} diff --git a/packages/apps/node-runtime/src/handlers/videoconference-handler.ts b/packages/apps/node-runtime/src/handlers/videoconference-handler.ts new file mode 100644 index 0000000000000..68ebef7b4c9d8 --- /dev/null +++ b/packages/apps/node-runtime/src/handlers/videoconference-handler.ts @@ -0,0 +1,53 @@ +import type { IVideoConfProvider } from '@rocket.chat/apps-engine/definition/videoConfProviders/IVideoConfProvider'; +import type { Defined } from 'jsonrpc-lite'; +import { JsonRpcError } from 'jsonrpc-lite'; + +import { AppObjectRegistry } from '../AppObjectRegistry'; +import { AppAccessorsInstance } from '../lib/accessors/mod'; +import type { RequestContext } from '../lib/requestContext'; +import { wrapComposedApp } from '../lib/wrapAppForRequest'; + +export default async function videoConferenceHandler(request: RequestContext): Promise { + const { method: call, params } = request; + const { logger } = request.context; + + const [, providerName, methodName] = call.split(':'); + + const provider = AppObjectRegistry.get(`videoConfProvider:${providerName}`); + + if (!provider) { + return new JsonRpcError(`Provider ${providerName} not found`, -32000); + } + + const method = provider[methodName as keyof IVideoConfProvider]; + + if (typeof method !== 'function') { + return JsonRpcError.methodNotFound({ + message: `Method ${methodName} not found on provider ${providerName}`, + }); + } + + const [videoconf, user, options] = params as Array; + + logger.debug(`Executing ${methodName} on video conference provider...`); + + const args = [...(videoconf ? [videoconf] : []), ...(user ? [user] : []), ...(options ? [options] : [])]; + + try { + // deno-lint-ignore ban-types + const result = await (method as Function).apply(wrapComposedApp(provider, request), [ + ...args, + AppAccessorsInstance.getReader(), + AppAccessorsInstance.getModifier(), + AppAccessorsInstance.getHttp(), + AppAccessorsInstance.getPersistence(), + ]); + + logger.debug(`Video Conference Provider's ${methodName} was successfully executed.`); + + return result; + } catch (e) { + logger.debug(`Video Conference Provider's ${methodName} was unsuccessful.`); + return new JsonRpcError(e.message, -32000); + } +} diff --git a/packages/apps/node-runtime/src/lib/accessors/builders/BlockBuilder.ts b/packages/apps/node-runtime/src/lib/accessors/builders/BlockBuilder.ts new file mode 100644 index 0000000000000..2af8fd4570e6d --- /dev/null +++ b/packages/apps/node-runtime/src/lib/accessors/builders/BlockBuilder.ts @@ -0,0 +1,20 @@ +import type { BlockBuilder as _AppsEngineBlockBuilder } from '@rocket.chat/apps-engine/definition/uikit/blocks/BlockBuilder'; + +import { AppObjectRegistry } from '../../../AppObjectRegistry'; + +const { BlockBuilder: AppsEngineBlockBuilder } = require('@rocket.chat/apps-engine/definition/uikit/blocks/BlockBuilder.js') as { + BlockBuilder: typeof _AppsEngineBlockBuilder; +}; + +/** + * Local BlockBuilder that extends the apps-engine BlockBuilder. + * It overrides the constructor to source the appId from the registry + * instead of requiring it as a constructor argument. + * + * @deprecated please prefer the rocket.chat/ui-kit components + */ +export class BlockBuilder extends AppsEngineBlockBuilder { + constructor() { + super(String(AppObjectRegistry.get('id') ?? '')); + } +} diff --git a/packages/apps/node-runtime/src/lib/accessors/builders/DiscussionBuilder.ts b/packages/apps/node-runtime/src/lib/accessors/builders/DiscussionBuilder.ts new file mode 100644 index 0000000000000..385418ab63e86 --- /dev/null +++ b/packages/apps/node-runtime/src/lib/accessors/builders/DiscussionBuilder.ts @@ -0,0 +1,56 @@ +import type { IDiscussionBuilder as _IDiscussionBuilder } from '@rocket.chat/apps-engine/definition/accessors/IDiscussionBuilder'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage'; +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; +import type { RoomType as _RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType'; + +import { RoomBuilder } from './RoomBuilder'; + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +const { RoomType } = require('@rocket.chat/apps-engine/definition/rooms/RoomType.js') as { RoomType: typeof _RoomType }; + +export type IDiscussionBuilder = _IDiscussionBuilder; + +export class DiscussionBuilder extends RoomBuilder implements IDiscussionBuilder { + public declare kind: _RocketChatAssociationModel.DISCUSSION; + + private reply?: string; + + private parentMessage?: IMessage; + + constructor(data?: Partial) { + super(data); + this.kind = RocketChatAssociationModel.DISCUSSION; + this.room.type = RoomType.PRIVATE_GROUP; + } + + public setParentRoom(parentRoom: IRoom): IDiscussionBuilder { + this.room.parentRoom = parentRoom; + return this; + } + + public getParentRoom(): IRoom { + return this.room.parentRoom!; + } + + public setReply(reply: string): IDiscussionBuilder { + this.reply = reply; + return this; + } + + public getReply(): string { + return this.reply!; + } + + public setParentMessage(parentMessage: IMessage): IDiscussionBuilder { + this.parentMessage = parentMessage; + return this; + } + + public getParentMessage(): IMessage { + return this.parentMessage!; + } +} diff --git a/packages/apps/node-runtime/src/lib/accessors/builders/LivechatMessageBuilder.ts b/packages/apps/node-runtime/src/lib/accessors/builders/LivechatMessageBuilder.ts new file mode 100644 index 0000000000000..f2938e76bc6b5 --- /dev/null +++ b/packages/apps/node-runtime/src/lib/accessors/builders/LivechatMessageBuilder.ts @@ -0,0 +1,202 @@ +import type { ILivechatMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/ILivechatMessageBuilder'; +import type { IMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/IMessageBuilder'; +import type { ILivechatMessage as EngineLivechatMessage } from '@rocket.chat/apps-engine/definition/livechat/ILivechatMessage'; +import type { IVisitor } from '@rocket.chat/apps-engine/definition/livechat/IVisitor'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage'; +import type { IMessageAttachment } from '@rocket.chat/apps-engine/definition/messages/IMessageAttachment'; +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; +import type { RoomType as _RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser'; + +import { MessageBuilder } from './MessageBuilder'; + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +const { RoomType } = require('@rocket.chat/apps-engine/definition/rooms/RoomType.js') as { RoomType: typeof _RoomType }; + +export interface ILivechatMessage extends EngineLivechatMessage, IMessage {} + +export class LivechatMessageBuilder implements ILivechatMessageBuilder { + public kind: _RocketChatAssociationModel.LIVECHAT_MESSAGE; + + private msg: ILivechatMessage; + + constructor(message?: ILivechatMessage) { + this.kind = RocketChatAssociationModel.LIVECHAT_MESSAGE; + this.msg = message || ({} as ILivechatMessage); + } + + public setData(data: ILivechatMessage): ILivechatMessageBuilder { + delete data.id; + this.msg = data; + + return this; + } + + public setRoom(room: IRoom): ILivechatMessageBuilder { + this.msg.room = room; + return this; + } + + public getRoom(): IRoom { + return this.msg.room; + } + + public setSender(sender: IUser): ILivechatMessageBuilder { + this.msg.sender = sender; + delete this.msg.visitor; + + return this; + } + + public getSender(): IUser { + return this.msg.sender; + } + + public setText(text: string): ILivechatMessageBuilder { + this.msg.text = text; + return this; + } + + public getText(): string { + return this.msg.text!; + } + + public setEmojiAvatar(emoji: string): ILivechatMessageBuilder { + this.msg.emoji = emoji; + return this; + } + + public getEmojiAvatar(): string { + return this.msg.emoji!; + } + + public setAvatarUrl(avatarUrl: string): ILivechatMessageBuilder { + this.msg.avatarUrl = avatarUrl; + return this; + } + + public getAvatarUrl(): string { + return this.msg.avatarUrl!; + } + + public setUsernameAlias(alias: string): ILivechatMessageBuilder { + this.msg.alias = alias; + return this; + } + + public getUsernameAlias(): string { + return this.msg.alias!; + } + + public addAttachment(attachment: IMessageAttachment): ILivechatMessageBuilder { + if (!this.msg.attachments) { + this.msg.attachments = []; + } + + this.msg.attachments.push(attachment); + return this; + } + + public setAttachments(attachments: Array): ILivechatMessageBuilder { + this.msg.attachments = attachments; + return this; + } + + public getAttachments(): Array { + return this.msg.attachments!; + } + + public replaceAttachment(position: number, attachment: IMessageAttachment): ILivechatMessageBuilder { + if (!this.msg.attachments) { + this.msg.attachments = []; + } + + if (!this.msg.attachments[position]) { + throw new Error(`No attachment found at the index of "${position}" to replace.`); + } + + this.msg.attachments[position] = attachment; + return this; + } + + public removeAttachment(position: number): ILivechatMessageBuilder { + if (!this.msg.attachments) { + this.msg.attachments = []; + } + + if (!this.msg.attachments[position]) { + throw new Error(`No attachment found at the index of "${position}" to remove.`); + } + + this.msg.attachments.splice(position, 1); + + return this; + } + + public setEditor(user: IUser): ILivechatMessageBuilder { + this.msg.editor = user; + return this; + } + + public getEditor(): IUser { + return this.msg.editor; + } + + public setGroupable(groupable: boolean): ILivechatMessageBuilder { + this.msg.groupable = groupable; + return this; + } + + public getGroupable(): boolean { + return this.msg.groupable!; + } + + public setParseUrls(parseUrls: boolean): ILivechatMessageBuilder { + this.msg.parseUrls = parseUrls; + return this; + } + + public getParseUrls(): boolean { + return this.msg.parseUrls!; + } + + public setToken(token: string): ILivechatMessageBuilder { + this.msg.token = token; + return this; + } + + public getToken(): string { + return this.msg.token!; + } + + public setVisitor(visitor: IVisitor): ILivechatMessageBuilder { + this.msg.visitor = visitor; + delete this.msg.sender; + + return this; + } + + public getVisitor(): IVisitor { + return this.msg.visitor; + } + + public getMessage(): ILivechatMessage { + if (!this.msg.room) { + throw new Error('The "room" property is required.'); + } + + if (this.msg.room.type !== RoomType.LIVE_CHAT) { + throw new Error('The room is not a Livechat room'); + } + + return this.msg; + } + + public getMessageBuilder(): IMessageBuilder { + return new MessageBuilder(this.msg as IMessage); + } +} diff --git a/packages/apps/node-runtime/src/lib/accessors/builders/MessageBuilder.ts b/packages/apps/node-runtime/src/lib/accessors/builders/MessageBuilder.ts new file mode 100644 index 0000000000000..4e204a70b0289 --- /dev/null +++ b/packages/apps/node-runtime/src/lib/accessors/builders/MessageBuilder.ts @@ -0,0 +1,271 @@ +import type { IMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/IMessageBuilder'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage'; +import type { IMessageAttachment } from '@rocket.chat/apps-engine/definition/messages/IMessageAttachment'; +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; +import type { IBlock } from '@rocket.chat/apps-engine/definition/uikit/blocks/Blocks'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser'; +import type { LayoutBlock } from '@rocket.chat/ui-kit'; + +import { BlockBuilder } from './BlockBuilder'; + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +export class MessageBuilder implements IMessageBuilder { + public kind: _RocketChatAssociationModel.MESSAGE; + + private msg: IMessage; + + private changes: Partial = {}; + + private attachmentsChanged = false; + + private customFieldsChanged = false; + + constructor(message?: IMessage) { + this.kind = RocketChatAssociationModel.MESSAGE; + this.msg = message || ({} as IMessage); + } + + public setData(data: IMessage): IMessageBuilder { + delete data.id; + this.msg = data; + + return this as IMessageBuilder; + } + + public setUpdateData(data: IMessage, editor: IUser): IMessageBuilder { + this.msg = data; + this.msg.editor = editor; + this.msg.editedAt = new Date(); + + this.changes = structuredClone(this.msg); + + return this as IMessageBuilder; + } + + public setThreadId(threadId: string): IMessageBuilder { + this.msg.threadId = threadId; + this.changes.threadId = threadId; + + return this as IMessageBuilder; + } + + public getThreadId(): string { + return this.msg.threadId!; + } + + public setRoom(room: IRoom): IMessageBuilder { + this.msg.room = room; + this.changes.room = room; + + return this as IMessageBuilder; + } + + public getRoom(): IRoom { + return this.msg.room; + } + + public setSender(sender: IUser): IMessageBuilder { + this.msg.sender = sender; + this.changes.sender = sender; + + return this as IMessageBuilder; + } + + public getSender(): IUser { + return this.msg.sender; + } + + public setText(text: string): IMessageBuilder { + this.msg.text = text; + this.changes.text = text; + + return this as IMessageBuilder; + } + + public getText(): string { + return this.msg.text!; + } + + public setEmojiAvatar(emoji: string): IMessageBuilder { + this.msg.emoji = emoji; + this.changes.emoji = emoji; + + return this as IMessageBuilder; + } + + public getEmojiAvatar(): string { + return this.msg.emoji!; + } + + public setAvatarUrl(avatarUrl: string): IMessageBuilder { + this.msg.avatarUrl = avatarUrl; + this.changes.avatarUrl = avatarUrl; + + return this as IMessageBuilder; + } + + public getAvatarUrl(): string { + return this.msg.avatarUrl!; + } + + public setUsernameAlias(alias: string): IMessageBuilder { + this.msg.alias = alias; + this.changes.alias = alias; + + return this as IMessageBuilder; + } + + public getUsernameAlias(): string { + return this.msg.alias!; + } + + public addAttachment(attachment: IMessageAttachment): IMessageBuilder { + if (!this.msg.attachments) { + this.msg.attachments = []; + } + + this.msg.attachments.push(attachment); + this.attachmentsChanged = true; + + return this as IMessageBuilder; + } + + public setAttachments(attachments: Array): IMessageBuilder { + this.msg.attachments = attachments; + this.attachmentsChanged = true; + + return this as IMessageBuilder; + } + + public getAttachments(): Array { + return this.msg.attachments!; + } + + public replaceAttachment(position: number, attachment: IMessageAttachment): IMessageBuilder { + if (!this.msg.attachments?.[position]) { + throw new Error(`No attachment found at the index of "${position}" to replace.`); + } + + this.msg.attachments[position] = attachment; + this.attachmentsChanged = true; + + return this as IMessageBuilder; + } + + public removeAttachment(position: number): IMessageBuilder { + if (!this.msg.attachments?.[position]) { + throw new Error(`No attachment found at the index of "${position}" to remove.`); + } + + this.msg.attachments.splice(position, 1); + this.attachmentsChanged = true; + + return this as IMessageBuilder; + } + + public setEditor(user: IUser): IMessageBuilder { + this.msg.editor = user; + this.changes.editor = user; + + return this as IMessageBuilder; + } + + public getEditor(): IUser { + return this.msg.editor; + } + + public setGroupable(groupable: boolean): IMessageBuilder { + this.msg.groupable = groupable; + this.changes.groupable = groupable; + + return this as IMessageBuilder; + } + + public getGroupable(): boolean { + return this.msg.groupable!; + } + + public setParseUrls(parseUrls: boolean): IMessageBuilder { + this.msg.parseUrls = parseUrls; + this.changes.parseUrls = parseUrls; + + return this as IMessageBuilder; + } + + public getParseUrls(): boolean { + return this.msg.parseUrls!; + } + + public getMessage(): IMessage { + if (!this.msg.room) { + throw new Error('The "room" property is required.'); + } + + return this.msg; + } + + public addBlocks(blocks: BlockBuilder | Array) { + if (!Array.isArray(this.msg.blocks)) { + this.msg.blocks = []; + } + + if (blocks instanceof BlockBuilder) { + this.msg.blocks.push(...blocks.getBlocks()); + } else { + this.msg.blocks.push(...blocks); + } + + return this as IMessageBuilder; + } + + public setBlocks(blocks: BlockBuilder | Array) { + const blockArray: Array = blocks instanceof BlockBuilder ? blocks.getBlocks() : blocks; + + this.msg.blocks = blockArray; + this.changes.blocks = blockArray; + + return this as IMessageBuilder; + } + + public getBlocks() { + return this.msg.blocks!; + } + + public addCustomField(key: string, value: unknown): IMessageBuilder { + if (!this.msg.customFields) { + this.msg.customFields = {}; + } + + if (this.msg.customFields[key]) { + throw new Error(`The message already contains a custom field by the key: ${key}`); + } + + if (key.includes('.')) { + throw new Error(`The given key contains a period, which is not allowed. Key: ${key}`); + } + + this.msg.customFields[key] = value; + + this.customFieldsChanged = true; + + return this as IMessageBuilder; + } + + public getChanges(): Partial { + const changes: typeof this.changes = structuredClone(this.changes); + + if (this.attachmentsChanged) { + changes.attachments = structuredClone(this.msg.attachments); + } + + if (this.customFieldsChanged) { + changes.customFields = structuredClone(this.msg.customFields); + } + + return changes; + } +} diff --git a/packages/apps/node-runtime/src/lib/accessors/builders/RoomBuilder.ts b/packages/apps/node-runtime/src/lib/accessors/builders/RoomBuilder.ts new file mode 100644 index 0000000000000..8f6a428eb8231 --- /dev/null +++ b/packages/apps/node-runtime/src/lib/accessors/builders/RoomBuilder.ts @@ -0,0 +1,196 @@ +import type { IRoomBuilder } from '@rocket.chat/apps-engine/definition/accessors/IRoomBuilder'; +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; +import type { RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser'; + + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +export class RoomBuilder implements IRoomBuilder { + public kind: _RocketChatAssociationModel.ROOM | _RocketChatAssociationModel.DISCUSSION; + + protected room: IRoom; + + private members: Array; + + private changes: Partial = {}; + + private customFieldsChanged = false; + + constructor(data?: Partial) { + this.kind = RocketChatAssociationModel.ROOM; + this.room = (data || { customFields: {} }) as IRoom; + this.members = []; + } + + public setData(data: Partial): IRoomBuilder { + delete data.id; + this.room = data as IRoom; + + this.changes = structuredClone(this.room); + + return this; + } + + public setDisplayName(name: string): IRoomBuilder { + this.room.displayName = name; + this.changes.displayName = name; + + return this; + } + + public getDisplayName(): string { + return this.room.displayName!; + } + + public setSlugifiedName(name: string): IRoomBuilder { + this.room.slugifiedName = name; + this.changes.slugifiedName = name; + + return this; + } + + public getSlugifiedName(): string { + return this.room.slugifiedName; + } + + public setType(type: RoomType): IRoomBuilder { + this.room.type = type; + this.changes.type = type; + + return this; + } + + public getType(): RoomType { + return this.room.type; + } + + public setCreator(creator: IUser): IRoomBuilder { + this.room.creator = creator; + this.changes.creator = creator; + + return this; + } + + public getCreator(): IUser { + return this.room.creator; + } + + /** + * @deprecated + */ + public addUsername(username: string): IRoomBuilder { + this.addMemberToBeAddedByUsername(username); + return this; + } + + /** + * @deprecated + */ + public setUsernames(usernames: Array): IRoomBuilder { + this.setMembersToBeAddedByUsernames(usernames); + return this; + } + + /** + * @deprecated + */ + public getUsernames(): Array { + const usernames = this.getMembersToBeAddedUsernames(); + if (usernames && usernames.length > 0) { + return usernames; + } + return this.room.usernames || []; + } + + public addMemberToBeAddedByUsername(username: string): IRoomBuilder { + this.members.push(username); + return this; + } + + public setMembersToBeAddedByUsernames(usernames: Array): IRoomBuilder { + this.members = usernames; + return this; + } + + public getMembersToBeAddedUsernames(): Array { + return this.members; + } + + public setDefault(isDefault: boolean): IRoomBuilder { + this.room.isDefault = isDefault; + this.changes.isDefault = isDefault; + + return this; + } + + public getIsDefault(): boolean { + return this.room.isDefault!; + } + + public setReadOnly(isReadOnly: boolean): IRoomBuilder { + this.room.isReadOnly = isReadOnly; + this.changes.isReadOnly = isReadOnly; + + return this; + } + + public getIsReadOnly(): boolean { + return this.room.isReadOnly!; + } + + public setDisplayingOfSystemMessages(displaySystemMessages: boolean): IRoomBuilder { + this.room.displaySystemMessages = displaySystemMessages; + this.changes.displaySystemMessages = displaySystemMessages; + + return this; + } + + public getDisplayingOfSystemMessages(): boolean { + return this.room.displaySystemMessages!; + } + + public addCustomField(key: string, value: object): IRoomBuilder { + if (typeof this.room.customFields !== 'object') { + this.room.customFields = {}; + } + + this.room.customFields[key] = value; + + this.customFieldsChanged = true; + + return this; + } + + public setCustomFields(fields: { [key: string]: object }): IRoomBuilder { + this.room.customFields = fields; + this.customFieldsChanged = true; + + return this; + } + + public getCustomFields(): { [key: string]: object } { + return this.room.customFields!; + } + + public getUserIds(): Array { + return this.room.userIds!; + } + + public getRoom(): IRoom { + return this.room; + } + + public getChanges() { + const changes: Partial = structuredClone(this.changes); + + if (this.customFieldsChanged) { + changes.customFields = structuredClone(this.room.customFields); + } + + return changes; + } +} diff --git a/packages/apps/node-runtime/src/lib/accessors/builders/UserBuilder.ts b/packages/apps/node-runtime/src/lib/accessors/builders/UserBuilder.ts new file mode 100644 index 0000000000000..1c516fa83f880 --- /dev/null +++ b/packages/apps/node-runtime/src/lib/accessors/builders/UserBuilder.ts @@ -0,0 +1,80 @@ +import type { IUserBuilder } from '@rocket.chat/apps-engine/definition/accessors/IUserBuilder'; +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser'; +import type { IUserEmail } from '@rocket.chat/apps-engine/definition/users/IUserEmail'; +import type { IUserSettings } from '@rocket.chat/apps-engine/definition/users/IUserSettings'; + + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +export class UserBuilder implements IUserBuilder { + public kind: _RocketChatAssociationModel.USER; + + private user: Partial; + + constructor(user?: Partial) { + this.kind = RocketChatAssociationModel.USER; + this.user = user || ({} as Partial); + } + + public setData(data: Partial): IUserBuilder { + delete data.id; + this.user = data; + + return this; + } + + public setEmails(emails: Array): IUserBuilder { + this.user.emails = emails; + return this; + } + + public getEmails(): Array { + return this.user.emails!; + } + + public setDisplayName(name: string): IUserBuilder { + this.user.name = name; + return this; + } + + public getDisplayName(): string { + return this.user.name!; + } + + public setUsername(username: string): IUserBuilder { + this.user.username = username; + return this; + } + + public getUsername(): string { + return this.user.username!; + } + + public setRoles(roles: Array): IUserBuilder { + this.user.roles = roles; + return this; + } + + public getRoles(): Array { + return this.user.roles!; + } + + public getSettings(): Partial { + return this.user.settings; + } + + public getUser(): Partial { + if (!this.user.username) { + throw new Error('The "username" property is required.'); + } + + if (!this.user.name) { + throw new Error('The "name" property is required.'); + } + + return this.user; + } +} diff --git a/packages/apps/node-runtime/src/lib/accessors/builders/VideoConferenceBuilder.ts b/packages/apps/node-runtime/src/lib/accessors/builders/VideoConferenceBuilder.ts new file mode 100644 index 0000000000000..fb9a4001ba2c9 --- /dev/null +++ b/packages/apps/node-runtime/src/lib/accessors/builders/VideoConferenceBuilder.ts @@ -0,0 +1,92 @@ +import type { IVideoConferenceBuilder } from '@rocket.chat/apps-engine/definition/accessors/IVideoConferenceBuilder'; +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; +import type { IGroupVideoConference } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConference'; + + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +export type AppVideoConference = Pick & { + createdBy: IGroupVideoConference['createdBy']['_id']; +}; + +export class VideoConferenceBuilder implements IVideoConferenceBuilder { + public kind: _RocketChatAssociationModel.VIDEO_CONFERENCE = RocketChatAssociationModel.VIDEO_CONFERENCE; + + protected call: AppVideoConference; + + constructor(data?: Partial) { + this.call = (data || {}) as AppVideoConference; + } + + public setData(data: Partial): IVideoConferenceBuilder { + this.call = { + rid: data.rid!, + createdBy: data.createdBy, + providerName: data.providerName!, + title: data.title!, + discussionRid: data.discussionRid, + }; + + return this; + } + + public setRoomId(rid: string): IVideoConferenceBuilder { + this.call.rid = rid; + return this; + } + + public getRoomId(): string { + return this.call.rid; + } + + public setCreatedBy(userId: string): IVideoConferenceBuilder { + this.call.createdBy = userId; + return this; + } + + public getCreatedBy(): string { + return this.call.createdBy; + } + + public setProviderName(userId: string): IVideoConferenceBuilder { + this.call.providerName = userId; + return this; + } + + public getProviderName(): string { + return this.call.providerName; + } + + public setProviderData(data: Record | undefined): IVideoConferenceBuilder { + this.call.providerData = data; + return this; + } + + public getProviderData(): Record { + return this.call.providerData!; + } + + public setTitle(userId: string): IVideoConferenceBuilder { + this.call.title = userId; + return this; + } + + public getTitle(): string { + return this.call.title; + } + + public setDiscussionRid(rid: AppVideoConference['discussionRid']): IVideoConferenceBuilder { + this.call.discussionRid = rid; + return this; + } + + public getDiscussionRid(): AppVideoConference['discussionRid'] { + return this.call.discussionRid; + } + + public getVideoConference(): AppVideoConference { + return this.call; + } +} diff --git a/packages/apps/node-runtime/src/lib/accessors/extenders/HttpExtender.ts b/packages/apps/node-runtime/src/lib/accessors/extenders/HttpExtender.ts new file mode 100644 index 0000000000000..03a39ff1fedc2 --- /dev/null +++ b/packages/apps/node-runtime/src/lib/accessors/extenders/HttpExtender.ts @@ -0,0 +1,58 @@ +import type { IHttpExtend, IHttpPreRequestHandler, IHttpPreResponseHandler } from '@rocket.chat/apps-engine/definition/accessors/IHttp'; + +export class HttpExtend implements IHttpExtend { + private headers: Map; + + private params: Map; + + private requests: Array; + + private responses: Array; + + constructor() { + this.headers = new Map(); + this.params = new Map(); + this.requests = []; + this.responses = []; + } + + public provideDefaultHeader(key: string, value: string): void { + this.headers.set(key, value); + } + + public provideDefaultHeaders(headers: { [key: string]: string }): void { + Object.keys(headers).forEach((key) => this.headers.set(key, headers[key])); + } + + public provideDefaultParam(key: string, value: string): void { + this.params.set(key, value); + } + + public provideDefaultParams(params: { [key: string]: string }): void { + Object.keys(params).forEach((key) => this.params.set(key, params[key])); + } + + public providePreRequestHandler(handler: IHttpPreRequestHandler): void { + this.requests.push(handler); + } + + public providePreResponseHandler(handler: IHttpPreResponseHandler): void { + this.responses.push(handler); + } + + public getDefaultHeaders(): Map { + return new Map(this.headers); + } + + public getDefaultParams(): Map { + return new Map(this.params); + } + + public getPreRequestHandlers(): Array { + return Array.from(this.requests); + } + + public getPreResponseHandlers(): Array { + return Array.from(this.responses); + } +} diff --git a/packages/apps/node-runtime/src/lib/accessors/extenders/MessageExtender.ts b/packages/apps/node-runtime/src/lib/accessors/extenders/MessageExtender.ts new file mode 100644 index 0000000000000..b274804d170de --- /dev/null +++ b/packages/apps/node-runtime/src/lib/accessors/extenders/MessageExtender.ts @@ -0,0 +1,65 @@ +import type { IMessageExtender } from '@rocket.chat/apps-engine/definition/accessors/IMessageExtender'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage'; +import type { IMessageAttachment } from '@rocket.chat/apps-engine/definition/messages/IMessageAttachment'; +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; + + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +export class MessageExtender implements IMessageExtender { + public readonly kind: _RocketChatAssociationModel.MESSAGE; + + constructor(private msg: IMessage) { + this.kind = RocketChatAssociationModel.MESSAGE; + + if (!Array.isArray(msg.attachments)) { + this.msg.attachments = []; + } + } + + public addCustomField(key: string, value: unknown): IMessageExtender { + if (!this.msg.customFields) { + this.msg.customFields = {}; + } + + if (this.msg.customFields[key]) { + throw new Error(`The message already contains a custom field by the key: ${key}`); + } + + if (key.includes('.')) { + throw new Error(`The given key contains a period, which is not allowed. Key: ${key}`); + } + + this.msg.customFields[key] = value; + + return this; + } + + public addAttachment(attachment: IMessageAttachment): IMessageExtender { + this.ensureAttachment(); + + this.msg.attachments!.push(attachment); + + return this; + } + + public addAttachments(attachments: Array): IMessageExtender { + this.ensureAttachment(); + + this.msg.attachments = this.msg.attachments!.concat(attachments); + + return this; + } + + public getMessage(): IMessage { + return structuredClone(this.msg); + } + + private ensureAttachment(): void { + if (!Array.isArray(this.msg.attachments)) { + this.msg.attachments = []; + } + } +} diff --git a/packages/apps/node-runtime/src/lib/accessors/extenders/RoomExtender.ts b/packages/apps/node-runtime/src/lib/accessors/extenders/RoomExtender.ts new file mode 100644 index 0000000000000..fb23a27e8d991 --- /dev/null +++ b/packages/apps/node-runtime/src/lib/accessors/extenders/RoomExtender.ts @@ -0,0 +1,60 @@ +import type { IRoomExtender } from '@rocket.chat/apps-engine/definition/accessors/IRoomExtender'; +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser'; + + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +export class RoomExtender implements IRoomExtender { + public kind: _RocketChatAssociationModel.ROOM; + + private members: Array; + + constructor(private room: IRoom) { + this.kind = RocketChatAssociationModel.ROOM; + this.members = []; + } + + public addCustomField(key: string, value: unknown): IRoomExtender { + if (!this.room.customFields) { + this.room.customFields = {}; + } + + if (this.room.customFields[key]) { + throw new Error(`The room already contains a custom field by the key: ${key}`); + } + + if (key.includes('.')) { + throw new Error(`The given key contains a period, which is not allowed. Key: ${key}`); + } + + this.room.customFields[key] = value; + + return this; + } + + public addMember(user: IUser): IRoomExtender { + if (this.members.find((u) => u.username === user.username)) { + throw new Error('The user is already in the room.'); + } + + this.members.push(user); + + return this; + } + + public getMembersBeingAdded(): Array { + return this.members; + } + + public getUsernamesOfMembersBeingAdded(): Array { + return this.members.map((u) => u.username); + } + + public getRoom(): IRoom { + return structuredClone(this.room); + } +} diff --git a/packages/apps/node-runtime/src/lib/accessors/extenders/VideoConferenceExtend.ts b/packages/apps/node-runtime/src/lib/accessors/extenders/VideoConferenceExtend.ts new file mode 100644 index 0000000000000..7faea6e2a2655 --- /dev/null +++ b/packages/apps/node-runtime/src/lib/accessors/extenders/VideoConferenceExtend.ts @@ -0,0 +1,68 @@ +import type { IVideoConferenceExtender } from '@rocket.chat/apps-engine/definition/accessors/IVideoConferenceExtend'; +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; +import type { VideoConference, VideoConferenceMember } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConference'; +import type { IVideoConferenceUser } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConferenceUser'; + + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +export class VideoConferenceExtender implements IVideoConferenceExtender { + public kind: _RocketChatAssociationModel.VIDEO_CONFERENCE; + + constructor(private videoConference: VideoConference) { + this.kind = RocketChatAssociationModel.VIDEO_CONFERENCE; + } + + public setProviderData(value: Record): IVideoConferenceExtender { + this.videoConference.providerData = value; + + return this; + } + + public setStatus(value: VideoConference['status']): IVideoConferenceExtender { + this.videoConference.status = value; + + return this; + } + + public setEndedBy(value: IVideoConferenceUser['_id']): IVideoConferenceExtender { + this.videoConference.endedBy = { + _id: value, + // Name and username will be loaded automatically by the bridge + username: '', + name: '', + }; + + return this; + } + + public setEndedAt(value: VideoConference['endedAt']): IVideoConferenceExtender { + this.videoConference.endedAt = value; + + return this; + } + + public addUser(userId: VideoConferenceMember['_id'], ts?: VideoConferenceMember['ts']): IVideoConferenceExtender { + this.videoConference.users.push({ + _id: userId, + ts, + // Name and username will be loaded automatically by the bridge + username: '', + name: '', + }); + + return this; + } + + public setDiscussionRid(rid: VideoConference['discussionRid']): IVideoConferenceExtender { + this.videoConference.discussionRid = rid; + + return this; + } + + public getVideoConference(): VideoConference { + return structuredClone(this.videoConference); + } +} diff --git a/packages/apps/node-runtime/src/lib/accessors/formatResponseErrorHandler.ts b/packages/apps/node-runtime/src/lib/accessors/formatResponseErrorHandler.ts new file mode 100644 index 0000000000000..6840c3ab5baa3 --- /dev/null +++ b/packages/apps/node-runtime/src/lib/accessors/formatResponseErrorHandler.ts @@ -0,0 +1,14 @@ +import { ErrorObject } from 'jsonrpc-lite'; + +// deno-lint-ignore no-explicit-any -- that is the type we get from `catch` +export const formatErrorResponse = (error: any): Error => { + if (error instanceof ErrorObject || typeof error?.error?.message === 'string') { + return new Error(error.error.message); + } + + if (error instanceof Error) { + return error; + } + + return new Error('An unknown error occurred', { cause: error }); +}; diff --git a/packages/apps/node-runtime/src/lib/accessors/http.ts b/packages/apps/node-runtime/src/lib/accessors/http.ts new file mode 100644 index 0000000000000..70b16e5250720 --- /dev/null +++ b/packages/apps/node-runtime/src/lib/accessors/http.ts @@ -0,0 +1,95 @@ +import type { IHttp, IHttpExtend, IHttpRequest, IHttpResponse } from '@rocket.chat/apps-engine/definition/accessors/IHttp'; +import type { IPersistence } from '@rocket.chat/apps-engine/definition/accessors/IPersistence'; +import type { IRead } from '@rocket.chat/apps-engine/definition/accessors/IRead'; + +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import type * as Messenger from '../messenger'; +import { formatErrorResponse } from './formatResponseErrorHandler'; + +type RequestMethod = 'get' | 'post' | 'put' | 'head' | 'delete' | 'patch'; + +export class Http implements IHttp { + private httpExtender: IHttpExtend; + + private read: IRead; + + private persistence: IPersistence; + + private senderFn: typeof Messenger.sendRequest; + + constructor(read: IRead, persistence: IPersistence, httpExtender: IHttpExtend, senderFn: typeof Messenger.sendRequest) { + this.read = read; + this.persistence = persistence; + this.httpExtender = httpExtender; + this.senderFn = senderFn; + // this.httpExtender = new HttpExtend(); + } + + public get(url: string, options?: IHttpRequest): Promise { + return this._processHandler(url, 'get', options); + } + + public put(url: string, options?: IHttpRequest): Promise { + return this._processHandler(url, 'put', options); + } + + public post(url: string, options?: IHttpRequest): Promise { + return this._processHandler(url, 'post', options); + } + + public del(url: string, options?: IHttpRequest): Promise { + return this._processHandler(url, 'delete', options); + } + + public patch(url: string, options?: IHttpRequest): Promise { + return this._processHandler(url, 'patch', options); + } + + private async _processHandler(url: string, method: RequestMethod, options?: IHttpRequest): Promise { + let request = options || {}; + + if (typeof request.headers === 'undefined') { + request.headers = {}; + } + + this.httpExtender.getDefaultHeaders().forEach((value: string, key: string) => { + if (typeof request.headers?.[key] !== 'string') { + request.headers![key] = value; + } + }); + + if (typeof request.params === 'undefined') { + request.params = {}; + } + + this.httpExtender.getDefaultParams().forEach((value: string, key: string) => { + if (typeof request.params?.[key] !== 'string') { + request.params![key] = value; + } + }); + + for (const handler of this.httpExtender.getPreRequestHandlers()) { + request = await handler.executePreHttpRequest(url, request, this.read, this.persistence); + } + + let { result: response } = await this.senderFn({ + method: `bridges:getHttpBridge:doCall`, + params: [ + { + appId: AppObjectRegistry.get('id'), + method, + url, + request, + }, + ], + }).catch((error) => { + throw formatErrorResponse(error); + }); + + for (const handler of this.httpExtender.getPreResponseHandlers()) { + response = await handler.executePreHttpResponse(response as IHttpResponse, this.read, this.persistence); + } + + return response as IHttpResponse; + } +} diff --git a/packages/apps/node-runtime/src/lib/accessors/mod.ts b/packages/apps/node-runtime/src/lib/accessors/mod.ts new file mode 100644 index 0000000000000..acd1df5a2a489 --- /dev/null +++ b/packages/apps/node-runtime/src/lib/accessors/mod.ts @@ -0,0 +1,356 @@ +import type { IApiExtend } from '@rocket.chat/apps-engine/definition/accessors/IApiExtend'; +import type { IAppAccessors } from '@rocket.chat/apps-engine/definition/accessors/IAppAccessors'; +import type { IConfigurationExtend } from '@rocket.chat/apps-engine/definition/accessors/IConfigurationExtend'; +import type { IConfigurationModify } from '@rocket.chat/apps-engine/definition/accessors/IConfigurationModify'; +import type { IEnvironmentRead } from '@rocket.chat/apps-engine/definition/accessors/IEnvironmentRead'; +import type { IEnvironmentWrite } from '@rocket.chat/apps-engine/definition/accessors/IEnvironmentWrite'; +import type { IHttp, IHttpExtend } from '@rocket.chat/apps-engine/definition/accessors/IHttp'; +import type { IModify } from '@rocket.chat/apps-engine/definition/accessors/IModify'; +import type { INotifier } from '@rocket.chat/apps-engine/definition/accessors/INotifier'; +import type { IOutboundCommunicationProviderExtend } from '@rocket.chat/apps-engine/definition/accessors/IOutboundCommunicationProviderExtend'; +import type { IPersistence } from '@rocket.chat/apps-engine/definition/accessors/IPersistence'; +import type { IRead } from '@rocket.chat/apps-engine/definition/accessors/IRead'; +import type { ISchedulerExtend } from '@rocket.chat/apps-engine/definition/accessors/ISchedulerExtend'; +import type { ISlashCommandsExtend } from '@rocket.chat/apps-engine/definition/accessors/ISlashCommandsExtend'; +import type { ISlashCommandsModify } from '@rocket.chat/apps-engine/definition/accessors/ISlashCommandsModify'; +import type { IVideoConfProvidersExtend } from '@rocket.chat/apps-engine/definition/accessors/IVideoConfProvidersExtend'; +import type { IApi } from '@rocket.chat/apps-engine/definition/api/IApi'; +import type { IApiEndpointMetadata } from '@rocket.chat/apps-engine/definition/api/IApiEndpointMetadata'; +import type { + IOutboundPhoneMessageProvider, + IOutboundEmailMessageProvider, +} from '@rocket.chat/apps-engine/definition/outboundCommunication/IOutboundCommsProvider'; +import type { IProcessor } from '@rocket.chat/apps-engine/definition/scheduler/IProcessor'; +import type { ISlashCommand } from '@rocket.chat/apps-engine/definition/slashcommands/ISlashCommand'; +import type { IVideoConfProvider } from '@rocket.chat/apps-engine/definition/videoConfProviders/IVideoConfProvider'; + +import { HttpExtend } from './extenders/HttpExtender'; +import { formatErrorResponse } from './formatResponseErrorHandler'; +import { Http } from './http'; +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import * as Messenger from '../messenger'; +import { ModifyCreator } from './modify/ModifyCreator'; +import { ModifyExtender } from './modify/ModifyExtender'; +import { ModifyUpdater } from './modify/ModifyUpdater'; +import { Notifier } from './notifier'; + +/** Helper: extends T with an internal _proxy property used for delegation. */ +type WithProxy = T & { _proxy: T }; + +const httpMethods = ['get', 'post', 'put', 'delete', 'head', 'options', 'patch'] as const; + +// We need to create this object first thing, as we'll handle references to it later on +if (!AppObjectRegistry.has('apiEndpoints')) { + AppObjectRegistry.set('apiEndpoints', []); +} + +export class AppAccessors { + private defaultAppAccessors?: IAppAccessors; + + private environmentRead?: IEnvironmentRead; + + private environmentWriter?: IEnvironmentWrite; + + private configModifier?: IConfigurationModify; + + private configExtender?: IConfigurationExtend; + + private reader?: IRead; + + private modifier?: IModify; + + private persistence?: IPersistence; + + private creator?: ModifyCreator; + + private updater?: ModifyUpdater; + + private extender?: ModifyExtender; + + private httpExtend: IHttpExtend = new HttpExtend(); + + private http?: IHttp; + + private notifier?: INotifier; + + private proxify: (namespace: string, overrides?: Record unknown>) => T; + + constructor(private readonly senderFn: typeof Messenger.sendRequest) { + this.proxify = (namespace: string, overrides: Record unknown> = {}): T => + new Proxy( + { __kind: `accessor:${namespace}` }, + { + get: + (_target: unknown, prop: string) => + (...params: unknown[]) => { + // We don't want to send a request for this prop + if (prop === 'toJSON') { + return {}; + } + + // If the prop is inteded to be overriden by the caller + if (prop in overrides) { + return overrides[prop].apply(undefined, params); + } + + return senderFn({ + method: `accessor:${namespace}:${prop}`, + params, + }) + .then((response) => response.result) + .catch((err) => { + throw formatErrorResponse(err); + }); + }, + }, + ) as T; + + this.http = new Http(this.getReader(), this.getPersistence(), this.httpExtend, this.getSenderFn()); + this.notifier = new Notifier(this.getSenderFn()); + } + + public getSenderFn() { + return this.senderFn; + } + + public getEnvironmentRead(): IEnvironmentRead { + if (!this.environmentRead) { + this.environmentRead = { + getSettings: () => this.proxify('getEnvironmentRead:getSettings'), + getServerSettings: () => this.proxify('getEnvironmentRead:getServerSettings'), + getEnvironmentVariables: () => this.proxify('getEnvironmentRead:getEnvironmentVariables'), + }; + } + + return this.environmentRead; + } + + public getEnvironmentWrite() { + if (!this.environmentWriter) { + this.environmentWriter = { + getSettings: () => this.proxify('getEnvironmentWrite:getSettings'), + getServerSettings: () => this.proxify('getEnvironmentWrite:getServerSettings'), + }; + } + + return this.environmentWriter; + } + + public getConfigurationModify() { + if (!this.configModifier) { + const slashCommandsModify: WithProxy = { + _proxy: this.proxify('getConfigurationModify:slashCommands'), + modifySlashCommand(slashcommand: ISlashCommand) { + // Store the slashcommand instance to use when the Apps-Engine calls the slashcommand + AppObjectRegistry.set(`slashcommand:${slashcommand.command}`, slashcommand); + + return this._proxy.modifySlashCommand(slashcommand); + }, + disableSlashCommand(command: string) { + return this._proxy.disableSlashCommand(command); + }, + enableSlashCommand(command: string) { + return this._proxy.enableSlashCommand(command); + }, + }; + + this.configModifier = { + scheduler: this.proxify('getConfigurationModify:scheduler'), + slashCommands: slashCommandsModify, + serverSettings: this.proxify('getConfigurationModify:serverSettings'), + }; + } + + return this.configModifier; + } + + public getConfigurationExtend() { + if (!this.configExtender) { + const { senderFn } = this; + + const apiExtend: WithProxy = { + _proxy: this.proxify('getConfigurationExtend:api'), + async provideApi(api: IApi) { + const apiEndpoints = AppObjectRegistry.get('apiEndpoints')!; + + api.endpoints.forEach((endpoint) => { + endpoint._availableMethods = httpMethods.filter((method) => typeof endpoint[method] === 'function'); + + // We need to keep a reference to the endpoint around for us to call the executor later + AppObjectRegistry.set(`api:${endpoint.path}`, endpoint); + }); + + const result = await this._proxy.provideApi(api); + + // Let's call the listApis method to cache the info from the endpoints + // Also, since this is a side-effect, we do it async so we can return to the caller + senderFn({ method: 'accessor:api:listApis' }) + .then((response) => apiEndpoints.push(...(response.result as IApiEndpointMetadata[]))) + .catch((err) => err.error); + + return result; + }, + }; + + const schedulerExtend: WithProxy = { + _proxy: this.proxify('getConfigurationExtend:scheduler'), + registerProcessors(processors: IProcessor[]) { + // Store the processor instance to use when the Apps-Engine calls the processor + processors.forEach((processor) => { + AppObjectRegistry.set(`scheduler:${processor.id}`, processor); + }); + + return this._proxy.registerProcessors(processors); + }, + }; + + const videoConfProviders: WithProxy = { + _proxy: this.proxify('getConfigurationExtend:videoConfProviders'), + provideVideoConfProvider(provider: IVideoConfProvider) { + // Store the videoConfProvider instance to use when the Apps-Engine calls the videoConfProvider + AppObjectRegistry.set(`videoConfProvider:${provider.name}`, provider); + + return this._proxy.provideVideoConfProvider(provider); + }, + }; + + const outboundCommunication: WithProxy = { + _proxy: this.proxify('getConfigurationExtend:outboundCommunication'), + registerEmailProvider(provider: IOutboundEmailMessageProvider) { + AppObjectRegistry.set(`outboundCommunication:${provider.name}-${provider.type}`, provider); + return this._proxy.registerEmailProvider(provider); + }, + registerPhoneProvider(provider: IOutboundPhoneMessageProvider) { + AppObjectRegistry.set(`outboundCommunication:${provider.name}-${provider.type}`, provider); + return this._proxy.registerPhoneProvider(provider); + }, + }; + + const slashCommandsExtend: WithProxy = { + _proxy: this.proxify('getConfigurationExtend:slashCommands'), + provideSlashCommand(slashcommand: ISlashCommand) { + // Store the slashcommand instance to use when the Apps-Engine calls the slashcommand + AppObjectRegistry.set(`slashcommand:${slashcommand.command}`, slashcommand); + + return this._proxy.provideSlashCommand(slashcommand); + }, + }; + + this.configExtender = { + ui: this.proxify('getConfigurationExtend:ui'), + http: this.httpExtend, + settings: this.proxify('getConfigurationExtend:settings'), + externalComponents: this.proxify('getConfigurationExtend:externalComponents'), + api: apiExtend, + scheduler: schedulerExtend, + videoConfProviders, + outboundCommunication, + slashCommands: slashCommandsExtend, + }; + } + + return this.configExtender; + } + + public getDefaultAppAccessors() { + if (!this.defaultAppAccessors) { + this.defaultAppAccessors = { + environmentReader: this.getEnvironmentRead(), + environmentWriter: this.getEnvironmentWrite(), + reader: this.getReader(), + http: this.getHttp(), + providedApiEndpoints: AppObjectRegistry.get('apiEndpoints') as IApiEndpointMetadata[], + }; + } + + return this.defaultAppAccessors; + } + + public getReader() { + if (!this.reader) { + this.reader = { + getEnvironmentReader: () => ({ + getSettings: () => this.proxify('getReader:getEnvironmentReader:getSettings'), + getServerSettings: () => this.proxify('getReader:getEnvironmentReader:getServerSettings'), + getEnvironmentVariables: () => this.proxify('getReader:getEnvironmentReader:getEnvironmentVariables'), + }), + getMessageReader: () => this.proxify('getReader:getMessageReader'), + getPersistenceReader: () => this.proxify('getReader:getPersistenceReader'), + getRoomReader: () => this.proxify('getReader:getRoomReader'), + getUserReader: () => this.proxify('getReader:getUserReader'), + getNotifier: () => this.getNotifier(), + getLivechatReader: () => this.proxify('getReader:getLivechatReader'), + getUploadReader: () => this.proxify('getReader:getUploadReader'), + getCloudWorkspaceReader: () => this.proxify('getReader:getCloudWorkspaceReader'), + getVideoConferenceReader: () => this.proxify('getReader:getVideoConferenceReader'), + getOAuthAppsReader: () => this.proxify('getReader:getOAuthAppsReader'), + getThreadReader: () => this.proxify('getReader:getThreadReader'), + getRoleReader: () => this.proxify('getReader:getRoleReader'), + getContactReader: () => this.proxify('getReader:getContactReader'), + getExperimentalReader: () => this.proxify('getReader:getExperimentalReader'), + }; + } + + return this.reader; + } + + public getModifier() { + if (!this.modifier) { + this.modifier = { + getCreator: this.getCreator.bind(this), + getUpdater: this.getUpdater.bind(this), + getExtender: this.getExtender.bind(this), + getDeleter: () => this.proxify('getModifier:getDeleter'), + getNotifier: () => this.getNotifier(), + getUiController: () => this.proxify('getModifier:getUiController'), + getScheduler: () => this.proxify('getModifier:getScheduler'), + getOAuthAppsModifier: () => this.proxify('getModifier:getOAuthAppsModifier'), + getModerationModifier: () => this.proxify('getModifier:getModerationModifier'), + }; + } + + return this.modifier; + } + + public getPersistence() { + if (!this.persistence) { + this.persistence = this.proxify('getPersistence'); + } + + return this.persistence; + } + + public getHttp() { + return this.http; + } + + private getCreator() { + if (!this.creator) { + this.creator = new ModifyCreator(this.senderFn); + } + + return this.creator; + } + + private getUpdater() { + if (!this.updater) { + this.updater = new ModifyUpdater(this.senderFn); + } + + return this.updater; + } + + private getExtender() { + if (!this.extender) { + this.extender = new ModifyExtender(this.senderFn); + } + + return this.extender; + } + + private getNotifier() { + return this.notifier; + } +} + +export const AppAccessorsInstance = new AppAccessors(Messenger.sendRequest); diff --git a/packages/apps/node-runtime/src/lib/accessors/modify/ModifyCreator.ts b/packages/apps/node-runtime/src/lib/accessors/modify/ModifyCreator.ts new file mode 100644 index 0000000000000..8b877419accea --- /dev/null +++ b/packages/apps/node-runtime/src/lib/accessors/modify/ModifyCreator.ts @@ -0,0 +1,385 @@ +import { randomBytes } from 'node:crypto'; + +import { UIHelper } from '@rocket.chat/apps/dist/server/misc/UIHelper'; +import type { IContactCreator } from '@rocket.chat/apps-engine/definition/accessors/IContactCreator'; +import type { IEmailCreator } from '@rocket.chat/apps-engine/definition/accessors/IEmailCreator'; +import type { ILivechatCreator } from '@rocket.chat/apps-engine/definition/accessors/ILivechatCreator'; +import type { ILivechatMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/ILivechatMessageBuilder'; +import type { IMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/IMessageBuilder'; +import type { IModifyCreator } from '@rocket.chat/apps-engine/definition/accessors/IModifyCreator'; +import type { IRoomBuilder } from '@rocket.chat/apps-engine/definition/accessors/IRoomBuilder'; +import type { IUploadCreator } from '@rocket.chat/apps-engine/definition/accessors/IUploadCreator'; +import type { IUserBuilder } from '@rocket.chat/apps-engine/definition/accessors/IUserBuilder'; +import type { IVideoConferenceBuilder } from '@rocket.chat/apps-engine/definition/accessors/IVideoConferenceBuilder'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage'; +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; +import type { RoomType as _RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType'; +import type { IBotUser } from '@rocket.chat/apps-engine/definition/users/IBotUser'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser'; +import type { UserType as _UserType } from '@rocket.chat/apps-engine/definition/users/UserType'; + +import { AppObjectRegistry } from '../../../AppObjectRegistry'; +import type * as Messenger from '../../messenger'; +import { BlockBuilder } from '../builders/BlockBuilder'; +import type { IDiscussionBuilder } from '../builders/DiscussionBuilder'; +import { DiscussionBuilder } from '../builders/DiscussionBuilder'; +import type { ILivechatMessage } from '../builders/LivechatMessageBuilder'; +import { LivechatMessageBuilder } from '../builders/LivechatMessageBuilder'; +import { MessageBuilder } from '../builders/MessageBuilder'; +import { RoomBuilder } from '../builders/RoomBuilder'; +import { UserBuilder } from '../builders/UserBuilder'; +import type { AppVideoConference } from '../builders/VideoConferenceBuilder'; +import { VideoConferenceBuilder } from '../builders/VideoConferenceBuilder'; +import { formatErrorResponse } from '../formatResponseErrorHandler'; + +const { RoomType } = require('@rocket.chat/apps-engine/definition/rooms/RoomType.js') as { RoomType: typeof _RoomType }; +const { UserType } = require('@rocket.chat/apps-engine/definition/users/UserType.js') as { UserType: typeof _UserType }; +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +export class ModifyCreator implements IModifyCreator { + constructor(private readonly senderFn: typeof Messenger.sendRequest) {} + + getLivechatCreator(): ILivechatCreator { + return new Proxy( + { __kind: 'getLivechatCreator' }, + { + get: (_target: unknown, prop: string) => { + // It's not worthwhile to make an asynchronous request for such a simple method + if (prop === 'createToken') { + return () => randomBytes(16).toString('hex'); + } + + if (prop === 'toJSON') { + return () => ({}); + } + + return (...params: unknown[]) => + this.senderFn({ + method: `accessor:getModifier:getCreator:getLivechatCreator:${prop}`, + params, + }) + .then((response) => response.result) + .catch((err) => { + throw formatErrorResponse(err); + }); + }, + }, + ) as ILivechatCreator; + } + + getUploadCreator(): IUploadCreator { + return new Proxy( + { __kind: 'getUploadCreator' }, + { + get: + (_target: unknown, prop: string) => + (...params: unknown[]) => + prop === 'toJSON' + ? {} + : this.senderFn({ + method: `accessor:getModifier:getCreator:getUploadCreator:${prop}`, + params, + }) + .then((response) => response.result) + .catch((err) => { + throw formatErrorResponse(err); + }), + }, + ) as IUploadCreator; + } + + getEmailCreator(): IEmailCreator { + return new Proxy( + { __kind: 'getEmailCreator' }, + { + get: + (_target: unknown, prop: string) => + (...params: unknown[]) => + prop === 'toJSON' + ? {} + : this.senderFn({ + method: `accessor:getModifier:getCreator:getEmailCreator:${prop}`, + params, + }) + .then((response) => response.result) + .catch((err) => { + throw formatErrorResponse(err); + }), + }, + ) as IEmailCreator; + } + + getContactCreator(): IContactCreator { + return new Proxy( + { __kind: 'getContactCreator' }, + { + get: + (_target: unknown, prop: string) => + (...params: unknown[]) => + prop === 'toJSON' + ? {} + : this.senderFn({ + method: `accessor:getModifier:getCreator:getContactCreator:${prop}`, + params, + }) + .then((response) => response.result) + .catch((err) => { + throw formatErrorResponse(err); + }), + }, + ) as IContactCreator; + } + + getBlockBuilder() { + return new BlockBuilder(); + } + + startMessage(data?: IMessage) { + if (data) { + delete data.id; + } + + return new MessageBuilder(data); + } + + startLivechatMessage(data?: ILivechatMessage) { + if (data) { + delete data.id; + } + + return new LivechatMessageBuilder(data); + } + + startRoom(data?: IRoom) { + if (data) { + // @ts-ignore - this has been imported from the Apps-Engine + delete data.id; + } + + return new RoomBuilder(data); + } + + startDiscussion(data?: Partial) { + if (data) { + delete data.id; + } + + return new DiscussionBuilder(data); + } + + startVideoConference(data?: Partial) { + return new VideoConferenceBuilder(data); + } + + startBotUser(data?: Partial) { + if (data) { + delete data.id; + + const { roles } = data; + + if (roles?.length) { + const hasRole = roles + .map((role: string) => role.toLocaleLowerCase()) + .some((role: string) => role === 'admin' || role === 'owner' || role === 'moderator'); + + if (hasRole) { + throw new Error('Invalid role assigned to the user. Should not be admin, owner or moderator.'); + } + } + + if (!data.type) { + data.type = UserType.BOT; + } + } + + return new UserBuilder(data); + } + + public finish( + builder: IMessageBuilder | ILivechatMessageBuilder | IRoomBuilder | IDiscussionBuilder | IVideoConferenceBuilder | IUserBuilder, + ): Promise { + switch (builder.kind) { + case RocketChatAssociationModel.MESSAGE: + return this._finishMessage(builder as IMessageBuilder); + case RocketChatAssociationModel.LIVECHAT_MESSAGE: + return this._finishLivechatMessage(builder as ILivechatMessageBuilder); + case RocketChatAssociationModel.ROOM: + return this._finishRoom(builder as IRoomBuilder); + case RocketChatAssociationModel.DISCUSSION: + return this._finishDiscussion(builder as IDiscussionBuilder); + case RocketChatAssociationModel.VIDEO_CONFERENCE: + return this._finishVideoConference(builder as IVideoConferenceBuilder); + case RocketChatAssociationModel.USER: + return this._finishUser(builder as IUserBuilder); + default: + throw new Error('Invalid builder passed to the ModifyCreator.finish function.'); + } + } + + private async _finishMessage(builder: IMessageBuilder): Promise { + const result = builder.getMessage(); + delete result.id; + + if (!result.sender?.id) { + const response = await this.senderFn({ + method: 'bridges:getUserBridge:doGetAppUser', + params: ['APP_ID'], + }).catch((err) => { + throw formatErrorResponse(err); + }); + + const appUser = response.result; + + if (!appUser) { + throw new Error('Invalid sender assigned to the message.'); + } + + result.sender = appUser as IUser; + } + + if (result.blocks?.length) { + // Can we move this elsewhere? This AppObjectRegistry usage doesn't really belong here, but where? + result.blocks = UIHelper.assignIds(result.blocks, AppObjectRegistry.get('id') || ''); + } + + const response = await this.senderFn({ + method: 'bridges:getMessageBridge:doCreate', + params: [result, AppObjectRegistry.get('id')], + }).catch((err) => { + throw formatErrorResponse(err); + }); + + return String(response.result); + } + + private async _finishLivechatMessage(builder: ILivechatMessageBuilder): Promise { + if (builder.getSender() && !builder.getVisitor()) { + return this._finishMessage(builder.getMessageBuilder()); + } + + const result = builder.getMessage(); + delete result.id; + + if (!result.token && !result.visitor?.token) { + throw new Error('Invalid visitor sending the message'); + } + + result.token = result.visitor ? result.visitor.token : result.token; + + const response = await this.senderFn({ + method: 'bridges:getLivechatBridge:doCreateMessage', + params: [result, AppObjectRegistry.get('id')], + }).catch((err) => { + throw formatErrorResponse(err); + }); + + return String(response.result); + } + + private async _finishRoom(builder: IRoomBuilder): Promise { + const result = builder.getRoom(); + delete result.id; + + if (!result.type) { + throw new Error('Invalid type assigned to the room.'); + } + + if (result.type !== RoomType.LIVE_CHAT) { + if (!result.creator?.id) { + throw new Error('Invalid creator assigned to the room.'); + } + } + + if (result.type !== RoomType.DIRECT_MESSAGE) { + if (result.type !== RoomType.LIVE_CHAT) { + if (!result.slugifiedName?.trim()) { + throw new Error('Invalid slugifiedName assigned to the room.'); + } + } + + if (!result.displayName?.trim()) { + throw new Error('Invalid displayName assigned to the room.'); + } + } + + const response = await this.senderFn({ + method: 'bridges:getRoomBridge:doCreate', + params: [result, builder.getMembersToBeAddedUsernames(), AppObjectRegistry.get('id')], + }).catch((err) => { + throw formatErrorResponse(err); + }); + + return String(response.result); + } + + private async _finishDiscussion(builder: IDiscussionBuilder): Promise { + const room = builder.getRoom(); + delete room.id; + + if (!room.creator?.id) { + throw new Error('Invalid creator assigned to the discussion.'); + } + + if (!room.slugifiedName?.trim()) { + throw new Error('Invalid slugifiedName assigned to the discussion.'); + } + + if (!room.displayName?.trim()) { + throw new Error('Invalid displayName assigned to the discussion.'); + } + + if (!room.parentRoom?.id) { + throw new Error('Invalid parentRoom assigned to the discussion.'); + } + + const response = await this.senderFn({ + method: 'bridges:getRoomBridge:doCreateDiscussion', + params: [room, builder.getParentMessage(), builder.getReply(), builder.getMembersToBeAddedUsernames(), AppObjectRegistry.get('id')], + }).catch((err) => { + throw formatErrorResponse(err); + }); + + return String(response.result); + } + + private async _finishVideoConference(builder: IVideoConferenceBuilder): Promise { + const videoConference = builder.getVideoConference(); + + if (!videoConference.createdBy) { + throw new Error('Invalid creator assigned to the video conference.'); + } + + if (!videoConference.providerName?.trim()) { + throw new Error('Invalid provider name assigned to the video conference.'); + } + + if (!videoConference.rid) { + throw new Error('Invalid roomId assigned to the video conference.'); + } + + const response = await this.senderFn({ + method: 'bridges:getVideoConferenceBridge:doCreate', + params: [videoConference, AppObjectRegistry.get('id')], + }).catch((err) => { + throw formatErrorResponse(err); + }); + + return String(response.result); + } + + private async _finishUser(builder: IUserBuilder): Promise { + const user = builder.getUser(); + + const response = await this.senderFn({ + method: 'bridges:getUserBridge:doCreate', + params: [user, AppObjectRegistry.get('id')], + }).catch((err) => { + throw formatErrorResponse(err); + }); + + return String(response.result); + } +} diff --git a/packages/apps/node-runtime/src/lib/accessors/modify/ModifyExtender.ts b/packages/apps/node-runtime/src/lib/accessors/modify/ModifyExtender.ts new file mode 100644 index 0000000000000..0f94deacbd626 --- /dev/null +++ b/packages/apps/node-runtime/src/lib/accessors/modify/ModifyExtender.ts @@ -0,0 +1,105 @@ +import type { IMessageExtender } from '@rocket.chat/apps-engine/definition/accessors/IMessageExtender'; +import type { IModifyExtender } from '@rocket.chat/apps-engine/definition/accessors/IModifyExtender'; +import type { IRoomExtender } from '@rocket.chat/apps-engine/definition/accessors/IRoomExtender'; +import type { IVideoConferenceExtender } from '@rocket.chat/apps-engine/definition/accessors/IVideoConferenceExtend'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage'; +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser'; +import type { VideoConference } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConference'; + +import { AppObjectRegistry } from '../../../AppObjectRegistry'; +import type * as Messenger from '../../messenger'; +import { MessageExtender } from '../extenders/MessageExtender'; +import { RoomExtender } from '../extenders/RoomExtender'; +import { VideoConferenceExtender } from '../extenders/VideoConferenceExtend'; +import { formatErrorResponse } from '../formatResponseErrorHandler'; + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +export class ModifyExtender implements IModifyExtender { + constructor(private readonly senderFn: typeof Messenger.sendRequest) {} + + public async extendMessage(messageId: string, updater: IUser): Promise { + const result = await this.senderFn({ + method: 'bridges:getMessageBridge:doGetById', + params: [messageId, AppObjectRegistry.get('id')], + }).catch((err) => { + throw formatErrorResponse(err); + }); + + const msg = result.result as IMessage; + + msg.editor = updater; + msg.editedAt = new Date(); + + return new MessageExtender(msg); + } + + public async extendRoom(roomId: string, _updater: IUser): Promise { + const result = await this.senderFn({ + method: 'bridges:getRoomBridge:doGetById', + params: [roomId, AppObjectRegistry.get('id')], + }).catch((err) => { + throw formatErrorResponse(err); + }); + + const room = result.result as IRoom; + + room.updatedAt = new Date(); + + return new RoomExtender(room); + } + + public async extendVideoConference(id: string): Promise { + const result = await this.senderFn({ + method: 'bridges:getVideoConferenceBridge:doGetById', + params: [id, AppObjectRegistry.get('id')], + }).catch((err) => { + throw formatErrorResponse(err); + }); + + const call = result.result as VideoConference; + + call._updatedAt = new Date(); + + return new VideoConferenceExtender(call); + } + + public async finish(extender: IMessageExtender | IRoomExtender | IVideoConferenceExtender): Promise { + switch (extender.kind) { + case RocketChatAssociationModel.MESSAGE: + await this.senderFn({ + method: 'bridges:getMessageBridge:doUpdate', + params: [(extender as IMessageExtender).getMessage(), AppObjectRegistry.get('id')], + }).catch((err) => { + throw formatErrorResponse(err); + }); + break; + case RocketChatAssociationModel.ROOM: + await this.senderFn({ + method: 'bridges:getRoomBridge:doUpdate', + params: [ + (extender as IRoomExtender).getRoom(), + (extender as IRoomExtender).getUsernamesOfMembersBeingAdded(), + AppObjectRegistry.get('id'), + ], + }).catch((err) => { + throw formatErrorResponse(err); + }); + break; + case RocketChatAssociationModel.VIDEO_CONFERENCE: + await this.senderFn({ + method: 'bridges:getVideoConferenceBridge:doUpdate', + params: [(extender as IVideoConferenceExtender).getVideoConference(), AppObjectRegistry.get('id')], + }).catch((err) => { + throw formatErrorResponse(err); + }); + break; + default: + throw new Error('Invalid extender passed to the ModifyExtender.finish function.'); + } + } +} diff --git a/packages/apps/node-runtime/src/lib/accessors/modify/ModifyUpdater.ts b/packages/apps/node-runtime/src/lib/accessors/modify/ModifyUpdater.ts new file mode 100644 index 0000000000000..cb20e061189ba --- /dev/null +++ b/packages/apps/node-runtime/src/lib/accessors/modify/ModifyUpdater.ts @@ -0,0 +1,169 @@ +import { UIHelper } from '@rocket.chat/apps/dist/server/misc/UIHelper'; +import type { ILivechatUpdater } from '@rocket.chat/apps-engine/definition/accessors/ILivechatUpdater'; +import type { IMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/IMessageBuilder'; +import type { IMessageUpdater } from '@rocket.chat/apps-engine/definition/accessors/IMessageUpdater'; +import type { IModifyUpdater } from '@rocket.chat/apps-engine/definition/accessors/IModifyUpdater'; +import type { IRoomBuilder } from '@rocket.chat/apps-engine/definition/accessors/IRoomBuilder'; +import type { IUserUpdater } from '@rocket.chat/apps-engine/definition/accessors/IUserUpdater'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage'; +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; +import type { RoomType as _RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser'; + +import { AppObjectRegistry } from '../../../AppObjectRegistry'; +import type * as Messenger from '../../messenger'; +import { MessageBuilder } from '../builders/MessageBuilder'; +import { RoomBuilder } from '../builders/RoomBuilder'; +import { formatErrorResponse } from '../formatResponseErrorHandler'; + +const { RoomType } = require('@rocket.chat/apps-engine/definition/rooms/RoomType.js') as { RoomType: typeof _RoomType }; +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +export class ModifyUpdater implements IModifyUpdater { + private readonly livechatUpdater: ILivechatUpdater; + + private readonly userUpdater: IUserUpdater; + + private readonly messageUpdater: IMessageUpdater; + + constructor(private readonly senderFn: typeof Messenger.sendRequest) { + this.livechatUpdater = this.proxify('getLivechatUpdater'); + this.userUpdater = this.proxify('getUserUpdater'); + this.messageUpdater = this.proxify('getMessageUpdater'); + } + + private proxify( + target: 'getLivechatUpdater' | 'getUserUpdater' | 'getMessageUpdater', + ): T { + return new Proxy( + { __kind: target }, + { + get: + (_target: unknown, prop: string) => + (...params: unknown[]) => + prop === 'toJSON' + ? {} + : this.senderFn({ + method: `accessor:getModifier:getUpdater:${target}:${prop}`, + params, + }) + .then((response) => response.result) + .catch((err) => { + throw formatErrorResponse(err); + }), + }, + ) as T; + } + + public getLivechatUpdater(): ILivechatUpdater { + return this.livechatUpdater; + } + + public getUserUpdater(): IUserUpdater { + return this.userUpdater; + } + + public getMessageUpdater(): IMessageUpdater { + return this.messageUpdater; + } + + public async message(messageId: string, editor: IUser): Promise { + const response = await this.senderFn({ + method: 'bridges:getMessageBridge:doGetById', + params: [messageId, AppObjectRegistry.get('id')], + }).catch((err) => { + throw formatErrorResponse(err); + }); + + const builder = new MessageBuilder(response.result as IMessage); + + builder.setEditor(editor); + + return builder; + } + + public async room(roomId: string, _updater: IUser): Promise { + const response = await this.senderFn({ + method: 'bridges:getRoomBridge:doGetById', + params: [roomId, AppObjectRegistry.get('id')], + }).catch((err) => { + throw formatErrorResponse(err); + }); + + return new RoomBuilder(response.result as IRoom); + } + + public finish(builder: IMessageBuilder | IRoomBuilder): Promise { + switch (builder.kind) { + case RocketChatAssociationModel.MESSAGE: + return this._finishMessage(builder as MessageBuilder); + case RocketChatAssociationModel.ROOM: + return this._finishRoom(builder as RoomBuilder); + default: + throw new Error('Invalid builder passed to the ModifyUpdater.finish function.'); + } + } + + private async _finishMessage(builder: MessageBuilder): Promise { + const result = builder.getMessage(); + + if (!result.id) { + throw new Error("Invalid message, can't update a message without an id."); + } + + if (!result.sender?.id) { + throw new Error('Invalid sender assigned to the message.'); + } + + if (result.blocks?.length) { + result.blocks = UIHelper.assignIds(result.blocks, AppObjectRegistry.get('id') || ''); + } + + const changes = { id: result.id, ...builder.getChanges() }; + + await this.senderFn({ + method: 'bridges:getMessageBridge:doUpdate', + params: [changes, AppObjectRegistry.get('id')], + }).catch((err) => { + throw formatErrorResponse(err); + }); + } + + private async _finishRoom(builder: RoomBuilder): Promise { + const room = builder.getRoom(); + + if (!room.id) { + throw new Error("Invalid room, can't update a room without an id."); + } + + if (!room.type) { + throw new Error('Invalid type assigned to the room.'); + } + + if (room.type !== RoomType.LIVE_CHAT) { + if (!room.creator?.id) { + throw new Error('Invalid creator assigned to the room.'); + } + + if (!room.slugifiedName?.trim()) { + throw new Error('Invalid slugifiedName assigned to the room.'); + } + } + + if (!room.displayName?.trim()) { + throw new Error('Invalid displayName assigned to the room.'); + } + + const changes = { id: room.id, ...builder.getChanges() }; + + await this.senderFn({ + method: 'bridges:getRoomBridge:doUpdate', + params: [changes, builder.getMembersToBeAddedUsernames(), AppObjectRegistry.get('id')], + }).catch((err) => { + throw formatErrorResponse(err); + }); + } +} diff --git a/packages/apps/node-runtime/src/lib/accessors/notifier.ts b/packages/apps/node-runtime/src/lib/accessors/notifier.ts new file mode 100644 index 0000000000000..a53f01ea1ebdf --- /dev/null +++ b/packages/apps/node-runtime/src/lib/accessors/notifier.ts @@ -0,0 +1,83 @@ +import type { IMessageBuilder, INotifier } from '@rocket.chat/apps-engine/definition/accessors'; +import type { ITypingOptions, TypingScope as _TypingScope } from '@rocket.chat/apps-engine/definition/accessors/INotifier'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users'; + +import { MessageBuilder } from './builders/MessageBuilder'; +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import type * as Messenger from '../messenger'; +import { formatErrorResponse } from './formatResponseErrorHandler'; + +const { TypingScope } = require('@rocket.chat/apps-engine/definition/accessors/INotifier.js') as { + TypingScope: typeof _TypingScope; +}; + +export class Notifier implements INotifier { + private senderFn: typeof Messenger.sendRequest; + + constructor(senderFn: typeof Messenger.sendRequest) { + this.senderFn = senderFn; + } + + public async notifyUser(user: IUser, message: IMessage): Promise { + if (!message.sender?.id) { + const appUser = await this.getAppUser(); + + message.sender = appUser; + } + + await this.callMessageBridge('doNotifyUser', [user, message, AppObjectRegistry.get('id')]); + } + + public async notifyRoom(room: IRoom, message: IMessage): Promise { + if (!message.sender?.id) { + const appUser = await this.getAppUser(); + + message.sender = appUser; + } + + await this.callMessageBridge('doNotifyRoom', [room, message, AppObjectRegistry.get('id')]); + } + + public async typing(options: ITypingOptions): Promise<() => Promise> { + options.scope = options.scope || TypingScope.Room; + + if (!options.username) { + const appUser = await this.getAppUser(); + options.username = (appUser && appUser.name) || ''; + } + + const appId = AppObjectRegistry.get('id'); + + await this.callMessageBridge('doTyping', [{ ...options, isTyping: true }, appId]); + + return async () => { + await this.callMessageBridge('doTyping', [{ ...options, isTyping: false }, appId]); + }; + } + + public getMessageBuilder(): IMessageBuilder { + return new MessageBuilder(); + } + + private async callMessageBridge(method: string, params: Array): Promise { + await this.senderFn({ + method: `bridges:getMessageBridge:${method}`, + params, + }).catch((err) => { + throw formatErrorResponse(err); + }); + } + + private async getAppUser(): Promise { + const response = await this.senderFn({ + method: 'bridges:getUserBridge:doGetAppUser', + params: [AppObjectRegistry.get('id')], + }).catch((err) => { + throw formatErrorResponse(err); + }); + + return response.result as IUser | undefined; + } +} diff --git a/packages/apps/node-runtime/src/lib/accessors/tests/AppAccessors.test.ts b/packages/apps/node-runtime/src/lib/accessors/tests/AppAccessors.test.ts new file mode 100644 index 0000000000000..e76834c034a0e --- /dev/null +++ b/packages/apps/node-runtime/src/lib/accessors/tests/AppAccessors.test.ts @@ -0,0 +1,122 @@ +import { assertEquals } from 'https://deno.land/std@0.203.0/assert/assert_equals'; +import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd'; + +import { AppObjectRegistry } from '../../../AppObjectRegistry'; +import { AppAccessors } from '../mod'; + +describe('AppAccessors', () => { + let appAccessors: AppAccessors; + const senderFn = (r: object) => + Promise.resolve({ + id: Math.random().toString(36).substring(2), + jsonrpc: '2.0', + result: r, + serialize() { + return JSON.stringify(this); + }, + }); + + beforeEach(() => { + appAccessors = new AppAccessors(senderFn); + AppObjectRegistry.clear(); + }); + + afterAll(() => { + AppObjectRegistry.clear(); + }); + + it('creates the correct format for IRead calls', async () => { + const roomRead = appAccessors.getReader().getRoomReader(); + const room = roomRead.getById('123'); + + assertEquals(room, { + params: ['123'], + method: 'accessor:getReader:getRoomReader:getById', + }); + }); + + it('creates the correct format for IEnvironmentRead calls from IRead', async () => { + const reader = appAccessors.getReader().getEnvironmentReader().getEnvironmentVariables(); + const room = await reader.getValueByName('NODE_ENV'); + + assertEquals(room, { + params: ['NODE_ENV'], + method: 'accessor:getReader:getEnvironmentReader:getEnvironmentVariables:getValueByName', + }); + }); + + it('creates the correct format for IEvironmentRead calls', async () => { + const envRead = appAccessors.getEnvironmentRead(); + const env = await envRead.getServerSettings().getValueById('123'); + + assertEquals(env, { + params: ['123'], + method: 'accessor:getEnvironmentRead:getServerSettings:getValueById', + }); + }); + + it('creates the correct format for IEvironmentWrite calls', async () => { + const envRead = appAccessors.getEnvironmentWrite(); + const env = await envRead.getServerSettings().incrementValue('123', 6); + + assertEquals(env, { + params: ['123', 6], + method: 'accessor:getEnvironmentWrite:getServerSettings:incrementValue', + }); + }); + + it('creates the correct format for IConfigurationModify calls', async () => { + const configModify = appAccessors.getConfigurationModify(); + const command = await configModify.slashCommands.modifySlashCommand({ + command: 'test', + i18nDescription: 'test', + i18nParamsExample: 'test', + providesPreview: true, + }); + + assertEquals(command, { + params: [ + { + command: 'test', + i18nDescription: 'test', + i18nParamsExample: 'test', + providesPreview: true, + }, + ], + method: 'accessor:getConfigurationModify:slashCommands:modifySlashCommand', + }); + }); + + it('correctly stores a reference to a slashcommand object and sends a request via proxy', async () => { + const configExtend = appAccessors.getConfigurationExtend(); + + const slashcommand = { + command: 'test', + i18nDescription: 'test', + i18nParamsExample: 'test', + providesPreview: true, + executor() { + return Promise.resolve(); + }, + }; + + const result = await configExtend.slashCommands.provideSlashCommand(slashcommand); + + assertEquals(AppObjectRegistry.get('slashcommand:test'), slashcommand); + + // The function will not be serialized and sent to the main process + delete result.params[0].executor; + + assertEquals(result, { + method: 'accessor:getConfigurationExtend:slashCommands:provideSlashCommand', + params: [ + { + command: 'test', + i18nDescription: 'test', + i18nParamsExample: 'test', + providesPreview: true, + }, + ], + }); + }); +}); diff --git a/packages/apps/node-runtime/src/lib/accessors/tests/ModifyCreator.test.ts b/packages/apps/node-runtime/src/lib/accessors/tests/ModifyCreator.test.ts new file mode 100644 index 0000000000000..b8728d19f9fd9 --- /dev/null +++ b/packages/apps/node-runtime/src/lib/accessors/tests/ModifyCreator.test.ts @@ -0,0 +1,259 @@ +// deno-lint-ignore-file no-explicit-any +import { assert, assertEquals, assertNotInstanceOf, assertRejects } from 'https://deno.land/std@0.203.0/assert/mod'; +import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd'; +import { assertSpyCall, spy } from 'https://deno.land/std@0.203.0/testing/mock'; + +import { AppObjectRegistry } from '../../../AppObjectRegistry'; +import { ModifyCreator } from '../modify/ModifyCreator'; + +describe('ModifyCreator', () => { + const senderFn = (r: any) => + Promise.resolve({ + id: Math.random().toString(36).substring(2), + jsonrpc: '2.0', + result: r, + serialize() { + return JSON.stringify(this); + }, + }); + + beforeEach(() => { + AppObjectRegistry.clear(); + AppObjectRegistry.set('id', 'deno-test'); + }); + + afterAll(() => { + AppObjectRegistry.clear(); + }); + + it('sends the correct payload in the request to create a message', async () => { + const spying = spy(senderFn); + const modifyCreator = new ModifyCreator(spying); + const messageBuilder = modifyCreator.startMessage(); + + // Importing types from the Apps-Engine is problematic, so we'll go with `any` here + messageBuilder + .setRoom({ id: '123' } as any) + .setSender({ id: '456' } as any) + .setText('Hello World') + .setUsernameAlias('alias') + .setAvatarUrl('https://avatars.com/123'); + + // We can't get a legitimate return value here, so we ignore it + // but we need to know that the request sent was well formed + await modifyCreator.finish(messageBuilder); + + assertSpyCall(spying, 0, { + args: [ + { + method: 'bridges:getMessageBridge:doCreate', + params: [ + { + room: { id: '123' }, + sender: { id: '456' }, + text: 'Hello World', + alias: 'alias', + avatarUrl: 'https://avatars.com/123', + }, + 'deno-test', + ], + }, + ], + }); + }); + + it('sends the correct payload in the request to upload a buffer', async () => { + const modifyCreator = new ModifyCreator(senderFn); + + const result = await modifyCreator.getUploadCreator().uploadBuffer(new Uint8Array([1, 2, 3, 4]), 'text/plain'); + + assertEquals(result, { + method: 'accessor:getModifier:getCreator:getUploadCreator:uploadBuffer', + params: [new Uint8Array([1, 2, 3, 4]), 'text/plain'], + }); + }); + + it('sends the correct payload in the request to create a visitor', async () => { + const modifyCreator = new ModifyCreator(senderFn); + + const result = (await modifyCreator.getLivechatCreator().createVisitor({ + token: 'random token', + username: 'random username for visitor', + name: 'Random Visitor', + })) as any; // We modified the send function so it changed the original return type of the function + + assertEquals(result, { + method: 'accessor:getModifier:getCreator:getLivechatCreator:createVisitor', + params: [ + { + token: 'random token', + username: 'random username for visitor', + name: 'Random Visitor', + }, + ], + }); + }); + + // This test is important because if we return a promise we break API compatibility + it('does not return a promise for calls of the createToken() method of the LivechatCreator', () => { + const modifyCreator = new ModifyCreator(senderFn); + + const result = modifyCreator.getLivechatCreator().createToken(); + + assertNotInstanceOf(result, Promise); + assert(typeof result === 'string', `Expected "${result}" to be of type "string", but got "${typeof result}"`); + }); + + it('throws an error when a proxy method of getLivechatCreator fails', async () => { + const failingSenderFn = () => Promise.reject(new Error('Test error')); + const modifyCreator = new ModifyCreator(failingSenderFn); + const livechatCreator = modifyCreator.getLivechatCreator(); + + await assertRejects( + () => + livechatCreator.createAndReturnVisitor({ + token: 'visitor-token', + username: 'visitor-username', + name: 'Visitor Name', + }), + Error, + 'Test error', + ); + }); + + it('throws an instance of Error when getLivechatCreator fails with a specific error object', async () => { + const failingSenderFn = () => Promise.reject({ error: { message: 'Livechat method error' } }); + const modifyCreator = new ModifyCreator(failingSenderFn); + const livechatCreator = modifyCreator.getLivechatCreator(); + + await assertRejects( + () => + livechatCreator.createVisitor({ + token: 'visitor-token', + username: 'visitor-username', + name: 'Visitor Name', + }), + Error, + 'Livechat method error', + ); + }); + + it('throws a default Error when getLivechatCreator fails with an unknown error object', async () => { + const failingSenderFn = () => Promise.reject({ error: {} }); + const modifyCreator = new ModifyCreator(failingSenderFn); + const livechatCreator = modifyCreator.getLivechatCreator(); + + await assertRejects( + () => + livechatCreator.createVisitor({ + token: 'visitor-token', + username: 'visitor-username', + name: 'Visitor Name', + }), + Error, + 'An unknown error occurred', + ); + }); + + it('throws an error when a proxy method of getUploadCreator fails', async () => { + const failingSenderFn = () => Promise.reject(new Error('Upload error')); + const modifyCreator = new ModifyCreator(failingSenderFn); + const uploadCreator = modifyCreator.getUploadCreator(); + + await assertRejects(() => uploadCreator.uploadBuffer(new Uint8Array([9, 10, 11, 12]), 'image/png'), Error, 'Upload error'); + }); + + it('throws an instance of Error when getUploadCreator fails with a specific error object', async () => { + const failingSenderFn = () => Promise.reject({ error: { message: 'Upload method error' } }); + const modifyCreator = new ModifyCreator(failingSenderFn); + const uploadCreator = modifyCreator.getUploadCreator(); + + await assertRejects(() => uploadCreator.uploadBuffer(new Uint8Array([1, 2, 3]), 'image/png'), Error, 'Upload method error'); + }); + + it('throws a default Error when getUploadCreator fails with an unknown error object', async () => { + const failingSenderFn = () => Promise.reject({ error: {} }); + const modifyCreator = new ModifyCreator(failingSenderFn); + const uploadCreator = modifyCreator.getUploadCreator(); + + await assertRejects(() => uploadCreator.uploadBuffer(new Uint8Array([1, 2, 3]), 'image/png'), Error, 'An unknown error occurred'); + }); + + it('throws an error when a proxy method of getEmailCreator fails', async () => { + const failingSenderFn = () => Promise.reject(new Error('Email error')); + const modifyCreator = new ModifyCreator(failingSenderFn); + const emailCreator = modifyCreator.getEmailCreator(); + + await assertRejects( + () => + emailCreator.send({ + to: 'test@example.com', + from: 'sender@example.com', + subject: 'Test Email', + text: 'This is a test email.', + }), + Error, + 'Email error', + ); + }); + + it('throws an instance of Error when getEmailCreator fails with a specific error object', async () => { + const failingSenderFn = () => Promise.reject({ error: { message: 'Email method error' } }); + const modifyCreator = new ModifyCreator(failingSenderFn); + const emailCreator = modifyCreator.getEmailCreator(); + + await assertRejects( + () => + emailCreator.send({ + to: 'test@example.com', + from: 'sender@example.com', + subject: 'Test Email', + text: 'This is a test email.', + }), + Error, + 'Email method error', + ); + }); + + it('throws a default Error when getEmailCreator fails with an unknown error object', async () => { + const failingSenderFn = () => Promise.reject({ error: {} }); + const modifyCreator = new ModifyCreator(failingSenderFn); + const emailCreator = modifyCreator.getEmailCreator(); + + await assertRejects( + () => + emailCreator.send({ + to: 'test@example.com', + from: 'sender@example.com', + subject: 'Test Email', + text: 'This is a test email.', + }), + Error, + 'An unknown error occurred', + ); + }); + + it('throws an error when a proxy method of getContactCreator fails', async () => { + const failingSenderFn = () => Promise.reject(new Error('Contact creation error')); + const modifyCreator = new ModifyCreator(failingSenderFn); + const contactCreator = modifyCreator.getContactCreator(); + + await assertRejects(() => contactCreator.addContactEmail('test-contact-id', 'test@example.com'), Error, 'Contact creation error'); + }); + + it('throws an instance of Error when getContactCreator fails with a specific error object', async () => { + const failingSenderFn = () => Promise.reject({ error: { message: 'Contact creation error' } }); + const modifyCreator = new ModifyCreator(failingSenderFn); + const contactCreator = modifyCreator.getContactCreator(); + + await assertRejects(() => contactCreator.addContactEmail('test-contact-id', 'test@example.com'), Error, 'Contact creation error'); + }); + + it('throws a default Error when getContactCreator fails with an unknown error object', async () => { + const failingSenderFn = () => Promise.reject({ error: {} }); + const modifyCreator = new ModifyCreator(failingSenderFn); + const contactCreator = modifyCreator.getContactCreator(); + + await assertRejects(() => contactCreator.addContactEmail('test-contact-id', 'test@example.com'), Error, 'An unknown error occurred'); + }); +}); diff --git a/packages/apps/node-runtime/src/lib/accessors/tests/ModifyExtender.test.ts b/packages/apps/node-runtime/src/lib/accessors/tests/ModifyExtender.test.ts new file mode 100644 index 0000000000000..3165af1020f91 --- /dev/null +++ b/packages/apps/node-runtime/src/lib/accessors/tests/ModifyExtender.test.ts @@ -0,0 +1,244 @@ +// deno-lint-ignore-file no-explicit-any +import { assertRejects } from 'https://deno.land/std@0.203.0/assert/mod'; +import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd'; +import { assertSpyCall, spy, stub } from 'https://deno.land/std@0.203.0/testing/mock'; +import jsonrpc from 'jsonrpc-lite'; + +import { AppObjectRegistry } from '../../../AppObjectRegistry'; +import { ModifyExtender } from '../modify/ModifyExtender'; + +describe('ModifyExtender', () => { + let extender: ModifyExtender; + + const senderFn = (r: any) => + Promise.resolve({ + id: Math.random().toString(36).substring(2), + jsonrpc: '2.0', + result: structuredClone(r), + serialize() { + return JSON.stringify(this); + }, + }); + + beforeEach(() => { + AppObjectRegistry.clear(); + AppObjectRegistry.set('id', 'deno-test'); + extender = new ModifyExtender(senderFn); + }); + + afterAll(() => { + AppObjectRegistry.clear(); + }); + + it('correctly formats requests for the extend message requests', async () => { + const _spy = spy(extender, 'senderFn' as keyof ModifyExtender); + + const messageExtender = await extender.extendMessage('message-id', { _id: 'user-id' } as any); + + assertSpyCall(_spy, 0, { + args: [ + { + method: 'bridges:getMessageBridge:doGetById', + params: ['message-id', 'deno-test'], + }, + ], + }); + + messageExtender.addCustomField('key', 'value'); + + await extender.finish(messageExtender); + + assertSpyCall(_spy, 1, { + args: [ + { + method: 'bridges:getMessageBridge:doUpdate', + params: [messageExtender.getMessage(), 'deno-test'], + }, + ], + }); + + _spy.restore(); + }); + + it('correctly formats requests for the extend room requests', async () => { + const _spy = spy(extender, 'senderFn' as keyof ModifyExtender); + + const roomExtender = await extender.extendRoom('room-id', { _id: 'user-id' } as any); + + assertSpyCall(_spy, 0, { + args: [ + { + method: 'bridges:getRoomBridge:doGetById', + params: ['room-id', 'deno-test'], + }, + ], + }); + + roomExtender.addCustomField('key', 'value'); + + await extender.finish(roomExtender); + + assertSpyCall(_spy, 1, { + args: [ + { + method: 'bridges:getRoomBridge:doUpdate', + params: [roomExtender.getRoom(), [], 'deno-test'], + }, + ], + }); + + _spy.restore(); + }); + + it('correctly formats requests for the extend video conference requests', async () => { + const _spy = spy(extender, 'senderFn' as keyof ModifyExtender); + + const videoConferenceExtender = await extender.extendVideoConference('video-conference-id'); + + assertSpyCall(_spy, 0, { + args: [ + { + method: 'bridges:getVideoConferenceBridge:doGetById', + params: ['video-conference-id', 'deno-test'], + }, + ], + }); + + videoConferenceExtender.setStatus(4); + + await extender.finish(videoConferenceExtender); + + assertSpyCall(_spy, 1, { + args: [ + { + method: 'bridges:getVideoConferenceBridge:doUpdate', + params: [videoConferenceExtender.getVideoConference(), 'deno-test'], + }, + ], + }); + + _spy.restore(); + }); + + describe('Error Handling', () => { + describe('extendMessage', () => { + it('throws an instance of Error when senderFn throws an error', async () => { + const _stub = stub(extender, 'senderFn' as keyof ModifyExtender, () => Promise.reject(new Error('unit-test-error')) as any); + + await assertRejects(() => extender.extendMessage('message-id', { _id: 'user-id' } as any), Error, 'unit-test-error'); + + _stub.restore(); + }); + + it('throws an instance of Error when senderFn throws a jsonrpc error', async () => { + const _stub = stub( + extender, + 'senderFn' as keyof ModifyExtender, + () => Promise.reject(jsonrpc.error('unit-test-error', new jsonrpc.JsonRpcError('unit-test-error', 1000))) as any, + ); + + await assertRejects(() => extender.extendMessage('message-id', { _id: 'user-id' } as any), Error, 'unit-test-error'); + + _stub.restore(); + }); + + it('throws an instance of Error when senderFn throws an unknown value', async () => { + const _stub = stub(extender, 'senderFn' as keyof ModifyExtender, () => Promise.reject({}) as any); + + await assertRejects(() => extender.extendMessage('message-id', { _id: 'user-id' } as any), Error, 'An unknown error occurred'); + + _stub.restore(); + }); + }); + + describe('extendRoom', () => { + it('throws an instance of Error when senderFn throws an error', async () => { + const _stub = stub(extender, 'senderFn' as keyof ModifyExtender, () => Promise.reject(new Error('unit-test-error')) as any); + + await assertRejects(() => extender.extendRoom('room-id', { _id: 'user-id' } as any), Error, 'unit-test-error'); + + _stub.restore(); + }); + + it('throws an instance of Error when senderFn throws a jsonrpc error', async () => { + const _stub = stub( + extender, + 'senderFn' as keyof ModifyExtender, + () => Promise.reject(jsonrpc.error('unit-test-error', new jsonrpc.JsonRpcError('unit-test-error', 1000))) as any, + ); + + await assertRejects(() => extender.extendRoom('room-id', { _id: 'user-id' } as any), Error, 'unit-test-error'); + + _stub.restore(); + }); + + it('throws an instance of Error when senderFn throws an unknown value', async () => { + const _stub = stub(extender, 'senderFn' as keyof ModifyExtender, () => Promise.reject({}) as any); + + await assertRejects(() => extender.extendRoom('room-id', { _id: 'user-id' } as any), Error, 'An unknown error occurred'); + + _stub.restore(); + }); + }); + + describe('extendVideoConference', () => { + it('throws an instance of Error when senderFn throws an error', async () => { + const _stub = stub(extender, 'senderFn' as keyof ModifyExtender, () => Promise.reject(new Error('unit-test-error')) as any); + + await assertRejects(() => extender.extendVideoConference('video-conference-id'), Error, 'unit-test-error'); + + _stub.restore(); + }); + + it('throws an instance of Error when senderFn throws a jsonrpc error', async () => { + const _stub = stub( + extender, + 'senderFn' as keyof ModifyExtender, + () => Promise.reject(jsonrpc.error('unit-test-error', new jsonrpc.JsonRpcError('unit-test-error', 1000))) as any, + ); + + await assertRejects(() => extender.extendVideoConference('video-conference-id'), Error, 'unit-test-error'); + + _stub.restore(); + }); + + it('throws an instance of Error when senderFn throws an unknown value', async () => { + const _stub = stub(extender, 'senderFn' as keyof ModifyExtender, () => Promise.reject({}) as any); + + await assertRejects(() => extender.extendVideoConference('video-conference-id'), Error, 'An unknown error occurred'); + + _stub.restore(); + }); + }); + + describe('finish', () => { + it('throws an instance of Error when senderFn throws an error', async () => { + const _stub = stub(extender, 'senderFn' as keyof ModifyExtender, () => Promise.reject(new Error('unit-test-error')) as any); + + await assertRejects(() => extender.finish({ kind: 'message', getMessage: () => ({}) } as any), Error, 'unit-test-error'); + + _stub.restore(); + }); + + it('throws an instance of Error when senderFn throws a jsonrpc error', async () => { + const _stub = stub( + extender, + 'senderFn' as keyof ModifyExtender, + () => Promise.reject(jsonrpc.error('unit-test-error', new jsonrpc.JsonRpcError('unit-test-error', 1000))) as any, + ); + + await assertRejects(() => extender.finish({ kind: 'message', getMessage: () => ({}) } as any), Error, 'unit-test-error'); + + _stub.restore(); + }); + + it('throws an instance of Error when senderFn throws an unknown value', async () => { + const _stub = stub(extender, 'senderFn' as keyof ModifyExtender, () => Promise.reject({}) as any); + + await assertRejects(() => extender.finish({ kind: 'message', getMessage: () => ({}) } as any), Error, 'An unknown error occurred'); + + _stub.restore(); + }); + }); + }); +}); diff --git a/packages/apps/node-runtime/src/lib/accessors/tests/ModifyUpdater.test.ts b/packages/apps/node-runtime/src/lib/accessors/tests/ModifyUpdater.test.ts new file mode 100644 index 0000000000000..9d63f35af85b7 --- /dev/null +++ b/packages/apps/node-runtime/src/lib/accessors/tests/ModifyUpdater.test.ts @@ -0,0 +1,243 @@ +// deno-lint-ignore-file no-explicit-any +import { assertEquals, assertRejects } from 'https://deno.land/std@0.203.0/assert/mod'; +import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd'; +import { assertSpyCall, spy, stub } from 'https://deno.land/std@0.203.0/testing/mock'; +import jsonrpc from 'jsonrpc-lite'; + +import { AppObjectRegistry } from '../../../AppObjectRegistry'; +import type { RoomBuilder } from '../builders/RoomBuilder'; +import { ModifyUpdater } from '../modify/ModifyUpdater'; + +describe('ModifyUpdater', () => { + let modifyUpdater: ModifyUpdater; + + const senderFn = (r: any) => + Promise.resolve({ + id: Math.random().toString(36).substring(2), + jsonrpc: '2.0', + result: structuredClone(r), + serialize() { + return JSON.stringify(this); + }, + }); + + beforeEach(() => { + AppObjectRegistry.clear(); + AppObjectRegistry.set('id', 'deno-test'); + modifyUpdater = new ModifyUpdater(senderFn); + }); + + afterAll(() => { + AppObjectRegistry.clear(); + }); + + it('correctly formats requests for the update message flow', async () => { + const _spy = spy(modifyUpdater, 'senderFn' as keyof ModifyUpdater); + + const messageBuilder = await modifyUpdater.message('123', { id: '456' } as any); + + assertSpyCall(_spy, 0, { + args: [ + { + method: 'bridges:getMessageBridge:doGetById', + params: ['123', 'deno-test'], + }, + ], + }); + + messageBuilder.setUpdateData( + { + id: '123', + room: { id: '123' }, + sender: { id: '456' }, + text: 'Hello World', + }, + { + id: '456', + }, + ); + + await modifyUpdater.finish(messageBuilder); + + assertSpyCall(_spy, 1, { + args: [ + { + method: 'bridges:getMessageBridge:doUpdate', + params: [{ id: '123', ...messageBuilder.getChanges() }, 'deno-test'], + }, + ], + }); + + _spy.restore(); + }); + + it('correctly formats requests for the update room flow', async () => { + const _spy = spy(modifyUpdater, 'senderFn' as keyof ModifyUpdater); + + const roomBuilder = (await modifyUpdater.room('123', { id: '456' } as any)) as RoomBuilder; + + assertSpyCall(_spy, 0, { + args: [ + { + method: 'bridges:getRoomBridge:doGetById', + params: ['123', 'deno-test'], + }, + ], + }); + + roomBuilder.setData({ + id: '123', + type: 'c', + displayName: 'Test Room', + slugifiedName: 'test-room', + creator: { id: '456' }, + }); + + roomBuilder.setMembersToBeAddedByUsernames(['username1', 'username2']); + + // We need to sneak in the id as the `modifyUpdater.room` call won't have legitimate data + roomBuilder.getRoom().id = '123'; + + await modifyUpdater.finish(roomBuilder); + + assertSpyCall(_spy, 1, { + args: [ + { + method: 'bridges:getRoomBridge:doUpdate', + params: [{ id: '123', ...roomBuilder.getChanges() }, roomBuilder.getMembersToBeAddedUsernames(), 'deno-test'], + }, + ], + }); + }); + + it('correctly formats requests to UserUpdater methods', async () => { + const result = (await modifyUpdater.getUserUpdater().updateStatusText({ id: '123' } as any, 'Hello World')) as any; + + assertEquals(result, { + method: 'accessor:getModifier:getUpdater:getUserUpdater:updateStatusText', + params: [{ id: '123' }, 'Hello World'], + }); + }); + + it('correctly formats requests to LivechatUpdater methods', async () => { + const result = (await modifyUpdater.getLivechatUpdater().closeRoom({ id: '123' } as any, 'close it!')) as any; + + assertEquals(result, { + method: 'accessor:getModifier:getUpdater:getLivechatUpdater:closeRoom', + params: [{ id: '123' }, 'close it!'], + }); + }); + + it('correctly formats requests to MessageUpdater methods', async () => { + const result = (await modifyUpdater.getMessageUpdater().addReaction('message-id', 'user-id', ':smile:')) as any; + + assertEquals(result, { + method: 'accessor:getModifier:getUpdater:getMessageUpdater:addReaction', + params: ['message-id', 'user-id', ':smile:'], + }); + }); + + describe('Error Handling', () => { + describe('message', () => { + it('throws an instance of Error when senderFn throws an error', async () => { + const _stub = stub(modifyUpdater, 'senderFn' as keyof ModifyUpdater, () => Promise.reject(new Error('unit-test-error')) as any); + + await assertRejects(() => modifyUpdater.message('message-id', { _id: 'user-id' } as any), Error, 'unit-test-error'); + + _stub.restore(); + }); + + it('throws an instance of Error when senderFn throws a jsonrpc error', async () => { + const _stub = stub( + modifyUpdater, + 'senderFn' as keyof ModifyUpdater, + () => Promise.reject(jsonrpc.error('unit-test-error', new jsonrpc.JsonRpcError('unit-test-error', 1000))) as any, + ); + + await assertRejects(() => modifyUpdater.message('message-id', { _id: 'user-id' } as any), Error, 'unit-test-error'); + + _stub.restore(); + }); + + it('throws an instance of Error when senderFn throws an unknown value', async () => { + const _stub = stub(modifyUpdater, 'senderFn' as keyof ModifyUpdater, () => Promise.reject({}) as any); + + await assertRejects(() => modifyUpdater.message('message-id', { _id: 'user-id' } as any), Error, 'An unknown error occurred'); + + _stub.restore(); + }); + }); + + describe('room', () => { + it('throws an instance of Error when senderFn throws an error', async () => { + const _stub = stub(modifyUpdater, 'senderFn' as keyof ModifyUpdater, () => Promise.reject(new Error('unit-test-error')) as any); + + await assertRejects(() => modifyUpdater.room('room-id', { _id: 'user-id' } as any), Error, 'unit-test-error'); + + _stub.restore(); + }); + + it('throws an instance of Error when senderFn throws a jsonrpc error', async () => { + const _stub = stub( + modifyUpdater, + 'senderFn' as keyof ModifyUpdater, + () => Promise.reject(jsonrpc.error('unit-test-error', new jsonrpc.JsonRpcError('unit-test-error', 1000))) as any, + ); + + await assertRejects(() => modifyUpdater.room('room-id', { _id: 'user-id' } as any), Error, 'unit-test-error'); + + _stub.restore(); + }); + + it('throws an instance of Error when senderFn throws an unknown value', async () => { + const _stub = stub(modifyUpdater, 'senderFn' as keyof ModifyUpdater, () => Promise.reject({}) as any); + + await assertRejects(() => modifyUpdater.room('room-id', { _id: 'user-id' } as any), Error, 'An unknown error occurred'); + + _stub.restore(); + }); + }); + + describe('finish', () => { + const messageUpdater = { + kind: 'message', + getMessage: () => ({ + id: 'message-id', + sender: { id: 'sender-id' }, + }), + getChanges: () => ({ + id: 'message-id', + sender: { id: 'sender-id' }, + }), + } as any; + + it('throws an instance of Error when senderFn throws an error', async () => { + const _stub = stub(modifyUpdater, 'senderFn' as keyof ModifyUpdater, () => Promise.reject(new Error('unit-test-error')) as any); + + await assertRejects(() => modifyUpdater.finish(messageUpdater), Error, 'unit-test-error'); + + _stub.restore(); + }); + + it('throws an instance of Error when senderFn throws a jsonrpc error', async () => { + const _stub = stub( + modifyUpdater, + 'senderFn' as keyof ModifyUpdater, + () => Promise.reject(jsonrpc.error('unit-test-error', new jsonrpc.JsonRpcError('unit-test-error', 1000))) as any, + ); + + await assertRejects(() => modifyUpdater.finish(messageUpdater), Error, 'unit-test-error'); + + _stub.restore(); + }); + + it('throws an instance of Error when senderFn throws an unknown value', async () => { + const _stub = stub(modifyUpdater, 'senderFn' as keyof ModifyUpdater, () => Promise.reject({}) as any); + + await assertRejects(() => modifyUpdater.finish(messageUpdater), Error, 'An unknown error occurred'); + + _stub.restore(); + }); + }); + }); +}); diff --git a/packages/apps/node-runtime/src/lib/accessors/tests/formatResponseErrorHandler.test.ts b/packages/apps/node-runtime/src/lib/accessors/tests/formatResponseErrorHandler.test.ts new file mode 100644 index 0000000000000..78f92e1e6d1b3 --- /dev/null +++ b/packages/apps/node-runtime/src/lib/accessors/tests/formatResponseErrorHandler.test.ts @@ -0,0 +1,211 @@ +// deno-lint-ignore-file no-explicit-any +import { assertEquals, assertInstanceOf, assertStrictEquals } from 'https://deno.land/std@0.203.0/assert/mod'; +import { describe, it } from 'https://deno.land/std@0.203.0/testing/bdd'; +import * as jsonrpc from 'jsonrpc-lite'; + +import { formatErrorResponse } from '../formatResponseErrorHandler'; + +describe('formatErrorResponse', () => { + describe('JSON-RPC ErrorObject handling', () => { + it('formats ErrorObject instances correctly', () => { + const errorObject = jsonrpc.error('test-id', new jsonrpc.JsonRpcError('Test error message', 1000)); + const result = formatErrorResponse(errorObject); + + assertInstanceOf(result, Error); + assertEquals(result.message, 'Test error message'); + }); + + it('formats objects with error.message structure', () => { + const errorLikeObject = { + error: { + message: 'Custom error message', + code: 404, + }, + }; + const result = formatErrorResponse(errorLikeObject); + + assertInstanceOf(result, Error); + assertEquals(result.message, 'Custom error message'); + }); + + it('handles nested error objects with complex structure', () => { + const complexError = { + error: { + message: 'Database connection failed', + details: { + host: 'localhost', + port: 5432, + }, + }, + id: 'req-123', + }; + const result = formatErrorResponse(complexError); + + assertInstanceOf(result, Error); + assertEquals(result.message, 'Database connection failed'); + }); + + it('handles error objects with empty message', () => { + const emptyMessageError = { + error: { + message: '', + code: 500, + }, + }; + const result = formatErrorResponse(emptyMessageError); + + assertInstanceOf(result, Error); + assertEquals(result.message, ''); + }); + }); + + describe('Error instance passthrough', () => { + it('returns existing Error instances unchanged', () => { + const originalError = new Error('Original error message'); + const result = formatErrorResponse(originalError); + + assertStrictEquals(result, originalError); + assertEquals(result.message, 'Original error message'); + }); + + it('returns custom Error subclasses unchanged', () => { + class CustomError extends Error { + constructor( + message: string, + public code: number, + ) { + super(message); + this.name = 'CustomError'; + } + } + + const customError = new CustomError('Custom error', 404); + const result = formatErrorResponse(customError); + + assertStrictEquals(result, customError); + assertEquals(result.message, 'Custom error'); + assertEquals((result as CustomError).code, 404); + }); + + it('handles Error instances with additional properties', () => { + const errorWithProps = new Error('Error with props') as any; + errorWithProps.statusCode = 500; + errorWithProps.details = { reason: 'timeout' }; + + const result = formatErrorResponse(errorWithProps); + + assertStrictEquals(result, errorWithProps); + assertEquals(result.message, 'Error with props'); + assertEquals((result as any).statusCode, 500); + }); + }); + + describe('Unknown error handling', () => { + it('wraps string errors with default message and cause', () => { + const stringError = 'Simple string error'; + const result = formatErrorResponse(stringError); + + assertInstanceOf(result, Error); + assertEquals(result.message, 'An unknown error occurred'); + assertEquals(result.cause, stringError); + }); + + it('wraps number errors with default message and cause', () => { + const numberError = 404; + const result = formatErrorResponse(numberError); + + assertInstanceOf(result, Error); + assertEquals(result.message, 'An unknown error occurred'); + assertEquals(result.cause, numberError); + }); + + it('wraps boolean errors with default message and cause', () => { + const booleanError = false; + const result = formatErrorResponse(booleanError); + + assertInstanceOf(result, Error); + assertEquals(result.message, 'An unknown error occurred'); + assertEquals(result.cause, booleanError); + }); + + it('wraps null with default message and cause', () => { + const result = formatErrorResponse(null); + + assertInstanceOf(result, Error); + assertEquals(result.message, 'An unknown error occurred'); + assertEquals(result.cause, null); + }); + + it('wraps undefined with default message and cause', () => { + const result = formatErrorResponse(undefined); + + assertInstanceOf(result, Error); + assertEquals(result.message, 'An unknown error occurred'); + assertEquals(result.cause, undefined); + }); + + it('wraps arrays with default message and cause', () => { + const arrayError = ['error', 'details']; + const result = formatErrorResponse(arrayError); + + assertInstanceOf(result, Error); + assertEquals(result.message, 'An unknown error occurred'); + assertEquals(result.cause, arrayError); + }); + + it('wraps functions with default message and cause', () => { + const functionError = () => 'error'; + const result = formatErrorResponse(functionError); + + assertInstanceOf(result, Error); + assertEquals(result.message, 'An unknown error occurred'); + assertEquals(result.cause, functionError); + }); + + it('wraps plain objects without error.message with default message and cause', () => { + const plainObject = { + status: 'failed', + reason: 'timeout', + data: { id: 123 }, + }; + const result = formatErrorResponse(plainObject); + + assertInstanceOf(result, Error); + assertEquals(result.message, 'An unknown error occurred'); + assertEquals(result.cause, plainObject); + }); + + it('wraps objects with error property but no message with default message and cause', () => { + const errorObjectNoMessage = { + error: { + code: 500, + details: 'Internal server error', + }, + }; + const result = formatErrorResponse(errorObjectNoMessage); + + assertInstanceOf(result, Error); + assertEquals(result.message, 'An unknown error occurred'); + assertEquals(result.cause, errorObjectNoMessage); + }); + }); + + it('ensures all returned values are proper Error instances', () => { + const testCases = ['string error', 123, null, undefined, { error: { message: 'test' } }, new Error('test'), { plain: 'object' }]; + + for (const testCase of testCases) { + const result = formatErrorResponse(testCase); + assertInstanceOf(result, Error, `Failed for input: ${JSON.stringify(testCase)}`); + } + }); + + it('prevents "[object Object]" error messages for plain objects', () => { + const plainObject = { status: 'error', code: 500 }; + const result = formatErrorResponse(plainObject); + + assertInstanceOf(result, Error); + assertEquals(result.message, 'An unknown error occurred'); + // Ensure the message is not "[object Object]" + assertEquals(result.message !== '[object Object]', true); + }); +}); diff --git a/packages/apps/node-runtime/src/lib/accessors/tests/http.test.ts b/packages/apps/node-runtime/src/lib/accessors/tests/http.test.ts new file mode 100644 index 0000000000000..1bbe10d95afd1 --- /dev/null +++ b/packages/apps/node-runtime/src/lib/accessors/tests/http.test.ts @@ -0,0 +1,164 @@ +// deno-lint-ignore-file no-explicit-any +import { assertRejects } from 'https://deno.land/std@0.203.0/assert/mod'; +import { beforeEach, describe, it, afterAll } from 'https://deno.land/std@0.203.0/testing/bdd'; +import { stub } from 'https://deno.land/std@0.203.0/testing/mock'; + +import { AppObjectRegistry } from '../../../AppObjectRegistry'; +import { Http } from '../http'; + +describe('Http accessor error handling integration', () => { + let http: Http; + + beforeEach(() => { + AppObjectRegistry.clear(); + AppObjectRegistry.set('id', 'test-app-id'); + + const mockHttpExtend = { + getDefaultHeaders: () => new Map(), + getDefaultParams: () => new Map(), + getPreRequestHandlers: () => [], + getPreResponseHandlers: () => [], + }; + + const mockRead = {}; + const mockPersistence = {}; + + http = new Http(mockRead as any, mockPersistence as any, mockHttpExtend as any, () => Promise.resolve({}) as any); + }); + + afterAll(() => { + AppObjectRegistry.clear(); + }); + + describe('HTTP method error handling', () => { + it('formats JSON-RPC errors correctly for GET requests', async () => { + const _stub = stub(http, 'senderFn' as keyof Http, () => + Promise.reject({ + error: { + message: 'HTTP GET request failed', + code: 404, + }, + }), + ); + + await assertRejects(() => http.get('https://api.example.com/data'), Error, 'HTTP GET request failed'); + + _stub.restore(); + }); + + it('formats JSON-RPC errors correctly for POST requests', async () => { + const _stub = stub(http, 'senderFn' as keyof Http, () => + Promise.reject({ + error: { + message: 'HTTP POST request validation failed', + code: 400, + }, + }), + ); + + await assertRejects( + () => http.post('https://api.example.com/create', { data: { name: 'test' } }), + Error, + 'HTTP POST request validation failed', + ); + + _stub.restore(); + }); + + it('formats JSON-RPC errors correctly for PUT requests', async () => { + const _stub = stub(http, 'senderFn' as keyof Http, () => + Promise.reject({ + error: { + message: 'HTTP PUT request unauthorized', + code: 401, + }, + }), + ); + + await assertRejects( + () => http.put('https://api.example.com/update/123', { data: { name: 'updated' } }), + Error, + 'HTTP PUT request unauthorized', + ); + + _stub.restore(); + }); + + it('formats JSON-RPC errors correctly for DELETE requests', async () => { + const _stub = stub(http, 'senderFn' as keyof Http, () => + Promise.reject({ + error: { + message: 'HTTP DELETE request forbidden', + code: 403, + }, + }), + ); + + await assertRejects(() => http.del('https://api.example.com/delete/123'), Error, 'HTTP DELETE request forbidden'); + + _stub.restore(); + }); + + it('formats JSON-RPC errors correctly for PATCH requests', async () => { + const _stub = stub(http, 'senderFn' as keyof Http, () => + Promise.reject({ + error: { + message: 'HTTP PATCH request conflict', + code: 409, + }, + }), + ); + + await assertRejects( + () => http.patch('https://api.example.com/patch/123', { data: { status: 'active' } }), + Error, + 'HTTP PATCH request conflict', + ); + + _stub.restore(); + }); + }); + + describe('Error instance passthrough', () => { + it('passes through existing Error instances unchanged for HTTP requests', async () => { + const originalError = new Error('Network timeout error'); + const _stub = stub(http, 'senderFn' as keyof Http, () => Promise.reject(originalError)); + + await assertRejects(() => http.get('https://api.example.com/data'), Error, 'Network timeout error'); + + _stub.restore(); + }); + }); + + describe('Unknown error handling', () => { + it('wraps unknown object errors with default message for HTTP requests', async () => { + const unknownError = { + status: 'failed', + details: 'Something went wrong', + timestamp: Date.now(), + }; + const _stub = stub(http, 'senderFn' as keyof Http, () => Promise.reject(unknownError)); + + await assertRejects(() => http.get('https://api.example.com/data'), Error, 'An unknown error occurred'); + + _stub.restore(); + }); + + it('wraps string errors with default message for HTTP requests', async () => { + const stringError = 'Connection refused'; + const _stub = stub(http, 'senderFn' as keyof Http, () => Promise.reject(stringError)); + + await assertRejects(() => http.get('https://api.example.com/data'), Error, 'An unknown error occurred'); + + _stub.restore(); + }); + + it('wraps null/undefined errors with default message for HTTP requests', async () => { + const _stub = stub(http, 'senderFn' as keyof Http, () => Promise.reject(null)); + + await assertRejects(() => http.get('https://api.example.com/data'), Error, 'An unknown error occurred'); + + _stub.restore(); + }); + }); +}); diff --git a/packages/apps/node-runtime/src/lib/ast/acorn-walk.d.ts b/packages/apps/node-runtime/src/lib/ast/acorn-walk.d.ts new file mode 100644 index 0000000000000..6bad95118975b --- /dev/null +++ b/packages/apps/node-runtime/src/lib/ast/acorn-walk.d.ts @@ -0,0 +1,16 @@ +// Augment the npm 'acorn-walk' module with a better-typed ancestor walker +// callback that uses the discriminated-union AnyNode (from our acorn augment) +// instead of the base Node class. The original FullAncestorWalkerCallback type +// cannot be changed via augmentation, so we add a new alias. + +import type { AnyNode } from 'acorn'; + +declare module 'acorn-walk' { + /** + * Strongly-typed variant of FullAncestorWalkerCallback where the node is + * narrowed to AnyNode (the discriminated union) and the state is TState only + * (not TState | Node[]). Use this instead of FullAncestorWalkerCallback when + * writing AST operations for this runtime. + */ + export type FullAncestorWalkerCallbackWithState = (node: AnyNode, state: TState, ancestors: AnyNode[], type: string) => void; +} diff --git a/packages/apps/node-runtime/src/lib/ast/acorn.d.ts b/packages/apps/node-runtime/src/lib/ast/acorn.d.ts new file mode 100644 index 0000000000000..77e59b28a8eb6 --- /dev/null +++ b/packages/apps/node-runtime/src/lib/ast/acorn.d.ts @@ -0,0 +1,622 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +// Make this a module file so that the declare module block below is treated +// as an AUGMENTATION (merged with the npm types) rather than a replacement. +export {}; + +// Augment the npm 'acorn' module with the specific AST node types that are +// missing from its public type declarations. These were originally written as +// a Deno-style global namespace declaration; here they are re-expressed as a +// proper TypeScript module augmentation so that they are visible to +// `import { ... } from 'acorn'` under Node / nodenext resolution. + +declare module 'acorn' { + export interface SourceLocation { + source?: string | null; + start: Position; + end: Position; + } + + export interface Identifier extends Node { + type: 'Identifier'; + name: string; + } + + export interface Literal extends Node { + type: 'Literal'; + value?: string | boolean | null | number | RegExp | bigint; + raw?: string; + regex?: { + pattern: string; + flags: string; + }; + bigint?: string; + } + + export interface Program extends Node { + type: 'Program'; + body: Array; + sourceType: 'script' | 'module'; + } + + export interface AcornFunction extends Node { + id?: Identifier | null; + params: Array; + body: BlockStatement | Expression; + generator: boolean; + expression: boolean; + async: boolean; + } + + export interface ExpressionStatement extends Node { + type: 'ExpressionStatement'; + expression: Expression | Literal; + directive?: string; + } + + export interface BlockStatement extends Node { + type: 'BlockStatement'; + body: Array; + } + + export interface EmptyStatement extends Node { + type: 'EmptyStatement'; + } + + export interface DebuggerStatement extends Node { + type: 'DebuggerStatement'; + } + + export interface WithStatement extends Node { + type: 'WithStatement'; + object: Expression; + body: Statement; + } + + export interface ReturnStatement extends Node { + type: 'ReturnStatement'; + argument?: Expression | null; + } + + export interface LabeledStatement extends Node { + type: 'LabeledStatement'; + label: Identifier; + body: Statement; + } + + export interface BreakStatement extends Node { + type: 'BreakStatement'; + label?: Identifier | null; + } + + export interface ContinueStatement extends Node { + type: 'ContinueStatement'; + label?: Identifier | null; + } + + export interface IfStatement extends Node { + type: 'IfStatement'; + test: Expression; + consequent: Statement; + alternate?: Statement | null; + } + + export interface SwitchStatement extends Node { + type: 'SwitchStatement'; + discriminant: Expression; + cases: Array; + } + + export interface SwitchCase extends Node { + type: 'SwitchCase'; + test?: Expression | null; + consequent: Array; + } + + export interface ThrowStatement extends Node { + type: 'ThrowStatement'; + argument: Expression; + } + + export interface TryStatement extends Node { + type: 'TryStatement'; + block: BlockStatement; + handler?: CatchClause | null; + finalizer?: BlockStatement | null; + } + + export interface CatchClause extends Node { + type: 'CatchClause'; + param?: Pattern | null; + body: BlockStatement; + } + + export interface WhileStatement extends Node { + type: 'WhileStatement'; + test: Expression; + body: Statement; + } + + export interface DoWhileStatement extends Node { + type: 'DoWhileStatement'; + body: Statement; + test: Expression; + } + + export interface ForStatement extends Node { + type: 'ForStatement'; + init?: VariableDeclaration | Expression | null; + test?: Expression | null; + update?: Expression | null; + body: Statement; + } + + export interface ForInStatement extends Node { + type: 'ForInStatement'; + left: VariableDeclaration | Pattern; + right: Expression; + body: Statement; + } + + export interface FunctionDeclaration extends AcornFunction { + type: 'FunctionDeclaration'; + id: Identifier; + body: BlockStatement; + } + + export interface VariableDeclaration extends Node { + type: 'VariableDeclaration'; + declarations: Array; + kind: 'var' | 'let' | 'const'; + } + + export interface VariableDeclarator extends Node { + type: 'VariableDeclarator'; + id: Pattern; + init?: Expression | null; + } + + export interface ThisExpression extends Node { + type: 'ThisExpression'; + } + + export interface ArrayExpression extends Node { + type: 'ArrayExpression'; + elements: Array; + } + + export interface ObjectExpression extends Node { + type: 'ObjectExpression'; + properties: Array; + } + + export interface Property extends Node { + type: 'Property'; + key: Expression; + value: Expression; + kind: 'init' | 'get' | 'set'; + method: boolean; + shorthand: boolean; + computed: boolean; + } + + export interface FunctionExpression extends AcornFunction { + type: 'FunctionExpression'; + body: BlockStatement; + } + + export interface UnaryExpression extends Node { + type: 'UnaryExpression'; + operator: UnaryOperator; + prefix: boolean; + argument: Expression; + } + + export type UnaryOperator = '-' | '+' | '!' | '~' | 'typeof' | 'void' | 'delete'; + + export interface UpdateExpression extends Node { + type: 'UpdateExpression'; + operator: UpdateOperator; + argument: Expression; + prefix: boolean; + } + + export type UpdateOperator = '++' | '--'; + + export interface BinaryExpression extends Node { + type: 'BinaryExpression'; + operator: BinaryOperator; + left: Expression | PrivateIdentifier; + right: Expression; + } + + export type BinaryOperator = + | '==' + | '!=' + | '===' + | '!==' + | '<' + | '<=' + | '>' + | '>=' + | '<<' + | '>>' + | '>>>' + | '+' + | '-' + | '*' + | '/' + | '%' + | '|' + | '^' + | '&' + | 'in' + | 'instanceof' + | '**'; + + export interface AssignmentExpression extends Node { + type: 'AssignmentExpression'; + operator: AssignmentOperator; + left: Pattern; + right: Expression; + } + + export type AssignmentOperator = + | '=' + | '+=' + | '-=' + | '*=' + | '/=' + | '%=' + | '<<=' + | '>>=' + | '>>>=' + | '|=' + | '^=' + | '&=' + | '**=' + | '||=' + | '&&=' + | '??='; + + export interface LogicalExpression extends Node { + type: 'LogicalExpression'; + operator: LogicalOperator; + left: Expression; + right: Expression; + } + + export type LogicalOperator = '||' | '&&' | '??'; + + export interface MemberExpression extends Node { + type: 'MemberExpression'; + object: Expression | Super; + property: Expression | PrivateIdentifier; + computed: boolean; + optional: boolean; + } + + export interface ConditionalExpression extends Node { + type: 'ConditionalExpression'; + test: Expression; + alternate: Expression; + consequent: Expression; + } + + export interface CallExpression extends Node { + type: 'CallExpression'; + callee: Expression | Super; + arguments: Array; + optional: boolean; + } + + export interface NewExpression extends Node { + type: 'NewExpression'; + callee: Expression; + arguments: Array; + } + + export interface SequenceExpression extends Node { + type: 'SequenceExpression'; + expressions: Array; + } + + export interface ForOfStatement extends Node { + type: 'ForOfStatement'; + left: VariableDeclaration | Pattern; + right: Expression; + body: Statement; + await: boolean; + } + + export interface Super extends Node { + type: 'Super'; + } + + export interface SpreadElement extends Node { + type: 'SpreadElement'; + argument: Expression; + } + + export interface ArrowFunctionExpression extends AcornFunction { + type: 'ArrowFunctionExpression'; + } + + export interface YieldExpression extends Node { + type: 'YieldExpression'; + argument?: Expression | null; + delegate: boolean; + } + + export interface TemplateLiteral extends Node { + type: 'TemplateLiteral'; + quasis: Array; + expressions: Array; + } + + export interface TaggedTemplateExpression extends Node { + type: 'TaggedTemplateExpression'; + tag: Expression; + quasi: TemplateLiteral; + } + + export interface TemplateElement extends Node { + type: 'TemplateElement'; + tail: boolean; + value: { + cooked?: string | null; + raw: string; + }; + } + + export interface AssignmentProperty extends Node { + type: 'Property'; + key: Expression; + value: Pattern; + kind: 'init'; + method: false; + shorthand: boolean; + computed: boolean; + } + + export interface ObjectPattern extends Node { + type: 'ObjectPattern'; + properties: Array; + } + + export interface ArrayPattern extends Node { + type: 'ArrayPattern'; + elements: Array; + } + + export interface RestElement extends Node { + type: 'RestElement'; + argument: Pattern; + } + + export interface AssignmentPattern extends Node { + type: 'AssignmentPattern'; + left: Pattern; + right: Expression; + } + + export interface AcornClass extends Node { + id?: Identifier | null; + superClass?: Expression | null; + body: ClassBody; + } + + export interface ClassBody extends Node { + type: 'ClassBody'; + body: Array; + } + + export interface MethodDefinition extends Node { + type: 'MethodDefinition'; + key: Expression | PrivateIdentifier; + value: FunctionExpression; + kind: 'constructor' | 'method' | 'get' | 'set'; + computed: boolean; + static: boolean; + } + + export interface ClassDeclaration extends AcornClass { + type: 'ClassDeclaration'; + id: Identifier; + } + + export interface ClassExpression extends AcornClass { + type: 'ClassExpression'; + } + + export interface MetaProperty extends Node { + type: 'MetaProperty'; + meta: Identifier; + property: Identifier; + } + + export interface ImportDeclaration extends Node { + type: 'ImportDeclaration'; + specifiers: Array; + source: Literal; + } + + export interface ImportSpecifier extends Node { + type: 'ImportSpecifier'; + imported: Identifier | Literal; + local: Identifier; + } + + export interface ImportDefaultSpecifier extends Node { + type: 'ImportDefaultSpecifier'; + local: Identifier; + } + + export interface ImportNamespaceSpecifier extends Node { + type: 'ImportNamespaceSpecifier'; + local: Identifier; + } + + export interface ExportNamedDeclaration extends Node { + type: 'ExportNamedDeclaration'; + declaration?: Declaration | null; + specifiers: Array; + source?: Literal | null; + } + + export interface ExportSpecifier extends Node { + type: 'ExportSpecifier'; + exported: Identifier | Literal; + local: Identifier | Literal; + } + + export interface AnonymousFunctionDeclaration extends AcornFunction { + type: 'FunctionDeclaration'; + id: null; + body: BlockStatement; + } + + export interface AnonymousClassDeclaration extends AcornClass { + type: 'ClassDeclaration'; + id: null; + } + + export interface ExportDefaultDeclaration extends Node { + type: 'ExportDefaultDeclaration'; + declaration: AnonymousFunctionDeclaration | FunctionDeclaration | AnonymousClassDeclaration | ClassDeclaration | Expression; + } + + export interface ExportAllDeclaration extends Node { + type: 'ExportAllDeclaration'; + source: Literal; + exported?: Identifier | Literal | null; + } + + export interface AwaitExpression extends Node { + type: 'AwaitExpression'; + argument: Expression; + } + + export interface ChainExpression extends Node { + type: 'ChainExpression'; + expression: MemberExpression | CallExpression; + } + + export interface ImportExpression extends Node { + type: 'ImportExpression'; + source: Expression; + } + + export interface ParenthesizedExpression extends Node { + type: 'ParenthesizedExpression'; + expression: Expression; + } + + export interface PropertyDefinition extends Node { + type: 'PropertyDefinition'; + key: Expression | PrivateIdentifier; + value?: Expression | null; + computed: boolean; + static: boolean; + } + + export interface PrivateIdentifier extends Node { + type: 'PrivateIdentifier'; + name: string; + } + + export interface StaticBlock extends Node { + type: 'StaticBlock'; + body: Array; + } + + export type Statement = + | ExpressionStatement + | BlockStatement + | EmptyStatement + | DebuggerStatement + | WithStatement + | ReturnStatement + | LabeledStatement + | BreakStatement + | ContinueStatement + | IfStatement + | SwitchStatement + | ThrowStatement + | TryStatement + | WhileStatement + | DoWhileStatement + | ForStatement + | ForInStatement + | ForOfStatement + | Declaration; + + export type Declaration = FunctionDeclaration | VariableDeclaration | ClassDeclaration; + + export type Expression = + | Identifier + | Literal + | ThisExpression + | ArrayExpression + | ObjectExpression + | FunctionExpression + | UnaryExpression + | UpdateExpression + | BinaryExpression + | AssignmentExpression + | LogicalExpression + | MemberExpression + | ConditionalExpression + | CallExpression + | NewExpression + | SequenceExpression + | ArrowFunctionExpression + | YieldExpression + | TemplateLiteral + | TaggedTemplateExpression + | ClassExpression + | MetaProperty + | AwaitExpression + | ChainExpression + | ImportExpression + | ParenthesizedExpression; + + export type Pattern = Identifier | MemberExpression | ObjectPattern | ArrayPattern | RestElement | AssignmentPattern; + + export type ModuleDeclaration = ImportDeclaration | ExportNamedDeclaration | ExportDefaultDeclaration | ExportAllDeclaration; + + export type AnyNode = + | Statement + | Expression + | Declaration + | ModuleDeclaration + | Literal + | Program + | SwitchCase + | CatchClause + | Property + | Super + | SpreadElement + | TemplateElement + | AssignmentProperty + | ObjectPattern + | ArrayPattern + | RestElement + | AssignmentPattern + | ClassBody + | MethodDefinition + | MetaProperty + | ImportSpecifier + | ImportDefaultSpecifier + | ImportNamespaceSpecifier + | ExportSpecifier + | AnonymousFunctionDeclaration + | AnonymousClassDeclaration + | PropertyDefinition + | PrivateIdentifier + | StaticBlock + | VariableDeclaration + | VariableDeclarator; + + /** Alias kept for back-compat with code that used the `Function` name from the old acorn.d.ts */ + export type Function = AcornFunction; +} diff --git a/packages/apps/node-runtime/src/lib/ast/mod.ts b/packages/apps/node-runtime/src/lib/ast/mod.ts new file mode 100644 index 0000000000000..84e340c77d412 --- /dev/null +++ b/packages/apps/node-runtime/src/lib/ast/mod.ts @@ -0,0 +1,69 @@ +import type { Node, AnyNode } from 'acorn'; +import { parse } from 'acorn'; +import { fullAncestor } from 'acorn-walk'; +import { generate } from 'astring'; + +import * as operations from './operations'; +import type { WalkerState } from './operations'; + +function fixAst(ast: Node): boolean { + const pendingOperations = [ + operations.fixLivechatIsOnlineCalls, + operations.checkReassignmentOfModifiedIdentifiers, + operations.fixRoomUsernamesCalls, + ]; + + // Have we touched the tree? + let isModified = false; + + while (pendingOperations.length) { + const ops = pendingOperations.splice(0); + const state: WalkerState = { + isModified: false, + functionIdentifiers: new Set(), + }; + + fullAncestor( + ast, + (node, state, ancestors, type) => { + ops.forEach((operation) => operation(node as AnyNode, state as WalkerState, ancestors as AnyNode[], type)); + }, + undefined, + state, + ); + + if (state.isModified) { + isModified = true; + } + + if (state.functionIdentifiers.size) { + pendingOperations.push( + operations.buildFixModifiedFunctionsOperation(state.functionIdentifiers), + operations.checkReassignmentOfModifiedIdentifiers, + ); + } + } + + return isModified; +} + +export function fixBrokenSynchronousAPICalls(appSource: string): string { + const astRootNode = parse(appSource, { + // Latest ecma version supported by this version of acorn. + ecmaVersion: 'latest', + // Allow everything, we don't want to complain if code is badly written + // Also, since the code itself has been transpiled, the chance of getting + // shenanigans is lower + allowReserved: true, + allowReturnOutsideFunction: true, + allowImportExportEverywhere: true, + allowAwaitOutsideFunction: true, + allowSuperOutsideMethod: true, + }); + + if (fixAst(astRootNode)) { + return generate(astRootNode); + } + + return appSource; +} diff --git a/packages/apps/node-runtime/src/lib/ast/operations.ts b/packages/apps/node-runtime/src/lib/ast/operations.ts new file mode 100644 index 0000000000000..79d929271a778 --- /dev/null +++ b/packages/apps/node-runtime/src/lib/ast/operations.ts @@ -0,0 +1,246 @@ +import type { + AnyNode, + AssignmentExpression, + AcornFunction as Function, + AwaitExpression, + Expression, + Identifier, + MethodDefinition, + Property, +} from 'acorn'; +import type { FullAncestorWalkerCallbackWithState } from 'acorn-walk'; + +export type WalkerState = { + isModified: boolean; + functionIdentifiers: Set; +}; + +export function getFunctionIdentifier(ancestors: AnyNode[], functionNodeIndex: number) { + const parent = ancestors[functionNodeIndex - 1]; + + // If there is a parent node and it's not a computed property, we can try to + // extract an identifier for our function from it. This needs to be done first + // because when functions are assigned to named symbols, this will be the only + // way to call it, even if the function itself has an identifier + // Consider the following block: + // + // const foo = function bar() {} + // + // Even though the function itself has a name, the only way to call it in the + // program is wiht `foo()` + if (parent && !(parent as Property | MethodDefinition).computed) { + // Several node types can have an id prop of type Identifier + const { id } = parent as unknown as { id?: Identifier }; + if (id?.type === 'Identifier') { + return id.name; + } + + // Usually assignments to object properties (MethodDefinition, Property) + const { key } = parent as MethodDefinition | Property; + if (key?.type === 'Identifier') { + return key.name; + } + + // Variable assignments have left hand side that can be used as Identifier + const { left } = parent as AssignmentExpression; + + // Simple assignment: `const fn = () => {}` + if (left?.type === 'Identifier') { + return left.name; + } + + // Object property assignment: `obj.fn = () => {}` + if (left?.type === 'MemberExpression' && !left.computed) { + return (left.property as Identifier).name; + } + } + + // nodeIndex needs to be the index of a Function node (either FunctionDeclaration or FunctionExpression) + const currentNode = ancestors[functionNodeIndex] as Function; + + // Function declarations or expressions can be directly named + if (currentNode.id?.type === 'Identifier') { + return currentNode.id.name; + } +} + +export function wrapWithAwait(node: Expression) { + if (!node.type.endsWith('Expression')) { + throw new Error(`Can't wrap "${node.type}" with await`); + } + + const innerNode: Expression = { ...node }; + + node.type = 'AwaitExpression'; + // starting here node has become an AwaitExpression + (node as AwaitExpression).argument = innerNode; + + Object.keys(node).forEach((key) => !['type', 'argument'].includes(key) && delete node[key as keyof AnyNode]); +} + +export function asyncifyScope(ancestors: AnyNode[], state: WalkerState) { + const functionNodeIndex = ancestors.findLastIndex((n) => 'async' in n); + if (functionNodeIndex === -1) return; + + // At this point this is a node with an "async" property, so it has to be + // of type Function - let TS know about that + const functionScopeNode = ancestors[functionNodeIndex] as Function; + + if (functionScopeNode.async) { + return; + } + + functionScopeNode.async = true; + + // If the parent of a function node is a call expression, we're talking about an IIFE + // Should we care about this case as well? + // const parentNode = ancestors[functionScopeIndex-1]; + // if (parentNode?.type === 'CallExpression' && ancestors[functionScopeIndex-2] && ancestors[functionScopeIndex-2].type !== 'AwaitExpression') { + // pendingOperations.push(buildFunctionPredicate(getFunctionIdentifier(ancestors, functionScopeIndex-2))); + // } + + const identifier = getFunctionIdentifier(ancestors, functionNodeIndex); + + // We can't fix calls of functions which name we can't determine at compile time + if (!identifier) return; + + state.functionIdentifiers.add(identifier); +} + +export function buildFixModifiedFunctionsOperation(functionIdentifiers: Set): FullAncestorWalkerCallbackWithState { + return function _fixModifiedFunctionsOperation(node, state, ancestors) { + if (node.type !== 'CallExpression') return; + + // This node is a simple call to a function, like `fn()` + let isWrappable = node.callee.type === 'Identifier' && functionIdentifiers.has(node.callee.name); + + // This node is a call to an object property or instance method, like `obj.fn()`, but not computed like `obj[fn]()` + isWrappable ||= + node.callee.type === 'MemberExpression' && + !node.callee.computed && + node.callee.property?.type === 'Identifier' && + functionIdentifiers.has(node.callee.property.name); + + // This is a weird dereferencing technique used by bundlers, and since we'll be dealing with bundled sources we have to check for it + // e.g. `r=(0,fn)(e)` + if (!isWrappable && node.callee.type === 'SequenceExpression') { + const [, secondExpression] = node.callee.expressions; + isWrappable = secondExpression?.type === 'Identifier' && functionIdentifiers.has(secondExpression.name); + isWrappable ||= + secondExpression?.type === 'MemberExpression' && + !secondExpression.computed && + secondExpression.property.type === 'Identifier' && + functionIdentifiers.has(secondExpression.property.name); + } + + if (!isWrappable) return; + + // ancestors[ancestors.length-1] === node, so here we're checking for parent node + const parentNode = ancestors[ancestors.length - 2]; + if (!parentNode || parentNode.type === 'AwaitExpression') return; + + wrapWithAwait(node); + asyncifyScope(ancestors, state); + + state.isModified = true; + }; +} + +export const checkReassignmentOfModifiedIdentifiers: FullAncestorWalkerCallbackWithState = ( + node, + { functionIdentifiers }, + _ancestors, +) => { + if (node.type === 'AssignmentExpression') { + if (node.operator !== '=') return; + + let identifier = ''; + + if (node.left.type === 'Identifier') identifier = node.left.name; + + if (node.left.type === 'MemberExpression' && !node.left.computed) { + identifier = (node.left.property as Identifier).name; + } + + if (!identifier || node.right.type !== 'Identifier' || !functionIdentifiers.has(node.right.name)) return; + + functionIdentifiers.add(identifier); + + return; + } + + if (node.type === 'VariableDeclarator') { + if (node.id.type !== 'Identifier' || functionIdentifiers.has(node.id.name)) return; + + if (node.init?.type !== 'Identifier' || !functionIdentifiers.has(node.init?.name)) return; + + functionIdentifiers.add(node.id.name); + + return; + } + + // "Property" is for plain objects, "PropertyDefinition" is for classes + // but both share the same structure + if (node.type === 'Property' || node.type === 'PropertyDefinition') { + if (node.key.type !== 'Identifier' || functionIdentifiers.has(node.key.name)) return; + + if (node.value?.type !== 'Identifier' || !functionIdentifiers.has(node.value.name)) return; + + functionIdentifiers.add(node.key.name); + } +}; + +export const fixLivechatIsOnlineCalls: FullAncestorWalkerCallbackWithState = (node, state, ancestors) => { + if (node.type !== 'MemberExpression' || node.computed) return; + + if ((node.property as Identifier).name !== 'isOnline') return; + + if (node.object.type !== 'CallExpression') return; + + if (node.object.callee.type !== 'MemberExpression') return; + + if ((node.object.callee.property as Identifier).name !== 'getLivechatReader') return; + + let parentIndex = ancestors.length - 2; + let targetNode = ancestors[parentIndex]; + + if (targetNode.type !== 'CallExpression') { + targetNode = node; + } else { + parentIndex--; + } + + // If we're already wrapped with an await, nothing to do + if (ancestors[parentIndex].type === 'AwaitExpression') return; + + // If we're in the middle of a chained member access, we can't wrap with await + if (ancestors[parentIndex].type === 'MemberExpression') return; + + wrapWithAwait(targetNode); + asyncifyScope(ancestors, state); + + state.isModified = true; +}; + +export const fixRoomUsernamesCalls: FullAncestorWalkerCallbackWithState = (node, state, ancestors) => { + if (node.type !== 'MemberExpression' || node.computed) return; + + if ((node.property as Identifier).name !== 'usernames') return; + + let parentIndex = ancestors.length - 2; + let targetNode = ancestors[parentIndex]; + + if (targetNode.type !== 'CallExpression') { + targetNode = node; + } else { + parentIndex--; + } + + // If we're already wrapped with an await, nothing to do + if (ancestors[parentIndex].type === 'AwaitExpression') return; + + wrapWithAwait(targetNode); + asyncifyScope(ancestors, state); + + state.isModified = true; +}; diff --git a/packages/apps/node-runtime/src/lib/ast/tests/data/ast_blocks.ts b/packages/apps/node-runtime/src/lib/ast/tests/data/ast_blocks.ts new file mode 100644 index 0000000000000..2f0d1b96fa263 --- /dev/null +++ b/packages/apps/node-runtime/src/lib/ast/tests/data/ast_blocks.ts @@ -0,0 +1,436 @@ +// @deno-types="../../../../acorn.d.ts" +import type { AnyNode, ClassDeclaration, ExpressionStatement, FunctionDeclaration, VariableDeclaration } from 'acorn'; + +/** + * Partial AST blocks to support testing. + * `start` and `end` properties are omitted for brevity. + */ + +type TestNodeExcerpt = { + code: string; + node: N; +}; + +export const FunctionDeclarationFoo: TestNodeExcerpt = { + code: 'function foo() {}', + node: { + type: 'FunctionDeclaration', + id: { + type: 'Identifier', + name: 'foo', + }, + expression: false, + generator: false, + async: false, + params: [], + body: { + type: 'BlockStatement', + body: [], + }, + }, +}; + +export const ConstFooAssignedFunctionExpression: TestNodeExcerpt = { + code: 'const foo = function() {}', + node: { + type: 'VariableDeclaration', + kind: 'const', + declarations: [ + { + type: 'VariableDeclarator', + id: { + type: 'Identifier', + name: 'foo', + }, + init: { + type: 'FunctionExpression', + id: null, + expression: false, + generator: false, + async: false, + params: [], + body: { + type: 'BlockStatement', + body: [], + }, + }, + }, + ], + }, +}; + +export const AssignmentExpressionOfArrowFunctionToFooIdentifier: TestNodeExcerpt = { + code: 'foo = () => {}', + node: { + type: 'ExpressionStatement', + expression: { + type: 'AssignmentExpression', + operator: '=', + left: { + type: 'Identifier', + name: 'foo', + }, + right: { + type: 'ArrowFunctionExpression', + id: null, + expression: false, + generator: false, + async: false, + params: [], + body: { + type: 'BlockStatement', + body: [], + }, + }, + }, + }, +}; + +export const AssignmentExpressionOfNamedFunctionToFooMemberExpression: TestNodeExcerpt = { + code: 'obj.foo = function bar() {}', + node: { + type: 'ExpressionStatement', + expression: { + type: 'AssignmentExpression', + operator: '=', + left: { + type: 'MemberExpression', + object: { + type: 'Identifier', + name: 'a', + }, + property: { + type: 'Identifier', + name: 'foo', + }, + computed: false, + optional: false, + }, + right: { + type: 'FunctionExpression', + id: null, + expression: false, + generator: false, + async: false, + params: [], + body: { + type: 'BlockStatement', + body: [], + }, + }, + }, + }, +}; + +export const MethodDefinitionOfFooInClassBar: TestNodeExcerpt = { + code: 'class Bar { foo() {} }', + node: { + type: 'ClassDeclaration', + id: { + type: 'Identifier', + name: 'Bar', + }, + superClass: null, + body: { + type: 'ClassBody', + body: [ + { + type: 'MethodDefinition', + key: { + type: 'Identifier', + name: 'foo', + }, + value: { + type: 'FunctionExpression', + id: null, + expression: false, + generator: false, + async: false, + params: [], + body: { + type: 'BlockStatement', + body: [], + }, + }, + kind: 'method', + computed: false, + static: false, + }, + ], + }, + }, +}; + +export const SimpleCallExpressionOfFoo: TestNodeExcerpt = { + code: 'foo()', + node: { + type: 'ExpressionStatement', + expression: { + type: 'CallExpression', + callee: { + type: 'Identifier', + name: 'foo', + }, + arguments: [], + optional: false, + }, + }, +}; + +export const SyncFunctionDeclarationWithAsyncCallExpression: TestNodeExcerpt = { + // NOTE: this is invalid syntax, it won't be parsed by acorn + // but it can be an intermediary state of the AST after we run + // `wrapWithAwait` on "bar" call expressions, for instance + code: 'function foo() { return () => await bar() }', + node: { + type: 'FunctionDeclaration', + id: { + type: 'Identifier', + name: 'foo', + }, + expression: false, + generator: false, + async: false, + params: [], + body: { + type: 'BlockStatement', + body: [ + { + type: 'ReturnStatement', + argument: { + type: 'ArrowFunctionExpression', + id: null, + expression: true, + generator: false, + async: false, + params: [], + body: { + type: 'AwaitExpression', + argument: { + type: 'CallExpression', + callee: { + type: 'Identifier', + name: 'bar', + }, + arguments: [], + optional: false, + }, + }, + }, + }, + ], + }, + }, +}; + +export const AssignmentOfFooToBar: TestNodeExcerpt = { + code: 'bar = foo', + node: { + type: 'ExpressionStatement', + expression: { + type: 'AssignmentExpression', + operator: '=', + left: { + type: 'Identifier', + name: 'bar', + }, + right: { + type: 'Identifier', + name: 'foo', + }, + }, + }, +}; + +export const AssignmentOfFooToBarMemberExpression: TestNodeExcerpt = { + code: 'obj.bar = foo', + node: { + type: 'ExpressionStatement', + expression: { + type: 'AssignmentExpression', + operator: '=', + left: { + type: 'MemberExpression', + computed: false, + optional: false, + object: { + type: 'Identifier', + name: 'obj', + }, + property: { + type: 'Identifier', + name: 'bar', + }, + }, + right: { + type: 'Identifier', + name: 'foo', + }, + }, + }, +}; + +export const AssignmentOfFooToBarVariableDeclarator: TestNodeExcerpt = { + code: 'const bar = foo', + node: { + type: 'VariableDeclaration', + kind: 'const', + declarations: [ + { + type: 'VariableDeclarator', + id: { + type: 'Identifier', + name: 'bar', + }, + init: { + type: 'Identifier', + name: 'foo', + }, + }, + ], + }, +}; + +export const AssignmentOfFooToBarPropertyDefinition: TestNodeExcerpt = { + code: 'class baz { bar = foo }', + node: { + type: 'ClassDeclaration', + id: { + type: 'Identifier', + name: 'baz', + }, + superClass: null, + body: { + type: 'ClassBody', + body: [ + { + type: 'PropertyDefinition', + static: false, + computed: false, + key: { + type: 'Identifier', + name: 'bar', + }, + value: { + type: 'Identifier', + name: 'foo', + }, + }, + ], + }, + }, +}; + +const fixSimpleCallExpressionCode = ` +function bar() { + const a = foo(); + + return a; +}`; + +export const FixSimpleCallExpression: TestNodeExcerpt = { + code: fixSimpleCallExpressionCode, + node: { + type: 'FunctionDeclaration', + id: { + type: 'Identifier', + name: 'bar', + }, + expression: false, + generator: false, + async: false, + params: [], + body: { + type: 'BlockStatement', + body: [ + { + type: 'VariableDeclaration', + kind: 'const', + declarations: [ + { + type: 'VariableDeclarator', + id: { + type: 'Identifier', + name: 'a', + }, + init: { + type: 'CallExpression', + callee: { + type: 'Identifier', + name: 'foo', + }, + arguments: [], + optional: false, + }, + }, + ], + }, + { + type: 'ReturnStatement', + argument: { + type: 'Identifier', + name: 'a', + }, + }, + ], + }, + }, +}; + +export const ArrowFunctionDerefCallExpression: TestNodeExcerpt = { + // NOTE: this call strategy is widely used by bundlers; it's used to sever the `this` + // reference in the method from the object that contains it. This is mostly because + // the bundler wants to ensure that it does not messes up the bindings in the code it + // generates. + // + // This would be similar to doing `foo.call(undefined)` + code: 'const bar = () => (0, e.foo)();', + node: { + type: 'VariableDeclaration', + kind: 'const', + declarations: [ + { + type: 'VariableDeclarator', + id: { + type: 'Identifier', + name: 'bar', + }, + init: { + type: 'ArrowFunctionExpression', + id: null, + expression: true, + generator: false, + async: false, + params: [], + body: { + type: 'CallExpression', + optional: false, + arguments: [], + callee: { + type: 'SequenceExpression', + expressions: [ + { + type: 'Literal', + value: 0, + }, + { + type: 'MemberExpression', + object: { + type: 'Identifier', + name: 'e', + }, + property: { + type: 'Identifier', + name: 'foo', + }, + computed: false, + optional: false, + }, + ], + }, + }, + }, + }, + ], + }, +}; diff --git a/packages/apps/node-runtime/src/lib/ast/tests/operations.test.ts b/packages/apps/node-runtime/src/lib/ast/tests/operations.test.ts new file mode 100644 index 0000000000000..0417f6c3c614f --- /dev/null +++ b/packages/apps/node-runtime/src/lib/ast/tests/operations.test.ts @@ -0,0 +1,261 @@ +import { assertNotEquals } from 'https://deno.land/std@0.203.0/assert/assert_not_equals'; +import { assertEquals, assertThrows } from 'https://deno.land/std@0.203.0/assert/mod'; +import { beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd'; + +import type { WalkerState } from '../operations'; +import { + asyncifyScope, + buildFixModifiedFunctionsOperation, + checkReassignmentOfModifiedIdentifiers, + getFunctionIdentifier, + wrapWithAwait, +} from '../operations'; +import { + ArrowFunctionDerefCallExpression, + AssignmentExpressionOfArrowFunctionToFooIdentifier, + AssignmentExpressionOfNamedFunctionToFooMemberExpression, + AssignmentOfFooToBar, + AssignmentOfFooToBarMemberExpression, + AssignmentOfFooToBarPropertyDefinition, + AssignmentOfFooToBarVariableDeclarator, + ConstFooAssignedFunctionExpression, + FixSimpleCallExpression, + FunctionDeclarationFoo, + MethodDefinitionOfFooInClassBar, + SimpleCallExpressionOfFoo, + SyncFunctionDeclarationWithAsyncCallExpression, +} from './data/ast_blocks'; +import type { + AnyNode, + ArrowFunctionExpression, + AssignmentExpression, + AwaitExpression, + Expression, + MethodDefinition, + ReturnStatement, + VariableDeclaration, +} from '../../../acorn.d'; + +describe('getFunctionIdentifier', () => { + it(`identifies the name "foo" for the code \`${FunctionDeclarationFoo.code}\``, () => { + // ancestors array is built by the walking lib + const nodeAncestors = [FunctionDeclarationFoo.node]; + const functionNodeIndex = 0; + assertEquals('foo', getFunctionIdentifier(nodeAncestors, functionNodeIndex)); + }); + + it(`identifies the name "foo" for the code \`${ConstFooAssignedFunctionExpression.code}\``, () => { + // ancestors array is built by the walking lib + const nodeAncestors = [ + ConstFooAssignedFunctionExpression.node, // VariableDeclaration + ConstFooAssignedFunctionExpression.node.declarations[0], // VariableDeclarator + ConstFooAssignedFunctionExpression.node.declarations[0].init!, // FunctionExpression + ]; + const functionNodeIndex = 2; + assertEquals('foo', getFunctionIdentifier(nodeAncestors, functionNodeIndex)); + }); + + it(`identifies the name "foo" for the code \`${AssignmentExpressionOfArrowFunctionToFooIdentifier.code}\``, () => { + // ancestors array is built by the walking lib + const nodeAncestors = [ + AssignmentExpressionOfArrowFunctionToFooIdentifier.node, // ExpressionStatement + AssignmentExpressionOfArrowFunctionToFooIdentifier.node.expression, // AssignmentExpression + (AssignmentExpressionOfArrowFunctionToFooIdentifier.node.expression as AssignmentExpression).right, // ArrowFunctionExpression + ]; + const functionNodeIndex = 2; + assertEquals('foo', getFunctionIdentifier(nodeAncestors, functionNodeIndex)); + }); + + it(`identifies the name "foo" for the code \`${AssignmentExpressionOfNamedFunctionToFooMemberExpression.code}\``, () => { + // ancestors array is built by the walking lib + const nodeAncestors = [ + AssignmentExpressionOfNamedFunctionToFooMemberExpression.node, // ExpressionStatement + AssignmentExpressionOfNamedFunctionToFooMemberExpression.node.expression, // AssignmentExpression + (AssignmentExpressionOfNamedFunctionToFooMemberExpression.node.expression as AssignmentExpression).right, // FunctionExpression + ]; + const functionNodeIndex = 2; + assertEquals('foo', getFunctionIdentifier(nodeAncestors, functionNodeIndex)); + }); + + it(`identifies the name "foo" for the code \`${MethodDefinitionOfFooInClassBar.code}\``, () => { + // ancestors array is built by the walking lib + const nodeAncestors = [ + MethodDefinitionOfFooInClassBar.node, // ClassDeclaration + MethodDefinitionOfFooInClassBar.node.body, // ClassBody + MethodDefinitionOfFooInClassBar.node.body!.body[0], // MethodDefinition + (MethodDefinitionOfFooInClassBar.node.body!.body[0] as MethodDefinition).value, // FunctionExpression + ]; + const functionNodeIndex = 3; + assertEquals('foo', getFunctionIdentifier(nodeAncestors, functionNodeIndex)); + }); +}); + +describe('wrapWithAwait', () => { + it('wraps a call expression with await', () => { + const node = structuredClone(SimpleCallExpressionOfFoo.node.expression); + wrapWithAwait(node); + + assertEquals('AwaitExpression', node.type); + assertNotEquals(SimpleCallExpressionOfFoo.node.expression.type, node.type); + assertEquals(SimpleCallExpressionOfFoo.node.expression, (node as AwaitExpression).argument); + }); + + it('throws if node is not an expression', () => { + const node = structuredClone(SimpleCallExpressionOfFoo.node); + assertThrows(() => wrapWithAwait(node as unknown as Expression)); + }); +}); + +describe('asyncifyScope', () => { + it('makes only the first function scope async', () => { + const node = structuredClone(SyncFunctionDeclarationWithAsyncCallExpression.node); + const ancestors: AnyNode[] = [ + node, // FunctionDeclaration + node.body, // BlockStatement + node.body!.body[0], // ReturnStatement + (node.body!.body[0] as ReturnStatement).argument!, // ArrowFunctionExpression + ((node.body!.body[0] as ReturnStatement).argument! as ArrowFunctionExpression).body, // AwaitExpression + (((node.body!.body[0] as ReturnStatement).argument! as ArrowFunctionExpression).body as AwaitExpression).argument, // CallExpression + ]; + const state: WalkerState = { + isModified: false, + functionIdentifiers: new Set(), + }; + + asyncifyScope(ancestors, state); + + // Assert the function did indeed change the expression to async + assertEquals(((node.body.body[0] as ReturnStatement).argument as ArrowFunctionExpression).async, true); + + // Assert the function did NOT change all ancestors in the chain + assertEquals(node.async, false); + + // Assert it couldn't find a function identifier + assertEquals(state.functionIdentifiers.size, 0); + }); +}); + +describe('checkReassignmentofModifiedIdentifiers', () => { + it(`identifies the reassignment of "foo" in the code "${AssignmentOfFooToBar.code}"`, () => { + const node = structuredClone(AssignmentOfFooToBar.node); + const ancestors: AnyNode[] = [ + node, // ExpressionStatement + node.expression, // AssignmentExpression + (node.expression as AssignmentExpression).right, // Identifier + ]; + const state: WalkerState = { + isModified: true, + functionIdentifiers: new Set(['foo']), + }; + + checkReassignmentOfModifiedIdentifiers(node.expression, state, ancestors, ''); + + assertEquals(state.functionIdentifiers.has('bar'), true); + }); + + it(`identifies the reassignment of "foo" in the code "${AssignmentOfFooToBarMemberExpression.code}"`, () => { + const node = structuredClone(AssignmentOfFooToBarMemberExpression.node); + const ancestors: AnyNode[] = [ + node, // ExpressionStatement + node.expression, // AssignmentExpression + (node.expression as AssignmentExpression).right, // Identifier + ]; + const state: WalkerState = { + isModified: true, + functionIdentifiers: new Set(['foo']), + }; + + checkReassignmentOfModifiedIdentifiers(node.expression, state, ancestors, ''); + + assertEquals(state.functionIdentifiers.has('bar'), true); + }); + + it(`identifies the reassignment of "foo" in the code "${AssignmentOfFooToBarVariableDeclarator.code}"`, () => { + const node = structuredClone(AssignmentOfFooToBarVariableDeclarator.node); + const ancestors: AnyNode[] = [ + node, // VariableDeclaration + node.declarations[0], // VariableDeclarator + ]; + const state: WalkerState = { + isModified: true, + functionIdentifiers: new Set(['foo']), + }; + + checkReassignmentOfModifiedIdentifiers(node.declarations[0], state, ancestors, ''); + + assertEquals(state.functionIdentifiers.has('bar'), true); + }); + + it(`identifies the reassignment of "foo" in the code "${AssignmentOfFooToBarPropertyDefinition.code}"`, () => { + const node = structuredClone(AssignmentOfFooToBarPropertyDefinition.node); + const ancestors: AnyNode[] = [ + node, // ClassDeclaration + node.body, // ClassBody + node.body.body[0], // PropertyDefinition + ]; + const state: WalkerState = { + isModified: true, + functionIdentifiers: new Set(['foo']), + }; + + checkReassignmentOfModifiedIdentifiers(node.body.body[0], state, ancestors, ''); + + assertEquals(state.functionIdentifiers.has('bar'), true); + }); +}); + +describe('buildFixModifiedFunctionsOperation', function () { + const state: WalkerState = { + isModified: false, + functionIdentifiers: new Set(['foo']), + }; + + const fixFunction = buildFixModifiedFunctionsOperation(state.functionIdentifiers); + + beforeEach(() => { + state.isModified = false; + state.functionIdentifiers = new Set(['foo']); + }); + + it(`fixes calls of "foo" in the code "${FixSimpleCallExpression.code}"`, () => { + const node = structuredClone(FixSimpleCallExpression.node); + const ancestors: AnyNode[] = [ + node, // FunctionDeclaration + node.body, // BlockStatement + node.body.body[0], // VariableDeclaration + (node.body.body[0] as VariableDeclaration).declarations[0], // VariableDeclarator + (node.body.body[0] as VariableDeclaration).declarations[0].init!, // CallExpression + ]; + + fixFunction(ancestors[4], state, ancestors, ''); + + assertEquals(state.isModified, true); + assertEquals(state.functionIdentifiers.has('bar'), true); + assertNotEquals(FixSimpleCallExpression.node, node); + assertEquals(node.async, true); + assertEquals(ancestors[4].type, 'AwaitExpression'); + }); + + it(`fixes calls of "foo" in the code "${ArrowFunctionDerefCallExpression.code}"`, () => { + const node = structuredClone(ArrowFunctionDerefCallExpression.node); + const ancestors: AnyNode[] = [ + node, // VariableDeclaration + node.declarations[0], // VariableDeclarator + node.declarations[0].init!, // ArrowFunctionExpression + (node.declarations[0].init as ArrowFunctionExpression).body, // CallExpression + ]; + + fixFunction(ancestors[3], state, ancestors, ''); + + // Recorded that a modification has been made + assertEquals(state.isModified, true); + // Recorded that the enclosing scope of the call also requires fixing + assertEquals(state.functionIdentifiers.has('bar'), true); + // Original node and fixed node are different + assertNotEquals(ArrowFunctionDerefCallExpression.node, node); + // The function call is now await'ed + assertEquals(ancestors[3].type, 'AwaitExpression'); + // The parent function of the call is now marked as async + assertEquals((ancestors[2] as ArrowFunctionExpression).async, true); + }); +}); diff --git a/packages/apps/node-runtime/src/lib/codec.ts b/packages/apps/node-runtime/src/lib/codec.ts new file mode 100644 index 0000000000000..6319f2fc76fbb --- /dev/null +++ b/packages/apps/node-runtime/src/lib/codec.ts @@ -0,0 +1,54 @@ +import { Buffer } from 'node:buffer'; + +import { decode, Decoder, Encoder, ExtensionCodec } from '@msgpack/msgpack'; +import type { App as _App } from '@rocket.chat/apps-engine/definition/App'; + +import { applySecureFields, type WithSecureFields } from './secureFields'; + +const FUNCTION_DISABLER_EXT = 0; +const BUFFER_HANDLER_EXT = 1; +const SECURE_FIELDS_HANDLER_EXT = 2; + +const { App } = require('@rocket.chat/apps-engine/definition/App.js') as { + App: typeof _App; +}; + +const extensionCodec = new ExtensionCodec(); + +extensionCodec.register({ + type: FUNCTION_DISABLER_EXT, + encode: (object: unknown) => { + // We don't care about functions, but also don't want to throw an error + if (typeof object === 'function' || object instanceof App) { + return new Uint8Array(0); + } + + return null; + }, + decode: (_data: Uint8Array) => undefined, +}); + +// Since Deno doesn't have Buffer by default, we need to use Uint8Array +extensionCodec.register({ + type: BUFFER_HANDLER_EXT, + encode: (object: unknown) => { + if (object instanceof Buffer) { + return new Uint8Array(object.buffer, object.byteOffset, object.byteLength); + } + + return null; + }, + // msgpack will reuse the Uint8Array instance, so WE NEED to copy it instead of simply creating a view + decode: (data: Uint8Array) => { + return Buffer.from(data); + }, +}); + +extensionCodec.register({ + type: SECURE_FIELDS_HANDLER_EXT, + encode: (_object: unknown) => null, + decode: (data: Uint8Array) => applySecureFields(decode(data, { extensionCodec }) as WithSecureFields>), +}); + +export const encoder = new Encoder({ extensionCodec }); +export const decoder = new Decoder({ extensionCodec }); diff --git a/packages/apps/node-runtime/src/lib/loader-hook.ts b/packages/apps/node-runtime/src/lib/loader-hook.ts new file mode 100644 index 0000000000000..1a21e86a89a8e --- /dev/null +++ b/packages/apps/node-runtime/src/lib/loader-hook.ts @@ -0,0 +1,21 @@ +import { registerHooks } from 'node:module'; +import path from 'node:path'; + +// This file compiles to dist/lib/loader-hook.js. +// Three levels up from dist/lib/ lands on packages/apps/ — the @rocket.chat/apps package root. +const appsPackageDir = path.resolve(__dirname, '../../..'); + +const PACKAGE_PREFIX = '@rocket.chat/apps'; + +registerHooks({ + resolve(specifier, context, nextResolve) { + // console.log({ specifier }); + if (specifier === PACKAGE_PREFIX || specifier.startsWith(`${PACKAGE_PREFIX}/`)) { + const subpath = specifier.slice(PACKAGE_PREFIX.length).replace(/^\//, ''); + const localPath = subpath ? path.join(appsPackageDir, subpath) : appsPackageDir; + console.error({ specifier, localPath }); + return nextResolve(localPath, context); + } + return nextResolve(specifier, context); + }, +}); diff --git a/packages/apps/node-runtime/src/lib/logger.ts b/packages/apps/node-runtime/src/lib/logger.ts new file mode 100644 index 0000000000000..5fa44b96cc6ac --- /dev/null +++ b/packages/apps/node-runtime/src/lib/logger.ts @@ -0,0 +1,164 @@ +import type { ILogEntry } from '@rocket.chat/apps-engine/definition/accessors/ILogEntry'; +import type { ILogger } from '@rocket.chat/apps-engine/definition/accessors/ILogger'; +import type { AppMethod } from '@rocket.chat/apps-engine/definition/metadata/AppMethod'; +import stackTrace from 'stack-trace'; + +import { AppObjectRegistry } from '../AppObjectRegistry'; + +export interface StackFrame { + getTypeName(): string; + getFunctionName(): string; + getMethodName(): string; + getFileName(): string; + getLineNumber(): number; + getColumnNumber(): number; + isNative(): boolean; + isConstructor(): boolean; +} + +enum LogMessageSeverity { + DEBUG = 'debug', + INFORMATION = 'info', + LOG = 'log', + WARNING = 'warning', + ERROR = 'error', + SUCCESS = 'success', +} + +type Entry = { + caller: string; + severity: LogMessageSeverity; + method: string; + timestamp: Date; + args: Array; +}; + +interface ILoggerStorageEntry { + appId: string; + method: string; + entries: Array; + startTime: Date; + endTime: Date; + totalTime: number; + _createdAt: Date; +} + +export class Logger implements ILogger { + public method: `${AppMethod}`; + + private entries: Array; + + private start: Date; + + constructor(method: string) { + this.method = method as `${AppMethod}`; + this.entries = []; + this.start = new Date(); + } + + public debug(...args: Array): void { + this.addEntry(LogMessageSeverity.DEBUG, this.getStack(stackTrace.get()), ...args); + } + + public info(...args: Array): void { + this.addEntry(LogMessageSeverity.INFORMATION, this.getStack(stackTrace.get()), ...args); + } + + public log(...args: Array): void { + this.addEntry(LogMessageSeverity.LOG, this.getStack(stackTrace.get()), ...args); + } + + public warn(...args: Array): void { + this.addEntry(LogMessageSeverity.WARNING, this.getStack(stackTrace.get()), ...args); + } + + public error(...args: Array): void { + this.addEntry(LogMessageSeverity.ERROR, this.getStack(stackTrace.get()), ...args); + } + + public success(...args: Array): void { + this.addEntry(LogMessageSeverity.SUCCESS, this.getStack(stackTrace.get()), ...args); + } + + private addEntry(severity: LogMessageSeverity, caller: string, ...items: Array): void { + const i = items.map((args) => { + if (args instanceof Error) { + return JSON.stringify(args, Object.getOwnPropertyNames(args)); + } + if (typeof args === 'object' && args !== null && 'stack' in args) { + return JSON.stringify(args, Object.getOwnPropertyNames(args)); + } + if (typeof args === 'object' && args !== null && 'message' in args) { + return JSON.stringify(args, Object.getOwnPropertyNames(args)); + } + const str = JSON.stringify(args, null, 2); + return str ? JSON.parse(str) : str; // force call toJSON to prevent circular references + }); + + this.entries.push({ + caller, + severity, + method: this.method, + timestamp: new Date(), + args: i, + }); + } + + private getStack(stack: Array): string { + let func = 'anonymous'; + + if (stack.length === 1) { + return func; + } + + const frame = stack[1]; + + if (frame.getMethodName() === null) { + func = 'anonymous OR constructor'; + } else { + func = frame.getMethodName(); + } + + if (frame.getFunctionName() !== null) { + func = `${func} -> ${frame.getFunctionName()}`; + } + + return func; + } + + public getTotalTime(): number { + return new Date().getTime() - this.start.getTime(); + } + + public getEntries(): Array { + return this.entries as Array; + } + + public getMethod(): `${AppMethod}` { + return this.method; + } + + public getStartTime(): Date { + return this.start; + } + + public getEndTime(): Date { + return new Date(); + } + + public hasEntries(): boolean { + return this.entries.length > 0; + } + + public getLogs(): ILoggerStorageEntry { + return { + appId: AppObjectRegistry.get('id')!, + method: this.method, + entries: this.entries, + startTime: this.start, + endTime: new Date(), + totalTime: this.getTotalTime(), + _createdAt: new Date(), + }; + } +} diff --git a/packages/apps/node-runtime/src/lib/messenger.ts b/packages/apps/node-runtime/src/lib/messenger.ts new file mode 100644 index 0000000000000..e74ea2d05996f --- /dev/null +++ b/packages/apps/node-runtime/src/lib/messenger.ts @@ -0,0 +1,204 @@ +import EventEmitter from 'node:events'; + +import * as jsonrpc from 'jsonrpc-lite'; + +import { encoder } from './codec'; +import type { RequestContext } from './requestContext'; + +export type RequestDescriptor = Pick; + +export type NotificationDescriptor = Pick; + +export type SuccessResponseDescriptor = Pick; + +export type ErrorResponseDescriptor = Pick; + +export type JsonRpcRequest = jsonrpc.IParsedObjectRequest | jsonrpc.IParsedObjectNotification; +export type JsonRpcResponse = jsonrpc.IParsedObjectSuccess | jsonrpc.IParsedObjectError; + +export function isRequest(message: jsonrpc.IParsedObject): message is JsonRpcRequest { + return message.type === 'request' || message.type === 'notification'; +} + +export function isResponse(message: jsonrpc.IParsedObject): message is JsonRpcResponse { + return message.type === 'success' || message.type === 'error'; +} + +export function isErrorResponse(message: jsonrpc.JsonRpc): message is jsonrpc.ErrorObject { + return message instanceof jsonrpc.ErrorObject; +} + +const COMMAND_PONG = '_zPONG'; + +export const RPCResponseObserver = new EventEmitter(); + +export const Queue = new (class Queue { + private queue: Uint8Array[] = []; + + private isProcessing = false; + + private async processQueue() { + if (this.isProcessing) { + return; + } + + this.isProcessing = true; + + while (this.queue.length) { + const message = this.queue.shift(); + + if (message) { + await Transport.send(message); + } + } + + this.isProcessing = false; + } + + public enqueue(message: jsonrpc.JsonRpc | typeof COMMAND_PONG) { + this.queue.push(encoder.encode(message)); + void this.processQueue(); + } + + public getCurrentSize() { + return this.queue.length; + } +})(); + +export const Transport = new (class Transporter { + private selectedTransport: Transporter['stdoutTransport'] | Transporter['noopTransport']; + + constructor() { + this.selectedTransport = this.stdoutTransport.bind(this); + } + + private async stdoutTransport(message: Uint8Array): Promise { + await new Promise((resolve, reject) => { + process.stdout.write(message, (err) => (err ? reject(err) : resolve())); + }); + } + + private async noopTransport(_message: Uint8Array): Promise {} + + public selectTransport(transport: 'stdout' | 'noop'): void { + switch (transport) { + case 'stdout': + this.selectedTransport = this.stdoutTransport.bind(this); + break; + case 'noop': + this.selectedTransport = this.noopTransport.bind(this); + break; + } + } + + public send(message: Uint8Array): Promise { + return this.selectedTransport(message); + } +})(); + +export function parseMessage(message: string | Record) { + let parsed: jsonrpc.IParsedObject | jsonrpc.IParsedObject[]; + + if (typeof message === 'string') { + parsed = jsonrpc.parse(message); + } else { + parsed = jsonrpc.parseObject(message); + } + + if (Array.isArray(parsed)) { + throw jsonrpc.error(null, jsonrpc.JsonRpcError.invalidRequest(null)); + } + + if (parsed.type === 'invalid') { + throw jsonrpc.error(null, parsed.payload); + } + + return parsed; +} + +export async function sendInvalidRequestError(): Promise { + const rpc = jsonrpc.error(null, jsonrpc.JsonRpcError.invalidRequest(null)); + + await Queue.enqueue(rpc); +} + +export async function sendInvalidParamsError(id: jsonrpc.ID): Promise { + const rpc = jsonrpc.error(id, jsonrpc.JsonRpcError.invalidParams(null)); + + await Queue.enqueue(rpc); +} + +export async function sendParseError(): Promise { + const rpc = jsonrpc.error(null, jsonrpc.JsonRpcError.parseError(null)); + + await Queue.enqueue(rpc); +} + +export async function sendMethodNotFound(id: jsonrpc.ID): Promise { + const rpc = jsonrpc.error(id, jsonrpc.JsonRpcError.methodNotFound(null)); + + await Queue.enqueue(rpc); +} + +export async function errorResponse( + { error: { message, code = -32000, data = {} }, id }: ErrorResponseDescriptor, + req?: RequestContext, +): Promise { + const { logger } = req?.context || {}; + + if (logger?.hasEntries()) { + data.logs = logger.getLogs(); + } + + const rpc = jsonrpc.error(id, new jsonrpc.JsonRpcError(message, code, data)); + + await Queue.enqueue(rpc); +} + +export async function successResponse({ id, result }: SuccessResponseDescriptor, req: RequestContext): Promise { + const payload = { value: result } as Record; + const { logger } = req.context; + + if (logger.hasEntries()) { + payload.logs = logger.getLogs(); + } + + const rpc = jsonrpc.success(id, payload); + + await Queue.enqueue(rpc); +} + +export function pongResponse(): Promise { + return Promise.resolve(Queue.enqueue(COMMAND_PONG)); +} + +export async function sendRequest(requestDescriptor: RequestDescriptor): Promise { + const request = jsonrpc.request(Math.random().toString(36).slice(2), requestDescriptor.method, requestDescriptor.params); + + // TODO: add timeout to this + const responsePromise = new Promise((resolve, reject) => { + const handler = (payload: { error: Error } | { detail: jsonrpc.SuccessObject }) => { + if ('error' in payload) { + return reject(payload.error); + } + + return resolve(payload.detail); + }; + + RPCResponseObserver.once(`response:${request.id}`, handler); + }); + + await Queue.enqueue(request); + + return responsePromise as Promise; +} + +export function sendNotification({ method, params }: NotificationDescriptor) { + const request = jsonrpc.notification(method, params); + + Queue.enqueue(request); +} + +export function log(params: jsonrpc.RpcParams) { + sendNotification({ method: 'log', params }); +} diff --git a/packages/apps/node-runtime/src/lib/metricsCollector.ts b/packages/apps/node-runtime/src/lib/metricsCollector.ts new file mode 100644 index 0000000000000..bb0de35cb4f7d --- /dev/null +++ b/packages/apps/node-runtime/src/lib/metricsCollector.ts @@ -0,0 +1,21 @@ +import { Queue } from './messenger'; + +export function collectMetrics() { + return { + pid: process.pid, + queueSize: Queue.getCurrentSize(), + }; +} + +const encoder = new TextEncoder(); + +/** + * Sends metrics collected from the system via stderr + */ +export async function sendMetrics() { + const metrics = collectMetrics(); + + await new Promise((resolve, reject) => { + process.stderr.write(encoder.encode(JSON.stringify(metrics)), (err) => (err ? reject(err) : resolve())); + }); +} diff --git a/packages/apps/node-runtime/src/lib/parseArgs.ts b/packages/apps/node-runtime/src/lib/parseArgs.ts new file mode 100644 index 0000000000000..801e77844a820 --- /dev/null +++ b/packages/apps/node-runtime/src/lib/parseArgs.ts @@ -0,0 +1,25 @@ +import { parseArgs as $parseArgs } from 'node:util'; + +export type ParsedArgs = { + subprocess: string; + spawnId: number; + metricsReportFrequencyInMs?: number; +}; + +export function parseArgs(args: string[]): ParsedArgs { + const { values } = $parseArgs({ + args, + options: { + subprocess: { type: 'string' }, + spawnId: { type: 'string' }, + metricsReportFrequencyInMs: { type: 'string' }, + }, + strict: false, + }); + + return { + subprocess: (values.subprocess as string) ?? '', + spawnId: Number(values.spawnId ?? 0), + metricsReportFrequencyInMs: values.metricsReportFrequencyInMs !== undefined ? Number(values.metricsReportFrequencyInMs) : undefined, + }; +} diff --git a/packages/apps/node-runtime/src/lib/requestContext.ts b/packages/apps/node-runtime/src/lib/requestContext.ts new file mode 100644 index 0000000000000..be5c5d6a98c07 --- /dev/null +++ b/packages/apps/node-runtime/src/lib/requestContext.ts @@ -0,0 +1,10 @@ +import type { RequestObject } from 'jsonrpc-lite'; + +import type { Logger } from './logger'; + +export type RequestContext = RequestObject & { + context: { + logger: Logger; + [key: string]: unknown; + }; +}; diff --git a/packages/apps/node-runtime/src/lib/room.ts b/packages/apps/node-runtime/src/lib/room.ts new file mode 100644 index 0000000000000..83b5ecd4c0308 --- /dev/null +++ b/packages/apps/node-runtime/src/lib/room.ts @@ -0,0 +1,118 @@ +import type { IAbacAttributeDefinition } from '@rocket.chat/apps-engine/definition/abac/AbacAttributes'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; +import type { RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser'; + +/** Minimal interface covering the only AppManager capability used by Room */ +interface IRoomManager { + getBridges(): { + getInternalBridge(): { + doGetUsernamesOfRoomById(id: string): Promise>; + }; + }; +} + +const PrivateManager = Symbol('RoomPrivateManager'); + +export class Room { + public id: string | undefined; + + public displayName?: string; + + public slugifiedName: string | undefined; + + public type: RoomType | undefined; + + public creator: IUser | undefined; + + public isDefault?: boolean; + + public isReadOnly?: boolean; + + public displaySystemMessages?: boolean; + + public messageCount?: number; + + public createdAt?: Date; + + public updatedAt?: Date; + + public lastModifiedAt?: Date; + + public customFields?: { [key: string]: unknown }; + + public userIds?: Array; + + public abacAttributes?: IAbacAttributeDefinition[]; + + private _USERNAMES: Promise> | undefined; + + private [PrivateManager]: IRoomManager | undefined; + + /** + * @deprecated + */ + public get usernames(): Promise> { + if (!this.id) return Promise.resolve([]); + + if (!this._USERNAMES) { + this._USERNAMES = this[PrivateManager]?.getBridges().getInternalBridge().doGetUsernamesOfRoomById(this.id); + } + + return this._USERNAMES || Promise.resolve([]); + } + + public set usernames(usernames) {} + + public constructor(room: IRoom, manager: IRoomManager) { + Object.assign(this, room); + + Object.defineProperty(this, PrivateManager, { + configurable: false, + enumerable: false, + writable: false, + value: manager, + }); + } + + get value(): object { + return { + id: this.id, + displayName: this.displayName, + slugifiedName: this.slugifiedName, + type: this.type, + creator: this.creator, + isDefault: this.isDefault, + isReadOnly: this.isReadOnly, + displaySystemMessages: this.displaySystemMessages, + messageCount: this.messageCount, + createdAt: this.createdAt, + updatedAt: this.updatedAt, + lastModifiedAt: this.lastModifiedAt, + customFields: this.customFields, + userIds: this.userIds, + abacAttributes: this.abacAttributes, + }; + } + + public async getUsernames(): Promise> { + // Get usernames + if (!this._USERNAMES) { + this._USERNAMES = this[PrivateManager]?.getBridges().getInternalBridge().doGetUsernamesOfRoomById(this.id!); + } + + return (await this._USERNAMES) || []; + } + + public toJSON() { + return this.value; + } + + public toString() { + return this.value; + } + + public valueOf() { + return this.value; + } +} diff --git a/packages/apps/node-runtime/src/lib/roomFactory.ts b/packages/apps/node-runtime/src/lib/roomFactory.ts new file mode 100644 index 0000000000000..803d55c2a537f --- /dev/null +++ b/packages/apps/node-runtime/src/lib/roomFactory.ts @@ -0,0 +1,29 @@ +import type { AppManager } from '@rocket.chat/apps/dist/server/AppManager'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; + +import { formatErrorResponse } from './accessors/formatResponseErrorHandler'; +import type { AppAccessors } from './accessors/mod'; +import { Room } from './room'; + +const getMockAppManager = (senderFn: AppAccessors['senderFn']) => ({ + getBridges: () => ({ + getInternalBridge: () => ({ + doGetUsernamesOfRoomById: (roomId: string) => { + return senderFn({ + method: 'bridges:getInternalBridge:doGetUsernamesOfRoomById', + params: [roomId], + }) + .then((result) => result.result) + .catch((err) => { + throw formatErrorResponse(err); + }); + }, + }), + }), +}); + +export default function createRoom(room: IRoom, senderFn: AppAccessors['senderFn']) { + const mockAppManager = getMockAppManager(senderFn); + + return new Room(room, mockAppManager as unknown as AppManager); +} diff --git a/packages/apps/node-runtime/src/lib/sanitizeDeprecatedUsage.ts b/packages/apps/node-runtime/src/lib/sanitizeDeprecatedUsage.ts new file mode 100644 index 0000000000000..aa2898418db81 --- /dev/null +++ b/packages/apps/node-runtime/src/lib/sanitizeDeprecatedUsage.ts @@ -0,0 +1,20 @@ +import { fixBrokenSynchronousAPICalls } from './ast/mod'; + +function hasPotentialDeprecatedUsage(source: string) { + return ( + // potential usage of Room.usernames getter + source.includes('.usernames') || + // potential usage of LivechatRead.isOnline method + source.includes('.isOnline(') || + // potential usage of LivechatCreator.createToken method + source.includes('.createToken(') + ); +} + +export function sanitizeDeprecatedUsage(source: string) { + if (!hasPotentialDeprecatedUsage(source)) { + return source; + } + + return fixBrokenSynchronousAPICalls(source); +} diff --git a/packages/apps/node-runtime/src/lib/secureFields.ts b/packages/apps/node-runtime/src/lib/secureFields.ts new file mode 100644 index 0000000000000..9d31ad745f5b3 --- /dev/null +++ b/packages/apps/node-runtime/src/lib/secureFields.ts @@ -0,0 +1,27 @@ +import type { WithSecureFields } from '@rocket.chat/apps/dist/lib/SecureFields'; +import { kSecureFields } from '@rocket.chat/apps/dist/lib/SecureFields'; +import type { App } from '@rocket.chat/apps-engine/definition/App'; + +import { AppObjectRegistry } from '../AppObjectRegistry'; + +export type { WithSecureFields } from '@rocket.chat/apps/dist/lib/SecureFields'; + +export function applySecureFields(object: WithSecureFields>) { + const { [kSecureFields]: secureFields, ...rest } = object; + + const app = AppObjectRegistry.get('app'); + + if (!app) { + throw new Error("App unavailable, can't parse object with secure fields"); + } + + secureFields.forEach(({ permission, name, value }) => { + if (!app.getInfo().permissions?.find((p) => p.name === permission)) { + return; + } + + rest[name] = value; + }); + + return rest; +} diff --git a/packages/apps/node-runtime/src/lib/tests/logger.test.ts b/packages/apps/node-runtime/src/lib/tests/logger.test.ts new file mode 100644 index 0000000000000..6e4eddb772d27 --- /dev/null +++ b/packages/apps/node-runtime/src/lib/tests/logger.test.ts @@ -0,0 +1,111 @@ +import { assertEquals } from 'https://deno.land/std@0.203.0/assert/mod'; +import { describe, it } from 'https://deno.land/std@0.203.0/testing/bdd'; + +import { Logger } from '../logger'; + +describe('Logger', () => { + it('getLogs should return an array of entries', () => { + const logger = new Logger('test'); + logger.info('test'); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 1); + assertEquals(logs.method, 'test'); + }); + + it('should be able to add entries of different severity', () => { + const logger = new Logger('test'); + logger.info('test'); + logger.debug('test'); + logger.error('test'); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 3); + assertEquals(logs.entries[0].severity, 'info'); + assertEquals(logs.entries[1].severity, 'debug'); + assertEquals(logs.entries[2].severity, 'error'); + }); + + it('should be able to add an info entry', () => { + const logger = new Logger('test'); + logger.info('test'); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 1); + assertEquals(logs.entries[0].args[0], 'test'); + assertEquals(logs.entries[0].method, 'test'); + assertEquals(logs.entries[0].severity, 'info'); + }); + + it('should be able to add an debug entry', () => { + const logger = new Logger('test'); + logger.debug('test'); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 1); + assertEquals(logs.entries[0].args[0], 'test'); + assertEquals(logs.entries[0].method, 'test'); + assertEquals(logs.entries[0].severity, 'debug'); + }); + + it('should be able to add an error entry', () => { + const logger = new Logger('test'); + logger.error('test'); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 1); + assertEquals(logs.entries[0].args[0], 'test'); + assertEquals(logs.entries[0].method, 'test'); + assertEquals(logs.entries[0].severity, 'error'); + }); + + it('should be able to add an success entry', () => { + const logger = new Logger('test'); + logger.success('test'); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 1); + assertEquals(logs.entries[0].args[0], 'test'); + assertEquals(logs.entries[0].method, 'test'); + assertEquals(logs.entries[0].severity, 'success'); + }); + + it('should be able to add an warning entry', () => { + const logger = new Logger('test'); + logger.warn('test'); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 1); + assertEquals(logs.entries[0].args[0], 'test'); + assertEquals(logs.entries[0].method, 'test'); + assertEquals(logs.entries[0].severity, 'warning'); + }); + + it('should be able to add an log entry', () => { + const logger = new Logger('test'); + logger.log('test'); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 1); + assertEquals(logs.entries[0].args[0], 'test'); + assertEquals(logs.entries[0].method, 'test'); + assertEquals(logs.entries[0].severity, 'log'); + }); + + it('should be able to add an entry with multiple arguments', () => { + const logger = new Logger('test'); + logger.log('test', 'test', 'test'); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 1); + assertEquals(logs.entries[0].args[0], 'test'); + assertEquals(logs.entries[0].args[1], 'test'); + assertEquals(logs.entries[0].args[2], 'test'); + assertEquals(logs.entries[0].method, 'test'); + assertEquals(logs.entries[0].severity, 'log'); + }); + + it('should be able to add an entry with multiple arguments of different types', () => { + const logger = new Logger('test'); + logger.log('test', 1, true, { foo: 'bar' }); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 1); + assertEquals(logs.entries[0].args[0], 'test'); + assertEquals(logs.entries[0].args[1], 1); + assertEquals(logs.entries[0].args[2], true); + assertEquals(logs.entries[0].args[3], { foo: 'bar' }); + assertEquals(logs.entries[0].method, 'test'); + assertEquals(logs.entries[0].severity, 'log'); + }); +}); diff --git a/packages/apps/node-runtime/src/lib/tests/messenger.test.ts b/packages/apps/node-runtime/src/lib/tests/messenger.test.ts new file mode 100644 index 0000000000000..5b439e9dc709d --- /dev/null +++ b/packages/apps/node-runtime/src/lib/tests/messenger.test.ts @@ -0,0 +1,99 @@ +import { assertEquals, assertObjectMatch } from 'https://deno.land/std@0.203.0/assert/mod'; +import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd'; +import { spy } from 'https://deno.land/std@0.203.0/testing/mock'; +import type { JsonRpc } from 'jsonrpc-lite'; + +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import { createMockRequest } from '../../handlers/tests/helpers/mod'; +import * as Messenger from '../messenger'; +import type { RequestContext } from '../requestContext'; + +describe('Messenger', () => { + let context: RequestContext; + + beforeEach(() => { + AppObjectRegistry.clear(); + AppObjectRegistry.set('id', 'test'); + Messenger.Transport.selectTransport('noop'); + + context = createMockRequest({ method: 'test', params: [] }); + }); + + afterAll(() => { + AppObjectRegistry.clear(); + Messenger.Transport.selectTransport('stdout'); + }); + + it('should add logs to success responses', async () => { + const theSpy = spy(Messenger.Queue, 'enqueue'); + const { logger } = context.context; + + logger.info('test'); + + await Messenger.successResponse({ id: 'test', result: 'test' }, context); + + assertEquals(theSpy.calls.length, 1); + + const [responseArgument] = theSpy.calls[0].args; + + assertObjectMatch(responseArgument as JsonRpc, { + jsonrpc: '2.0', + id: 'test', + result: { + value: 'test', + logs: { + appId: 'test', + method: 'test', + entries: [ + { + severity: 'info', + method: 'test', + args: ['test'], + caller: 'anonymous OR constructor', + }, + ], + }, + }, + }); + + theSpy.restore(); + }); + + it('should add logs to error responses', async () => { + const theSpy = spy(Messenger.Queue, 'enqueue'); + const { logger } = context.context; + + logger.info('test'); + + await Messenger.errorResponse({ id: 'test', error: { code: -32000, message: 'test' } }, context); + + assertEquals(theSpy.calls.length, 1); + + const [responseArgument] = theSpy.calls[0].args; + + assertObjectMatch(responseArgument as JsonRpc, { + jsonrpc: '2.0', + id: 'test', + error: { + code: -32000, + message: 'test', + data: { + logs: { + appId: 'test', + method: 'test', + entries: [ + { + severity: 'info', + method: 'test', + args: ['test'], + caller: 'anonymous OR constructor', + }, + ], + }, + }, + }, + }); + + theSpy.restore(); + }); +}); diff --git a/packages/apps/node-runtime/src/lib/tests/secureFields.test.ts b/packages/apps/node-runtime/src/lib/tests/secureFields.test.ts new file mode 100644 index 0000000000000..b286c30057d56 --- /dev/null +++ b/packages/apps/node-runtime/src/lib/tests/secureFields.test.ts @@ -0,0 +1,59 @@ +import { assertEquals, assertThrows } from 'https://deno.land/std@0.203.0/assert/mod'; +import { beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd'; + +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import { applySecureFields } from '../secureFields'; + +const SECURE_FIELDS_KEY = '@@SecureFields'; + +describe('applySecureFields', () => { + beforeEach(() => { + AppObjectRegistry.clear(); + }); + + it('throws when app is unavailable', () => { + assertThrows( + () => applySecureFields({ foo: 'bar', [SECURE_FIELDS_KEY]: [] } as any), + Error, + "App unavailable, can't parse object with secure fields", + ); + }); + + it('applies only secure fields with matching permissions', () => { + AppObjectRegistry.set('app', { + getInfo: () => ({ + permissions: [{ name: 'abac.read' }], + }), + }); + + const parsed = applySecureFields({ + foo: 'bar', + [SECURE_FIELDS_KEY]: [ + { permission: 'abac.read', name: 'abacAttributes', value: { department: 'support' } }, + { permission: 'api.read', name: 'apiToken', value: 'secret' }, + ], + } as any); + + assertEquals(parsed, { + foo: 'bar', + abacAttributes: { department: 'support' }, + }); + }); + + it('overwrites an existing field when permission is granted', () => { + AppObjectRegistry.set('app', { + getInfo: () => ({ + permissions: [{ name: 'abac.read' }], + }), + }); + + const parsed = applySecureFields({ + abacAttributes: null, + [SECURE_FIELDS_KEY]: [{ permission: 'abac.read', name: 'abacAttributes', value: { tenant: 'alpha' } }], + } as any); + + assertEquals(parsed, { + abacAttributes: { tenant: 'alpha' }, + }); + }); +}); diff --git a/packages/apps/node-runtime/src/lib/wrapAppForRequest.ts b/packages/apps/node-runtime/src/lib/wrapAppForRequest.ts new file mode 100644 index 0000000000000..a30274c20eaed --- /dev/null +++ b/packages/apps/node-runtime/src/lib/wrapAppForRequest.ts @@ -0,0 +1,60 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App'; + +import type { RequestContext } from './requestContext'; +import { isApp, isRecord } from '../handlers/lib/assertions'; + +export function wrapAppForRequest(app: App, req: RequestContext): App { + return new Proxy(app, { + get(target, property, receiver) { + if (property === 'logger') { + return req.context.logger; + } + + return Reflect.get(target, property, receiver); + }, + }); +} + +// Instances of objects that have a reference to an App instance won't change throughout the +// lifetime of the runtime, so we can cache the results to avoid iterating the same object multiple times +const composedCache = new WeakMap, ReturnType>(); + +function findAppProperty(v: NonNullable): [string, App] | undefined { + const cachedEntry = composedCache.get(v); + + if (cachedEntry) { + return cachedEntry; + } + + if (!isRecord(v)) { + // Enables us to avoid having to determine whether the value is a record again + composedCache.set(v, undefined); + return undefined; + } + + const entry = Object.entries(v).find(([_, v]) => isApp(v)) as [string, App] | undefined; + + composedCache.set(v, entry); + + return entry; +} + +export function wrapComposedApp>(composed: T, req: RequestContext): T { + const prop = findAppProperty(composed); + + if (!prop) { + return composed; + } + + const proxy = wrapAppForRequest(prop[1], req); + + return new Proxy(composed, { + get(target, property, receiver) { + if (property === prop[0]) { + return proxy; + } + + return Reflect.get(target, property, receiver); + }, + }); +} diff --git a/packages/apps/node-runtime/src/main.ts b/packages/apps/node-runtime/src/main.ts new file mode 100644 index 0000000000000..24e3280532897 --- /dev/null +++ b/packages/apps/node-runtime/src/main.ts @@ -0,0 +1,132 @@ +import './lib/loader-hook'; + +import { JsonRpcError, type SuccessObject } from 'jsonrpc-lite'; + +import registerErrorListeners from './error-handlers'; +import apiHandler from './handlers/api-handler'; +import handleApp from './handlers/app/handler'; +import outboundMessageHandler from './handlers/outboundcomms-handler'; +import handleScheduler from './handlers/scheduler-handler'; +import slashcommandHandler from './handlers/slashcommand-handler'; +import videoConferenceHandler from './handlers/videoconference-handler'; +import { decoder } from './lib/codec'; +import { Logger } from './lib/logger'; +import * as Messenger from './lib/messenger'; +import { sendMetrics } from './lib/metricsCollector'; +import type { RequestContext } from './lib/requestContext'; + +if (!process.argv.includes('--subprocess')) { + process.stderr.write( + new TextEncoder().encode(` + This is a Deno wrapper for Rocket.Chat Apps. It is not meant to be executed stand-alone; + It is instead meant to be executed as a subprocess by the Apps-Engine framework. + `), + ); + process.exit(1001); +} + +type Handlers = { + app: typeof handleApp; + api: typeof apiHandler; + slashcommand: typeof slashcommandHandler; + videoconference: typeof videoConferenceHandler; + outboundCommunication: typeof outboundMessageHandler; + scheduler: typeof handleScheduler; + ping: (request: RequestContext) => 'pong'; +}; + +const COMMAND_PING = '_zPING'; + +async function requestRouter({ type, payload }: Messenger.JsonRpcRequest): Promise { + const methodHandlers: Handlers = { + app: handleApp, + api: apiHandler, + slashcommand: slashcommandHandler, + videoconference: videoConferenceHandler, + outboundCommunication: outboundMessageHandler, + scheduler: handleScheduler, + ping: (_request) => 'pong', + }; + + // We're not handling notifications at the moment + if (type === 'notification') { + return Messenger.sendInvalidRequestError(); + } + + const { id, method } = payload; + + const logger = new Logger(method); + + const context: RequestContext = Object.assign(payload, { + context: { logger }, + }); + + const [methodPrefix] = method.split(':') as [keyof Handlers]; + const handler = methodHandlers[methodPrefix]; + + if (!handler) { + return Messenger.errorResponse( + { + error: { message: 'Method not found', code: -32601 }, + id, + }, + context, + ); + } + + const result = await handler(context); + + if (result instanceof JsonRpcError) { + return Messenger.errorResponse({ id, error: result }, context); + } + + return Messenger.successResponse({ id, result }, context); +} + +function handleResponse(response: Messenger.JsonRpcResponse): void { + let payload: { error: Error } | { detail: SuccessObject }; + + if (Messenger.isErrorResponse(response.payload)) { + payload = { error: new Error(response.payload.error.message) }; + } else { + payload = { detail: response.payload }; + } + + Messenger.RPCResponseObserver.emit(`response:${response.payload.id}`, payload); +} + +async function main() { + Messenger.sendNotification({ method: 'ready', params: [] }); + + for await (const message of decoder.decodeStream(process.stdin)) { + try { + // Process PING command first as it is not JSON RPC + if (message === COMMAND_PING) { + void Messenger.pongResponse(); + void sendMetrics(); + continue; + } + + const JSONRPCMessage = Messenger.parseMessage(message as Record); + + if (Messenger.isRequest(JSONRPCMessage)) { + void requestRouter(JSONRPCMessage); + continue; + } + + if (Messenger.isResponse(JSONRPCMessage)) { + handleResponse(JSONRPCMessage); + } + } catch (error) { + if (Messenger.isErrorResponse(error)) { + await Messenger.errorResponse(error); + } else { + await Messenger.sendParseError(); + } + } + } +} + +registerErrorListeners(); + +void main(); diff --git a/packages/apps/node-runtime/tsconfig.json b/packages/apps/node-runtime/tsconfig.json new file mode 100644 index 0000000000000..178705918947c --- /dev/null +++ b/packages/apps/node-runtime/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist", + "types": ["node"], + "lib": ["es2023"], + "module": "nodenext", + "moduleResolution": "nodenext", + "target": "es2023", + "declaration": false, + }, + "include": ["./src/**/*"], + "exclude": ["./src/**/*.spec.ts", "./src/**/*.test.ts","./src/**/tests/*"] +} From a20cdc5965491b4cdbea42b8efe8aa1c1421fe63 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Fri, 26 Jun 2026 13:58:14 -0300 Subject: [PATCH 02/11] refactor(apps): drop .ts extensions and use direct ESM imports in deno-runtime Remove .ts extensions from relative imports and replace the require(...) helper pattern with direct ESM imports, aligning the deno-runtime sources with node-runtime so the two trees converge toward a shared base runtime. Mechanical, no behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/apps/deno-runtime/acorn-walk.d.ts | 2 +- packages/apps/deno-runtime/error-handlers.ts | 2 +- .../apps/deno-runtime/handlers/api-handler.ts | 8 ++--- .../deno-runtime/handlers/app/construct.ts | 10 +++--- .../handlers/app/handleGetStatus.ts | 2 +- .../handlers/app/handleInitialize.ts | 8 ++--- .../handlers/app/handleOnDisable.ts | 8 ++--- .../handlers/app/handleOnEnable.ts | 8 ++--- .../handlers/app/handleOnInstall.ts | 8 ++--- .../handlers/app/handleOnPreSettingUpdate.ts | 8 ++--- .../handlers/app/handleOnSettingUpdated.ts | 8 ++--- .../handlers/app/handleOnUninstall.ts | 8 ++--- .../handlers/app/handleOnUpdate.ts | 8 ++--- .../handlers/app/handleSetStatus.ts | 13 +++---- .../handlers/app/handleUploadEvents.ts | 10 +++--- .../apps/deno-runtime/handlers/app/handler.ts | 32 ++++++++--------- .../deno-runtime/handlers/listener/handler.ts | 27 ++++++-------- .../handlers/outboundcomms-handler.ts | 8 ++--- .../handlers/scheduler-handler.ts | 10 +++--- .../handlers/slashcommand-handler.ts | 10 +++--- .../handlers/tests/api-handler.test.ts | 6 ++-- .../handlers/tests/helpers/mod.ts | 6 ++-- .../handlers/tests/listener-handler.test.ts | 14 ++++---- .../handlers/tests/scheduler-handler.test.ts | 8 ++--- .../tests/slashcommand-handler.test.ts | 10 +++--- .../handlers/tests/uikit-handler.test.ts | 4 +-- .../tests/upload-event-handler.test.ts | 8 ++--- .../tests/videoconference-handler.test.ts | 6 ++-- .../deno-runtime/handlers/uikit/handler.ts | 25 +++++++------ .../handlers/videoconference-handler.ts | 8 ++--- .../lib/accessors/builders/BlockBuilder.ts | 14 +++----- .../accessors/builders/DiscussionBuilder.ts | 15 +++----- .../builders/LivechatMessageBuilder.ts | 15 +++----- .../lib/accessors/builders/MessageBuilder.ts | 11 ++---- .../lib/accessors/builders/RoomBuilder.ts | 10 ++---- .../lib/accessors/builders/UserBuilder.ts | 10 ++---- .../builders/VideoConferenceBuilder.ts | 10 ++---- .../accessors/extenders/MessageExtender.ts | 10 ++---- .../lib/accessors/extenders/RoomExtender.ts | 10 ++---- .../extenders/VideoConferenceExtend.ts | 10 ++---- .../apps/deno-runtime/lib/accessors/http.ts | 6 ++-- .../apps/deno-runtime/lib/accessors/mod.ts | 18 +++++----- .../lib/accessors/modify/ModifyCreator.ts | 35 ++++++++----------- .../lib/accessors/modify/ModifyExtender.ts | 21 +++++------ .../lib/accessors/modify/ModifyUpdater.ts | 20 ++++------- .../deno-runtime/lib/accessors/notifier.ts | 15 +++----- .../lib/accessors/tests/AppAccessors.test.ts | 4 +-- .../lib/accessors/tests/ModifyCreator.test.ts | 4 +-- .../accessors/tests/ModifyExtender.test.ts | 4 +-- .../lib/accessors/tests/ModifyUpdater.test.ts | 6 ++-- .../tests/formatResponseErrorHandler.test.ts | 2 +- .../lib/accessors/tests/http.test.ts | 4 +-- packages/apps/deno-runtime/lib/ast/mod.ts | 4 +-- .../lib/ast/tests/operations.test.ts | 6 ++-- packages/apps/deno-runtime/lib/codec.ts | 9 ++--- packages/apps/deno-runtime/lib/logger.ts | 2 +- packages/apps/deno-runtime/lib/messenger.ts | 4 +-- .../apps/deno-runtime/lib/metricsCollector.ts | 2 +- .../apps/deno-runtime/lib/requestContext.ts | 2 +- packages/apps/deno-runtime/lib/roomFactory.ts | 6 ++-- .../lib/sanitizeDeprecatedUsage.ts | 2 +- .../apps/deno-runtime/lib/secureFields.ts | 2 +- .../deno-runtime/lib/tests/logger.test.ts | 2 +- .../deno-runtime/lib/tests/messenger.test.ts | 8 ++--- .../lib/tests/secureFields.test.ts | 4 +-- .../deno-runtime/lib/wrapAppForRequest.ts | 4 +-- packages/apps/deno-runtime/main.ts | 26 +++++++------- 67 files changed, 269 insertions(+), 361 deletions(-) diff --git a/packages/apps/deno-runtime/acorn-walk.d.ts b/packages/apps/deno-runtime/acorn-walk.d.ts index 56db3bc38e9d2..49f3d6e33df3a 100644 --- a/packages/apps/deno-runtime/acorn-walk.d.ts +++ b/packages/apps/deno-runtime/acorn-walk.d.ts @@ -1,4 +1,4 @@ -import type acorn from './acorn.d.ts'; +import type acorn from './acorn.d'; export type FullWalkerCallback = ( node: acorn.AnyNode, diff --git a/packages/apps/deno-runtime/error-handlers.ts b/packages/apps/deno-runtime/error-handlers.ts index e26a5ad6b2d86..5ce6542cd679e 100644 --- a/packages/apps/deno-runtime/error-handlers.ts +++ b/packages/apps/deno-runtime/error-handlers.ts @@ -1,4 +1,4 @@ -import * as Messenger from './lib/messenger.ts'; +import * as Messenger from './lib/messenger'; export function unhandledRejectionListener(event: PromiseRejectionEvent) { event.preventDefault(); diff --git a/packages/apps/deno-runtime/handlers/api-handler.ts b/packages/apps/deno-runtime/handlers/api-handler.ts index d014366c99d1f..b59b471981942 100644 --- a/packages/apps/deno-runtime/handlers/api-handler.ts +++ b/packages/apps/deno-runtime/handlers/api-handler.ts @@ -1,10 +1,10 @@ import type { IApiEndpoint } from '@rocket.chat/apps-engine/definition/api/IApiEndpoint'; import { Defined, JsonRpcError } from 'jsonrpc-lite'; -import { AppObjectRegistry } from '../AppObjectRegistry.ts'; -import { AppAccessorsInstance } from '../lib/accessors/mod.ts'; -import { RequestContext } from '../lib/requestContext.ts'; -import { wrapComposedApp } from '../lib/wrapAppForRequest.ts'; +import { AppObjectRegistry } from '../AppObjectRegistry'; +import { AppAccessorsInstance } from '../lib/accessors/mod'; +import { RequestContext } from '../lib/requestContext'; +import { wrapComposedApp } from '../lib/wrapAppForRequest'; export default async function apiHandler(request: RequestContext): Promise { const { method: call, params } = request; diff --git a/packages/apps/deno-runtime/handlers/app/construct.ts b/packages/apps/deno-runtime/handlers/app/construct.ts index 7ad0baeb7823b..19a2743169efb 100644 --- a/packages/apps/deno-runtime/handlers/app/construct.ts +++ b/packages/apps/deno-runtime/handlers/app/construct.ts @@ -2,11 +2,11 @@ import { Socket } from 'node:net'; import type { IParseAppPackageResult } from '@rocket.chat/apps/dist/server/compiler/IParseAppPackageResult'; -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import { require } from '../../lib/require.ts'; -import { sanitizeDeprecatedUsage } from '../../lib/sanitizeDeprecatedUsage.ts'; -import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; -import { RequestContext } from '../../lib/requestContext.ts'; +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import { require } from '../../lib/require'; +import { sanitizeDeprecatedUsage } from '../../lib/sanitizeDeprecatedUsage'; +import { AppAccessorsInstance } from '../../lib/accessors/mod'; +import { RequestContext } from '../../lib/requestContext'; const ALLOWED_NATIVE_MODULES = ['path', 'url', 'crypto', 'buffer', 'stream', 'net', 'http', 'https', 'zlib', 'util', 'punycode', 'os', 'querystring', 'fs']; const ALLOWED_EXTERNAL_MODULES = ['uuid']; diff --git a/packages/apps/deno-runtime/handlers/app/handleGetStatus.ts b/packages/apps/deno-runtime/handlers/app/handleGetStatus.ts index 30c78d2430904..38635a02c60ae 100644 --- a/packages/apps/deno-runtime/handlers/app/handleGetStatus.ts +++ b/packages/apps/deno-runtime/handlers/app/handleGetStatus.ts @@ -1,6 +1,6 @@ import type { App } from '@rocket.chat/apps-engine/definition/App'; -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { AppObjectRegistry } from '../../AppObjectRegistry'; export default function handleGetStatus(): Promise { const app = AppObjectRegistry.get('app'); diff --git a/packages/apps/deno-runtime/handlers/app/handleInitialize.ts b/packages/apps/deno-runtime/handlers/app/handleInitialize.ts index 4be65241c8f56..136e93d2af46a 100644 --- a/packages/apps/deno-runtime/handlers/app/handleInitialize.ts +++ b/packages/apps/deno-runtime/handlers/app/handleInitialize.ts @@ -1,9 +1,9 @@ import type { App } from '@rocket.chat/apps-engine/definition/App'; -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; -import { RequestContext } from '../../lib/requestContext.ts'; -import { wrapAppForRequest } from '../../lib/wrapAppForRequest.ts'; +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import { AppAccessorsInstance } from '../../lib/accessors/mod'; +import { RequestContext } from '../../lib/requestContext'; +import { wrapAppForRequest } from '../../lib/wrapAppForRequest'; export default async function handleInitialize(request: RequestContext): Promise { const app = AppObjectRegistry.get('app'); diff --git a/packages/apps/deno-runtime/handlers/app/handleOnDisable.ts b/packages/apps/deno-runtime/handlers/app/handleOnDisable.ts index 170f25f326009..f84e6a34abb5e 100644 --- a/packages/apps/deno-runtime/handlers/app/handleOnDisable.ts +++ b/packages/apps/deno-runtime/handlers/app/handleOnDisable.ts @@ -1,9 +1,9 @@ import type { App } from '@rocket.chat/apps-engine/definition/App'; -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; -import { RequestContext } from '../../lib/requestContext.ts'; -import { wrapAppForRequest } from '../../lib/wrapAppForRequest.ts'; +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import { AppAccessorsInstance } from '../../lib/accessors/mod'; +import { RequestContext } from '../../lib/requestContext'; +import { wrapAppForRequest } from '../../lib/wrapAppForRequest'; export default async function handleOnDisable(request: RequestContext): Promise { const app = AppObjectRegistry.get('app'); diff --git a/packages/apps/deno-runtime/handlers/app/handleOnEnable.ts b/packages/apps/deno-runtime/handlers/app/handleOnEnable.ts index 320d5772a01d0..f36eaecf951b2 100644 --- a/packages/apps/deno-runtime/handlers/app/handleOnEnable.ts +++ b/packages/apps/deno-runtime/handlers/app/handleOnEnable.ts @@ -1,9 +1,9 @@ import type { App } from '@rocket.chat/apps-engine/definition/App'; -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; -import { RequestContext } from '../../lib/requestContext.ts'; -import { wrapAppForRequest } from '../../lib/wrapAppForRequest.ts'; +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import { AppAccessorsInstance } from '../../lib/accessors/mod'; +import { RequestContext } from '../../lib/requestContext'; +import { wrapAppForRequest } from '../../lib/wrapAppForRequest'; export default function handleOnEnable(request: RequestContext): Promise { const app = AppObjectRegistry.get('app'); diff --git a/packages/apps/deno-runtime/handlers/app/handleOnInstall.ts b/packages/apps/deno-runtime/handlers/app/handleOnInstall.ts index d779728bc0c9c..ca1bbfd2f56f7 100644 --- a/packages/apps/deno-runtime/handlers/app/handleOnInstall.ts +++ b/packages/apps/deno-runtime/handlers/app/handleOnInstall.ts @@ -1,9 +1,9 @@ import type { App } from '@rocket.chat/apps-engine/definition/App'; -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; -import { RequestContext } from '../../lib/requestContext.ts'; -import { wrapAppForRequest } from '../../lib/wrapAppForRequest.ts'; +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import { AppAccessorsInstance } from '../../lib/accessors/mod'; +import { RequestContext } from '../../lib/requestContext'; +import { wrapAppForRequest } from '../../lib/wrapAppForRequest'; export default async function handleOnInstall(request: RequestContext): Promise { const { params } = request; diff --git a/packages/apps/deno-runtime/handlers/app/handleOnPreSettingUpdate.ts b/packages/apps/deno-runtime/handlers/app/handleOnPreSettingUpdate.ts index 301dd5aa7f2c8..97a99e151eb02 100644 --- a/packages/apps/deno-runtime/handlers/app/handleOnPreSettingUpdate.ts +++ b/packages/apps/deno-runtime/handlers/app/handleOnPreSettingUpdate.ts @@ -1,9 +1,9 @@ import type { App } from '@rocket.chat/apps-engine/definition/App'; -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; -import { RequestContext } from '../../lib/requestContext.ts'; -import { wrapAppForRequest } from '../../lib/wrapAppForRequest.ts'; +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import { AppAccessorsInstance } from '../../lib/accessors/mod'; +import { RequestContext } from '../../lib/requestContext'; +import { wrapAppForRequest } from '../../lib/wrapAppForRequest'; export default function handleOnPreSettingUpdate(request: RequestContext): Promise { const { params } = request; diff --git a/packages/apps/deno-runtime/handlers/app/handleOnSettingUpdated.ts b/packages/apps/deno-runtime/handlers/app/handleOnSettingUpdated.ts index 7a2111cbfd990..cdd5d93669353 100644 --- a/packages/apps/deno-runtime/handlers/app/handleOnSettingUpdated.ts +++ b/packages/apps/deno-runtime/handlers/app/handleOnSettingUpdated.ts @@ -1,9 +1,9 @@ import type { App } from '@rocket.chat/apps-engine/definition/App'; -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; -import { RequestContext } from '../../lib/requestContext.ts'; -import { wrapAppForRequest } from '../../lib/wrapAppForRequest.ts'; +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import { AppAccessorsInstance } from '../../lib/accessors/mod'; +import { RequestContext } from '../../lib/requestContext'; +import { wrapAppForRequest } from '../../lib/wrapAppForRequest'; export default async function handleOnSettingUpdated(request: RequestContext): Promise { const { params } = request; diff --git a/packages/apps/deno-runtime/handlers/app/handleOnUninstall.ts b/packages/apps/deno-runtime/handlers/app/handleOnUninstall.ts index 7901f21f903b2..3008f822c9611 100644 --- a/packages/apps/deno-runtime/handlers/app/handleOnUninstall.ts +++ b/packages/apps/deno-runtime/handlers/app/handleOnUninstall.ts @@ -1,9 +1,9 @@ import type { App } from '@rocket.chat/apps-engine/definition/App'; -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; -import { RequestContext } from '../../lib/requestContext.ts'; -import { wrapAppForRequest } from '../../lib/wrapAppForRequest.ts'; +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import { AppAccessorsInstance } from '../../lib/accessors/mod'; +import { RequestContext } from '../../lib/requestContext'; +import { wrapAppForRequest } from '../../lib/wrapAppForRequest'; export default async function handleOnUninstall(request: RequestContext): Promise { const { params } = request; diff --git a/packages/apps/deno-runtime/handlers/app/handleOnUpdate.ts b/packages/apps/deno-runtime/handlers/app/handleOnUpdate.ts index 65f7986e0cef3..c4651c2fb25b0 100644 --- a/packages/apps/deno-runtime/handlers/app/handleOnUpdate.ts +++ b/packages/apps/deno-runtime/handlers/app/handleOnUpdate.ts @@ -1,9 +1,9 @@ import type { App } from '@rocket.chat/apps-engine/definition/App'; -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; -import { RequestContext } from '../../lib/requestContext.ts'; -import { wrapAppForRequest } from '../../lib/wrapAppForRequest.ts'; +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import { AppAccessorsInstance } from '../../lib/accessors/mod'; +import { RequestContext } from '../../lib/requestContext'; +import { wrapAppForRequest } from '../../lib/wrapAppForRequest'; export default async function handleOnUpdate(request: RequestContext): Promise { const { params } = request; diff --git a/packages/apps/deno-runtime/handlers/app/handleSetStatus.ts b/packages/apps/deno-runtime/handlers/app/handleSetStatus.ts index 136e8ccc6271d..b9d6e76f6e3f7 100644 --- a/packages/apps/deno-runtime/handlers/app/handleSetStatus.ts +++ b/packages/apps/deno-runtime/handlers/app/handleSetStatus.ts @@ -1,14 +1,9 @@ import type { App } from '@rocket.chat/apps-engine/definition/App'; -import type { AppStatus as _AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; +import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus.js'; -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import { require } from '../../lib/require.ts'; -import { RequestContext } from '../../lib/requestContext.ts'; -import { wrapAppForRequest } from '../../lib/wrapAppForRequest.ts'; - -const { AppStatus } = require('@rocket.chat/apps-engine/definition/AppStatus.js') as { - AppStatus: typeof _AppStatus; -}; +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import { RequestContext } from '../../lib/requestContext'; +import { wrapAppForRequest } from '../../lib/wrapAppForRequest'; export default async function handleSetStatus(request: RequestContext): Promise { const { params } = request; diff --git a/packages/apps/deno-runtime/handlers/app/handleUploadEvents.ts b/packages/apps/deno-runtime/handlers/app/handleUploadEvents.ts index 1e203dca5d6b4..dc4b286970575 100644 --- a/packages/apps/deno-runtime/handlers/app/handleUploadEvents.ts +++ b/packages/apps/deno-runtime/handlers/app/handleUploadEvents.ts @@ -7,11 +7,11 @@ import type { IUploadDetails } from '@rocket.chat/apps-engine/definition/uploads import { toArrayBuffer } from '@std/streams'; import { Defined, JsonRpcError } from 'jsonrpc-lite'; -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import { assertAppAvailable, assertHandlerFunction, isPlainObject } from '../lib/assertions.ts'; -import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; -import { RequestContext } from '../../lib/requestContext.ts'; -import { wrapAppForRequest } from '../../lib/wrapAppForRequest.ts'; +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import { assertAppAvailable, assertHandlerFunction, isPlainObject } from '../lib/assertions'; +import { AppAccessorsInstance } from '../../lib/accessors/mod'; +import { RequestContext } from '../../lib/requestContext'; +import { wrapAppForRequest } from '../../lib/wrapAppForRequest'; export const uploadEvents = ['executePreFileUpload'] as const; diff --git a/packages/apps/deno-runtime/handlers/app/handler.ts b/packages/apps/deno-runtime/handlers/app/handler.ts index e0e8085813347..b873bd4b09de0 100644 --- a/packages/apps/deno-runtime/handlers/app/handler.ts +++ b/packages/apps/deno-runtime/handlers/app/handler.ts @@ -1,21 +1,21 @@ import { Defined, JsonRpcError } from 'jsonrpc-lite'; -import handleConstructApp from './construct.ts'; -import handleInitialize from './handleInitialize.ts'; -import handleGetStatus from './handleGetStatus.ts'; -import handleSetStatus from './handleSetStatus.ts'; -import handleOnEnable from './handleOnEnable.ts'; -import handleOnInstall from './handleOnInstall.ts'; -import handleOnDisable from './handleOnDisable.ts'; -import handleOnUninstall from './handleOnUninstall.ts'; -import handleOnPreSettingUpdate from './handleOnPreSettingUpdate.ts'; -import handleOnSettingUpdated from './handleOnSettingUpdated.ts'; -import handleOnUpdate from './handleOnUpdate.ts'; -import handleUploadEvents, { uploadEvents } from './handleUploadEvents.ts'; -import { isOneOf } from '../lib/assertions.ts'; -import handleListener from '../listener/handler.ts'; -import handleUIKitInteraction, { uikitInteractions } from '../uikit/handler.ts'; -import { RequestContext } from '../../lib/requestContext.ts'; +import handleConstructApp from './construct'; +import handleInitialize from './handleInitialize'; +import handleGetStatus from './handleGetStatus'; +import handleSetStatus from './handleSetStatus'; +import handleOnEnable from './handleOnEnable'; +import handleOnInstall from './handleOnInstall'; +import handleOnDisable from './handleOnDisable'; +import handleOnUninstall from './handleOnUninstall'; +import handleOnPreSettingUpdate from './handleOnPreSettingUpdate'; +import handleOnSettingUpdated from './handleOnSettingUpdated'; +import handleOnUpdate from './handleOnUpdate'; +import handleUploadEvents, { uploadEvents } from './handleUploadEvents'; +import { isOneOf } from '../lib/assertions'; +import handleListener from '../listener/handler'; +import handleUIKitInteraction, { uikitInteractions } from '../uikit/handler'; +import { RequestContext } from '../../lib/requestContext'; export default async function handleApp(request: RequestContext): Promise { const { method } = request; diff --git a/packages/apps/deno-runtime/handlers/listener/handler.ts b/packages/apps/deno-runtime/handlers/listener/handler.ts index 5dc98dea614c7..2ca138a3770c2 100644 --- a/packages/apps/deno-runtime/handlers/listener/handler.ts +++ b/packages/apps/deno-runtime/handlers/listener/handler.ts @@ -1,24 +1,19 @@ import type { App } from '@rocket.chat/apps-engine/definition/App'; import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; -import type { AppsEngineException as _AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions/AppsEngineException'; +import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions/AppsEngineException.js'; import { Defined, JsonRpcError } from 'jsonrpc-lite'; -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import { MessageExtender } from '../../lib/accessors/extenders/MessageExtender.ts'; -import { RoomExtender } from '../../lib/accessors/extenders/RoomExtender.ts'; -import { MessageBuilder } from '../../lib/accessors/builders/MessageBuilder.ts'; -import { RoomBuilder } from '../../lib/accessors/builders/RoomBuilder.ts'; -import { AppAccessors, AppAccessorsInstance } from '../../lib/accessors/mod.ts'; -import { require } from '../../lib/require.ts'; -import createRoom from '../../lib/roomFactory.ts'; -import { Room } from '../../lib/room.ts'; -import { RequestContext } from '../../lib/requestContext.ts'; -import { wrapAppForRequest } from '../../lib/wrapAppForRequest.ts'; - -const { AppsEngineException } = require('@rocket.chat/apps-engine/definition/exceptions/AppsEngineException.js') as { - AppsEngineException: typeof _AppsEngineException; -}; +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import { MessageExtender } from '../../lib/accessors/extenders/MessageExtender'; +import { RoomExtender } from '../../lib/accessors/extenders/RoomExtender'; +import { MessageBuilder } from '../../lib/accessors/builders/MessageBuilder'; +import { RoomBuilder } from '../../lib/accessors/builders/RoomBuilder'; +import { AppAccessors, AppAccessorsInstance } from '../../lib/accessors/mod'; +import createRoom from '../../lib/roomFactory'; +import { Room } from '../../lib/room'; +import { RequestContext } from '../../lib/requestContext'; +import { wrapAppForRequest } from '../../lib/wrapAppForRequest'; export default async function handleListener(request: RequestContext): Promise { const { method, params } = request; diff --git a/packages/apps/deno-runtime/handlers/outboundcomms-handler.ts b/packages/apps/deno-runtime/handlers/outboundcomms-handler.ts index 1c4dee24dd025..4a74dd6788a7c 100644 --- a/packages/apps/deno-runtime/handlers/outboundcomms-handler.ts +++ b/packages/apps/deno-runtime/handlers/outboundcomms-handler.ts @@ -1,10 +1,10 @@ import type { IOutboundMessageProviders } from '@rocket.chat/apps-engine/definition/outboundCommunication/IOutboundCommsProvider'; import { JsonRpcError, Defined } from 'jsonrpc-lite'; -import { AppObjectRegistry } from '../AppObjectRegistry.ts'; -import { AppAccessorsInstance } from '../lib/accessors/mod.ts'; -import { RequestContext } from '../lib/requestContext.ts'; -import { wrapComposedApp } from '../lib/wrapAppForRequest.ts'; +import { AppObjectRegistry } from '../AppObjectRegistry'; +import { AppAccessorsInstance } from '../lib/accessors/mod'; +import { RequestContext } from '../lib/requestContext'; +import { wrapComposedApp } from '../lib/wrapAppForRequest'; export default async function outboundMessageHandler(request: RequestContext): Promise { const { method: call, params } = request; diff --git a/packages/apps/deno-runtime/handlers/scheduler-handler.ts b/packages/apps/deno-runtime/handlers/scheduler-handler.ts index 9b6dfcad14d3b..88e53b7af5ac2 100644 --- a/packages/apps/deno-runtime/handlers/scheduler-handler.ts +++ b/packages/apps/deno-runtime/handlers/scheduler-handler.ts @@ -2,11 +2,11 @@ import type { App } from '@rocket.chat/apps-engine/definition/App'; import type { IProcessor } from '@rocket.chat/apps-engine/definition/scheduler/IProcessor'; import { Defined, JsonRpcError } from 'jsonrpc-lite'; -import { AppObjectRegistry } from '../AppObjectRegistry.ts'; -import { AppAccessorsInstance } from '../lib/accessors/mod.ts'; -import { RequestContext } from '../lib/requestContext.ts'; -import { wrapAppForRequest } from '../lib/wrapAppForRequest.ts'; -import { assertAppAvailable } from './lib/assertions.ts'; +import { AppObjectRegistry } from '../AppObjectRegistry'; +import { AppAccessorsInstance } from '../lib/accessors/mod'; +import { RequestContext } from '../lib/requestContext'; +import { wrapAppForRequest } from '../lib/wrapAppForRequest'; +import { assertAppAvailable } from './lib/assertions'; export default async function handleScheduler(request: RequestContext): Promise { const { method, params } = request; diff --git a/packages/apps/deno-runtime/handlers/slashcommand-handler.ts b/packages/apps/deno-runtime/handlers/slashcommand-handler.ts index 52e3c1419d913..b796992a23948 100644 --- a/packages/apps/deno-runtime/handlers/slashcommand-handler.ts +++ b/packages/apps/deno-runtime/handlers/slashcommand-handler.ts @@ -3,11 +3,11 @@ import type { ISlashCommand } from '@rocket.chat/apps-engine/definition/slashcom import { SlashCommandContext } from '@rocket.chat/apps-engine/definition/slashcommands/SlashCommandContext'; import { Defined, JsonRpcError } from 'jsonrpc-lite'; -import { AppObjectRegistry } from '../AppObjectRegistry.ts'; -import { AppAccessors, AppAccessorsInstance } from '../lib/accessors/mod.ts'; -import createRoom from '../lib/roomFactory.ts'; -import { RequestContext } from '../lib/requestContext.ts'; -import { wrapComposedApp } from '../lib/wrapAppForRequest.ts'; +import { AppObjectRegistry } from '../AppObjectRegistry'; +import { AppAccessors, AppAccessorsInstance } from '../lib/accessors/mod'; +import createRoom from '../lib/roomFactory'; +import { RequestContext } from '../lib/requestContext'; +import { wrapComposedApp } from '../lib/wrapAppForRequest'; export default async function slashCommandHandler(request: RequestContext): Promise { const { method: call, params } = request; diff --git a/packages/apps/deno-runtime/handlers/tests/api-handler.test.ts b/packages/apps/deno-runtime/handlers/tests/api-handler.test.ts index 70f68082bdab3..501174cf36d8e 100644 --- a/packages/apps/deno-runtime/handlers/tests/api-handler.test.ts +++ b/packages/apps/deno-runtime/handlers/tests/api-handler.test.ts @@ -6,9 +6,9 @@ import { spy } from 'https://deno.land/std@0.203.0/testing/mock.ts'; import { assertInstanceOf } from 'https://deno.land/std@0.203.0/assert/assert_instance_of.ts'; import { JsonRpcError } from 'jsonrpc-lite'; -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import apiHandler from '../api-handler.ts'; -import { createMockRequest } from './helpers/mod.ts'; +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import apiHandler from '../api-handler'; +import { createMockRequest } from './helpers/mod'; describe('handlers > api', () => { const mockEndpoint: IApiEndpoint = { diff --git a/packages/apps/deno-runtime/handlers/tests/helpers/mod.ts b/packages/apps/deno-runtime/handlers/tests/helpers/mod.ts index 0bd5d97489489..ce4f4df19a030 100644 --- a/packages/apps/deno-runtime/handlers/tests/helpers/mod.ts +++ b/packages/apps/deno-runtime/handlers/tests/helpers/mod.ts @@ -1,8 +1,8 @@ import type { App } from '@rocket.chat/apps-engine/definition/App'; -import { Logger } from '../../../lib/logger.ts'; -import { RequestDescriptor } from '../../../lib/messenger.ts'; -import { RequestContext } from '../../../lib/requestContext.ts'; +import { Logger } from '../../../lib/logger'; +import { RequestDescriptor } from '../../../lib/messenger'; +import { RequestContext } from '../../../lib/requestContext'; export function createMockRequest({ method, params }: RequestDescriptor): RequestContext { return { diff --git a/packages/apps/deno-runtime/handlers/tests/listener-handler.test.ts b/packages/apps/deno-runtime/handlers/tests/listener-handler.test.ts index 8e355f6ac4d3d..6b22d1acdea64 100644 --- a/packages/apps/deno-runtime/handlers/tests/listener-handler.test.ts +++ b/packages/apps/deno-runtime/handlers/tests/listener-handler.test.ts @@ -2,13 +2,13 @@ import { assertEquals, assertInstanceOf, assertObjectMatch } from 'https://deno.land/std@0.203.0/assert/mod.ts'; import { describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; -import { parseArgs } from '../listener/handler.ts'; -import { AppAccessors } from '../../lib/accessors/mod.ts'; -import { Room } from '../../lib/room.ts'; -import { MessageExtender } from '../../lib/accessors/extenders/MessageExtender.ts'; -import { RoomExtender } from '../../lib/accessors/extenders/RoomExtender.ts'; -import { MessageBuilder } from '../../lib/accessors/builders/MessageBuilder.ts'; -import { RoomBuilder } from '../../lib/accessors/builders/RoomBuilder.ts'; +import { parseArgs } from '../listener/handler'; +import { AppAccessors } from '../../lib/accessors/mod'; +import { Room } from '../../lib/room'; +import { MessageExtender } from '../../lib/accessors/extenders/MessageExtender'; +import { RoomExtender } from '../../lib/accessors/extenders/RoomExtender'; +import { MessageBuilder } from '../../lib/accessors/builders/MessageBuilder'; +import { RoomBuilder } from '../../lib/accessors/builders/RoomBuilder'; describe('handlers > listeners', () => { const mockAppAccessors = { diff --git a/packages/apps/deno-runtime/handlers/tests/scheduler-handler.test.ts b/packages/apps/deno-runtime/handlers/tests/scheduler-handler.test.ts index 7f5c6eccaf569..94037b2f96427 100644 --- a/packages/apps/deno-runtime/handlers/tests/scheduler-handler.test.ts +++ b/packages/apps/deno-runtime/handlers/tests/scheduler-handler.test.ts @@ -1,10 +1,10 @@ import { assertEquals } from 'https://deno.land/std@0.203.0/assert/mod.ts'; import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import { AppAccessors } from '../../lib/accessors/mod.ts'; -import handleScheduler from '../scheduler-handler.ts'; -import { createMockApp, createMockRequest } from './helpers/mod.ts'; +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import { AppAccessors } from '../../lib/accessors/mod'; +import handleScheduler from '../scheduler-handler'; +import { createMockApp, createMockRequest } from './helpers/mod'; describe('handlers > scheduler', () => { const mockAppAccessors = new AppAccessors(() => diff --git a/packages/apps/deno-runtime/handlers/tests/slashcommand-handler.test.ts b/packages/apps/deno-runtime/handlers/tests/slashcommand-handler.test.ts index 7114aa1f85bea..e1a8df72ef96a 100644 --- a/packages/apps/deno-runtime/handlers/tests/slashcommand-handler.test.ts +++ b/packages/apps/deno-runtime/handlers/tests/slashcommand-handler.test.ts @@ -3,11 +3,11 @@ import { assertEquals, assertInstanceOf } from 'https://deno.land/std@0.203.0/as import { beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; import { spy } from 'https://deno.land/std@0.203.0/testing/mock.ts'; -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import { AppAccessors } from '../../lib/accessors/mod.ts'; -import { handleExecutor, handlePreviewItem } from '../slashcommand-handler.ts'; -import { Room } from '../../lib/room.ts'; -import { createMockRequest } from './helpers/mod.ts'; +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import { AppAccessors } from '../../lib/accessors/mod'; +import { handleExecutor, handlePreviewItem } from '../slashcommand-handler'; +import { Room } from '../../lib/room'; +import { createMockRequest } from './helpers/mod'; describe('handlers > slashcommand', () => { const mockAppAccessors = { diff --git a/packages/apps/deno-runtime/handlers/tests/uikit-handler.test.ts b/packages/apps/deno-runtime/handlers/tests/uikit-handler.test.ts index b663bd2ae6833..cdc08e214f716 100644 --- a/packages/apps/deno-runtime/handlers/tests/uikit-handler.test.ts +++ b/packages/apps/deno-runtime/handlers/tests/uikit-handler.test.ts @@ -3,14 +3,14 @@ import { assertInstanceOf } from 'https://deno.land/std@0.203.0/assert/mod.ts'; import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; import jsonrpc from 'jsonrpc-lite'; -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { AppObjectRegistry } from '../../AppObjectRegistry'; import handleUIKitInteraction, { UIKitActionButtonInteractionContext, UIKitBlockInteractionContext, UIKitLivechatBlockInteractionContext, UIKitViewCloseInteractionContext, UIKitViewSubmitInteractionContext, -} from '../uikit/handler.ts'; +} from '../uikit/handler'; describe('handlers > uikit', () => { const mockApp = { diff --git a/packages/apps/deno-runtime/handlers/tests/upload-event-handler.test.ts b/packages/apps/deno-runtime/handlers/tests/upload-event-handler.test.ts index 80fb459ba171d..8813d77892403 100644 --- a/packages/apps/deno-runtime/handlers/tests/upload-event-handler.test.ts +++ b/packages/apps/deno-runtime/handlers/tests/upload-event-handler.test.ts @@ -9,10 +9,10 @@ import { afterEach, beforeEach, describe, it } from 'https://deno.land/std@0.203 import { assertSpyCalls, spy } from 'https://deno.land/std@0.203.0/testing/mock.ts'; import { JsonRpcError } from 'jsonrpc-lite'; -import { createMockRequest } from './helpers/mod.ts'; -import handleUploadEvents from '../app/handleUploadEvents.ts'; -import { Errors } from '../lib/assertions.ts'; -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { createMockRequest } from './helpers/mod'; +import handleUploadEvents from '../app/handleUploadEvents'; +import { Errors } from '../lib/assertions'; +import { AppObjectRegistry } from '../../AppObjectRegistry'; describe('handlers > upload', () => { let app: App & IPreFileUpload; diff --git a/packages/apps/deno-runtime/handlers/tests/videoconference-handler.test.ts b/packages/apps/deno-runtime/handlers/tests/videoconference-handler.test.ts index 7632b08c39258..a204b6d410b68 100644 --- a/packages/apps/deno-runtime/handlers/tests/videoconference-handler.test.ts +++ b/packages/apps/deno-runtime/handlers/tests/videoconference-handler.test.ts @@ -4,9 +4,9 @@ import { beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/ import { spy } from 'https://deno.land/std@0.203.0/testing/mock.ts'; import { JsonRpcError } from 'jsonrpc-lite'; -import { createMockRequest } from './helpers/mod.ts'; -import videoconfHandler from '../videoconference-handler.ts'; -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { createMockRequest } from './helpers/mod'; +import videoconfHandler from '../videoconference-handler'; +import { AppObjectRegistry } from '../../AppObjectRegistry'; describe('handlers > videoconference', () => { // deno-lint-ignore no-unused-vars diff --git a/packages/apps/deno-runtime/handlers/uikit/handler.ts b/packages/apps/deno-runtime/handlers/uikit/handler.ts index 9f45989a03ffb..295b8d44306fd 100644 --- a/packages/apps/deno-runtime/handlers/uikit/handler.ts +++ b/packages/apps/deno-runtime/handlers/uikit/handler.ts @@ -1,12 +1,18 @@ import type { App } from '@rocket.chat/apps-engine/definition/App'; +import { + UIKitBlockInteractionContext, + UIKitViewSubmitInteractionContext, + UIKitViewCloseInteractionContext, + UIKitActionButtonInteractionContext, +} from '@rocket.chat/apps-engine/definition/uikit/UIKitInteractionContext.js'; +import { UIKitLivechatBlockInteractionContext } from '@rocket.chat/apps-engine/definition/uikit/livechat/UIKitLivechatInteractionContext.js'; import { Defined, JsonRpcError } from 'jsonrpc-lite'; -import { require } from '../../lib/require.ts'; -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; -import { RequestContext } from '../../lib/requestContext.ts'; -import { isOneOf } from '../lib/assertions.ts'; -import { wrapAppForRequest } from '../../lib/wrapAppForRequest.ts'; +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import { AppAccessorsInstance } from '../../lib/accessors/mod'; +import { RequestContext } from '../../lib/requestContext'; +import { isOneOf } from '../lib/assertions'; +import { wrapAppForRequest } from '../../lib/wrapAppForRequest'; export const uikitInteractions = [ 'executeBlockActionHandler', @@ -16,14 +22,13 @@ export const uikitInteractions = [ 'executeLivechatBlockActionHandler', ] as const; -export const { +export { UIKitBlockInteractionContext, UIKitViewSubmitInteractionContext, UIKitViewCloseInteractionContext, UIKitActionButtonInteractionContext, -} = require('@rocket.chat/apps-engine/definition/uikit/UIKitInteractionContext.js'); - -export const { UIKitLivechatBlockInteractionContext } = require('@rocket.chat/apps-engine/definition/uikit/livechat/UIKitLivechatInteractionContext.js'); + UIKitLivechatBlockInteractionContext, +}; export default async function handleUIKitInteraction(request: RequestContext): Promise { const { method: reqMethod, params } = request; diff --git a/packages/apps/deno-runtime/handlers/videoconference-handler.ts b/packages/apps/deno-runtime/handlers/videoconference-handler.ts index b05c967ca5293..e1fda87000cc8 100644 --- a/packages/apps/deno-runtime/handlers/videoconference-handler.ts +++ b/packages/apps/deno-runtime/handlers/videoconference-handler.ts @@ -1,10 +1,10 @@ import type { IVideoConfProvider } from '@rocket.chat/apps-engine/definition/videoConfProviders/IVideoConfProvider'; import { Defined, JsonRpcError } from 'jsonrpc-lite'; -import { AppObjectRegistry } from '../AppObjectRegistry.ts'; -import { AppAccessorsInstance } from '../lib/accessors/mod.ts'; -import { RequestContext } from '../lib/requestContext.ts'; -import { wrapComposedApp } from '../lib/wrapAppForRequest.ts'; +import { AppObjectRegistry } from '../AppObjectRegistry'; +import { AppAccessorsInstance } from '../lib/accessors/mod'; +import { RequestContext } from '../lib/requestContext'; +import { wrapComposedApp } from '../lib/wrapAppForRequest'; export default async function videoConferenceHandler(request: RequestContext): Promise { const { method: call, params } = request; diff --git a/packages/apps/deno-runtime/lib/accessors/builders/BlockBuilder.ts b/packages/apps/deno-runtime/lib/accessors/builders/BlockBuilder.ts index 77b4c3e8e1696..f7fb6b96c1524 100644 --- a/packages/apps/deno-runtime/lib/accessors/builders/BlockBuilder.ts +++ b/packages/apps/deno-runtime/lib/accessors/builders/BlockBuilder.ts @@ -1,7 +1,6 @@ import { v1 as uuid } from 'uuid'; import type { - BlockType as _BlockType, IActionsBlock, IBlock, IConditionalBlock, @@ -11,8 +10,8 @@ import type { IInputBlock, ISectionBlock, } from '@rocket.chat/apps-engine/definition/uikit/blocks/Blocks'; +import { BlockType } from '@rocket.chat/apps-engine/definition/uikit/blocks/Blocks.js'; import type { - BlockElementType as _BlockElementType, IBlockElement, IButtonElement, IImageElement, @@ -24,14 +23,11 @@ import type { ISelectElement, IStaticSelectElement, } from '@rocket.chat/apps-engine/definition/uikit/blocks/Elements'; -import type { ITextObject, TextObjectType as _TextObjectType } from '@rocket.chat/apps-engine/definition/uikit/blocks/Objects'; +import { BlockElementType } from '@rocket.chat/apps-engine/definition/uikit/blocks/Elements.js'; +import type { ITextObject } from '@rocket.chat/apps-engine/definition/uikit/blocks/Objects'; +import { TextObjectType } from '@rocket.chat/apps-engine/definition/uikit/blocks/Objects.js'; -import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; -import { require } from '../../../lib/require.ts'; - -const { BlockType } = require('@rocket.chat/apps-engine/definition/uikit/blocks/Blocks.js') as { BlockType: typeof _BlockType }; -const { BlockElementType } = require('@rocket.chat/apps-engine/definition/uikit/blocks/Elements.js') as { BlockElementType: typeof _BlockElementType }; -const { TextObjectType } = require('@rocket.chat/apps-engine/definition/uikit/blocks/Objects.js') as { TextObjectType: typeof _TextObjectType }; +import { AppObjectRegistry } from '../../../AppObjectRegistry'; type BlockFunctionParameter = Omit; type ElementFunctionParameter = T extends IInteractiveElement ? Omit | Partial> diff --git a/packages/apps/deno-runtime/lib/accessors/builders/DiscussionBuilder.ts b/packages/apps/deno-runtime/lib/accessors/builders/DiscussionBuilder.ts index d927844a3377f..5498c8eb516f1 100644 --- a/packages/apps/deno-runtime/lib/accessors/builders/DiscussionBuilder.ts +++ b/packages/apps/deno-runtime/lib/accessors/builders/DiscussionBuilder.ts @@ -3,22 +3,15 @@ import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMes import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; import type { IRoomBuilder } from '@rocket.chat/apps-engine/definition/accessors/IRoomBuilder'; -import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; -import type { RoomType as _RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType'; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js'; +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType.js'; -import { RoomBuilder } from './RoomBuilder.ts'; -import { require } from '../../../lib/require.ts'; - -const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { - RocketChatAssociationModel: typeof _RocketChatAssociationModel; -}; - -const { RoomType } = require('@rocket.chat/apps-engine/definition/rooms/RoomType.js') as { RoomType: typeof _RoomType }; +import { RoomBuilder } from './RoomBuilder'; export interface IDiscussionBuilder extends _IDiscussionBuilder, IRoomBuilder {} export class DiscussionBuilder extends RoomBuilder implements IDiscussionBuilder { - public kind: _RocketChatAssociationModel.DISCUSSION; + public kind: RocketChatAssociationModel.DISCUSSION; private reply?: string; diff --git a/packages/apps/deno-runtime/lib/accessors/builders/LivechatMessageBuilder.ts b/packages/apps/deno-runtime/lib/accessors/builders/LivechatMessageBuilder.ts index 27426179386dd..bdcf6e571147a 100644 --- a/packages/apps/deno-runtime/lib/accessors/builders/LivechatMessageBuilder.ts +++ b/packages/apps/deno-runtime/lib/accessors/builders/LivechatMessageBuilder.ts @@ -1,5 +1,5 @@ -import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; -import type { RoomType as _RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType'; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js'; +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType.js'; import type { ILivechatMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/ILivechatMessageBuilder'; import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage'; @@ -10,19 +10,12 @@ import type { ILivechatMessage as EngineLivechatMessage } from '@rocket.chat/app import type { IVisitor } from '@rocket.chat/apps-engine/definition/livechat/IVisitor'; import type { IMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/IMessageBuilder'; -import { MessageBuilder } from './MessageBuilder.ts'; -import { require } from '../../../lib/require.ts'; - -const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { - RocketChatAssociationModel: typeof _RocketChatAssociationModel; -}; - -const { RoomType } = require('@rocket.chat/apps-engine/definition/rooms/RoomType.js') as { RoomType: typeof _RoomType }; +import { MessageBuilder } from './MessageBuilder'; export interface ILivechatMessage extends EngineLivechatMessage, IMessage {} export class LivechatMessageBuilder implements ILivechatMessageBuilder { - public kind: _RocketChatAssociationModel.LIVECHAT_MESSAGE; + public kind: RocketChatAssociationModel.LIVECHAT_MESSAGE; private msg: ILivechatMessage; diff --git a/packages/apps/deno-runtime/lib/accessors/builders/MessageBuilder.ts b/packages/apps/deno-runtime/lib/accessors/builders/MessageBuilder.ts index acea8aacc0cc8..300f8bb6710d9 100644 --- a/packages/apps/deno-runtime/lib/accessors/builders/MessageBuilder.ts +++ b/packages/apps/deno-runtime/lib/accessors/builders/MessageBuilder.ts @@ -1,22 +1,17 @@ import { LayoutBlock } from '@rocket.chat/ui-kit'; import type { IMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/IMessageBuilder'; -import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js'; import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage'; import type { IMessageAttachment } from '@rocket.chat/apps-engine/definition/messages/IMessageAttachment'; import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; import type { IBlock } from '@rocket.chat/apps-engine/definition/uikit/blocks/Blocks'; -import { BlockBuilder } from './BlockBuilder.ts'; -import { require } from '../../../lib/require.ts'; - -const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { - RocketChatAssociationModel: typeof _RocketChatAssociationModel; -}; +import { BlockBuilder } from './BlockBuilder'; export class MessageBuilder implements IMessageBuilder { - public kind: _RocketChatAssociationModel.MESSAGE; + public kind: RocketChatAssociationModel.MESSAGE; private msg: IMessage; diff --git a/packages/apps/deno-runtime/lib/accessors/builders/RoomBuilder.ts b/packages/apps/deno-runtime/lib/accessors/builders/RoomBuilder.ts index a2912e44913dd..9543b0905b490 100644 --- a/packages/apps/deno-runtime/lib/accessors/builders/RoomBuilder.ts +++ b/packages/apps/deno-runtime/lib/accessors/builders/RoomBuilder.ts @@ -3,16 +3,10 @@ import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser'; import type { RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType'; -import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; - -import { require } from '../../../lib/require.ts'; - -const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { - RocketChatAssociationModel: typeof _RocketChatAssociationModel; -}; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js'; export class RoomBuilder implements IRoomBuilder { - public kind: _RocketChatAssociationModel.ROOM | _RocketChatAssociationModel.DISCUSSION; + public kind: RocketChatAssociationModel.ROOM | RocketChatAssociationModel.DISCUSSION; protected room: IRoom; diff --git a/packages/apps/deno-runtime/lib/accessors/builders/UserBuilder.ts b/packages/apps/deno-runtime/lib/accessors/builders/UserBuilder.ts index 079607b4ecfd0..9c1db45a247a7 100644 --- a/packages/apps/deno-runtime/lib/accessors/builders/UserBuilder.ts +++ b/packages/apps/deno-runtime/lib/accessors/builders/UserBuilder.ts @@ -2,16 +2,10 @@ import type { IUserBuilder } from '@rocket.chat/apps-engine/definition/accessors import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser'; import type { IUserSettings } from '@rocket.chat/apps-engine/definition/users/IUserSettings'; import type { IUserEmail } from '@rocket.chat/apps-engine/definition/users/IUserEmail'; -import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; - -import { require } from '../../../lib/require.ts'; - -const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { - RocketChatAssociationModel: typeof _RocketChatAssociationModel; -}; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js'; export class UserBuilder implements IUserBuilder { - public kind: _RocketChatAssociationModel.USER; + public kind: RocketChatAssociationModel.USER; private user: Partial; diff --git a/packages/apps/deno-runtime/lib/accessors/builders/VideoConferenceBuilder.ts b/packages/apps/deno-runtime/lib/accessors/builders/VideoConferenceBuilder.ts index 2ab47745d4b64..a4326ce2304de 100644 --- a/packages/apps/deno-runtime/lib/accessors/builders/VideoConferenceBuilder.ts +++ b/packages/apps/deno-runtime/lib/accessors/builders/VideoConferenceBuilder.ts @@ -1,20 +1,14 @@ import type { IVideoConferenceBuilder } from '@rocket.chat/apps-engine/definition/accessors/IVideoConferenceBuilder'; import type { IGroupVideoConference } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConference'; -import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; - -import { require } from '../../../lib/require.ts'; - -const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { - RocketChatAssociationModel: typeof _RocketChatAssociationModel; -}; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js'; export type AppVideoConference = Pick & { createdBy: IGroupVideoConference['createdBy']['_id']; }; export class VideoConferenceBuilder implements IVideoConferenceBuilder { - public kind: _RocketChatAssociationModel.VIDEO_CONFERENCE = RocketChatAssociationModel.VIDEO_CONFERENCE; + public kind: RocketChatAssociationModel.VIDEO_CONFERENCE = RocketChatAssociationModel.VIDEO_CONFERENCE; protected call: AppVideoConference; diff --git a/packages/apps/deno-runtime/lib/accessors/extenders/MessageExtender.ts b/packages/apps/deno-runtime/lib/accessors/extenders/MessageExtender.ts index 0364202b0f320..55b62c207dcc8 100644 --- a/packages/apps/deno-runtime/lib/accessors/extenders/MessageExtender.ts +++ b/packages/apps/deno-runtime/lib/accessors/extenders/MessageExtender.ts @@ -1,16 +1,10 @@ import type { IMessageExtender } from '@rocket.chat/apps-engine/definition/accessors/IMessageExtender'; -import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js'; import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage'; import type { IMessageAttachment } from '@rocket.chat/apps-engine/definition/messages/IMessageAttachment'; -import { require } from '../../../lib/require.ts'; - -const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { - RocketChatAssociationModel: typeof _RocketChatAssociationModel; -}; - export class MessageExtender implements IMessageExtender { - public readonly kind: _RocketChatAssociationModel.MESSAGE; + public readonly kind: RocketChatAssociationModel.MESSAGE; constructor(private msg: IMessage) { this.kind = RocketChatAssociationModel.MESSAGE; diff --git a/packages/apps/deno-runtime/lib/accessors/extenders/RoomExtender.ts b/packages/apps/deno-runtime/lib/accessors/extenders/RoomExtender.ts index ae3ebeaf34f9e..1a59081151c0c 100644 --- a/packages/apps/deno-runtime/lib/accessors/extenders/RoomExtender.ts +++ b/packages/apps/deno-runtime/lib/accessors/extenders/RoomExtender.ts @@ -1,16 +1,10 @@ import type { IRoomExtender } from '@rocket.chat/apps-engine/definition/accessors/IRoomExtender'; -import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser'; -import { require } from '../../../lib/require.ts'; - -const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { - RocketChatAssociationModel: typeof _RocketChatAssociationModel; -}; - export class RoomExtender implements IRoomExtender { - public kind: _RocketChatAssociationModel.ROOM; + public kind: RocketChatAssociationModel.ROOM; private members: Array; diff --git a/packages/apps/deno-runtime/lib/accessors/extenders/VideoConferenceExtend.ts b/packages/apps/deno-runtime/lib/accessors/extenders/VideoConferenceExtend.ts index de4e4a0121af2..ba11b818ce271 100644 --- a/packages/apps/deno-runtime/lib/accessors/extenders/VideoConferenceExtend.ts +++ b/packages/apps/deno-runtime/lib/accessors/extenders/VideoConferenceExtend.ts @@ -1,16 +1,10 @@ import type { IVideoConferenceExtender } from '@rocket.chat/apps-engine/definition/accessors/IVideoConferenceExtend'; import type { VideoConference, VideoConferenceMember } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConference'; import type { IVideoConferenceUser } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConferenceUser'; -import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; - -import { require } from '../../../lib/require.ts'; - -const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { - RocketChatAssociationModel: typeof _RocketChatAssociationModel; -}; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js'; export class VideoConferenceExtender implements IVideoConferenceExtender { - public kind: _RocketChatAssociationModel.VIDEO_CONFERENCE; + public kind: RocketChatAssociationModel.VIDEO_CONFERENCE; constructor(private videoConference: VideoConference) { this.kind = RocketChatAssociationModel.VIDEO_CONFERENCE; diff --git a/packages/apps/deno-runtime/lib/accessors/http.ts b/packages/apps/deno-runtime/lib/accessors/http.ts index 76a2a3ac9fc00..78e4e8d7f8d37 100644 --- a/packages/apps/deno-runtime/lib/accessors/http.ts +++ b/packages/apps/deno-runtime/lib/accessors/http.ts @@ -2,9 +2,9 @@ import type { IHttp, IHttpExtend, IHttpRequest, IHttpResponse } from '@rocket.ch import type { IPersistence } from '@rocket.chat/apps-engine/definition/accessors/IPersistence'; import type { IRead } from '@rocket.chat/apps-engine/definition/accessors/IRead'; -import * as Messenger from '../messenger.ts'; -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import { formatErrorResponse } from './formatResponseErrorHandler.ts'; +import * as Messenger from '../messenger'; +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import { formatErrorResponse } from './formatResponseErrorHandler'; type RequestMethod = 'get' | 'post' | 'put' | 'head' | 'delete' | 'patch'; diff --git a/packages/apps/deno-runtime/lib/accessors/mod.ts b/packages/apps/deno-runtime/lib/accessors/mod.ts index cfc6bcf629316..17073545edeb0 100644 --- a/packages/apps/deno-runtime/lib/accessors/mod.ts +++ b/packages/apps/deno-runtime/lib/accessors/mod.ts @@ -18,15 +18,15 @@ import type { IOutboundEmailMessageProvider, } from '@rocket.chat/apps-engine/definition/outboundCommunication/IOutboundCommsProvider'; -import { Http } from './http.ts'; -import { HttpExtend } from './extenders/HttpExtender.ts'; -import * as Messenger from '../messenger.ts'; -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import { ModifyCreator } from './modify/ModifyCreator.ts'; -import { ModifyUpdater } from './modify/ModifyUpdater.ts'; -import { ModifyExtender } from './modify/ModifyExtender.ts'; -import { Notifier } from './notifier.ts'; -import { formatErrorResponse } from './formatResponseErrorHandler.ts'; +import { Http } from './http'; +import { HttpExtend } from './extenders/HttpExtender'; +import * as Messenger from '../messenger'; +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import { ModifyCreator } from './modify/ModifyCreator'; +import { ModifyUpdater } from './modify/ModifyUpdater'; +import { ModifyExtender } from './modify/ModifyExtender'; +import { Notifier } from './notifier'; +import { formatErrorResponse } from './formatResponseErrorHandler'; const httpMethods = ['get', 'post', 'put', 'delete', 'head', 'options', 'patch'] as const; diff --git a/packages/apps/deno-runtime/lib/accessors/modify/ModifyCreator.ts b/packages/apps/deno-runtime/lib/accessors/modify/ModifyCreator.ts index fea76bacddc4a..9422f093f2e21 100644 --- a/packages/apps/deno-runtime/lib/accessors/modify/ModifyCreator.ts +++ b/packages/apps/deno-runtime/lib/accessors/modify/ModifyCreator.ts @@ -9,33 +9,26 @@ import type { ILivechatCreator } from '@rocket.chat/apps-engine/definition/acces import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; import type { IBotUser } from '@rocket.chat/apps-engine/definition/users/IBotUser'; -import type { UserType as _UserType } from '@rocket.chat/apps-engine/definition/users/UserType'; -import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; +import { UserType } from '@rocket.chat/apps-engine/definition/users/UserType.js'; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js'; import type { IMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/IMessageBuilder'; import type { IRoomBuilder } from '@rocket.chat/apps-engine/definition/accessors/IRoomBuilder'; import type { IUserBuilder } from '@rocket.chat/apps-engine/definition/accessors/IUserBuilder'; import type { IVideoConferenceBuilder } from '@rocket.chat/apps-engine/definition/accessors/IVideoConferenceBuilder'; -import type { RoomType as _RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType'; +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType.js'; import type { ILivechatMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/ILivechatMessageBuilder'; -import * as Messenger from '../../messenger.ts'; - -import { BlockBuilder } from '../builders/BlockBuilder.ts'; -import { MessageBuilder } from '../builders/MessageBuilder.ts'; -import { DiscussionBuilder, IDiscussionBuilder } from '../builders/DiscussionBuilder.ts'; -import { ILivechatMessage, LivechatMessageBuilder } from '../builders/LivechatMessageBuilder.ts'; -import { RoomBuilder } from '../builders/RoomBuilder.ts'; -import { UserBuilder } from '../builders/UserBuilder.ts'; -import { AppVideoConference, VideoConferenceBuilder } from '../builders/VideoConferenceBuilder.ts'; -import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; -import { require } from '../../../lib/require.ts'; -import { formatErrorResponse } from '../formatResponseErrorHandler.ts'; - -const { RoomType } = require('@rocket.chat/apps-engine/definition/rooms/RoomType.js') as { RoomType: typeof _RoomType }; -const { UserType } = require('@rocket.chat/apps-engine/definition/users/UserType.js') as { UserType: typeof _UserType }; -const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { - RocketChatAssociationModel: typeof _RocketChatAssociationModel; -}; +import * as Messenger from '../../messenger'; + +import { BlockBuilder } from '../builders/BlockBuilder'; +import { MessageBuilder } from '../builders/MessageBuilder'; +import { DiscussionBuilder, IDiscussionBuilder } from '../builders/DiscussionBuilder'; +import { ILivechatMessage, LivechatMessageBuilder } from '../builders/LivechatMessageBuilder'; +import { RoomBuilder } from '../builders/RoomBuilder'; +import { UserBuilder } from '../builders/UserBuilder'; +import { AppVideoConference, VideoConferenceBuilder } from '../builders/VideoConferenceBuilder'; +import { AppObjectRegistry } from '../../../AppObjectRegistry'; +import { formatErrorResponse } from '../formatResponseErrorHandler'; export class ModifyCreator implements IModifyCreator { constructor(private readonly senderFn: typeof Messenger.sendRequest) {} diff --git a/packages/apps/deno-runtime/lib/accessors/modify/ModifyExtender.ts b/packages/apps/deno-runtime/lib/accessors/modify/ModifyExtender.ts index 01bbca2ab0ee0..e35315d1e765e 100644 --- a/packages/apps/deno-runtime/lib/accessors/modify/ModifyExtender.ts +++ b/packages/apps/deno-runtime/lib/accessors/modify/ModifyExtender.ts @@ -6,19 +6,14 @@ import type { IVideoConferenceExtender } from '@rocket.chat/apps-engine/definiti import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser'; import type { VideoConference } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConference'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; -import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; - -import * as Messenger from '../../messenger.ts'; -import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; -import { MessageExtender } from '../extenders/MessageExtender.ts'; -import { RoomExtender } from '../extenders/RoomExtender.ts'; -import { VideoConferenceExtender } from '../extenders/VideoConferenceExtend.ts'; -import { require } from '../../../lib/require.ts'; -import { formatErrorResponse } from '../formatResponseErrorHandler.ts'; - -const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { - RocketChatAssociationModel: typeof _RocketChatAssociationModel; -}; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js'; + +import * as Messenger from '../../messenger'; +import { AppObjectRegistry } from '../../../AppObjectRegistry'; +import { MessageExtender } from '../extenders/MessageExtender'; +import { RoomExtender } from '../extenders/RoomExtender'; +import { VideoConferenceExtender } from '../extenders/VideoConferenceExtend'; +import { formatErrorResponse } from '../formatResponseErrorHandler'; export class ModifyExtender implements IModifyExtender { constructor(private readonly senderFn: typeof Messenger.sendRequest) {} diff --git a/packages/apps/deno-runtime/lib/accessors/modify/ModifyUpdater.ts b/packages/apps/deno-runtime/lib/accessors/modify/ModifyUpdater.ts index 06bcb07f8765e..0903d1c1506f5 100644 --- a/packages/apps/deno-runtime/lib/accessors/modify/ModifyUpdater.ts +++ b/packages/apps/deno-runtime/lib/accessors/modify/ModifyUpdater.ts @@ -9,22 +9,16 @@ import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser'; import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; -import type { RoomType as _RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType'; -import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType.js'; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js'; -import * as Messenger from '../../messenger.ts'; +import * as Messenger from '../../messenger'; -import { MessageBuilder } from '../builders/MessageBuilder.ts'; -import { RoomBuilder } from '../builders/RoomBuilder.ts'; -import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; +import { MessageBuilder } from '../builders/MessageBuilder'; +import { RoomBuilder } from '../builders/RoomBuilder'; +import { AppObjectRegistry } from '../../../AppObjectRegistry'; -import { require } from '../../../lib/require.ts'; -import { formatErrorResponse } from '../formatResponseErrorHandler.ts'; - -const { RoomType } = require('@rocket.chat/apps-engine/definition/rooms/RoomType.js') as { RoomType: typeof _RoomType }; -const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { - RocketChatAssociationModel: typeof _RocketChatAssociationModel; -}; +import { formatErrorResponse } from '../formatResponseErrorHandler'; export class ModifyUpdater implements IModifyUpdater { private readonly livechatUpdater: ILivechatUpdater; diff --git a/packages/apps/deno-runtime/lib/accessors/notifier.ts b/packages/apps/deno-runtime/lib/accessors/notifier.ts index 3a6ac012a2797..d88dc82493c84 100644 --- a/packages/apps/deno-runtime/lib/accessors/notifier.ts +++ b/packages/apps/deno-runtime/lib/accessors/notifier.ts @@ -1,18 +1,13 @@ import type { IMessageBuilder, INotifier } from '@rocket.chat/apps-engine/definition/accessors'; import type { ITypingOptions } from '@rocket.chat/apps-engine/definition/accessors/INotifier'; -import type { _TypingScope } from '@rocket.chat/apps-engine/definition/accessors/INotifier'; +import { TypingScope } from '@rocket.chat/apps-engine/definition/accessors/INotifier.js'; import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; import type { IUser } from '@rocket.chat/apps-engine/definition/users'; -import { MessageBuilder } from './builders/MessageBuilder.ts'; -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import * as Messenger from '../messenger.ts'; -import { require } from '../require.ts'; -import { formatErrorResponse } from './formatResponseErrorHandler.ts'; - -const { TypingScope } = require('@rocket.chat/apps-engine/definition/accessors/INotifier.js') as { - TypingScope: typeof _TypingScope; -}; +import { MessageBuilder } from './builders/MessageBuilder'; +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import * as Messenger from '../messenger'; +import { formatErrorResponse } from './formatResponseErrorHandler'; export class Notifier implements INotifier { private senderFn: typeof Messenger.sendRequest; diff --git a/packages/apps/deno-runtime/lib/accessors/tests/AppAccessors.test.ts b/packages/apps/deno-runtime/lib/accessors/tests/AppAccessors.test.ts index ffc77b6904bb7..52cca5f35cac6 100644 --- a/packages/apps/deno-runtime/lib/accessors/tests/AppAccessors.test.ts +++ b/packages/apps/deno-runtime/lib/accessors/tests/AppAccessors.test.ts @@ -1,8 +1,8 @@ import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; import { assertEquals } from 'https://deno.land/std@0.203.0/assert/assert_equals.ts'; -import { AppAccessors } from '../mod.ts'; -import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; +import { AppAccessors } from '../mod'; +import { AppObjectRegistry } from '../../../AppObjectRegistry'; describe('AppAccessors', () => { let appAccessors: AppAccessors; diff --git a/packages/apps/deno-runtime/lib/accessors/tests/ModifyCreator.test.ts b/packages/apps/deno-runtime/lib/accessors/tests/ModifyCreator.test.ts index d88690a77dbfa..3133ced740f8a 100644 --- a/packages/apps/deno-runtime/lib/accessors/tests/ModifyCreator.test.ts +++ b/packages/apps/deno-runtime/lib/accessors/tests/ModifyCreator.test.ts @@ -3,8 +3,8 @@ import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203. import { assertSpyCall, spy } from 'https://deno.land/std@0.203.0/testing/mock.ts'; import { assert, assertEquals, assertNotInstanceOf, assertRejects } from 'https://deno.land/std@0.203.0/assert/mod.ts'; -import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; -import { ModifyCreator } from '../modify/ModifyCreator.ts'; +import { AppObjectRegistry } from '../../../AppObjectRegistry'; +import { ModifyCreator } from '../modify/ModifyCreator'; describe('ModifyCreator', () => { const senderFn = (r: any) => diff --git a/packages/apps/deno-runtime/lib/accessors/tests/ModifyExtender.test.ts b/packages/apps/deno-runtime/lib/accessors/tests/ModifyExtender.test.ts index de6fd4a7053b3..a636e5ba0470c 100644 --- a/packages/apps/deno-runtime/lib/accessors/tests/ModifyExtender.test.ts +++ b/packages/apps/deno-runtime/lib/accessors/tests/ModifyExtender.test.ts @@ -3,8 +3,8 @@ import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203. import { assertSpyCall, spy, stub } from 'https://deno.land/std@0.203.0/testing/mock.ts'; import { assertRejects } from 'https://deno.land/std@0.203.0/assert/mod.ts'; -import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; -import { ModifyExtender } from '../modify/ModifyExtender.ts'; +import { AppObjectRegistry } from '../../../AppObjectRegistry'; +import { ModifyExtender } from '../modify/ModifyExtender'; import jsonrpc from 'jsonrpc-lite'; describe('ModifyExtender', () => { diff --git a/packages/apps/deno-runtime/lib/accessors/tests/ModifyUpdater.test.ts b/packages/apps/deno-runtime/lib/accessors/tests/ModifyUpdater.test.ts index d351d6ebba721..dd2bddc4ab363 100644 --- a/packages/apps/deno-runtime/lib/accessors/tests/ModifyUpdater.test.ts +++ b/packages/apps/deno-runtime/lib/accessors/tests/ModifyUpdater.test.ts @@ -3,9 +3,9 @@ import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203. import { assertSpyCall, spy, stub } from 'https://deno.land/std@0.203.0/testing/mock.ts'; import { assertEquals, assertRejects } from 'https://deno.land/std@0.203.0/assert/mod.ts'; -import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; -import { ModifyUpdater } from '../modify/ModifyUpdater.ts'; -import { RoomBuilder } from '../builders/RoomBuilder.ts'; +import { AppObjectRegistry } from '../../../AppObjectRegistry'; +import { ModifyUpdater } from '../modify/ModifyUpdater'; +import { RoomBuilder } from '../builders/RoomBuilder'; import jsonrpc from 'jsonrpc-lite'; describe('ModifyUpdater', () => { diff --git a/packages/apps/deno-runtime/lib/accessors/tests/formatResponseErrorHandler.test.ts b/packages/apps/deno-runtime/lib/accessors/tests/formatResponseErrorHandler.test.ts index c909fecdd04a1..45e9fac18c560 100644 --- a/packages/apps/deno-runtime/lib/accessors/tests/formatResponseErrorHandler.test.ts +++ b/packages/apps/deno-runtime/lib/accessors/tests/formatResponseErrorHandler.test.ts @@ -3,7 +3,7 @@ import { assertEquals, assertInstanceOf, assertStrictEquals } from 'https://deno import { describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; import * as jsonrpc from 'jsonrpc-lite'; -import { formatErrorResponse } from '../formatResponseErrorHandler.ts'; +import { formatErrorResponse } from '../formatResponseErrorHandler'; describe('formatErrorResponse', () => { describe('JSON-RPC ErrorObject handling', () => { diff --git a/packages/apps/deno-runtime/lib/accessors/tests/http.test.ts b/packages/apps/deno-runtime/lib/accessors/tests/http.test.ts index 88392dec774cc..732ffe6c5d1f0 100644 --- a/packages/apps/deno-runtime/lib/accessors/tests/http.test.ts +++ b/packages/apps/deno-runtime/lib/accessors/tests/http.test.ts @@ -2,8 +2,8 @@ import { assertRejects } from 'https://deno.land/std@0.203.0/assert/mod.ts'; import { beforeEach, describe, it, afterAll } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; -import { Http } from '../http.ts'; -import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; +import { Http } from '../http'; +import { AppObjectRegistry } from '../../../AppObjectRegistry'; import { stub } from 'https://deno.land/std@0.203.0/testing/mock.ts'; describe('Http accessor error handling integration', () => { diff --git a/packages/apps/deno-runtime/lib/ast/mod.ts b/packages/apps/deno-runtime/lib/ast/mod.ts index 555b4defc36a0..f18d3de4ffc9d 100644 --- a/packages/apps/deno-runtime/lib/ast/mod.ts +++ b/packages/apps/deno-runtime/lib/ast/mod.ts @@ -4,8 +4,8 @@ import { parse, Program } from 'acorn'; // @deno-types="../../acorn-walk.d.ts" import { fullAncestor } from 'acorn-walk'; -import * as operations from './operations.ts'; -import type { WalkerState } from './operations.ts'; +import * as operations from './operations'; +import type { WalkerState } from './operations'; function fixAst(ast: Program): boolean { const pendingOperations = [ diff --git a/packages/apps/deno-runtime/lib/ast/tests/operations.test.ts b/packages/apps/deno-runtime/lib/ast/tests/operations.test.ts index 809de475013c9..9086e1c1de39b 100644 --- a/packages/apps/deno-runtime/lib/ast/tests/operations.test.ts +++ b/packages/apps/deno-runtime/lib/ast/tests/operations.test.ts @@ -8,7 +8,7 @@ import { getFunctionIdentifier, WalkerState, wrapWithAwait, -} from '../operations.ts'; +} from '../operations'; import { ArrowFunctionDerefCallExpression, AssignmentExpressionOfArrowFunctionToFooIdentifier, @@ -23,7 +23,7 @@ import { MethodDefinitionOfFooInClassBar, SimpleCallExpressionOfFoo, SyncFunctionDeclarationWithAsyncCallExpression, -} from './data/ast_blocks.ts'; +} from './data/ast_blocks'; import { AnyNode, ArrowFunctionExpression, @@ -33,7 +33,7 @@ import { MethodDefinition, ReturnStatement, VariableDeclaration, -} from '../../../acorn.d.ts'; +} from '../../../acorn.d'; import { assertNotEquals } from 'https://deno.land/std@0.203.0/assert/assert_not_equals.ts'; describe('getFunctionIdentifier', () => { diff --git a/packages/apps/deno-runtime/lib/codec.ts b/packages/apps/deno-runtime/lib/codec.ts index 90456c76968c2..0ca0ecb0c9c49 100644 --- a/packages/apps/deno-runtime/lib/codec.ts +++ b/packages/apps/deno-runtime/lib/codec.ts @@ -1,19 +1,14 @@ import { Buffer } from 'node:buffer'; import { decode, Decoder, Encoder, ExtensionCodec } from '@msgpack/msgpack'; -import type { App as _App } from '@rocket.chat/apps-engine/definition/App'; +import { App } from '@rocket.chat/apps-engine/definition/App.js'; -import { require } from './require.ts'; -import { applySecureFields, type WithSecureFields } from './secureFields.ts'; +import { applySecureFields, type WithSecureFields } from './secureFields'; const FUNCTION_DISABLER_EXT = 0; const BUFFER_HANDLER_EXT = 1; const SECURE_FIELDS_HANDLER_EXT = 2; -const { App } = require('@rocket.chat/apps-engine/definition/App.js') as { - App: typeof _App; -}; - const extensionCodec = new ExtensionCodec(); extensionCodec.register({ diff --git a/packages/apps/deno-runtime/lib/logger.ts b/packages/apps/deno-runtime/lib/logger.ts index 336c420080d37..59b6206471518 100644 --- a/packages/apps/deno-runtime/lib/logger.ts +++ b/packages/apps/deno-runtime/lib/logger.ts @@ -1,5 +1,5 @@ import stackTrace from 'stack-trace'; -import { AppObjectRegistry } from '../AppObjectRegistry.ts'; +import { AppObjectRegistry } from '../AppObjectRegistry'; export interface StackFrame { getTypeName(): string; diff --git a/packages/apps/deno-runtime/lib/messenger.ts b/packages/apps/deno-runtime/lib/messenger.ts index 3a55aba594f7c..4b938459e9715 100644 --- a/packages/apps/deno-runtime/lib/messenger.ts +++ b/packages/apps/deno-runtime/lib/messenger.ts @@ -2,8 +2,8 @@ import { writeAll } from '@std/io'; import * as jsonrpc from 'jsonrpc-lite'; -import { encoder } from './codec.ts'; -import { RequestContext } from './requestContext.ts'; +import { encoder } from './codec'; +import { RequestContext } from './requestContext'; export type RequestDescriptor = Pick; diff --git a/packages/apps/deno-runtime/lib/metricsCollector.ts b/packages/apps/deno-runtime/lib/metricsCollector.ts index 8484aef826f9b..ac968caccace3 100644 --- a/packages/apps/deno-runtime/lib/metricsCollector.ts +++ b/packages/apps/deno-runtime/lib/metricsCollector.ts @@ -1,5 +1,5 @@ import { writeAll } from '@std/io'; -import { Queue } from './messenger.ts'; +import { Queue } from './messenger'; export function collectMetrics() { return { diff --git a/packages/apps/deno-runtime/lib/requestContext.ts b/packages/apps/deno-runtime/lib/requestContext.ts index 91e9346f34bd4..28b36cdeacb3d 100644 --- a/packages/apps/deno-runtime/lib/requestContext.ts +++ b/packages/apps/deno-runtime/lib/requestContext.ts @@ -1,6 +1,6 @@ import { RequestObject } from 'jsonrpc-lite'; -import { Logger } from './logger.ts'; +import { Logger } from './logger'; export type RequestContext = RequestObject & { context: { diff --git a/packages/apps/deno-runtime/lib/roomFactory.ts b/packages/apps/deno-runtime/lib/roomFactory.ts index 5f7f71a5e7852..be643ec774e49 100644 --- a/packages/apps/deno-runtime/lib/roomFactory.ts +++ b/packages/apps/deno-runtime/lib/roomFactory.ts @@ -1,9 +1,9 @@ import type { AppManager } from '@rocket.chat/apps/dist/server/AppManager'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; -import { AppAccessors } from './accessors/mod.ts'; -import { formatErrorResponse } from './accessors/formatResponseErrorHandler.ts'; -import { Room } from './room.ts'; +import { AppAccessors } from './accessors/mod'; +import { formatErrorResponse } from './accessors/formatResponseErrorHandler'; +import { Room } from './room'; const getMockAppManager = (senderFn: AppAccessors['senderFn']) => ({ getBridges: () => ({ diff --git a/packages/apps/deno-runtime/lib/sanitizeDeprecatedUsage.ts b/packages/apps/deno-runtime/lib/sanitizeDeprecatedUsage.ts index 4b5838bce12d1..aa2898418db81 100644 --- a/packages/apps/deno-runtime/lib/sanitizeDeprecatedUsage.ts +++ b/packages/apps/deno-runtime/lib/sanitizeDeprecatedUsage.ts @@ -1,4 +1,4 @@ -import { fixBrokenSynchronousAPICalls } from './ast/mod.ts'; +import { fixBrokenSynchronousAPICalls } from './ast/mod'; function hasPotentialDeprecatedUsage(source: string) { return ( diff --git a/packages/apps/deno-runtime/lib/secureFields.ts b/packages/apps/deno-runtime/lib/secureFields.ts index aabb5cf854b5c..e028c824c5951 100644 --- a/packages/apps/deno-runtime/lib/secureFields.ts +++ b/packages/apps/deno-runtime/lib/secureFields.ts @@ -1,7 +1,7 @@ import { kSecureFields, WithSecureFields } from '@rocket.chat/apps/dist/lib/SecureFields'; import type { App } from '@rocket.chat/apps-engine/definition/App'; -import { AppObjectRegistry } from '../AppObjectRegistry.ts'; +import { AppObjectRegistry } from '../AppObjectRegistry'; export type { WithSecureFields } from '@rocket.chat/apps/dist/lib/SecureFields'; diff --git a/packages/apps/deno-runtime/lib/tests/logger.test.ts b/packages/apps/deno-runtime/lib/tests/logger.test.ts index 7ccc49b3b9ca4..ee3da4bc8340d 100644 --- a/packages/apps/deno-runtime/lib/tests/logger.test.ts +++ b/packages/apps/deno-runtime/lib/tests/logger.test.ts @@ -1,6 +1,6 @@ import { assertEquals } from 'https://deno.land/std@0.203.0/assert/mod.ts'; import { describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; -import { Logger } from '../logger.ts'; +import { Logger } from '../logger'; describe('Logger', () => { it('getLogs should return an array of entries', () => { diff --git a/packages/apps/deno-runtime/lib/tests/messenger.test.ts b/packages/apps/deno-runtime/lib/tests/messenger.test.ts index 47b46f0db6e33..d278c4ccbd545 100644 --- a/packages/apps/deno-runtime/lib/tests/messenger.test.ts +++ b/packages/apps/deno-runtime/lib/tests/messenger.test.ts @@ -2,10 +2,10 @@ import { assertEquals, assertObjectMatch } from 'https://deno.land/std@0.203.0/a import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; import { spy } from 'https://deno.land/std@0.203.0/testing/mock.ts'; -import * as Messenger from '../messenger.ts'; -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import { createMockRequest } from '../../handlers/tests/helpers/mod.ts'; -import { RequestContext } from '../requestContext.ts'; +import * as Messenger from '../messenger'; +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import { createMockRequest } from '../../handlers/tests/helpers/mod'; +import { RequestContext } from '../requestContext'; import { JsonRpc } from 'jsonrpc-lite'; describe('Messenger', () => { diff --git a/packages/apps/deno-runtime/lib/tests/secureFields.test.ts b/packages/apps/deno-runtime/lib/tests/secureFields.test.ts index f48a0af8d6f6a..d6d780c0fffaf 100644 --- a/packages/apps/deno-runtime/lib/tests/secureFields.test.ts +++ b/packages/apps/deno-runtime/lib/tests/secureFields.test.ts @@ -1,8 +1,8 @@ import { assertEquals, assertThrows } from 'https://deno.land/std@0.203.0/assert/mod.ts'; import { beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; -import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; -import { applySecureFields } from '../secureFields.ts'; +import { AppObjectRegistry } from '../../AppObjectRegistry'; +import { applySecureFields } from '../secureFields'; const SECURE_FIELDS_KEY = '@@SecureFields'; diff --git a/packages/apps/deno-runtime/lib/wrapAppForRequest.ts b/packages/apps/deno-runtime/lib/wrapAppForRequest.ts index 40bc49eca744c..33ea2d8cdb4dd 100644 --- a/packages/apps/deno-runtime/lib/wrapAppForRequest.ts +++ b/packages/apps/deno-runtime/lib/wrapAppForRequest.ts @@ -1,7 +1,7 @@ import type { App } from '@rocket.chat/apps-engine/definition/App'; -import { RequestContext } from './requestContext.ts'; -import { isApp, isRecord } from '../handlers/lib/assertions.ts'; +import { RequestContext } from './requestContext'; +import { isApp, isRecord } from '../handlers/lib/assertions'; export function wrapAppForRequest(app: App, req: RequestContext): App { return new Proxy(app, { diff --git a/packages/apps/deno-runtime/main.ts b/packages/apps/deno-runtime/main.ts index 1ce1b15b7a2cd..13ce43302923c 100644 --- a/packages/apps/deno-runtime/main.ts +++ b/packages/apps/deno-runtime/main.ts @@ -10,19 +10,19 @@ if (!Deno.args.includes('--subprocess')) { import { JsonRpcError } from 'jsonrpc-lite'; -import * as Messenger from './lib/messenger.ts'; -import { decoder } from './lib/codec.ts'; -import { Logger } from './lib/logger.ts'; - -import slashcommandHandler from './handlers/slashcommand-handler.ts'; -import videoConferenceHandler from './handlers/videoconference-handler.ts'; -import apiHandler from './handlers/api-handler.ts'; -import handleApp from './handlers/app/handler.ts'; -import handleScheduler from './handlers/scheduler-handler.ts'; -import registerErrorListeners from './error-handlers.ts'; -import { sendMetrics } from './lib/metricsCollector.ts'; -import outboundMessageHandler from './handlers/outboundcomms-handler.ts'; -import { RequestContext } from './lib/requestContext.ts'; +import * as Messenger from './lib/messenger'; +import { decoder } from './lib/codec'; +import { Logger } from './lib/logger'; + +import slashcommandHandler from './handlers/slashcommand-handler'; +import videoConferenceHandler from './handlers/videoconference-handler'; +import apiHandler from './handlers/api-handler'; +import handleApp from './handlers/app/handler'; +import handleScheduler from './handlers/scheduler-handler'; +import registerErrorListeners from './error-handlers'; +import { sendMetrics } from './lib/metricsCollector'; +import outboundMessageHandler from './handlers/outboundcomms-handler'; +import { RequestContext } from './lib/requestContext'; type Handlers = { app: typeof handleApp; From cc58d264564418b811617d05541c34322fe949d8 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Fri, 19 Jun 2026 15:49:15 -0300 Subject: [PATCH 03/11] feat(apps): duplicate deno-runtime as node-runtime --- .../node-runtime/src/handlers/api-handler.ts | 2 +- .../src/handlers/app/construct.ts | 2 +- .../src/handlers/lib/assertions.ts | 2 +- .../src/handlers/outboundcomms-handler.ts | 2 +- .../src/handlers/tests/api-handler.test.ts | 81 +- .../handlers/tests/listener-handler.test.ts | 139 ++-- .../handlers/tests/scheduler-handler.test.ts | 8 +- .../tests/slashcommand-handler.test.ts | 84 +-- .../src/handlers/tests/uikit-handler.test.ts | 18 +- .../tests/upload-event-handler.test.ts | 74 +- .../tests/videoconference-handler.test.ts | 83 +-- .../src/handlers/videoconference-handler.ts | 2 +- .../src/lib/accessors/builders/RoomBuilder.ts | 1 - .../src/lib/accessors/builders/UserBuilder.ts | 1 - .../builders/VideoConferenceBuilder.ts | 1 - .../accessors/extenders/MessageExtender.ts | 1 - .../lib/accessors/extenders/RoomExtender.ts | 1 - .../extenders/VideoConferenceExtend.ts | 1 - .../lib/accessors/tests/AppAccessors.test.ts | 20 +- .../lib/accessors/tests/ModifyCreator.test.ts | 100 ++- .../accessors/tests/ModifyExtender.test.ts | 189 +++-- .../lib/accessors/tests/ModifyUpdater.test.ts | 139 ++-- .../tests/formatResponseErrorHandler.test.ts | 100 +-- .../src/lib/accessors/tests/http.test.ts | 80 +- .../src/lib/ast/tests/operations.test.ts | 57 +- packages/apps/node-runtime/src/lib/logger.ts | 4 +- packages/apps/node-runtime/src/lib/room.ts | 6 +- .../node-runtime/src/lib/tests/logger.test.ts | 90 +-- .../src/lib/tests/messenger.test.ts | 100 +-- .../src/lib/tests/secureFields.test.ts | 16 +- .../runtime/node/AppsEngineNodeRuntime.ts | 698 ++++++++++++++++++ .../server/runtime/node/LivenessManager.ts | 254 +++++++ .../server/runtime/node/ProcessMessenger.ts | 57 ++ .../apps/src/server/runtime/node/bundler.ts | 90 +++ .../apps/src/server/runtime/node/codec.ts | 78 ++ 35 files changed, 1841 insertions(+), 740 deletions(-) create mode 100644 packages/apps/src/server/runtime/node/AppsEngineNodeRuntime.ts create mode 100644 packages/apps/src/server/runtime/node/LivenessManager.ts create mode 100644 packages/apps/src/server/runtime/node/ProcessMessenger.ts create mode 100644 packages/apps/src/server/runtime/node/bundler.ts create mode 100644 packages/apps/src/server/runtime/node/codec.ts diff --git a/packages/apps/node-runtime/src/handlers/api-handler.ts b/packages/apps/node-runtime/src/handlers/api-handler.ts index aabc4e12dc09e..2c471b4d0e583 100644 --- a/packages/apps/node-runtime/src/handlers/api-handler.ts +++ b/packages/apps/node-runtime/src/handlers/api-handler.ts @@ -31,7 +31,7 @@ export default async function apiHandler(request: RequestContext): Promise unknown) => Pr const result = (async (exports,module,require,console,globalThis) => { ${code}; - })(exports,module,require,Buffer,_console,undefined,undefined); + })(exports,module,require,_console,undefined,undefined); return result.then(() => module.exports);`, ) as (require: (module: string) => unknown) => Promise>; diff --git a/packages/apps/node-runtime/src/handlers/lib/assertions.ts b/packages/apps/node-runtime/src/handlers/lib/assertions.ts index 65d0ad8885473..54879b06614ca 100644 --- a/packages/apps/node-runtime/src/handlers/lib/assertions.ts +++ b/packages/apps/node-runtime/src/handlers/lib/assertions.ts @@ -43,7 +43,7 @@ export function assertAppAvailable(v: unknown): asserts v is App { throw JsonRpcError.internalError({ err: 'App object not available', code: Errors.DRT_APP_NOT_AVAILABLE }); } -// deno-lint-ignore ban-types -- Function is the best we can do at this time +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type export function assertHandlerFunction(v: unknown): asserts v is Function { if (v instanceof Function) return; diff --git a/packages/apps/node-runtime/src/handlers/outboundcomms-handler.ts b/packages/apps/node-runtime/src/handlers/outboundcomms-handler.ts index 500c4700fb49f..da78a8b2a5784 100644 --- a/packages/apps/node-runtime/src/handlers/outboundcomms-handler.ts +++ b/packages/apps/node-runtime/src/handlers/outboundcomms-handler.ts @@ -24,7 +24,7 @@ export default async function outboundMessageHandler(request: RequestContext): P try { logger.debug(`Executing ${methodName} on outbound communication provider...`); - // deno-lint-ignore ban-types + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-function-type return await (method as Function).apply(wrapComposedApp(provider, request), [ ...args, AppAccessorsInstance.getReader(), diff --git a/packages/apps/node-runtime/src/handlers/tests/api-handler.test.ts b/packages/apps/node-runtime/src/handlers/tests/api-handler.test.ts index 280469e3905ff..75a3702a1ee4e 100644 --- a/packages/apps/node-runtime/src/handlers/tests/api-handler.test.ts +++ b/packages/apps/node-runtime/src/handlers/tests/api-handler.test.ts @@ -1,9 +1,7 @@ -// deno-lint-ignore-file no-explicit-any +import * as assert from 'node:assert'; +import { beforeEach, describe, it, mock } from 'node:test'; + import type { IApiEndpoint } from '@rocket.chat/apps-engine/definition/api/IApiEndpoint'; -import { assertInstanceOf } from 'https://deno.land/std@0.203.0/assert/assert_instance_of'; -import { assertEquals, assertObjectMatch } from 'https://deno.land/std@0.203.0/assert/mod'; -import { beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd'; -import { spy } from 'https://deno.land/std@0.203.0/testing/mock'; import { JsonRpcError } from 'jsonrpc-lite'; import { AppObjectRegistry } from '../../AppObjectRegistry'; @@ -13,11 +11,8 @@ import { createMockRequest } from './helpers/mod'; describe('handlers > api', () => { const mockEndpoint: IApiEndpoint = { path: '/test', - // deno-lint-ignore no-unused-vars get: (request: any, endpoint: any, read: any, modify: any, http: any, persis: any) => Promise.resolve('ok'), - // deno-lint-ignore no-unused-vars post: (request: any, endpoint: any, read: any, modify: any, http: any, persis: any) => Promise.resolve('ok'), - // deno-lint-ignore no-unused-vars put: (request: any, endpoint: any, read: any, modify: any, http: any, persis: any) => { throw new Error('Method execution error example'); }, @@ -29,90 +24,90 @@ describe('handlers > api', () => { }); it('correctly handles execution of an api endpoint method GET', async () => { - const _spy = spy(mockEndpoint, 'get'); + const _spy = mock.method(mockEndpoint, 'get'); const result = await apiHandler(createMockRequest({ method: 'api:/test:get', params: ['request', 'endpointInfo'] })); - assertEquals(result, 'ok'); - assertEquals(_spy.calls[0].args.length, 6); - assertEquals(_spy.calls[0].args[0], 'request'); - assertEquals(_spy.calls[0].args[1], 'endpointInfo'); + assert.deepStrictEqual(result, 'ok'); + assert.deepStrictEqual(_spy.mock.calls[0].arguments.length, 6); + assert.deepStrictEqual(_spy.mock.calls[0].arguments[0], 'request'); + assert.deepStrictEqual(_spy.mock.calls[0].arguments[1], 'endpointInfo'); + + _spy.mock.restore(); }); it('correctly handles execution of an api endpoint method POST', async () => { - const _spy = spy(mockEndpoint, 'post'); + const _spy = mock.method(mockEndpoint, 'post'); const result = await apiHandler(createMockRequest({ method: 'api:/test:post', params: ['request', 'endpointInfo'] })); - assertEquals(result, 'ok'); - assertEquals(_spy.calls[0].args.length, 6); - assertEquals(_spy.calls[0].args[0], 'request'); - assertEquals(_spy.calls[0].args[1], 'endpointInfo'); + assert.deepStrictEqual(result, 'ok'); + assert.deepStrictEqual(_spy.mock.calls[0].arguments.length, 6); + assert.deepStrictEqual(_spy.mock.calls[0].arguments[0], 'request'); + assert.deepStrictEqual(_spy.mock.calls[0].arguments[1], 'endpointInfo'); + + _spy.mock.restore(); }); it('correctly handles an error if the method not exists for the selected endpoint', async () => { const result = await apiHandler(createMockRequest({ method: `api:/test:delete`, params: ['request', 'endpointInfo'] })); - assertInstanceOf(result, JsonRpcError); - assertObjectMatch(result, { - message: `/test's delete not exists`, - code: -32000, - }); + assert.ok(result instanceof JsonRpcError, `Expected instance of ${JsonRpcError.name}`); + assert.strictEqual((result as any).message, `/test's delete not exists`); + assert.strictEqual((result as any).code, -32000); }); it('correctly handles an error if endpoint not exists', async () => { const result = await apiHandler(createMockRequest({ method: `api:/error:get`, params: ['request', 'endpointInfo'] })); - assertInstanceOf(result, JsonRpcError); - assertObjectMatch(result, { - message: `Endpoint /error not found`, - code: -32000, - }); + assert.ok(result instanceof JsonRpcError, `Expected instance of ${JsonRpcError.name}`); + assert.strictEqual((result as any).message, `Endpoint /error not found`); + assert.strictEqual((result as any).code, -32000); }); it('correctly handles an error if the method execution fails', async () => { const result = await apiHandler(createMockRequest({ method: `api:/test:put`, params: ['request', 'endpointInfo'] })); - assertInstanceOf(result, JsonRpcError); - assertObjectMatch(result, { - message: `Method execution error example`, - code: -32000, - }); + assert.ok(result instanceof JsonRpcError, `Expected instance of ${JsonRpcError.name}`); + assert.strictEqual((result as any).message, `Method execution error example`); + assert.strictEqual((result as any).code, -32000); }); it('correctly handles dynamic paths with parameters (e.g., webhook/:event)', async () => { const mockDynamicEndpoint: IApiEndpoint = { path: 'webhook/:event', - // deno-lint-ignore no-unused-vars post: (request: any, endpoint: any, read: any, modify: any, http: any, persis: any) => Promise.resolve('webhook handled'), }; AppObjectRegistry.set('api:webhook/:event', mockDynamicEndpoint); - const _spy = spy(mockDynamicEndpoint, 'post'); + const _spy = mock.method(mockDynamicEndpoint, 'post'); const result = await apiHandler(createMockRequest({ method: 'api:webhook/:event:post', params: ['request', 'endpointInfo'] })); - assertEquals(result, 'webhook handled'); - assertEquals(_spy.calls[0].args.length, 6); - assertEquals(_spy.calls[0].args[0], 'request'); - assertEquals(_spy.calls[0].args[1], 'endpointInfo'); + assert.deepStrictEqual(result, 'webhook handled'); + assert.deepStrictEqual(_spy.mock.calls[0].arguments.length, 6); + assert.deepStrictEqual(_spy.mock.calls[0].arguments[0], 'request'); + assert.deepStrictEqual(_spy.mock.calls[0].arguments[1], 'endpointInfo'); + + _spy.mock.restore(); }); it('correctly handles paths with multiple segments and colons', async () => { const mockComplexEndpoint: IApiEndpoint = { path: 'api/v1/:resource/:id', - // deno-lint-ignore no-unused-vars get: (request: any, endpoint: any, read: any, modify: any, http: any, persis: any) => Promise.resolve('complex path'), }; AppObjectRegistry.set('api:api/v1/:resource/:id', mockComplexEndpoint); - const _spy = spy(mockComplexEndpoint, 'get'); + const _spy = mock.method(mockComplexEndpoint, 'get'); const result = await apiHandler(createMockRequest({ method: 'api:api/v1/:resource/:id:get', params: ['request', 'endpointInfo'] })); - assertEquals(result, 'complex path'); - assertEquals(_spy.calls[0].args.length, 6); + assert.deepStrictEqual(result, 'complex path'); + assert.deepStrictEqual(_spy.mock.calls[0].arguments.length, 6); + + _spy.mock.restore(); }); }); diff --git a/packages/apps/node-runtime/src/handlers/tests/listener-handler.test.ts b/packages/apps/node-runtime/src/handlers/tests/listener-handler.test.ts index f5aebbcbb56d3..e277ab56d8803 100644 --- a/packages/apps/node-runtime/src/handlers/tests/listener-handler.test.ts +++ b/packages/apps/node-runtime/src/handlers/tests/listener-handler.test.ts @@ -1,6 +1,5 @@ -// deno-lint-ignore-file no-explicit-any -import { assertEquals, assertInstanceOf, assertObjectMatch } from 'https://deno.land/std@0.203.0/assert/mod'; -import { describe, it } from 'https://deno.land/std@0.203.0/testing/bdd'; +import * as assert from 'node:assert'; +import { describe, it } from 'node:test'; import { MessageBuilder } from '../../lib/accessors/builders/MessageBuilder'; import { RoomBuilder } from '../../lib/accessors/builders/RoomBuilder'; @@ -26,10 +25,10 @@ describe('handlers > listeners', () => { const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); - assertEquals(params.length, 3); - assertEquals(params[0], { __type: 'context' }); - assertEquals(params[1], { __type: 'reader' }); - assertEquals(params[2], { __type: 'http' }); + assert.deepStrictEqual(params.length, 3); + assert.deepStrictEqual(params[0], { __type: 'context' }); + assert.deepStrictEqual(params[1], { __type: 'reader' }); + assert.deepStrictEqual(params[2], { __type: 'http' }); }); it('correctly parses the arguments for a request to trigger the "checkPostMessageDeleted" method', () => { @@ -40,11 +39,11 @@ describe('handlers > listeners', () => { const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); - assertEquals(params.length, 4); - assertEquals(params[0], { __type: 'context' }); - assertEquals(params[1], { __type: 'reader' }); - assertEquals(params[2], { __type: 'http' }); - assertEquals(params[3], { __type: 'extraContext' }); + assert.deepStrictEqual(params.length, 4); + assert.deepStrictEqual(params[0], { __type: 'context' }); + assert.deepStrictEqual(params[1], { __type: 'reader' }); + assert.deepStrictEqual(params[2], { __type: 'http' }); + assert.deepStrictEqual(params[3], { __type: 'extraContext' }); }); it('correctly parses the arguments for a request to trigger the "checkPreRoomCreateExtend" method', () => { @@ -62,11 +61,11 @@ describe('handlers > listeners', () => { const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); - assertEquals(params.length, 3); + assert.deepStrictEqual(params.length, 3); - assertInstanceOf(params[0], Room); - assertEquals(params[1], { __type: 'reader' }); - assertEquals(params[2], { __type: 'http' }); + assert.ok(params[0] instanceof Room, `Expected instance of ${Room.name}`); + assert.deepStrictEqual(params[1], { __type: 'reader' }); + assert.deepStrictEqual(params[2], { __type: 'http' }); }); it('correctly parses the arguments for a request to trigger the "executePreMessageSentExtend" method', () => { @@ -76,15 +75,13 @@ describe('handlers > listeners', () => { const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); - assertEquals(params.length, 5); + assert.deepStrictEqual(params.length, 5); // Instantiating the MessageExtender might modify the original object, so we need to assert it matches instead of equals - assertObjectMatch(params[0] as Record, { - __type: 'context', - }); - assertInstanceOf(params[1], MessageExtender); - assertEquals(params[2], { __type: 'reader' }); - assertEquals(params[3], { __type: 'http' }); - assertEquals(params[4], { __type: 'persistence' }); + assert.strictEqual((params[0] as any).__type, 'context'); + assert.ok(params[1] instanceof MessageExtender, `Expected instance of ${MessageExtender.name}`); + assert.deepStrictEqual(params[2], { __type: 'reader' }); + assert.deepStrictEqual(params[3], { __type: 'http' }); + assert.deepStrictEqual(params[4], { __type: 'persistence' }); }); it('correctly parses the arguments for a request to trigger the "executePreRoomCreateExtend" method', () => { @@ -94,15 +91,13 @@ describe('handlers > listeners', () => { const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); - assertEquals(params.length, 5); + assert.deepStrictEqual(params.length, 5); // Instantiating the RoomExtender might modify the original object, so we need to assert it matches instead of equals - assertObjectMatch(params[0] as Record, { - __type: 'context', - }); - assertInstanceOf(params[1], RoomExtender); - assertEquals(params[2], { __type: 'reader' }); - assertEquals(params[3], { __type: 'http' }); - assertEquals(params[4], { __type: 'persistence' }); + assert.strictEqual((params[0] as any).__type, 'context'); + assert.ok(params[1] instanceof RoomExtender, `Expected instance of ${RoomExtender.name}`); + assert.deepStrictEqual(params[2], { __type: 'reader' }); + assert.deepStrictEqual(params[3], { __type: 'http' }); + assert.deepStrictEqual(params[4], { __type: 'persistence' }); }); it('correctly parses the arguments for a request to trigger the "executePreMessageSentModify" method', () => { @@ -112,15 +107,13 @@ describe('handlers > listeners', () => { const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); - assertEquals(params.length, 5); + assert.deepStrictEqual(params.length, 5); // Instantiating the MessageBuilder might modify the original object, so we need to assert it matches instead of equals - assertObjectMatch(params[0] as Record, { - __type: 'context', - }); - assertInstanceOf(params[1], MessageBuilder); - assertEquals(params[2], { __type: 'reader' }); - assertEquals(params[3], { __type: 'http' }); - assertEquals(params[4], { __type: 'persistence' }); + assert.strictEqual((params[0] as any).__type, 'context'); + assert.ok(params[1] instanceof MessageBuilder, `Expected instance of ${MessageBuilder.name}`); + assert.deepStrictEqual(params[2], { __type: 'reader' }); + assert.deepStrictEqual(params[3], { __type: 'http' }); + assert.deepStrictEqual(params[4], { __type: 'persistence' }); }); it('correctly parses the arguments for a request to trigger the "executePreRoomCreateModify" method', () => { @@ -130,15 +123,13 @@ describe('handlers > listeners', () => { const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); - assertEquals(params.length, 5); + assert.deepStrictEqual(params.length, 5); // Instantiating the RoomBuilder might modify the original object, so we need to assert it matches instead of equals - assertObjectMatch(params[0] as Record, { - __type: 'context', - }); - assertInstanceOf(params[1], RoomBuilder); - assertEquals(params[2], { __type: 'reader' }); - assertEquals(params[3], { __type: 'http' }); - assertEquals(params[4], { __type: 'persistence' }); + assert.strictEqual((params[0] as any).__type, 'context'); + assert.ok(params[1] instanceof RoomBuilder, `Expected instance of ${RoomBuilder.name}`); + assert.deepStrictEqual(params[2], { __type: 'reader' }); + assert.deepStrictEqual(params[3], { __type: 'http' }); + assert.deepStrictEqual(params[4], { __type: 'persistence' }); }); it('correctly parses the arguments for a request to trigger the "executePostRoomUserJoined" method', () => { @@ -156,12 +147,12 @@ describe('handlers > listeners', () => { const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); - assertEquals(params.length, 5); - assertInstanceOf((params[0] as any).room, Room); - assertEquals(params[1], { __type: 'reader' }); - assertEquals(params[2], { __type: 'http' }); - assertEquals(params[3], { __type: 'persistence' }); - assertEquals(params[4], { __type: 'modifier' }); + assert.deepStrictEqual(params.length, 5); + assert.ok((params[0] as any).room instanceof Room, `Expected instance of ${Room.name}`); + assert.deepStrictEqual(params[1], { __type: 'reader' }); + assert.deepStrictEqual(params[2], { __type: 'http' }); + assert.deepStrictEqual(params[3], { __type: 'persistence' }); + assert.deepStrictEqual(params[4], { __type: 'modifier' }); }); it('correctly parses the arguments for a request to trigger the "executePostRoomUserLeave" method', () => { @@ -179,12 +170,12 @@ describe('handlers > listeners', () => { const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); - assertEquals(params.length, 5); - assertInstanceOf((params[0] as any).room, Room); - assertEquals(params[1], { __type: 'reader' }); - assertEquals(params[2], { __type: 'http' }); - assertEquals(params[3], { __type: 'persistence' }); - assertEquals(params[4], { __type: 'modifier' }); + assert.deepStrictEqual(params.length, 5); + assert.ok((params[0] as any).room instanceof Room, `Expected instance of ${Room.name}`); + assert.deepStrictEqual(params[1], { __type: 'reader' }); + assert.deepStrictEqual(params[2], { __type: 'http' }); + assert.deepStrictEqual(params[3], { __type: 'persistence' }); + assert.deepStrictEqual(params[4], { __type: 'modifier' }); }); it('correctly parses the arguments for a request to trigger the "executePostMessageDeleted" method', () => { @@ -194,13 +185,13 @@ describe('handlers > listeners', () => { const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); - assertEquals(params.length, 6); - assertEquals(params[0], { __type: 'context' }); - assertEquals(params[1], { __type: 'reader' }); - assertEquals(params[2], { __type: 'http' }); - assertEquals(params[3], { __type: 'persistence' }); - assertEquals(params[4], { __type: 'modifier' }); - assertEquals(params[5], { __type: 'extraContext' }); + assert.deepStrictEqual(params.length, 6); + assert.deepStrictEqual(params[0], { __type: 'context' }); + assert.deepStrictEqual(params[1], { __type: 'reader' }); + assert.deepStrictEqual(params[2], { __type: 'http' }); + assert.deepStrictEqual(params[3], { __type: 'persistence' }); + assert.deepStrictEqual(params[4], { __type: 'modifier' }); + assert.deepStrictEqual(params[5], { __type: 'extraContext' }); }); it('correctly parses the arguments for a request to trigger the "executePostMessageSent" method', () => { @@ -223,12 +214,12 @@ describe('handlers > listeners', () => { const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); - assertEquals(params.length, 5); - assertObjectMatch(params[0] as Record, { id: 'fake' }); - assertInstanceOf((params[0] as any).room, Room); - assertEquals(params[1], { __type: 'reader' }); - assertEquals(params[2], { __type: 'http' }); - assertEquals(params[3], { __type: 'persistence' }); - assertEquals(params[4], { __type: 'modifier' }); + assert.deepStrictEqual(params.length, 5); + assert.strictEqual((params[0] as any).id, 'fake'); + assert.ok((params[0] as any).room instanceof Room, `Expected instance of ${Room.name}`); + assert.deepStrictEqual(params[1], { __type: 'reader' }); + assert.deepStrictEqual(params[2], { __type: 'http' }); + assert.deepStrictEqual(params[3], { __type: 'persistence' }); + assert.deepStrictEqual(params[4], { __type: 'modifier' }); }); }); diff --git a/packages/apps/node-runtime/src/handlers/tests/scheduler-handler.test.ts b/packages/apps/node-runtime/src/handlers/tests/scheduler-handler.test.ts index 322a95b8aa89c..f71b2c4d03117 100644 --- a/packages/apps/node-runtime/src/handlers/tests/scheduler-handler.test.ts +++ b/packages/apps/node-runtime/src/handlers/tests/scheduler-handler.test.ts @@ -1,5 +1,5 @@ -import { assertEquals } from 'https://deno.land/std@0.203.0/assert/mod'; -import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd'; +import * as assert from 'node:assert'; +import { after, beforeEach, describe, it } from 'node:test'; import { AppObjectRegistry } from '../../AppObjectRegistry'; import { AppAccessors } from '../../lib/accessors/mod'; @@ -29,13 +29,13 @@ describe('handlers > scheduler', () => { ]); }); - afterAll(() => { + after(() => { AppObjectRegistry.clear(); }); it('correctly executes a request to a processor', async () => { const result = await handleScheduler(createMockRequest({ method: 'scheduler:mockId', params: [{}] })); - assertEquals(result, null); + assert.deepStrictEqual(result, null); }); }); diff --git a/packages/apps/node-runtime/src/handlers/tests/slashcommand-handler.test.ts b/packages/apps/node-runtime/src/handlers/tests/slashcommand-handler.test.ts index 5007ef1b45060..7d7dcbc7a86b3 100644 --- a/packages/apps/node-runtime/src/handlers/tests/slashcommand-handler.test.ts +++ b/packages/apps/node-runtime/src/handlers/tests/slashcommand-handler.test.ts @@ -1,7 +1,5 @@ -// deno-lint-ignore-file no-explicit-any -import { assertEquals, assertInstanceOf } from 'https://deno.land/std@0.203.0/assert/mod'; -import { beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd'; -import { spy } from 'https://deno.land/std@0.203.0/testing/mock'; +import * as assert from 'node:assert'; +import { beforeEach, describe, it, mock } from 'node:test'; import { AppObjectRegistry } from '../../AppObjectRegistry'; import { createMockRequest } from './helpers/mod'; @@ -23,7 +21,6 @@ describe('handlers > slashcommand', () => { i18nParamsExample: 'test', i18nDescription: 'test', providesPreview: false, - // deno-lint-ignore no-unused-vars async executor(context: any, read: any, modify: any, http: any, persis: any): Promise {}, }; @@ -32,11 +29,8 @@ describe('handlers > slashcommand', () => { i18nParamsExample: 'test', i18nDescription: 'test', providesPreview: true, - // deno-lint-ignore no-unused-vars async executor(context: any, read: any, modify: any, http: any, persis: any): Promise {}, - // deno-lint-ignore no-unused-vars async previewer(context: any, read: any, modify: any, http: any, persis: any): Promise {}, - // deno-lint-ignore no-unused-vars async executePreviewItem(previewItem: any, context: any, read: any, modify: any, http: any, persis: any): Promise {}, }; @@ -45,9 +39,7 @@ describe('handlers > slashcommand', () => { i18nParamsExample: 'test', i18nDescription: 'test', providesPreview: true, - // deno-lint-ignore no-unused-vars async previewer(context: any, read: any, modify: any, http: any, persis: any): Promise {}, - // deno-lint-ignore no-unused-vars async executePreviewItem(previewItem: any, context: any, read: any, modify: any, http: any, persis: any): Promise {}, }; @@ -67,7 +59,7 @@ describe('handlers > slashcommand', () => { triggerId: 'triggerId', }; - const _spy = spy(mockCommandExecutorOnly, 'executor'); + const _spy = mock.method(mockCommandExecutorOnly, 'executor'); const mockRequest = createMockRequest({ method: 'slashcommand:executor-only:executor', params: [mockContext] }); @@ -75,20 +67,20 @@ describe('handlers > slashcommand', () => { mockContext, ]); - const context = _spy.calls[0].args[0]; + const context = _spy.mock.calls[0].arguments[0]; - assertInstanceOf(context.getRoom(), Room); - assertEquals(context.getSender(), { __type: 'sender' }); - assertEquals(context.getArguments(), { __type: 'params' }); - assertEquals(context.getThreadId(), 'threadId'); - assertEquals(context.getTriggerId(), 'triggerId'); + assert.ok(context.getRoom() instanceof Room, `Expected instance of ${Room.name}`); + assert.deepStrictEqual(context.getSender(), { __type: 'sender' }); + assert.deepStrictEqual(context.getArguments(), { __type: 'params' }); + assert.deepStrictEqual(context.getThreadId(), 'threadId'); + assert.deepStrictEqual(context.getTriggerId(), 'triggerId'); - assertEquals(_spy.calls[0].args[1], mockAppAccessors.getReader()); - assertEquals(_spy.calls[0].args[2], mockAppAccessors.getModifier()); - assertEquals(_spy.calls[0].args[3], mockAppAccessors.getHttp()); - assertEquals(_spy.calls[0].args[4], mockAppAccessors.getPersistence()); + assert.deepStrictEqual(_spy.mock.calls[0].arguments[1], mockAppAccessors.getReader()); + assert.deepStrictEqual(_spy.mock.calls[0].arguments[2], mockAppAccessors.getModifier()); + assert.deepStrictEqual(_spy.mock.calls[0].arguments[3], mockAppAccessors.getHttp()); + assert.deepStrictEqual(_spy.mock.calls[0].arguments[4], mockAppAccessors.getPersistence()); - _spy.restore(); + _spy.mock.restore(); }); it('correctly handles execution of a slash command previewer', async () => { @@ -100,7 +92,7 @@ describe('handlers > slashcommand', () => { triggerId: 'triggerId', }; - const _spy = spy(mockCommandExecutorAndPreview, 'previewer'); + const _spy = mock.method(mockCommandExecutorAndPreview, 'previewer'); const mockRequest = createMockRequest({ method: 'slashcommand:executor-and-preview:previewer', params: [mockContext] }); @@ -108,20 +100,20 @@ describe('handlers > slashcommand', () => { mockContext, ]); - const context = _spy.calls[0].args[0]; + const context = _spy.mock.calls[0].arguments[0]; - assertInstanceOf(context.getRoom(), Room); - assertEquals(context.getSender(), { __type: 'sender' }); - assertEquals(context.getArguments(), { __type: 'params' }); - assertEquals(context.getThreadId(), 'threadId'); - assertEquals(context.getTriggerId(), 'triggerId'); + assert.ok(context.getRoom() instanceof Room, `Expected instance of ${Room.name}`); + assert.deepStrictEqual(context.getSender(), { __type: 'sender' }); + assert.deepStrictEqual(context.getArguments(), { __type: 'params' }); + assert.deepStrictEqual(context.getThreadId(), 'threadId'); + assert.deepStrictEqual(context.getTriggerId(), 'triggerId'); - assertEquals(_spy.calls[0].args[1], mockAppAccessors.getReader()); - assertEquals(_spy.calls[0].args[2], mockAppAccessors.getModifier()); - assertEquals(_spy.calls[0].args[3], mockAppAccessors.getHttp()); - assertEquals(_spy.calls[0].args[4], mockAppAccessors.getPersistence()); + assert.deepStrictEqual(_spy.mock.calls[0].arguments[1], mockAppAccessors.getReader()); + assert.deepStrictEqual(_spy.mock.calls[0].arguments[2], mockAppAccessors.getModifier()); + assert.deepStrictEqual(_spy.mock.calls[0].arguments[3], mockAppAccessors.getHttp()); + assert.deepStrictEqual(_spy.mock.calls[0].arguments[4], mockAppAccessors.getPersistence()); - _spy.restore(); + _spy.mock.restore(); }); it('correctly handles execution of a slash command preview item executor', async () => { @@ -139,7 +131,7 @@ describe('handlers > slashcommand', () => { value: 'https://example.com/image.png', }; - const _spy = spy(mockCommandExecutorAndPreview, 'executePreviewItem'); + const _spy = mock.method(mockCommandExecutorAndPreview, 'executePreviewItem'); const mockRequest = createMockRequest({ method: 'slashcommand:executor-and-preview:executePreviewItem', @@ -151,19 +143,19 @@ describe('handlers > slashcommand', () => { mockContext, ]); - const context = _spy.calls[0].args[1]; + const context = _spy.mock.calls[0].arguments[1]; - assertInstanceOf(context.getRoom(), Room); - assertEquals(context.getSender(), { __type: 'sender' }); - assertEquals(context.getArguments(), { __type: 'params' }); - assertEquals(context.getThreadId(), 'threadId'); - assertEquals(context.getTriggerId(), 'triggerId'); + assert.ok(context.getRoom() instanceof Room, `Expected instance of ${Room.name}`); + assert.deepStrictEqual(context.getSender(), { __type: 'sender' }); + assert.deepStrictEqual(context.getArguments(), { __type: 'params' }); + assert.deepStrictEqual(context.getThreadId(), 'threadId'); + assert.deepStrictEqual(context.getTriggerId(), 'triggerId'); - assertEquals(_spy.calls[0].args[2], mockAppAccessors.getReader()); - assertEquals(_spy.calls[0].args[3], mockAppAccessors.getModifier()); - assertEquals(_spy.calls[0].args[4], mockAppAccessors.getHttp()); - assertEquals(_spy.calls[0].args[5], mockAppAccessors.getPersistence()); + assert.deepStrictEqual(_spy.mock.calls[0].arguments[2], mockAppAccessors.getReader()); + assert.deepStrictEqual(_spy.mock.calls[0].arguments[3], mockAppAccessors.getModifier()); + assert.deepStrictEqual(_spy.mock.calls[0].arguments[4], mockAppAccessors.getHttp()); + assert.deepStrictEqual(_spy.mock.calls[0].arguments[5], mockAppAccessors.getPersistence()); - _spy.restore(); + _spy.mock.restore(); }); }); diff --git a/packages/apps/node-runtime/src/handlers/tests/uikit-handler.test.ts b/packages/apps/node-runtime/src/handlers/tests/uikit-handler.test.ts index e12f0bd3597b2..71bdacb408464 100644 --- a/packages/apps/node-runtime/src/handlers/tests/uikit-handler.test.ts +++ b/packages/apps/node-runtime/src/handlers/tests/uikit-handler.test.ts @@ -1,6 +1,6 @@ -// deno-lint-ignore-file no-explicit-any -import { assertInstanceOf } from 'https://deno.land/std@0.203.0/assert/mod'; -import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd'; +import * as assert from 'node:assert'; +import { after, beforeEach, describe, it } from 'node:test'; + import jsonrpc from 'jsonrpc-lite'; import { AppObjectRegistry } from '../../AppObjectRegistry'; @@ -26,7 +26,7 @@ describe('handlers > uikit', () => { AppObjectRegistry.set('app', mockApp); }); - afterAll(() => { + after(() => { AppObjectRegistry.clear(); }); @@ -41,7 +41,7 @@ describe('handlers > uikit', () => { ]); const result = await handleUIKitInteraction(request); - assertInstanceOf(result, UIKitBlockInteractionContext); + assert.ok(result instanceof UIKitBlockInteractionContext, `Expected instance of ${UIKitBlockInteractionContext.name}`); }); it('successfully handles a call for "executeViewSubmitHandler"', async () => { @@ -56,7 +56,7 @@ describe('handlers > uikit', () => { ]); const result = await handleUIKitInteraction(request); - assertInstanceOf(result, UIKitViewSubmitInteractionContext); + assert.ok(result instanceof UIKitViewSubmitInteractionContext, `Expected instance of ${UIKitViewSubmitInteractionContext.name}`); }); it('successfully handles a call for "executeViewClosedHandler"', async () => { @@ -70,7 +70,7 @@ describe('handlers > uikit', () => { ]); const result = await handleUIKitInteraction(request); - assertInstanceOf(result, UIKitViewCloseInteractionContext); + assert.ok(result instanceof UIKitViewCloseInteractionContext, `Expected instance of ${UIKitViewCloseInteractionContext.name}`); }); it('successfully handles a call for "executeActionButtonHandler"', async () => { @@ -84,7 +84,7 @@ describe('handlers > uikit', () => { ]); const result = await handleUIKitInteraction(request); - assertInstanceOf(result, UIKitActionButtonInteractionContext); + assert.ok(result instanceof UIKitActionButtonInteractionContext, `Expected instance of ${UIKitActionButtonInteractionContext.name}`); }); it('successfully handles a call for "executeLivechatBlockActionHandler"', async () => { @@ -100,6 +100,6 @@ describe('handlers > uikit', () => { ]); const result = await handleUIKitInteraction(request); - assertInstanceOf(result, UIKitLivechatBlockInteractionContext); + assert.ok(result instanceof UIKitLivechatBlockInteractionContext, `Expected instance of ${UIKitLivechatBlockInteractionContext.name}`); }); }); diff --git a/packages/apps/node-runtime/src/handlers/tests/upload-event-handler.test.ts b/packages/apps/node-runtime/src/handlers/tests/upload-event-handler.test.ts index 0b6bbf50aafd7..7cf0abdd7abed 100644 --- a/packages/apps/node-runtime/src/handlers/tests/upload-event-handler.test.ts +++ b/packages/apps/node-runtime/src/handlers/tests/upload-event-handler.test.ts @@ -1,12 +1,14 @@ -// deno-lint-ignore-file no-explicit-any +import * as assert from 'node:assert'; import { Buffer } from 'node:buffer'; +import { randomUUID } from 'node:crypto'; +import { writeFile, unlink } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, beforeEach, describe, it, mock } from 'node:test'; import type { App } from '@rocket.chat/apps-engine/definition/App'; import type { IPreFileUpload } from '@rocket.chat/apps-engine/definition/uploads/IPreFileUpload'; import type { IUploadDetails } from '@rocket.chat/apps-engine/definition/uploads/IUploadDetails'; -import { assertInstanceOf, assertNotInstanceOf, assertEquals, assertStringIncludes } from 'https://deno.land/std@0.203.0/assert/mod'; -import { afterEach, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd'; -import { assertSpyCalls, spy } from 'https://deno.land/std@0.203.0/testing/mock'; import { JsonRpcError } from 'jsonrpc-lite'; import { createMockRequest } from './helpers/mod'; @@ -16,13 +18,13 @@ import { Errors } from '../lib/assertions'; describe('handlers > upload', () => { let app: App & IPreFileUpload; - let path: string; + let tmpFilePath: string; let file: IUploadDetails; beforeEach(async () => { AppObjectRegistry.clear(); - path = await Deno.makeTempFile(); + tmpFilePath = join(tmpdir(), `test-upload-${randomUUID()}.txt`); app = { extendConfiguration: () => {}, @@ -33,7 +35,7 @@ describe('handlers > upload', () => { const content = 'Temp file for testing'; - await Deno.writeTextFile(path, content); + await writeFile(tmpFilePath, content, 'utf-8'); file = { name: 'TempFile.txt', @@ -45,65 +47,77 @@ describe('handlers > upload', () => { }); afterEach(async () => { - await Deno.remove(path).catch((e) => e?.code !== 'ENOENT' && console.warn(`Failed to remove temp file at ${path}`, e)); + await unlink(tmpFilePath).catch((e: any) => e?.code !== 'ENOENT' && console.warn(`Failed to remove temp file at ${tmpFilePath}`, e)); }); it('correctly handles valid parameters', async () => { - const result = await handleUploadEvents(createMockRequest({ method: 'app:executePreFileUpload', params: [{ file, path }] })); + const result = await handleUploadEvents( + createMockRequest({ method: 'app:executePreFileUpload', params: [{ file, path: tmpFilePath }] }), + ); - assertNotInstanceOf(result, JsonRpcError, 'result is JsonRpcError'); + assert.ok(!(result instanceof JsonRpcError), 'result is JsonRpcError'); }); it('correctly loads the file contents for IPreFileUpload', async () => { - const _spy = spy(app as any, 'executePreFileUpload'); + const _spy = mock.method(app as any, 'executePreFileUpload'); + + const result = await handleUploadEvents( + createMockRequest({ method: 'app:executePreFileUpload', params: [{ file, path: tmpFilePath }] }), + ); - const result = await handleUploadEvents(createMockRequest({ method: 'app:executePreFileUpload', params: [{ file, path }] })); + assert.ok(!(result instanceof JsonRpcError), 'result is JsonRpcError'); + assert.strictEqual(_spy.mock.calls.length, 1); + assert.ok((_spy.mock.calls[0].arguments[0] as any)?.content instanceof Buffer, 'Expected content to be a Buffer'); - assertNotInstanceOf(result, JsonRpcError, 'result is JsonRpcError'); - assertSpyCalls(_spy, 1); - assertInstanceOf((_spy.calls[0].args[0] as any)?.content, Buffer); + _spy.mock.restore(); }); it('fails when app object is not on registry', async () => { AppObjectRegistry.clear(); - const result = await handleUploadEvents(createMockRequest({ method: 'app:executePreFileUpload', params: [{ file, path }] })); + const result = await handleUploadEvents( + createMockRequest({ method: 'app:executePreFileUpload', params: [{ file, path: tmpFilePath }] }), + ); - assertInstanceOf(result, JsonRpcError); - assertEquals(result.data.code, Errors.DRT_APP_NOT_AVAILABLE); + assert.ok(result instanceof JsonRpcError, `Expected instance of ${JsonRpcError.name}`); + assert.deepStrictEqual((result as JsonRpcError).data.code, Errors.DRT_APP_NOT_AVAILABLE); }); it('fails when the app does not implement the IPreFileUpload event handler', async () => { delete (app as any).executePreFileUpload; - const result = await handleUploadEvents(createMockRequest({ method: 'app:executePreFileUpload', params: [{ file, path }] })); + const result = await handleUploadEvents( + createMockRequest({ method: 'app:executePreFileUpload', params: [{ file, path: tmpFilePath }] }), + ); - assertInstanceOf(result, JsonRpcError); - assertEquals(result.data.code, Errors.DRT_EVENT_HANDLER_FUNCTION_MISSING); + assert.ok(result instanceof JsonRpcError, `Expected instance of ${JsonRpcError.name}`); + assert.deepStrictEqual((result as JsonRpcError).data.code, Errors.DRT_EVENT_HANDLER_FUNCTION_MISSING); }); it('fails when "file" is not a proper IUploadDetails object', async () => { const result = await handleUploadEvents( - createMockRequest({ method: 'app:executePreFileUpload', params: [{ file: { nope: 'bad' }, path }] }), + createMockRequest({ method: 'app:executePreFileUpload', params: [{ file: { nope: 'bad' }, path: tmpFilePath }] }), ); - assertInstanceOf(result, JsonRpcError); - assertStringIncludes(result.data.err, 'Expected IUploadDetails'); + assert.ok(result instanceof JsonRpcError, `Expected instance of ${JsonRpcError.name}`); + assert.ok((result as JsonRpcError).data.err.includes('Expected IUploadDetails')); }); it('fails when "path" is not a proper string', async () => { const result = await handleUploadEvents(createMockRequest({ method: 'app:executePreFileUpload', params: [{ file, path: {} }] })); - assertInstanceOf(result, JsonRpcError); - assertStringIncludes(result.data.err, 'Expected string'); + assert.ok(result instanceof JsonRpcError, `Expected instance of ${JsonRpcError.name}`); + assert.ok((result as JsonRpcError).data.err.includes('Expected string')); }); it('fails when "path" is not a readable file path', async () => { - await Deno.remove(path); + await unlink(tmpFilePath); - const result = await handleUploadEvents(createMockRequest({ method: 'app:executePreFileUpload', params: [{ file, path }] })); + const result = await handleUploadEvents( + createMockRequest({ method: 'app:executePreFileUpload', params: [{ file, path: tmpFilePath }] }), + ); - assertInstanceOf(result, JsonRpcError); - assertEquals(result.data.code, 'ENOENT'); + assert.ok(result instanceof JsonRpcError, `Expected instance of ${JsonRpcError.name}`); + assert.deepStrictEqual((result as JsonRpcError).data.code, 'ENOENT'); }); }); diff --git a/packages/apps/node-runtime/src/handlers/tests/videoconference-handler.test.ts b/packages/apps/node-runtime/src/handlers/tests/videoconference-handler.test.ts index 20ca5d49a6688..325c7e772921e 100644 --- a/packages/apps/node-runtime/src/handlers/tests/videoconference-handler.test.ts +++ b/packages/apps/node-runtime/src/handlers/tests/videoconference-handler.test.ts @@ -1,7 +1,6 @@ -// deno-lint-ignore-file no-explicit-any -import { assertEquals, assertObjectMatch, assertInstanceOf } from 'https://deno.land/std@0.203.0/assert/mod'; -import { beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd'; -import { spy } from 'https://deno.land/std@0.203.0/testing/mock'; +import * as assert from 'node:assert'; +import { beforeEach, describe, it, mock } from 'node:test'; + import { JsonRpcError } from 'jsonrpc-lite'; import videoconfHandler from '../videoconference-handler'; @@ -9,14 +8,10 @@ import { createMockRequest } from './helpers/mod'; import { AppObjectRegistry } from '../../AppObjectRegistry'; describe('handlers > videoconference', () => { - // deno-lint-ignore no-unused-vars const mockMethodWithoutParam = (read: any, modify: any, http: any, persis: any): Promise => Promise.resolve('ok none'); - // deno-lint-ignore no-unused-vars const mockMethodWithOneParam = (call: any, read: any, modify: any, http: any, persis: any): Promise => Promise.resolve('ok one'); - // deno-lint-ignore no-unused-vars const mockMethodWithTwoParam = (call: any, user: any, read: any, modify: any, http: any, persis: any): Promise => Promise.resolve('ok two'); - // deno-lint-ignore no-unused-vars const mockMethodWithThreeParam = (call: any, user: any, options: any, read: any, modify: any, http: any, persis: any): Promise => Promise.resolve('ok three'); const mockProvider = { @@ -36,76 +31,72 @@ describe('handlers > videoconference', () => { }); it('correctly handles execution of a videoconf method without additional params', async () => { - const _spy = spy(mockProvider, 'empty'); + const _spy = mock.method(mockProvider, 'empty'); const result = await videoconfHandler(createMockRequest({ method: 'videoconference:test-provider:empty', params: [] })); - assertEquals(result, 'ok none'); - assertEquals(_spy.calls[0].args.length, 4); + assert.deepStrictEqual(result, 'ok none'); + assert.deepStrictEqual(_spy.mock.calls[0].arguments.length, 4); - _spy.restore(); + _spy.mock.restore(); }); it('correctly handles execution of a videoconf method with one param', async () => { - const _spy = spy(mockProvider, 'one'); + const _spy = mock.method(mockProvider, 'one'); const result = await videoconfHandler(createMockRequest({ method: 'videoconference:test-provider:one', params: ['call'] })); - assertEquals(result, 'ok one'); - assertEquals(_spy.calls[0].args.length, 5); - assertEquals(_spy.calls[0].args[0], 'call'); + assert.deepStrictEqual(result, 'ok one'); + assert.deepStrictEqual(_spy.mock.calls[0].arguments.length, 5); + assert.deepStrictEqual(_spy.mock.calls[0].arguments[0], 'call'); - _spy.restore(); + _spy.mock.restore(); }); it('correctly handles execution of a videoconf method with two params', async () => { - const _spy = spy(mockProvider, 'two'); + const _spy = mock.method(mockProvider, 'two'); const result = await videoconfHandler(createMockRequest({ method: 'videoconference:test-provider:two', params: ['call', 'user'] })); - assertEquals(result, 'ok two'); - assertEquals(_spy.calls[0].args.length, 6); - assertEquals(_spy.calls[0].args[0], 'call'); - assertEquals(_spy.calls[0].args[1], 'user'); + assert.deepStrictEqual(result, 'ok two'); + assert.deepStrictEqual(_spy.mock.calls[0].arguments.length, 6); + assert.deepStrictEqual(_spy.mock.calls[0].arguments[0], 'call'); + assert.deepStrictEqual(_spy.mock.calls[0].arguments[1], 'user'); - _spy.restore(); + _spy.mock.restore(); }); it('correctly handles execution of a videoconf method with three params', async () => { - const _spy = spy(mockProvider, 'three'); + const _spy = mock.method(mockProvider, 'three'); const result = await videoconfHandler( createMockRequest({ method: 'videoconference:test-provider:three', params: ['call', 'user', 'options'] }), ); - assertEquals(result, 'ok three'); - assertEquals(_spy.calls[0].args.length, 7); - assertEquals(_spy.calls[0].args[0], 'call'); - assertEquals(_spy.calls[0].args[1], 'user'); - assertEquals(_spy.calls[0].args[2], 'options'); + assert.deepStrictEqual(result, 'ok three'); + assert.deepStrictEqual(_spy.mock.calls[0].arguments.length, 7); + assert.deepStrictEqual(_spy.mock.calls[0].arguments[0], 'call'); + assert.deepStrictEqual(_spy.mock.calls[0].arguments[1], 'user'); + assert.deepStrictEqual(_spy.mock.calls[0].arguments[2], 'options'); - _spy.restore(); + _spy.mock.restore(); }); it('correctly handles an error on execution of a videoconf method', async () => { const result = await videoconfHandler(createMockRequest({ method: 'videoconference:test-provider:error', params: [] })); - assertInstanceOf(result, JsonRpcError); - assertObjectMatch(result, { - message: 'Method execution error example', - code: -32000, - }); + assert.ok(result instanceof JsonRpcError, `Expected instance of ${JsonRpcError.name}`); + assert.strictEqual((result as any).message, 'Method execution error example'); + assert.strictEqual((result as any).code, -32000); }); it('correctly handles an error when provider is not found', async () => { const providerName = 'error-provider'; const result = await videoconfHandler(createMockRequest({ method: `videoconference:${providerName}:method`, params: [] })); - assertInstanceOf(result, JsonRpcError); - assertObjectMatch(result, { - message: `Provider ${providerName} not found`, - code: -32000, - }); + assert.ok(result instanceof JsonRpcError, `Expected instance of ${JsonRpcError.name}`); + assert.strictEqual((result as any).message, `Provider ${providerName} not found`); + assert.strictEqual((result as any).code, -32000); }); it('correctly handles an error if method is not a function of provider', async () => { @@ -113,13 +104,9 @@ describe('handlers > videoconference', () => { const providerName = 'test-provider'; const result = await videoconfHandler(createMockRequest({ method: `videoconference:${providerName}:${methodName}`, params: [] })); - assertInstanceOf(result, JsonRpcError); - assertObjectMatch(result, { - message: 'Method not found', - code: -32601, - data: { - message: `Method ${methodName} not found on provider ${providerName}`, - }, - }); + assert.ok(result instanceof JsonRpcError, `Expected instance of ${JsonRpcError.name}`); + assert.strictEqual((result as any).message, 'Method not found'); + assert.strictEqual((result as any).code, -32601); + assert.strictEqual((result as any).data.message, `Method ${methodName} not found on provider ${providerName}`); }); }); diff --git a/packages/apps/node-runtime/src/handlers/videoconference-handler.ts b/packages/apps/node-runtime/src/handlers/videoconference-handler.ts index 68ebef7b4c9d8..59a3e33409753 100644 --- a/packages/apps/node-runtime/src/handlers/videoconference-handler.ts +++ b/packages/apps/node-runtime/src/handlers/videoconference-handler.ts @@ -34,7 +34,7 @@ export default async function videoConferenceHandler(request: RequestContext): P const args = [...(videoconf ? [videoconf] : []), ...(user ? [user] : []), ...(options ? [options] : [])]; try { - // deno-lint-ignore ban-types + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type const result = await (method as Function).apply(wrapComposedApp(provider, request), [ ...args, AppAccessorsInstance.getReader(), diff --git a/packages/apps/node-runtime/src/lib/accessors/builders/RoomBuilder.ts b/packages/apps/node-runtime/src/lib/accessors/builders/RoomBuilder.ts index 8f6a428eb8231..3a62a8bb62ad4 100644 --- a/packages/apps/node-runtime/src/lib/accessors/builders/RoomBuilder.ts +++ b/packages/apps/node-runtime/src/lib/accessors/builders/RoomBuilder.ts @@ -4,7 +4,6 @@ import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; import type { RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType'; import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser'; - const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { RocketChatAssociationModel: typeof _RocketChatAssociationModel; }; diff --git a/packages/apps/node-runtime/src/lib/accessors/builders/UserBuilder.ts b/packages/apps/node-runtime/src/lib/accessors/builders/UserBuilder.ts index 1c516fa83f880..ad78b2c82b767 100644 --- a/packages/apps/node-runtime/src/lib/accessors/builders/UserBuilder.ts +++ b/packages/apps/node-runtime/src/lib/accessors/builders/UserBuilder.ts @@ -4,7 +4,6 @@ import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser'; import type { IUserEmail } from '@rocket.chat/apps-engine/definition/users/IUserEmail'; import type { IUserSettings } from '@rocket.chat/apps-engine/definition/users/IUserSettings'; - const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { RocketChatAssociationModel: typeof _RocketChatAssociationModel; }; diff --git a/packages/apps/node-runtime/src/lib/accessors/builders/VideoConferenceBuilder.ts b/packages/apps/node-runtime/src/lib/accessors/builders/VideoConferenceBuilder.ts index fb9a4001ba2c9..28e5db7043f4d 100644 --- a/packages/apps/node-runtime/src/lib/accessors/builders/VideoConferenceBuilder.ts +++ b/packages/apps/node-runtime/src/lib/accessors/builders/VideoConferenceBuilder.ts @@ -2,7 +2,6 @@ import type { IVideoConferenceBuilder } from '@rocket.chat/apps-engine/definitio import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; import type { IGroupVideoConference } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConference'; - const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { RocketChatAssociationModel: typeof _RocketChatAssociationModel; }; diff --git a/packages/apps/node-runtime/src/lib/accessors/extenders/MessageExtender.ts b/packages/apps/node-runtime/src/lib/accessors/extenders/MessageExtender.ts index b274804d170de..29e84b53a7e66 100644 --- a/packages/apps/node-runtime/src/lib/accessors/extenders/MessageExtender.ts +++ b/packages/apps/node-runtime/src/lib/accessors/extenders/MessageExtender.ts @@ -3,7 +3,6 @@ import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMes import type { IMessageAttachment } from '@rocket.chat/apps-engine/definition/messages/IMessageAttachment'; import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; - const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { RocketChatAssociationModel: typeof _RocketChatAssociationModel; }; diff --git a/packages/apps/node-runtime/src/lib/accessors/extenders/RoomExtender.ts b/packages/apps/node-runtime/src/lib/accessors/extenders/RoomExtender.ts index fb23a27e8d991..0fa1637c36bb8 100644 --- a/packages/apps/node-runtime/src/lib/accessors/extenders/RoomExtender.ts +++ b/packages/apps/node-runtime/src/lib/accessors/extenders/RoomExtender.ts @@ -3,7 +3,6 @@ import type { RocketChatAssociationModel as _RocketChatAssociationModel } from ' import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser'; - const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { RocketChatAssociationModel: typeof _RocketChatAssociationModel; }; diff --git a/packages/apps/node-runtime/src/lib/accessors/extenders/VideoConferenceExtend.ts b/packages/apps/node-runtime/src/lib/accessors/extenders/VideoConferenceExtend.ts index 7faea6e2a2655..2442d1133c237 100644 --- a/packages/apps/node-runtime/src/lib/accessors/extenders/VideoConferenceExtend.ts +++ b/packages/apps/node-runtime/src/lib/accessors/extenders/VideoConferenceExtend.ts @@ -3,7 +3,6 @@ import type { RocketChatAssociationModel as _RocketChatAssociationModel } from ' import type { VideoConference, VideoConferenceMember } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConference'; import type { IVideoConferenceUser } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConferenceUser'; - const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { RocketChatAssociationModel: typeof _RocketChatAssociationModel; }; diff --git a/packages/apps/node-runtime/src/lib/accessors/tests/AppAccessors.test.ts b/packages/apps/node-runtime/src/lib/accessors/tests/AppAccessors.test.ts index e76834c034a0e..59febbbac883a 100644 --- a/packages/apps/node-runtime/src/lib/accessors/tests/AppAccessors.test.ts +++ b/packages/apps/node-runtime/src/lib/accessors/tests/AppAccessors.test.ts @@ -1,5 +1,5 @@ -import { assertEquals } from 'https://deno.land/std@0.203.0/assert/assert_equals'; -import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd'; +import * as assert from 'node:assert'; +import { after, beforeEach, describe, it } from 'node:test'; import { AppObjectRegistry } from '../../../AppObjectRegistry'; import { AppAccessors } from '../mod'; @@ -21,7 +21,7 @@ describe('AppAccessors', () => { AppObjectRegistry.clear(); }); - afterAll(() => { + after(() => { AppObjectRegistry.clear(); }); @@ -29,7 +29,7 @@ describe('AppAccessors', () => { const roomRead = appAccessors.getReader().getRoomReader(); const room = roomRead.getById('123'); - assertEquals(room, { + assert.deepStrictEqual(room, { params: ['123'], method: 'accessor:getReader:getRoomReader:getById', }); @@ -39,7 +39,7 @@ describe('AppAccessors', () => { const reader = appAccessors.getReader().getEnvironmentReader().getEnvironmentVariables(); const room = await reader.getValueByName('NODE_ENV'); - assertEquals(room, { + assert.deepStrictEqual(room, { params: ['NODE_ENV'], method: 'accessor:getReader:getEnvironmentReader:getEnvironmentVariables:getValueByName', }); @@ -49,7 +49,7 @@ describe('AppAccessors', () => { const envRead = appAccessors.getEnvironmentRead(); const env = await envRead.getServerSettings().getValueById('123'); - assertEquals(env, { + assert.deepStrictEqual(env, { params: ['123'], method: 'accessor:getEnvironmentRead:getServerSettings:getValueById', }); @@ -59,7 +59,7 @@ describe('AppAccessors', () => { const envRead = appAccessors.getEnvironmentWrite(); const env = await envRead.getServerSettings().incrementValue('123', 6); - assertEquals(env, { + assert.deepStrictEqual(env, { params: ['123', 6], method: 'accessor:getEnvironmentWrite:getServerSettings:incrementValue', }); @@ -74,7 +74,7 @@ describe('AppAccessors', () => { providesPreview: true, }); - assertEquals(command, { + assert.deepStrictEqual(command, { params: [ { command: 'test', @@ -102,12 +102,12 @@ describe('AppAccessors', () => { const result = await configExtend.slashCommands.provideSlashCommand(slashcommand); - assertEquals(AppObjectRegistry.get('slashcommand:test'), slashcommand); + assert.deepStrictEqual(AppObjectRegistry.get('slashcommand:test'), slashcommand); // The function will not be serialized and sent to the main process delete result.params[0].executor; - assertEquals(result, { + assert.deepStrictEqual(result, { method: 'accessor:getConfigurationExtend:slashCommands:provideSlashCommand', params: [ { diff --git a/packages/apps/node-runtime/src/lib/accessors/tests/ModifyCreator.test.ts b/packages/apps/node-runtime/src/lib/accessors/tests/ModifyCreator.test.ts index b8728d19f9fd9..3834bddf289fb 100644 --- a/packages/apps/node-runtime/src/lib/accessors/tests/ModifyCreator.test.ts +++ b/packages/apps/node-runtime/src/lib/accessors/tests/ModifyCreator.test.ts @@ -1,7 +1,5 @@ -// deno-lint-ignore-file no-explicit-any -import { assert, assertEquals, assertNotInstanceOf, assertRejects } from 'https://deno.land/std@0.203.0/assert/mod'; -import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd'; -import { assertSpyCall, spy } from 'https://deno.land/std@0.203.0/testing/mock'; +import * as assert from 'node:assert'; +import { after, beforeEach, describe, it, mock } from 'node:test'; import { AppObjectRegistry } from '../../../AppObjectRegistry'; import { ModifyCreator } from '../modify/ModifyCreator'; @@ -22,12 +20,12 @@ describe('ModifyCreator', () => { AppObjectRegistry.set('id', 'deno-test'); }); - afterAll(() => { + after(() => { AppObjectRegistry.clear(); }); it('sends the correct payload in the request to create a message', async () => { - const spying = spy(senderFn); + const spying = mock.fn(senderFn); const modifyCreator = new ModifyCreator(spying); const messageBuilder = modifyCreator.startMessage(); @@ -43,23 +41,21 @@ describe('ModifyCreator', () => { // but we need to know that the request sent was well formed await modifyCreator.finish(messageBuilder); - assertSpyCall(spying, 0, { - args: [ - { - method: 'bridges:getMessageBridge:doCreate', - params: [ - { - room: { id: '123' }, - sender: { id: '456' }, - text: 'Hello World', - alias: 'alias', - avatarUrl: 'https://avatars.com/123', - }, - 'deno-test', - ], - }, - ], - }); + assert.deepStrictEqual(spying.mock.calls[0].arguments, [ + { + method: 'bridges:getMessageBridge:doCreate', + params: [ + { + room: { id: '123' }, + sender: { id: '456' }, + text: 'Hello World', + alias: 'alias', + avatarUrl: 'https://avatars.com/123', + }, + 'deno-test', + ], + }, + ]); }); it('sends the correct payload in the request to upload a buffer', async () => { @@ -67,7 +63,7 @@ describe('ModifyCreator', () => { const result = await modifyCreator.getUploadCreator().uploadBuffer(new Uint8Array([1, 2, 3, 4]), 'text/plain'); - assertEquals(result, { + assert.deepStrictEqual(result, { method: 'accessor:getModifier:getCreator:getUploadCreator:uploadBuffer', params: [new Uint8Array([1, 2, 3, 4]), 'text/plain'], }); @@ -82,7 +78,7 @@ describe('ModifyCreator', () => { name: 'Random Visitor', })) as any; // We modified the send function so it changed the original return type of the function - assertEquals(result, { + assert.deepStrictEqual(result, { method: 'accessor:getModifier:getCreator:getLivechatCreator:createVisitor', params: [ { @@ -100,8 +96,8 @@ describe('ModifyCreator', () => { const result = modifyCreator.getLivechatCreator().createToken(); - assertNotInstanceOf(result, Promise); - assert(typeof result === 'string', `Expected "${result}" to be of type "string", but got "${typeof result}"`); + assert.ok(!(result instanceof Promise)); + assert.ok(typeof result === 'string', `Expected "${result}" to be of type "string", but got "${typeof result}"`); }); it('throws an error when a proxy method of getLivechatCreator fails', async () => { @@ -109,15 +105,14 @@ describe('ModifyCreator', () => { const modifyCreator = new ModifyCreator(failingSenderFn); const livechatCreator = modifyCreator.getLivechatCreator(); - await assertRejects( + await assert.rejects( () => livechatCreator.createAndReturnVisitor({ token: 'visitor-token', username: 'visitor-username', name: 'Visitor Name', }), - Error, - 'Test error', + { message: 'Test error' }, ); }); @@ -126,15 +121,14 @@ describe('ModifyCreator', () => { const modifyCreator = new ModifyCreator(failingSenderFn); const livechatCreator = modifyCreator.getLivechatCreator(); - await assertRejects( + await assert.rejects( () => livechatCreator.createVisitor({ token: 'visitor-token', username: 'visitor-username', name: 'Visitor Name', }), - Error, - 'Livechat method error', + { message: 'Livechat method error' }, ); }); @@ -143,15 +137,14 @@ describe('ModifyCreator', () => { const modifyCreator = new ModifyCreator(failingSenderFn); const livechatCreator = modifyCreator.getLivechatCreator(); - await assertRejects( + await assert.rejects( () => livechatCreator.createVisitor({ token: 'visitor-token', username: 'visitor-username', name: 'Visitor Name', }), - Error, - 'An unknown error occurred', + { message: 'An unknown error occurred' }, ); }); @@ -160,7 +153,7 @@ describe('ModifyCreator', () => { const modifyCreator = new ModifyCreator(failingSenderFn); const uploadCreator = modifyCreator.getUploadCreator(); - await assertRejects(() => uploadCreator.uploadBuffer(new Uint8Array([9, 10, 11, 12]), 'image/png'), Error, 'Upload error'); + await assert.rejects(() => uploadCreator.uploadBuffer(new Uint8Array([9, 10, 11, 12]), 'image/png'), { message: 'Upload error' }); }); it('throws an instance of Error when getUploadCreator fails with a specific error object', async () => { @@ -168,7 +161,7 @@ describe('ModifyCreator', () => { const modifyCreator = new ModifyCreator(failingSenderFn); const uploadCreator = modifyCreator.getUploadCreator(); - await assertRejects(() => uploadCreator.uploadBuffer(new Uint8Array([1, 2, 3]), 'image/png'), Error, 'Upload method error'); + await assert.rejects(() => uploadCreator.uploadBuffer(new Uint8Array([1, 2, 3]), 'image/png'), { message: 'Upload method error' }); }); it('throws a default Error when getUploadCreator fails with an unknown error object', async () => { @@ -176,7 +169,9 @@ describe('ModifyCreator', () => { const modifyCreator = new ModifyCreator(failingSenderFn); const uploadCreator = modifyCreator.getUploadCreator(); - await assertRejects(() => uploadCreator.uploadBuffer(new Uint8Array([1, 2, 3]), 'image/png'), Error, 'An unknown error occurred'); + await assert.rejects(() => uploadCreator.uploadBuffer(new Uint8Array([1, 2, 3]), 'image/png'), { + message: 'An unknown error occurred', + }); }); it('throws an error when a proxy method of getEmailCreator fails', async () => { @@ -184,7 +179,7 @@ describe('ModifyCreator', () => { const modifyCreator = new ModifyCreator(failingSenderFn); const emailCreator = modifyCreator.getEmailCreator(); - await assertRejects( + await assert.rejects( () => emailCreator.send({ to: 'test@example.com', @@ -192,8 +187,7 @@ describe('ModifyCreator', () => { subject: 'Test Email', text: 'This is a test email.', }), - Error, - 'Email error', + { message: 'Email error' }, ); }); @@ -202,7 +196,7 @@ describe('ModifyCreator', () => { const modifyCreator = new ModifyCreator(failingSenderFn); const emailCreator = modifyCreator.getEmailCreator(); - await assertRejects( + await assert.rejects( () => emailCreator.send({ to: 'test@example.com', @@ -210,8 +204,7 @@ describe('ModifyCreator', () => { subject: 'Test Email', text: 'This is a test email.', }), - Error, - 'Email method error', + { message: 'Email method error' }, ); }); @@ -220,7 +213,7 @@ describe('ModifyCreator', () => { const modifyCreator = new ModifyCreator(failingSenderFn); const emailCreator = modifyCreator.getEmailCreator(); - await assertRejects( + await assert.rejects( () => emailCreator.send({ to: 'test@example.com', @@ -228,8 +221,7 @@ describe('ModifyCreator', () => { subject: 'Test Email', text: 'This is a test email.', }), - Error, - 'An unknown error occurred', + { message: 'An unknown error occurred' }, ); }); @@ -238,7 +230,9 @@ describe('ModifyCreator', () => { const modifyCreator = new ModifyCreator(failingSenderFn); const contactCreator = modifyCreator.getContactCreator(); - await assertRejects(() => contactCreator.addContactEmail('test-contact-id', 'test@example.com'), Error, 'Contact creation error'); + await assert.rejects(() => contactCreator.addContactEmail('test-contact-id', 'test@example.com'), { + message: 'Contact creation error', + }); }); it('throws an instance of Error when getContactCreator fails with a specific error object', async () => { @@ -246,7 +240,9 @@ describe('ModifyCreator', () => { const modifyCreator = new ModifyCreator(failingSenderFn); const contactCreator = modifyCreator.getContactCreator(); - await assertRejects(() => contactCreator.addContactEmail('test-contact-id', 'test@example.com'), Error, 'Contact creation error'); + await assert.rejects(() => contactCreator.addContactEmail('test-contact-id', 'test@example.com'), { + message: 'Contact creation error', + }); }); it('throws a default Error when getContactCreator fails with an unknown error object', async () => { @@ -254,6 +250,8 @@ describe('ModifyCreator', () => { const modifyCreator = new ModifyCreator(failingSenderFn); const contactCreator = modifyCreator.getContactCreator(); - await assertRejects(() => contactCreator.addContactEmail('test-contact-id', 'test@example.com'), Error, 'An unknown error occurred'); + await assert.rejects(() => contactCreator.addContactEmail('test-contact-id', 'test@example.com'), { + message: 'An unknown error occurred', + }); }); }); diff --git a/packages/apps/node-runtime/src/lib/accessors/tests/ModifyExtender.test.ts b/packages/apps/node-runtime/src/lib/accessors/tests/ModifyExtender.test.ts index 3165af1020f91..00b53ea64dfc3 100644 --- a/packages/apps/node-runtime/src/lib/accessors/tests/ModifyExtender.test.ts +++ b/packages/apps/node-runtime/src/lib/accessors/tests/ModifyExtender.test.ts @@ -1,7 +1,6 @@ -// deno-lint-ignore-file no-explicit-any -import { assertRejects } from 'https://deno.land/std@0.203.0/assert/mod'; -import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd'; -import { assertSpyCall, spy, stub } from 'https://deno.land/std@0.203.0/testing/mock'; +import * as assert from 'node:assert'; +import { after, beforeEach, describe, it, mock } from 'node:test'; + import jsonrpc from 'jsonrpc-lite'; import { AppObjectRegistry } from '../../../AppObjectRegistry'; @@ -26,218 +25,210 @@ describe('ModifyExtender', () => { extender = new ModifyExtender(senderFn); }); - afterAll(() => { + after(() => { AppObjectRegistry.clear(); }); it('correctly formats requests for the extend message requests', async () => { - const _spy = spy(extender, 'senderFn' as keyof ModifyExtender); + const _spy = mock.method(extender, 'senderFn' as any); const messageExtender = await extender.extendMessage('message-id', { _id: 'user-id' } as any); - assertSpyCall(_spy, 0, { - args: [ - { - method: 'bridges:getMessageBridge:doGetById', - params: ['message-id', 'deno-test'], - }, - ], - }); + assert.deepStrictEqual(_spy.mock.calls[0].arguments, [ + { + method: 'bridges:getMessageBridge:doGetById', + params: ['message-id', 'deno-test'], + }, + ]); messageExtender.addCustomField('key', 'value'); await extender.finish(messageExtender); - assertSpyCall(_spy, 1, { - args: [ - { - method: 'bridges:getMessageBridge:doUpdate', - params: [messageExtender.getMessage(), 'deno-test'], - }, - ], - }); + assert.deepStrictEqual(_spy.mock.calls[1].arguments, [ + { + method: 'bridges:getMessageBridge:doUpdate', + params: [messageExtender.getMessage(), 'deno-test'], + }, + ]); - _spy.restore(); + _spy.mock.restore(); }); it('correctly formats requests for the extend room requests', async () => { - const _spy = spy(extender, 'senderFn' as keyof ModifyExtender); + const _spy = mock.method(extender, 'senderFn' as any); const roomExtender = await extender.extendRoom('room-id', { _id: 'user-id' } as any); - assertSpyCall(_spy, 0, { - args: [ - { - method: 'bridges:getRoomBridge:doGetById', - params: ['room-id', 'deno-test'], - }, - ], - }); + assert.deepStrictEqual(_spy.mock.calls[0].arguments, [ + { + method: 'bridges:getRoomBridge:doGetById', + params: ['room-id', 'deno-test'], + }, + ]); roomExtender.addCustomField('key', 'value'); await extender.finish(roomExtender); - assertSpyCall(_spy, 1, { - args: [ - { - method: 'bridges:getRoomBridge:doUpdate', - params: [roomExtender.getRoom(), [], 'deno-test'], - }, - ], - }); + assert.deepStrictEqual(_spy.mock.calls[1].arguments, [ + { + method: 'bridges:getRoomBridge:doUpdate', + params: [roomExtender.getRoom(), [], 'deno-test'], + }, + ]); - _spy.restore(); + _spy.mock.restore(); }); it('correctly formats requests for the extend video conference requests', async () => { - const _spy = spy(extender, 'senderFn' as keyof ModifyExtender); + const _spy = mock.method(extender, 'senderFn' as any); const videoConferenceExtender = await extender.extendVideoConference('video-conference-id'); - assertSpyCall(_spy, 0, { - args: [ - { - method: 'bridges:getVideoConferenceBridge:doGetById', - params: ['video-conference-id', 'deno-test'], - }, - ], - }); + assert.deepStrictEqual(_spy.mock.calls[0].arguments, [ + { + method: 'bridges:getVideoConferenceBridge:doGetById', + params: ['video-conference-id', 'deno-test'], + }, + ]); videoConferenceExtender.setStatus(4); await extender.finish(videoConferenceExtender); - assertSpyCall(_spy, 1, { - args: [ - { - method: 'bridges:getVideoConferenceBridge:doUpdate', - params: [videoConferenceExtender.getVideoConference(), 'deno-test'], - }, - ], - }); + assert.deepStrictEqual(_spy.mock.calls[1].arguments, [ + { + method: 'bridges:getVideoConferenceBridge:doUpdate', + params: [videoConferenceExtender.getVideoConference(), 'deno-test'], + }, + ]); - _spy.restore(); + _spy.mock.restore(); }); describe('Error Handling', () => { describe('extendMessage', () => { it('throws an instance of Error when senderFn throws an error', async () => { - const _stub = stub(extender, 'senderFn' as keyof ModifyExtender, () => Promise.reject(new Error('unit-test-error')) as any); + const _stub = mock.method(extender, 'senderFn' as any, () => Promise.reject(new Error('unit-test-error')) as any); - await assertRejects(() => extender.extendMessage('message-id', { _id: 'user-id' } as any), Error, 'unit-test-error'); + await assert.rejects(() => extender.extendMessage('message-id', { _id: 'user-id' } as any), { message: 'unit-test-error' }); - _stub.restore(); + _stub.mock.restore(); }); it('throws an instance of Error when senderFn throws a jsonrpc error', async () => { - const _stub = stub( + const _stub = mock.method( extender, - 'senderFn' as keyof ModifyExtender, + 'senderFn' as any, () => Promise.reject(jsonrpc.error('unit-test-error', new jsonrpc.JsonRpcError('unit-test-error', 1000))) as any, ); - await assertRejects(() => extender.extendMessage('message-id', { _id: 'user-id' } as any), Error, 'unit-test-error'); + await assert.rejects(() => extender.extendMessage('message-id', { _id: 'user-id' } as any), { message: 'unit-test-error' }); - _stub.restore(); + _stub.mock.restore(); }); it('throws an instance of Error when senderFn throws an unknown value', async () => { - const _stub = stub(extender, 'senderFn' as keyof ModifyExtender, () => Promise.reject({}) as any); + const _stub = mock.method(extender, 'senderFn' as any, () => Promise.reject({}) as any); - await assertRejects(() => extender.extendMessage('message-id', { _id: 'user-id' } as any), Error, 'An unknown error occurred'); + await assert.rejects(() => extender.extendMessage('message-id', { _id: 'user-id' } as any), { + message: 'An unknown error occurred', + }); - _stub.restore(); + _stub.mock.restore(); }); }); describe('extendRoom', () => { it('throws an instance of Error when senderFn throws an error', async () => { - const _stub = stub(extender, 'senderFn' as keyof ModifyExtender, () => Promise.reject(new Error('unit-test-error')) as any); + const _stub = mock.method(extender, 'senderFn' as any, () => Promise.reject(new Error('unit-test-error')) as any); - await assertRejects(() => extender.extendRoom('room-id', { _id: 'user-id' } as any), Error, 'unit-test-error'); + await assert.rejects(() => extender.extendRoom('room-id', { _id: 'user-id' } as any), { message: 'unit-test-error' }); - _stub.restore(); + _stub.mock.restore(); }); it('throws an instance of Error when senderFn throws a jsonrpc error', async () => { - const _stub = stub( + const _stub = mock.method( extender, - 'senderFn' as keyof ModifyExtender, + 'senderFn' as any, () => Promise.reject(jsonrpc.error('unit-test-error', new jsonrpc.JsonRpcError('unit-test-error', 1000))) as any, ); - await assertRejects(() => extender.extendRoom('room-id', { _id: 'user-id' } as any), Error, 'unit-test-error'); + await assert.rejects(() => extender.extendRoom('room-id', { _id: 'user-id' } as any), { message: 'unit-test-error' }); - _stub.restore(); + _stub.mock.restore(); }); it('throws an instance of Error when senderFn throws an unknown value', async () => { - const _stub = stub(extender, 'senderFn' as keyof ModifyExtender, () => Promise.reject({}) as any); + const _stub = mock.method(extender, 'senderFn' as any, () => Promise.reject({}) as any); - await assertRejects(() => extender.extendRoom('room-id', { _id: 'user-id' } as any), Error, 'An unknown error occurred'); + await assert.rejects(() => extender.extendRoom('room-id', { _id: 'user-id' } as any), { message: 'An unknown error occurred' }); - _stub.restore(); + _stub.mock.restore(); }); }); describe('extendVideoConference', () => { it('throws an instance of Error when senderFn throws an error', async () => { - const _stub = stub(extender, 'senderFn' as keyof ModifyExtender, () => Promise.reject(new Error('unit-test-error')) as any); + const _stub = mock.method(extender, 'senderFn' as any, () => Promise.reject(new Error('unit-test-error')) as any); - await assertRejects(() => extender.extendVideoConference('video-conference-id'), Error, 'unit-test-error'); + await assert.rejects(() => extender.extendVideoConference('video-conference-id'), { message: 'unit-test-error' }); - _stub.restore(); + _stub.mock.restore(); }); it('throws an instance of Error when senderFn throws a jsonrpc error', async () => { - const _stub = stub( + const _stub = mock.method( extender, - 'senderFn' as keyof ModifyExtender, + 'senderFn' as any, () => Promise.reject(jsonrpc.error('unit-test-error', new jsonrpc.JsonRpcError('unit-test-error', 1000))) as any, ); - await assertRejects(() => extender.extendVideoConference('video-conference-id'), Error, 'unit-test-error'); + await assert.rejects(() => extender.extendVideoConference('video-conference-id'), { message: 'unit-test-error' }); - _stub.restore(); + _stub.mock.restore(); }); it('throws an instance of Error when senderFn throws an unknown value', async () => { - const _stub = stub(extender, 'senderFn' as keyof ModifyExtender, () => Promise.reject({}) as any); + const _stub = mock.method(extender, 'senderFn' as any, () => Promise.reject({}) as any); - await assertRejects(() => extender.extendVideoConference('video-conference-id'), Error, 'An unknown error occurred'); + await assert.rejects(() => extender.extendVideoConference('video-conference-id'), { message: 'An unknown error occurred' }); - _stub.restore(); + _stub.mock.restore(); }); }); describe('finish', () => { it('throws an instance of Error when senderFn throws an error', async () => { - const _stub = stub(extender, 'senderFn' as keyof ModifyExtender, () => Promise.reject(new Error('unit-test-error')) as any); + const _stub = mock.method(extender, 'senderFn' as any, () => Promise.reject(new Error('unit-test-error')) as any); - await assertRejects(() => extender.finish({ kind: 'message', getMessage: () => ({}) } as any), Error, 'unit-test-error'); + await assert.rejects(() => extender.finish({ kind: 'message', getMessage: () => ({}) } as any), { message: 'unit-test-error' }); - _stub.restore(); + _stub.mock.restore(); }); it('throws an instance of Error when senderFn throws a jsonrpc error', async () => { - const _stub = stub( + const _stub = mock.method( extender, - 'senderFn' as keyof ModifyExtender, + 'senderFn' as any, () => Promise.reject(jsonrpc.error('unit-test-error', new jsonrpc.JsonRpcError('unit-test-error', 1000))) as any, ); - await assertRejects(() => extender.finish({ kind: 'message', getMessage: () => ({}) } as any), Error, 'unit-test-error'); + await assert.rejects(() => extender.finish({ kind: 'message', getMessage: () => ({}) } as any), { message: 'unit-test-error' }); - _stub.restore(); + _stub.mock.restore(); }); it('throws an instance of Error when senderFn throws an unknown value', async () => { - const _stub = stub(extender, 'senderFn' as keyof ModifyExtender, () => Promise.reject({}) as any); + const _stub = mock.method(extender, 'senderFn' as any, () => Promise.reject({}) as any); - await assertRejects(() => extender.finish({ kind: 'message', getMessage: () => ({}) } as any), Error, 'An unknown error occurred'); + await assert.rejects(() => extender.finish({ kind: 'message', getMessage: () => ({}) } as any), { + message: 'An unknown error occurred', + }); - _stub.restore(); + _stub.mock.restore(); }); }); }); diff --git a/packages/apps/node-runtime/src/lib/accessors/tests/ModifyUpdater.test.ts b/packages/apps/node-runtime/src/lib/accessors/tests/ModifyUpdater.test.ts index 9d63f35af85b7..827356bfd4b97 100644 --- a/packages/apps/node-runtime/src/lib/accessors/tests/ModifyUpdater.test.ts +++ b/packages/apps/node-runtime/src/lib/accessors/tests/ModifyUpdater.test.ts @@ -1,7 +1,6 @@ -// deno-lint-ignore-file no-explicit-any -import { assertEquals, assertRejects } from 'https://deno.land/std@0.203.0/assert/mod'; -import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd'; -import { assertSpyCall, spy, stub } from 'https://deno.land/std@0.203.0/testing/mock'; +import * as assert from 'node:assert'; +import { after, beforeEach, describe, it, mock } from 'node:test'; + import jsonrpc from 'jsonrpc-lite'; import { AppObjectRegistry } from '../../../AppObjectRegistry'; @@ -27,23 +26,21 @@ describe('ModifyUpdater', () => { modifyUpdater = new ModifyUpdater(senderFn); }); - afterAll(() => { + after(() => { AppObjectRegistry.clear(); }); it('correctly formats requests for the update message flow', async () => { - const _spy = spy(modifyUpdater, 'senderFn' as keyof ModifyUpdater); + const _spy = mock.method(modifyUpdater, 'senderFn' as any); const messageBuilder = await modifyUpdater.message('123', { id: '456' } as any); - assertSpyCall(_spy, 0, { - args: [ - { - method: 'bridges:getMessageBridge:doGetById', - params: ['123', 'deno-test'], - }, - ], - }); + assert.deepStrictEqual(_spy.mock.calls[0].arguments, [ + { + method: 'bridges:getMessageBridge:doGetById', + params: ['123', 'deno-test'], + }, + ]); messageBuilder.setUpdateData( { @@ -59,31 +56,27 @@ describe('ModifyUpdater', () => { await modifyUpdater.finish(messageBuilder); - assertSpyCall(_spy, 1, { - args: [ - { - method: 'bridges:getMessageBridge:doUpdate', - params: [{ id: '123', ...messageBuilder.getChanges() }, 'deno-test'], - }, - ], - }); + assert.deepStrictEqual(_spy.mock.calls[1].arguments, [ + { + method: 'bridges:getMessageBridge:doUpdate', + params: [{ id: '123', ...messageBuilder.getChanges() }, 'deno-test'], + }, + ]); - _spy.restore(); + _spy.mock.restore(); }); it('correctly formats requests for the update room flow', async () => { - const _spy = spy(modifyUpdater, 'senderFn' as keyof ModifyUpdater); + const _spy = mock.method(modifyUpdater, 'senderFn' as any); const roomBuilder = (await modifyUpdater.room('123', { id: '456' } as any)) as RoomBuilder; - assertSpyCall(_spy, 0, { - args: [ - { - method: 'bridges:getRoomBridge:doGetById', - params: ['123', 'deno-test'], - }, - ], - }); + assert.deepStrictEqual(_spy.mock.calls[0].arguments, [ + { + method: 'bridges:getRoomBridge:doGetById', + params: ['123', 'deno-test'], + }, + ]); roomBuilder.setData({ id: '123', @@ -100,20 +93,18 @@ describe('ModifyUpdater', () => { await modifyUpdater.finish(roomBuilder); - assertSpyCall(_spy, 1, { - args: [ - { - method: 'bridges:getRoomBridge:doUpdate', - params: [{ id: '123', ...roomBuilder.getChanges() }, roomBuilder.getMembersToBeAddedUsernames(), 'deno-test'], - }, - ], - }); + assert.deepStrictEqual(_spy.mock.calls[1].arguments, [ + { + method: 'bridges:getRoomBridge:doUpdate', + params: [{ id: '123', ...roomBuilder.getChanges() }, roomBuilder.getMembersToBeAddedUsernames(), 'deno-test'], + }, + ]); }); it('correctly formats requests to UserUpdater methods', async () => { const result = (await modifyUpdater.getUserUpdater().updateStatusText({ id: '123' } as any, 'Hello World')) as any; - assertEquals(result, { + assert.deepStrictEqual(result, { method: 'accessor:getModifier:getUpdater:getUserUpdater:updateStatusText', params: [{ id: '123' }, 'Hello World'], }); @@ -122,7 +113,7 @@ describe('ModifyUpdater', () => { it('correctly formats requests to LivechatUpdater methods', async () => { const result = (await modifyUpdater.getLivechatUpdater().closeRoom({ id: '123' } as any, 'close it!')) as any; - assertEquals(result, { + assert.deepStrictEqual(result, { method: 'accessor:getModifier:getUpdater:getLivechatUpdater:closeRoom', params: [{ id: '123' }, 'close it!'], }); @@ -131,7 +122,7 @@ describe('ModifyUpdater', () => { it('correctly formats requests to MessageUpdater methods', async () => { const result = (await modifyUpdater.getMessageUpdater().addReaction('message-id', 'user-id', ':smile:')) as any; - assertEquals(result, { + assert.deepStrictEqual(result, { method: 'accessor:getModifier:getUpdater:getMessageUpdater:addReaction', params: ['message-id', 'user-id', ':smile:'], }); @@ -140,61 +131,63 @@ describe('ModifyUpdater', () => { describe('Error Handling', () => { describe('message', () => { it('throws an instance of Error when senderFn throws an error', async () => { - const _stub = stub(modifyUpdater, 'senderFn' as keyof ModifyUpdater, () => Promise.reject(new Error('unit-test-error')) as any); + const _stub = mock.method(modifyUpdater, 'senderFn' as any, () => Promise.reject(new Error('unit-test-error')) as any); - await assertRejects(() => modifyUpdater.message('message-id', { _id: 'user-id' } as any), Error, 'unit-test-error'); + await assert.rejects(() => modifyUpdater.message('message-id', { _id: 'user-id' } as any), { message: 'unit-test-error' }); - _stub.restore(); + _stub.mock.restore(); }); it('throws an instance of Error when senderFn throws a jsonrpc error', async () => { - const _stub = stub( + const _stub = mock.method( modifyUpdater, - 'senderFn' as keyof ModifyUpdater, + 'senderFn' as any, () => Promise.reject(jsonrpc.error('unit-test-error', new jsonrpc.JsonRpcError('unit-test-error', 1000))) as any, ); - await assertRejects(() => modifyUpdater.message('message-id', { _id: 'user-id' } as any), Error, 'unit-test-error'); + await assert.rejects(() => modifyUpdater.message('message-id', { _id: 'user-id' } as any), { message: 'unit-test-error' }); - _stub.restore(); + _stub.mock.restore(); }); it('throws an instance of Error when senderFn throws an unknown value', async () => { - const _stub = stub(modifyUpdater, 'senderFn' as keyof ModifyUpdater, () => Promise.reject({}) as any); + const _stub = mock.method(modifyUpdater, 'senderFn' as any, () => Promise.reject({}) as any); - await assertRejects(() => modifyUpdater.message('message-id', { _id: 'user-id' } as any), Error, 'An unknown error occurred'); + await assert.rejects(() => modifyUpdater.message('message-id', { _id: 'user-id' } as any), { + message: 'An unknown error occurred', + }); - _stub.restore(); + _stub.mock.restore(); }); }); describe('room', () => { it('throws an instance of Error when senderFn throws an error', async () => { - const _stub = stub(modifyUpdater, 'senderFn' as keyof ModifyUpdater, () => Promise.reject(new Error('unit-test-error')) as any); + const _stub = mock.method(modifyUpdater, 'senderFn' as any, () => Promise.reject(new Error('unit-test-error')) as any); - await assertRejects(() => modifyUpdater.room('room-id', { _id: 'user-id' } as any), Error, 'unit-test-error'); + await assert.rejects(() => modifyUpdater.room('room-id', { _id: 'user-id' } as any), { message: 'unit-test-error' }); - _stub.restore(); + _stub.mock.restore(); }); it('throws an instance of Error when senderFn throws a jsonrpc error', async () => { - const _stub = stub( + const _stub = mock.method( modifyUpdater, - 'senderFn' as keyof ModifyUpdater, + 'senderFn' as any, () => Promise.reject(jsonrpc.error('unit-test-error', new jsonrpc.JsonRpcError('unit-test-error', 1000))) as any, ); - await assertRejects(() => modifyUpdater.room('room-id', { _id: 'user-id' } as any), Error, 'unit-test-error'); + await assert.rejects(() => modifyUpdater.room('room-id', { _id: 'user-id' } as any), { message: 'unit-test-error' }); - _stub.restore(); + _stub.mock.restore(); }); it('throws an instance of Error when senderFn throws an unknown value', async () => { - const _stub = stub(modifyUpdater, 'senderFn' as keyof ModifyUpdater, () => Promise.reject({}) as any); + const _stub = mock.method(modifyUpdater, 'senderFn' as any, () => Promise.reject({}) as any); - await assertRejects(() => modifyUpdater.room('room-id', { _id: 'user-id' } as any), Error, 'An unknown error occurred'); + await assert.rejects(() => modifyUpdater.room('room-id', { _id: 'user-id' } as any), { message: 'An unknown error occurred' }); - _stub.restore(); + _stub.mock.restore(); }); }); @@ -212,31 +205,31 @@ describe('ModifyUpdater', () => { } as any; it('throws an instance of Error when senderFn throws an error', async () => { - const _stub = stub(modifyUpdater, 'senderFn' as keyof ModifyUpdater, () => Promise.reject(new Error('unit-test-error')) as any); + const _stub = mock.method(modifyUpdater, 'senderFn' as any, () => Promise.reject(new Error('unit-test-error')) as any); - await assertRejects(() => modifyUpdater.finish(messageUpdater), Error, 'unit-test-error'); + await assert.rejects(() => modifyUpdater.finish(messageUpdater), { message: 'unit-test-error' }); - _stub.restore(); + _stub.mock.restore(); }); it('throws an instance of Error when senderFn throws a jsonrpc error', async () => { - const _stub = stub( + const _stub = mock.method( modifyUpdater, - 'senderFn' as keyof ModifyUpdater, + 'senderFn' as any, () => Promise.reject(jsonrpc.error('unit-test-error', new jsonrpc.JsonRpcError('unit-test-error', 1000))) as any, ); - await assertRejects(() => modifyUpdater.finish(messageUpdater), Error, 'unit-test-error'); + await assert.rejects(() => modifyUpdater.finish(messageUpdater), { message: 'unit-test-error' }); - _stub.restore(); + _stub.mock.restore(); }); it('throws an instance of Error when senderFn throws an unknown value', async () => { - const _stub = stub(modifyUpdater, 'senderFn' as keyof ModifyUpdater, () => Promise.reject({}) as any); + const _stub = mock.method(modifyUpdater, 'senderFn' as any, () => Promise.reject({}) as any); - await assertRejects(() => modifyUpdater.finish(messageUpdater), Error, 'An unknown error occurred'); + await assert.rejects(() => modifyUpdater.finish(messageUpdater), { message: 'An unknown error occurred' }); - _stub.restore(); + _stub.mock.restore(); }); }); }); diff --git a/packages/apps/node-runtime/src/lib/accessors/tests/formatResponseErrorHandler.test.ts b/packages/apps/node-runtime/src/lib/accessors/tests/formatResponseErrorHandler.test.ts index 78f92e1e6d1b3..1fe4a1d3e7181 100644 --- a/packages/apps/node-runtime/src/lib/accessors/tests/formatResponseErrorHandler.test.ts +++ b/packages/apps/node-runtime/src/lib/accessors/tests/formatResponseErrorHandler.test.ts @@ -1,6 +1,6 @@ -// deno-lint-ignore-file no-explicit-any -import { assertEquals, assertInstanceOf, assertStrictEquals } from 'https://deno.land/std@0.203.0/assert/mod'; -import { describe, it } from 'https://deno.land/std@0.203.0/testing/bdd'; +import * as assert from 'node:assert'; +import { describe, it } from 'node:test'; + import * as jsonrpc from 'jsonrpc-lite'; import { formatErrorResponse } from '../formatResponseErrorHandler'; @@ -11,8 +11,8 @@ describe('formatErrorResponse', () => { const errorObject = jsonrpc.error('test-id', new jsonrpc.JsonRpcError('Test error message', 1000)); const result = formatErrorResponse(errorObject); - assertInstanceOf(result, Error); - assertEquals(result.message, 'Test error message'); + assert.ok(result instanceof Error, `Expected instance of Error`); + assert.deepStrictEqual(result.message, 'Test error message'); }); it('formats objects with error.message structure', () => { @@ -24,8 +24,8 @@ describe('formatErrorResponse', () => { }; const result = formatErrorResponse(errorLikeObject); - assertInstanceOf(result, Error); - assertEquals(result.message, 'Custom error message'); + assert.ok(result instanceof Error, `Expected instance of Error`); + assert.deepStrictEqual(result.message, 'Custom error message'); }); it('handles nested error objects with complex structure', () => { @@ -41,8 +41,8 @@ describe('formatErrorResponse', () => { }; const result = formatErrorResponse(complexError); - assertInstanceOf(result, Error); - assertEquals(result.message, 'Database connection failed'); + assert.ok(result instanceof Error, `Expected instance of Error`); + assert.deepStrictEqual(result.message, 'Database connection failed'); }); it('handles error objects with empty message', () => { @@ -54,8 +54,8 @@ describe('formatErrorResponse', () => { }; const result = formatErrorResponse(emptyMessageError); - assertInstanceOf(result, Error); - assertEquals(result.message, ''); + assert.ok(result instanceof Error, `Expected instance of Error`); + assert.deepStrictEqual(result.message, ''); }); }); @@ -64,8 +64,8 @@ describe('formatErrorResponse', () => { const originalError = new Error('Original error message'); const result = formatErrorResponse(originalError); - assertStrictEquals(result, originalError); - assertEquals(result.message, 'Original error message'); + assert.strictEqual(result, originalError); + assert.deepStrictEqual(result.message, 'Original error message'); }); it('returns custom Error subclasses unchanged', () => { @@ -82,9 +82,9 @@ describe('formatErrorResponse', () => { const customError = new CustomError('Custom error', 404); const result = formatErrorResponse(customError); - assertStrictEquals(result, customError); - assertEquals(result.message, 'Custom error'); - assertEquals((result as CustomError).code, 404); + assert.strictEqual(result, customError); + assert.deepStrictEqual(result.message, 'Custom error'); + assert.deepStrictEqual((result as CustomError).code, 404); }); it('handles Error instances with additional properties', () => { @@ -94,9 +94,9 @@ describe('formatErrorResponse', () => { const result = formatErrorResponse(errorWithProps); - assertStrictEquals(result, errorWithProps); - assertEquals(result.message, 'Error with props'); - assertEquals((result as any).statusCode, 500); + assert.strictEqual(result, errorWithProps); + assert.deepStrictEqual(result.message, 'Error with props'); + assert.deepStrictEqual((result as any).statusCode, 500); }); }); @@ -105,61 +105,61 @@ describe('formatErrorResponse', () => { const stringError = 'Simple string error'; const result = formatErrorResponse(stringError); - assertInstanceOf(result, Error); - assertEquals(result.message, 'An unknown error occurred'); - assertEquals(result.cause, stringError); + assert.ok(result instanceof Error, `Expected instance of Error`); + assert.deepStrictEqual(result.message, 'An unknown error occurred'); + assert.deepStrictEqual(result.cause, stringError); }); it('wraps number errors with default message and cause', () => { const numberError = 404; const result = formatErrorResponse(numberError); - assertInstanceOf(result, Error); - assertEquals(result.message, 'An unknown error occurred'); - assertEquals(result.cause, numberError); + assert.ok(result instanceof Error, `Expected instance of Error`); + assert.deepStrictEqual(result.message, 'An unknown error occurred'); + assert.deepStrictEqual(result.cause, numberError); }); it('wraps boolean errors with default message and cause', () => { const booleanError = false; const result = formatErrorResponse(booleanError); - assertInstanceOf(result, Error); - assertEquals(result.message, 'An unknown error occurred'); - assertEquals(result.cause, booleanError); + assert.ok(result instanceof Error, `Expected instance of Error`); + assert.deepStrictEqual(result.message, 'An unknown error occurred'); + assert.deepStrictEqual(result.cause, booleanError); }); it('wraps null with default message and cause', () => { const result = formatErrorResponse(null); - assertInstanceOf(result, Error); - assertEquals(result.message, 'An unknown error occurred'); - assertEquals(result.cause, null); + assert.ok(result instanceof Error, `Expected instance of Error`); + assert.deepStrictEqual(result.message, 'An unknown error occurred'); + assert.deepStrictEqual(result.cause, null); }); it('wraps undefined with default message and cause', () => { const result = formatErrorResponse(undefined); - assertInstanceOf(result, Error); - assertEquals(result.message, 'An unknown error occurred'); - assertEquals(result.cause, undefined); + assert.ok(result instanceof Error, `Expected instance of Error`); + assert.deepStrictEqual(result.message, 'An unknown error occurred'); + assert.deepStrictEqual(result.cause, undefined); }); it('wraps arrays with default message and cause', () => { const arrayError = ['error', 'details']; const result = formatErrorResponse(arrayError); - assertInstanceOf(result, Error); - assertEquals(result.message, 'An unknown error occurred'); - assertEquals(result.cause, arrayError); + assert.ok(result instanceof Error, `Expected instance of Error`); + assert.deepStrictEqual(result.message, 'An unknown error occurred'); + assert.deepStrictEqual(result.cause, arrayError); }); it('wraps functions with default message and cause', () => { const functionError = () => 'error'; const result = formatErrorResponse(functionError); - assertInstanceOf(result, Error); - assertEquals(result.message, 'An unknown error occurred'); - assertEquals(result.cause, functionError); + assert.ok(result instanceof Error, `Expected instance of Error`); + assert.deepStrictEqual(result.message, 'An unknown error occurred'); + assert.deepStrictEqual(result.cause, functionError); }); it('wraps plain objects without error.message with default message and cause', () => { @@ -170,9 +170,9 @@ describe('formatErrorResponse', () => { }; const result = formatErrorResponse(plainObject); - assertInstanceOf(result, Error); - assertEquals(result.message, 'An unknown error occurred'); - assertEquals(result.cause, plainObject); + assert.ok(result instanceof Error, `Expected instance of Error`); + assert.deepStrictEqual(result.message, 'An unknown error occurred'); + assert.deepStrictEqual(result.cause, plainObject); }); it('wraps objects with error property but no message with default message and cause', () => { @@ -184,9 +184,9 @@ describe('formatErrorResponse', () => { }; const result = formatErrorResponse(errorObjectNoMessage); - assertInstanceOf(result, Error); - assertEquals(result.message, 'An unknown error occurred'); - assertEquals(result.cause, errorObjectNoMessage); + assert.ok(result instanceof Error, `Expected instance of Error`); + assert.deepStrictEqual(result.message, 'An unknown error occurred'); + assert.deepStrictEqual(result.cause, errorObjectNoMessage); }); }); @@ -195,7 +195,7 @@ describe('formatErrorResponse', () => { for (const testCase of testCases) { const result = formatErrorResponse(testCase); - assertInstanceOf(result, Error, `Failed for input: ${JSON.stringify(testCase)}`); + assert.ok(result instanceof Error, `Failed for input: ${JSON.stringify(testCase)}`); } }); @@ -203,9 +203,9 @@ describe('formatErrorResponse', () => { const plainObject = { status: 'error', code: 500 }; const result = formatErrorResponse(plainObject); - assertInstanceOf(result, Error); - assertEquals(result.message, 'An unknown error occurred'); + assert.ok(result instanceof Error, `Expected instance of Error`); + assert.deepStrictEqual(result.message, 'An unknown error occurred'); // Ensure the message is not "[object Object]" - assertEquals(result.message !== '[object Object]', true); + assert.deepStrictEqual(result.message !== '[object Object]', true); }); }); diff --git a/packages/apps/node-runtime/src/lib/accessors/tests/http.test.ts b/packages/apps/node-runtime/src/lib/accessors/tests/http.test.ts index 1bbe10d95afd1..e586bb6e24cbe 100644 --- a/packages/apps/node-runtime/src/lib/accessors/tests/http.test.ts +++ b/packages/apps/node-runtime/src/lib/accessors/tests/http.test.ts @@ -1,7 +1,5 @@ -// deno-lint-ignore-file no-explicit-any -import { assertRejects } from 'https://deno.land/std@0.203.0/assert/mod'; -import { beforeEach, describe, it, afterAll } from 'https://deno.land/std@0.203.0/testing/bdd'; -import { stub } from 'https://deno.land/std@0.203.0/testing/mock'; +import * as assert from 'node:assert'; +import { beforeEach, describe, it, after, mock } from 'node:test'; import { AppObjectRegistry } from '../../../AppObjectRegistry'; import { Http } from '../http'; @@ -26,13 +24,13 @@ describe('Http accessor error handling integration', () => { http = new Http(mockRead as any, mockPersistence as any, mockHttpExtend as any, () => Promise.resolve({}) as any); }); - afterAll(() => { + after(() => { AppObjectRegistry.clear(); }); describe('HTTP method error handling', () => { it('formats JSON-RPC errors correctly for GET requests', async () => { - const _stub = stub(http, 'senderFn' as keyof Http, () => + const _stub = mock.method(http, 'senderFn' as any, () => Promise.reject({ error: { message: 'HTTP GET request failed', @@ -41,13 +39,13 @@ describe('Http accessor error handling integration', () => { }), ); - await assertRejects(() => http.get('https://api.example.com/data'), Error, 'HTTP GET request failed'); + await assert.rejects(() => http.get('https://api.example.com/data'), { message: 'HTTP GET request failed' }); - _stub.restore(); + _stub.mock.restore(); }); it('formats JSON-RPC errors correctly for POST requests', async () => { - const _stub = stub(http, 'senderFn' as keyof Http, () => + const _stub = mock.method(http, 'senderFn' as any, () => Promise.reject({ error: { message: 'HTTP POST request validation failed', @@ -56,17 +54,15 @@ describe('Http accessor error handling integration', () => { }), ); - await assertRejects( - () => http.post('https://api.example.com/create', { data: { name: 'test' } }), - Error, - 'HTTP POST request validation failed', - ); + await assert.rejects(() => http.post('https://api.example.com/create', { data: { name: 'test' } }), { + message: 'HTTP POST request validation failed', + }); - _stub.restore(); + _stub.mock.restore(); }); it('formats JSON-RPC errors correctly for PUT requests', async () => { - const _stub = stub(http, 'senderFn' as keyof Http, () => + const _stub = mock.method(http, 'senderFn' as any, () => Promise.reject({ error: { message: 'HTTP PUT request unauthorized', @@ -75,17 +71,15 @@ describe('Http accessor error handling integration', () => { }), ); - await assertRejects( - () => http.put('https://api.example.com/update/123', { data: { name: 'updated' } }), - Error, - 'HTTP PUT request unauthorized', - ); + await assert.rejects(() => http.put('https://api.example.com/update/123', { data: { name: 'updated' } }), { + message: 'HTTP PUT request unauthorized', + }); - _stub.restore(); + _stub.mock.restore(); }); it('formats JSON-RPC errors correctly for DELETE requests', async () => { - const _stub = stub(http, 'senderFn' as keyof Http, () => + const _stub = mock.method(http, 'senderFn' as any, () => Promise.reject({ error: { message: 'HTTP DELETE request forbidden', @@ -94,13 +88,13 @@ describe('Http accessor error handling integration', () => { }), ); - await assertRejects(() => http.del('https://api.example.com/delete/123'), Error, 'HTTP DELETE request forbidden'); + await assert.rejects(() => http.del('https://api.example.com/delete/123'), { message: 'HTTP DELETE request forbidden' }); - _stub.restore(); + _stub.mock.restore(); }); it('formats JSON-RPC errors correctly for PATCH requests', async () => { - const _stub = stub(http, 'senderFn' as keyof Http, () => + const _stub = mock.method(http, 'senderFn' as any, () => Promise.reject({ error: { message: 'HTTP PATCH request conflict', @@ -109,24 +103,22 @@ describe('Http accessor error handling integration', () => { }), ); - await assertRejects( - () => http.patch('https://api.example.com/patch/123', { data: { status: 'active' } }), - Error, - 'HTTP PATCH request conflict', - ); + await assert.rejects(() => http.patch('https://api.example.com/patch/123', { data: { status: 'active' } }), { + message: 'HTTP PATCH request conflict', + }); - _stub.restore(); + _stub.mock.restore(); }); }); describe('Error instance passthrough', () => { it('passes through existing Error instances unchanged for HTTP requests', async () => { const originalError = new Error('Network timeout error'); - const _stub = stub(http, 'senderFn' as keyof Http, () => Promise.reject(originalError)); + const _stub = mock.method(http, 'senderFn' as any, () => Promise.reject(originalError)); - await assertRejects(() => http.get('https://api.example.com/data'), Error, 'Network timeout error'); + await assert.rejects(() => http.get('https://api.example.com/data'), { message: 'Network timeout error' }); - _stub.restore(); + _stub.mock.restore(); }); }); @@ -137,28 +129,28 @@ describe('Http accessor error handling integration', () => { details: 'Something went wrong', timestamp: Date.now(), }; - const _stub = stub(http, 'senderFn' as keyof Http, () => Promise.reject(unknownError)); + const _stub = mock.method(http, 'senderFn' as any, () => Promise.reject(unknownError)); - await assertRejects(() => http.get('https://api.example.com/data'), Error, 'An unknown error occurred'); + await assert.rejects(() => http.get('https://api.example.com/data'), { message: 'An unknown error occurred' }); - _stub.restore(); + _stub.mock.restore(); }); it('wraps string errors with default message for HTTP requests', async () => { const stringError = 'Connection refused'; - const _stub = stub(http, 'senderFn' as keyof Http, () => Promise.reject(stringError)); + const _stub = mock.method(http, 'senderFn' as any, () => Promise.reject(stringError)); - await assertRejects(() => http.get('https://api.example.com/data'), Error, 'An unknown error occurred'); + await assert.rejects(() => http.get('https://api.example.com/data'), { message: 'An unknown error occurred' }); - _stub.restore(); + _stub.mock.restore(); }); it('wraps null/undefined errors with default message for HTTP requests', async () => { - const _stub = stub(http, 'senderFn' as keyof Http, () => Promise.reject(null)); + const _stub = mock.method(http, 'senderFn' as any, () => Promise.reject(null)); - await assertRejects(() => http.get('https://api.example.com/data'), Error, 'An unknown error occurred'); + await assert.rejects(() => http.get('https://api.example.com/data'), { message: 'An unknown error occurred' }); - _stub.restore(); + _stub.mock.restore(); }); }); }); diff --git a/packages/apps/node-runtime/src/lib/ast/tests/operations.test.ts b/packages/apps/node-runtime/src/lib/ast/tests/operations.test.ts index 0417f6c3c614f..92b83161daff1 100644 --- a/packages/apps/node-runtime/src/lib/ast/tests/operations.test.ts +++ b/packages/apps/node-runtime/src/lib/ast/tests/operations.test.ts @@ -1,6 +1,5 @@ -import { assertNotEquals } from 'https://deno.land/std@0.203.0/assert/assert_not_equals'; -import { assertEquals, assertThrows } from 'https://deno.land/std@0.203.0/assert/mod'; -import { beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd'; +import * as assert from 'node:assert'; +import { beforeEach, describe, it } from 'node:test'; import type { WalkerState } from '../operations'; import { @@ -41,7 +40,7 @@ describe('getFunctionIdentifier', () => { // ancestors array is built by the walking lib const nodeAncestors = [FunctionDeclarationFoo.node]; const functionNodeIndex = 0; - assertEquals('foo', getFunctionIdentifier(nodeAncestors, functionNodeIndex)); + assert.deepStrictEqual('foo', getFunctionIdentifier(nodeAncestors, functionNodeIndex)); }); it(`identifies the name "foo" for the code \`${ConstFooAssignedFunctionExpression.code}\``, () => { @@ -52,7 +51,7 @@ describe('getFunctionIdentifier', () => { ConstFooAssignedFunctionExpression.node.declarations[0].init!, // FunctionExpression ]; const functionNodeIndex = 2; - assertEquals('foo', getFunctionIdentifier(nodeAncestors, functionNodeIndex)); + assert.deepStrictEqual('foo', getFunctionIdentifier(nodeAncestors, functionNodeIndex)); }); it(`identifies the name "foo" for the code \`${AssignmentExpressionOfArrowFunctionToFooIdentifier.code}\``, () => { @@ -63,7 +62,7 @@ describe('getFunctionIdentifier', () => { (AssignmentExpressionOfArrowFunctionToFooIdentifier.node.expression as AssignmentExpression).right, // ArrowFunctionExpression ]; const functionNodeIndex = 2; - assertEquals('foo', getFunctionIdentifier(nodeAncestors, functionNodeIndex)); + assert.deepStrictEqual('foo', getFunctionIdentifier(nodeAncestors, functionNodeIndex)); }); it(`identifies the name "foo" for the code \`${AssignmentExpressionOfNamedFunctionToFooMemberExpression.code}\``, () => { @@ -74,7 +73,7 @@ describe('getFunctionIdentifier', () => { (AssignmentExpressionOfNamedFunctionToFooMemberExpression.node.expression as AssignmentExpression).right, // FunctionExpression ]; const functionNodeIndex = 2; - assertEquals('foo', getFunctionIdentifier(nodeAncestors, functionNodeIndex)); + assert.deepStrictEqual('foo', getFunctionIdentifier(nodeAncestors, functionNodeIndex)); }); it(`identifies the name "foo" for the code \`${MethodDefinitionOfFooInClassBar.code}\``, () => { @@ -86,7 +85,7 @@ describe('getFunctionIdentifier', () => { (MethodDefinitionOfFooInClassBar.node.body!.body[0] as MethodDefinition).value, // FunctionExpression ]; const functionNodeIndex = 3; - assertEquals('foo', getFunctionIdentifier(nodeAncestors, functionNodeIndex)); + assert.deepStrictEqual('foo', getFunctionIdentifier(nodeAncestors, functionNodeIndex)); }); }); @@ -95,14 +94,14 @@ describe('wrapWithAwait', () => { const node = structuredClone(SimpleCallExpressionOfFoo.node.expression); wrapWithAwait(node); - assertEquals('AwaitExpression', node.type); - assertNotEquals(SimpleCallExpressionOfFoo.node.expression.type, node.type); - assertEquals(SimpleCallExpressionOfFoo.node.expression, (node as AwaitExpression).argument); + assert.deepStrictEqual('AwaitExpression', node.type); + assert.notDeepStrictEqual(SimpleCallExpressionOfFoo.node.expression.type, node.type); + assert.deepStrictEqual(SimpleCallExpressionOfFoo.node.expression, (node as AwaitExpression).argument); }); it('throws if node is not an expression', () => { const node = structuredClone(SimpleCallExpressionOfFoo.node); - assertThrows(() => wrapWithAwait(node as unknown as Expression)); + assert.throws(() => wrapWithAwait(node as unknown as Expression)); }); }); @@ -125,13 +124,13 @@ describe('asyncifyScope', () => { asyncifyScope(ancestors, state); // Assert the function did indeed change the expression to async - assertEquals(((node.body.body[0] as ReturnStatement).argument as ArrowFunctionExpression).async, true); + assert.deepStrictEqual(((node.body.body[0] as ReturnStatement).argument as ArrowFunctionExpression).async, true); // Assert the function did NOT change all ancestors in the chain - assertEquals(node.async, false); + assert.deepStrictEqual(node.async, false); // Assert it couldn't find a function identifier - assertEquals(state.functionIdentifiers.size, 0); + assert.deepStrictEqual(state.functionIdentifiers.size, 0); }); }); @@ -150,7 +149,7 @@ describe('checkReassignmentofModifiedIdentifiers', () => { checkReassignmentOfModifiedIdentifiers(node.expression, state, ancestors, ''); - assertEquals(state.functionIdentifiers.has('bar'), true); + assert.deepStrictEqual(state.functionIdentifiers.has('bar'), true); }); it(`identifies the reassignment of "foo" in the code "${AssignmentOfFooToBarMemberExpression.code}"`, () => { @@ -167,7 +166,7 @@ describe('checkReassignmentofModifiedIdentifiers', () => { checkReassignmentOfModifiedIdentifiers(node.expression, state, ancestors, ''); - assertEquals(state.functionIdentifiers.has('bar'), true); + assert.deepStrictEqual(state.functionIdentifiers.has('bar'), true); }); it(`identifies the reassignment of "foo" in the code "${AssignmentOfFooToBarVariableDeclarator.code}"`, () => { @@ -183,7 +182,7 @@ describe('checkReassignmentofModifiedIdentifiers', () => { checkReassignmentOfModifiedIdentifiers(node.declarations[0], state, ancestors, ''); - assertEquals(state.functionIdentifiers.has('bar'), true); + assert.deepStrictEqual(state.functionIdentifiers.has('bar'), true); }); it(`identifies the reassignment of "foo" in the code "${AssignmentOfFooToBarPropertyDefinition.code}"`, () => { @@ -200,7 +199,7 @@ describe('checkReassignmentofModifiedIdentifiers', () => { checkReassignmentOfModifiedIdentifiers(node.body.body[0], state, ancestors, ''); - assertEquals(state.functionIdentifiers.has('bar'), true); + assert.deepStrictEqual(state.functionIdentifiers.has('bar'), true); }); }); @@ -229,11 +228,11 @@ describe('buildFixModifiedFunctionsOperation', function () { fixFunction(ancestors[4], state, ancestors, ''); - assertEquals(state.isModified, true); - assertEquals(state.functionIdentifiers.has('bar'), true); - assertNotEquals(FixSimpleCallExpression.node, node); - assertEquals(node.async, true); - assertEquals(ancestors[4].type, 'AwaitExpression'); + assert.deepStrictEqual(state.isModified, true); + assert.deepStrictEqual(state.functionIdentifiers.has('bar'), true); + assert.notDeepStrictEqual(FixSimpleCallExpression.node, node); + assert.deepStrictEqual(node.async, true); + assert.deepStrictEqual(ancestors[4].type, 'AwaitExpression'); }); it(`fixes calls of "foo" in the code "${ArrowFunctionDerefCallExpression.code}"`, () => { @@ -248,14 +247,14 @@ describe('buildFixModifiedFunctionsOperation', function () { fixFunction(ancestors[3], state, ancestors, ''); // Recorded that a modification has been made - assertEquals(state.isModified, true); + assert.deepStrictEqual(state.isModified, true); // Recorded that the enclosing scope of the call also requires fixing - assertEquals(state.functionIdentifiers.has('bar'), true); + assert.deepStrictEqual(state.functionIdentifiers.has('bar'), true); // Original node and fixed node are different - assertNotEquals(ArrowFunctionDerefCallExpression.node, node); + assert.notDeepStrictEqual(ArrowFunctionDerefCallExpression.node, node); // The function call is now await'ed - assertEquals(ancestors[3].type, 'AwaitExpression'); + assert.deepStrictEqual(ancestors[3].type, 'AwaitExpression'); // The parent function of the call is now marked as async - assertEquals((ancestors[2] as ArrowFunctionExpression).async, true); + assert.deepStrictEqual((ancestors[2] as ArrowFunctionExpression).async, true); }); }); diff --git a/packages/apps/node-runtime/src/lib/logger.ts b/packages/apps/node-runtime/src/lib/logger.ts index 5fa44b96cc6ac..b7d6ba1c2a705 100644 --- a/packages/apps/node-runtime/src/lib/logger.ts +++ b/packages/apps/node-runtime/src/lib/logger.ts @@ -5,7 +5,7 @@ import stackTrace from 'stack-trace'; import { AppObjectRegistry } from '../AppObjectRegistry'; -export interface StackFrame { +export interface IStackFrame { getTypeName(): string; getFunctionName(): string; getMethodName(): string; @@ -104,7 +104,7 @@ export class Logger implements ILogger { }); } - private getStack(stack: Array): string { + private getStack(stack: Array): string { let func = 'anonymous'; if (stack.length === 1) { diff --git a/packages/apps/node-runtime/src/lib/room.ts b/packages/apps/node-runtime/src/lib/room.ts index 83b5ecd4c0308..540c7cba69202 100644 --- a/packages/apps/node-runtime/src/lib/room.ts +++ b/packages/apps/node-runtime/src/lib/room.ts @@ -55,11 +55,11 @@ export class Room { public get usernames(): Promise> { if (!this.id) return Promise.resolve([]); - if (!this._USERNAMES) { + if (typeof this._USERNAMES === 'undefined') { this._USERNAMES = this[PrivateManager]?.getBridges().getInternalBridge().doGetUsernamesOfRoomById(this.id); } - return this._USERNAMES || Promise.resolve([]); + return this._USERNAMES ?? Promise.resolve([]); } public set usernames(usernames) {} @@ -97,7 +97,7 @@ export class Room { public async getUsernames(): Promise> { // Get usernames - if (!this._USERNAMES) { + if (typeof this._USERNAMES === 'undefined') { this._USERNAMES = this[PrivateManager]?.getBridges().getInternalBridge().doGetUsernamesOfRoomById(this.id!); } diff --git a/packages/apps/node-runtime/src/lib/tests/logger.test.ts b/packages/apps/node-runtime/src/lib/tests/logger.test.ts index 6e4eddb772d27..e3110a70390b2 100644 --- a/packages/apps/node-runtime/src/lib/tests/logger.test.ts +++ b/packages/apps/node-runtime/src/lib/tests/logger.test.ts @@ -1,5 +1,5 @@ -import { assertEquals } from 'https://deno.land/std@0.203.0/assert/mod'; -import { describe, it } from 'https://deno.land/std@0.203.0/testing/bdd'; +import * as assert from 'node:assert'; +import { describe, it } from 'node:test'; import { Logger } from '../logger'; @@ -8,8 +8,8 @@ describe('Logger', () => { const logger = new Logger('test'); logger.info('test'); const logs = logger.getLogs(); - assertEquals(logs.entries.length, 1); - assertEquals(logs.method, 'test'); + assert.deepStrictEqual(logs.entries.length, 1); + assert.deepStrictEqual(logs.method, 'test'); }); it('should be able to add entries of different severity', () => { @@ -18,94 +18,94 @@ describe('Logger', () => { logger.debug('test'); logger.error('test'); const logs = logger.getLogs(); - assertEquals(logs.entries.length, 3); - assertEquals(logs.entries[0].severity, 'info'); - assertEquals(logs.entries[1].severity, 'debug'); - assertEquals(logs.entries[2].severity, 'error'); + assert.deepStrictEqual(logs.entries.length, 3); + assert.deepStrictEqual(logs.entries[0].severity, 'info'); + assert.deepStrictEqual(logs.entries[1].severity, 'debug'); + assert.deepStrictEqual(logs.entries[2].severity, 'error'); }); it('should be able to add an info entry', () => { const logger = new Logger('test'); logger.info('test'); const logs = logger.getLogs(); - assertEquals(logs.entries.length, 1); - assertEquals(logs.entries[0].args[0], 'test'); - assertEquals(logs.entries[0].method, 'test'); - assertEquals(logs.entries[0].severity, 'info'); + assert.deepStrictEqual(logs.entries.length, 1); + assert.deepStrictEqual(logs.entries[0].args[0], 'test'); + assert.deepStrictEqual(logs.entries[0].method, 'test'); + assert.deepStrictEqual(logs.entries[0].severity, 'info'); }); it('should be able to add an debug entry', () => { const logger = new Logger('test'); logger.debug('test'); const logs = logger.getLogs(); - assertEquals(logs.entries.length, 1); - assertEquals(logs.entries[0].args[0], 'test'); - assertEquals(logs.entries[0].method, 'test'); - assertEquals(logs.entries[0].severity, 'debug'); + assert.deepStrictEqual(logs.entries.length, 1); + assert.deepStrictEqual(logs.entries[0].args[0], 'test'); + assert.deepStrictEqual(logs.entries[0].method, 'test'); + assert.deepStrictEqual(logs.entries[0].severity, 'debug'); }); it('should be able to add an error entry', () => { const logger = new Logger('test'); logger.error('test'); const logs = logger.getLogs(); - assertEquals(logs.entries.length, 1); - assertEquals(logs.entries[0].args[0], 'test'); - assertEquals(logs.entries[0].method, 'test'); - assertEquals(logs.entries[0].severity, 'error'); + assert.deepStrictEqual(logs.entries.length, 1); + assert.deepStrictEqual(logs.entries[0].args[0], 'test'); + assert.deepStrictEqual(logs.entries[0].method, 'test'); + assert.deepStrictEqual(logs.entries[0].severity, 'error'); }); it('should be able to add an success entry', () => { const logger = new Logger('test'); logger.success('test'); const logs = logger.getLogs(); - assertEquals(logs.entries.length, 1); - assertEquals(logs.entries[0].args[0], 'test'); - assertEquals(logs.entries[0].method, 'test'); - assertEquals(logs.entries[0].severity, 'success'); + assert.deepStrictEqual(logs.entries.length, 1); + assert.deepStrictEqual(logs.entries[0].args[0], 'test'); + assert.deepStrictEqual(logs.entries[0].method, 'test'); + assert.deepStrictEqual(logs.entries[0].severity, 'success'); }); it('should be able to add an warning entry', () => { const logger = new Logger('test'); logger.warn('test'); const logs = logger.getLogs(); - assertEquals(logs.entries.length, 1); - assertEquals(logs.entries[0].args[0], 'test'); - assertEquals(logs.entries[0].method, 'test'); - assertEquals(logs.entries[0].severity, 'warning'); + assert.deepStrictEqual(logs.entries.length, 1); + assert.deepStrictEqual(logs.entries[0].args[0], 'test'); + assert.deepStrictEqual(logs.entries[0].method, 'test'); + assert.deepStrictEqual(logs.entries[0].severity, 'warning'); }); it('should be able to add an log entry', () => { const logger = new Logger('test'); logger.log('test'); const logs = logger.getLogs(); - assertEquals(logs.entries.length, 1); - assertEquals(logs.entries[0].args[0], 'test'); - assertEquals(logs.entries[0].method, 'test'); - assertEquals(logs.entries[0].severity, 'log'); + assert.deepStrictEqual(logs.entries.length, 1); + assert.deepStrictEqual(logs.entries[0].args[0], 'test'); + assert.deepStrictEqual(logs.entries[0].method, 'test'); + assert.deepStrictEqual(logs.entries[0].severity, 'log'); }); it('should be able to add an entry with multiple arguments', () => { const logger = new Logger('test'); logger.log('test', 'test', 'test'); const logs = logger.getLogs(); - assertEquals(logs.entries.length, 1); - assertEquals(logs.entries[0].args[0], 'test'); - assertEquals(logs.entries[0].args[1], 'test'); - assertEquals(logs.entries[0].args[2], 'test'); - assertEquals(logs.entries[0].method, 'test'); - assertEquals(logs.entries[0].severity, 'log'); + assert.deepStrictEqual(logs.entries.length, 1); + assert.deepStrictEqual(logs.entries[0].args[0], 'test'); + assert.deepStrictEqual(logs.entries[0].args[1], 'test'); + assert.deepStrictEqual(logs.entries[0].args[2], 'test'); + assert.deepStrictEqual(logs.entries[0].method, 'test'); + assert.deepStrictEqual(logs.entries[0].severity, 'log'); }); it('should be able to add an entry with multiple arguments of different types', () => { const logger = new Logger('test'); logger.log('test', 1, true, { foo: 'bar' }); const logs = logger.getLogs(); - assertEquals(logs.entries.length, 1); - assertEquals(logs.entries[0].args[0], 'test'); - assertEquals(logs.entries[0].args[1], 1); - assertEquals(logs.entries[0].args[2], true); - assertEquals(logs.entries[0].args[3], { foo: 'bar' }); - assertEquals(logs.entries[0].method, 'test'); - assertEquals(logs.entries[0].severity, 'log'); + assert.deepStrictEqual(logs.entries.length, 1); + assert.deepStrictEqual(logs.entries[0].args[0], 'test'); + assert.deepStrictEqual(logs.entries[0].args[1], 1); + assert.deepStrictEqual(logs.entries[0].args[2], true); + assert.deepStrictEqual(logs.entries[0].args[3], { foo: 'bar' }); + assert.deepStrictEqual(logs.entries[0].method, 'test'); + assert.deepStrictEqual(logs.entries[0].severity, 'log'); }); }); diff --git a/packages/apps/node-runtime/src/lib/tests/messenger.test.ts b/packages/apps/node-runtime/src/lib/tests/messenger.test.ts index 5b439e9dc709d..aaf81a354190f 100644 --- a/packages/apps/node-runtime/src/lib/tests/messenger.test.ts +++ b/packages/apps/node-runtime/src/lib/tests/messenger.test.ts @@ -1,7 +1,5 @@ -import { assertEquals, assertObjectMatch } from 'https://deno.land/std@0.203.0/assert/mod'; -import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd'; -import { spy } from 'https://deno.land/std@0.203.0/testing/mock'; -import type { JsonRpc } from 'jsonrpc-lite'; +import * as assert from 'node:assert'; +import { after, beforeEach, describe, it, mock } from 'node:test'; import { AppObjectRegistry } from '../../AppObjectRegistry'; import { createMockRequest } from '../../handlers/tests/helpers/mod'; @@ -19,81 +17,63 @@ describe('Messenger', () => { context = createMockRequest({ method: 'test', params: [] }); }); - afterAll(() => { + after(() => { AppObjectRegistry.clear(); Messenger.Transport.selectTransport('stdout'); }); it('should add logs to success responses', async () => { - const theSpy = spy(Messenger.Queue, 'enqueue'); + const theSpy = mock.method(Messenger.Queue, 'enqueue'); const { logger } = context.context; logger.info('test'); await Messenger.successResponse({ id: 'test', result: 'test' }, context); - assertEquals(theSpy.calls.length, 1); - - const [responseArgument] = theSpy.calls[0].args; - - assertObjectMatch(responseArgument as JsonRpc, { - jsonrpc: '2.0', - id: 'test', - result: { - value: 'test', - logs: { - appId: 'test', - method: 'test', - entries: [ - { - severity: 'info', - method: 'test', - args: ['test'], - caller: 'anonymous OR constructor', - }, - ], - }, - }, - }); - - theSpy.restore(); + assert.strictEqual(theSpy.mock.calls.length, 1); + + const [responseArgument] = theSpy.mock.calls[0].arguments; + const resp = responseArgument as any; + + assert.strictEqual(resp.jsonrpc, '2.0'); + assert.strictEqual(resp.id, 'test'); + assert.strictEqual(resp.result.value, 'test'); + assert.strictEqual(resp.result.logs.appId, 'test'); + assert.strictEqual(resp.result.logs.method, 'test'); + assert.strictEqual(resp.result.logs.entries.length, 1); + assert.strictEqual(resp.result.logs.entries[0].severity, 'info'); + assert.strictEqual(resp.result.logs.entries[0].method, 'test'); + assert.deepStrictEqual(resp.result.logs.entries[0].args, ['test']); + assert.strictEqual(resp.result.logs.entries[0].caller, 'anonymous OR constructor'); + + theSpy.mock.restore(); }); it('should add logs to error responses', async () => { - const theSpy = spy(Messenger.Queue, 'enqueue'); + const theSpy = mock.method(Messenger.Queue, 'enqueue'); const { logger } = context.context; logger.info('test'); await Messenger.errorResponse({ id: 'test', error: { code: -32000, message: 'test' } }, context); - assertEquals(theSpy.calls.length, 1); - - const [responseArgument] = theSpy.calls[0].args; - - assertObjectMatch(responseArgument as JsonRpc, { - jsonrpc: '2.0', - id: 'test', - error: { - code: -32000, - message: 'test', - data: { - logs: { - appId: 'test', - method: 'test', - entries: [ - { - severity: 'info', - method: 'test', - args: ['test'], - caller: 'anonymous OR constructor', - }, - ], - }, - }, - }, - }); - - theSpy.restore(); + assert.strictEqual(theSpy.mock.calls.length, 1); + + const [responseArgument] = theSpy.mock.calls[0].arguments; + const resp = responseArgument as any; + + assert.strictEqual(resp.jsonrpc, '2.0'); + assert.strictEqual(resp.id, 'test'); + assert.strictEqual(resp.error.code, -32000); + assert.strictEqual(resp.error.message, 'test'); + assert.strictEqual(resp.error.data.logs.appId, 'test'); + assert.strictEqual(resp.error.data.logs.method, 'test'); + assert.strictEqual(resp.error.data.logs.entries.length, 1); + assert.strictEqual(resp.error.data.logs.entries[0].severity, 'info'); + assert.strictEqual(resp.error.data.logs.entries[0].method, 'test'); + assert.deepStrictEqual(resp.error.data.logs.entries[0].args, ['test']); + assert.strictEqual(resp.error.data.logs.entries[0].caller, 'anonymous OR constructor'); + + theSpy.mock.restore(); }); }); diff --git a/packages/apps/node-runtime/src/lib/tests/secureFields.test.ts b/packages/apps/node-runtime/src/lib/tests/secureFields.test.ts index b286c30057d56..3b0643c3d7868 100644 --- a/packages/apps/node-runtime/src/lib/tests/secureFields.test.ts +++ b/packages/apps/node-runtime/src/lib/tests/secureFields.test.ts @@ -1,5 +1,5 @@ -import { assertEquals, assertThrows } from 'https://deno.land/std@0.203.0/assert/mod'; -import { beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd'; +import * as assert from 'node:assert'; +import { beforeEach, describe, it } from 'node:test'; import { AppObjectRegistry } from '../../AppObjectRegistry'; import { applySecureFields } from '../secureFields'; @@ -12,11 +12,9 @@ describe('applySecureFields', () => { }); it('throws when app is unavailable', () => { - assertThrows( - () => applySecureFields({ foo: 'bar', [SECURE_FIELDS_KEY]: [] } as any), - Error, - "App unavailable, can't parse object with secure fields", - ); + assert.throws(() => applySecureFields({ foo: 'bar', [SECURE_FIELDS_KEY]: [] } as any), { + message: "App unavailable, can't parse object with secure fields", + }); }); it('applies only secure fields with matching permissions', () => { @@ -34,7 +32,7 @@ describe('applySecureFields', () => { ], } as any); - assertEquals(parsed, { + assert.deepStrictEqual(parsed, { foo: 'bar', abacAttributes: { department: 'support' }, }); @@ -52,7 +50,7 @@ describe('applySecureFields', () => { [SECURE_FIELDS_KEY]: [{ permission: 'abac.read', name: 'abacAttributes', value: { tenant: 'alpha' } }], } as any); - assertEquals(parsed, { + assert.deepStrictEqual(parsed, { abacAttributes: { tenant: 'alpha' }, }); }); diff --git a/packages/apps/src/server/runtime/node/AppsEngineNodeRuntime.ts b/packages/apps/src/server/runtime/node/AppsEngineNodeRuntime.ts new file mode 100644 index 0000000000000..20fcc1ac9e550 --- /dev/null +++ b/packages/apps/src/server/runtime/node/AppsEngineNodeRuntime.ts @@ -0,0 +1,698 @@ +import * as child_process from 'node:child_process'; +import * as path from 'node:path'; +import { type Readable, EventEmitter } from 'node:stream'; +import { inspect as utilInspect } from 'node:util'; + +import { AppStatus, AppStatusUtils } from '@rocket.chat/apps-engine/definition/AppStatus'; +import type { AppMethod } from '@rocket.chat/apps-engine/definition/metadata'; +import debugFactory from 'debug'; +import * as jsonrpc from 'jsonrpc-lite'; + +import { LivenessManager } from './LivenessManager'; +import { ProcessMessenger } from './ProcessMessenger'; +import { bundleLegacyApp } from './bundler'; +import { newDecoder } from './codec'; +import type { AppManager } from '../../AppManager'; +import type { AppBridges } from '../../bridges'; +import type { IParseAppPackageResult } from '../../compiler'; +import { AppConsole, type ILoggerStorageEntry } from '../../logging'; +import type { AppAccessorManager, AppApiManager } from '../../managers'; +import type { AppLogStorage, IAppStorageItem } from '../../storage'; +import type { IRuntimeController } from '../IRuntimeController'; + +const baseDebug = debugFactory('appsEngine:runtime:node'); + +const inspect = (value: unknown) => utilInspect(value, { depth: 10, compact: true, breakLength: Infinity }); + +export const ALLOWED_ACCESSOR_METHODS = [ + 'getConfigurationExtend', + 'getEnvironmentRead', + 'getEnvironmentWrite', + 'getConfigurationModify', + 'getReader', + 'getPersistence', + 'getHttp', + 'getModifier', +] as Array< + keyof Pick< + AppAccessorManager, + | 'getConfigurationExtend' + | 'getEnvironmentRead' + | 'getEnvironmentWrite' + | 'getConfigurationModify' + | 'getReader' + | 'getPersistence' + | 'getHttp' + | 'getModifier' + > +>; + +const COMMAND_PONG = '_zPONG'; + +export const JSONRPC_METHOD_NOT_FOUND = -32601; + +function getRuntimeTimeout() { + const defaultTimeout = 30000; + const envValue = isFinite(process.env.APPS_ENGINE_RUNTIME_TIMEOUT as any) + ? Number(process.env.APPS_ENGINE_RUNTIME_TIMEOUT) + : defaultTimeout; + + if (envValue < 0) { + console.log('Environment variable APPS_ENGINE_RUNTIME_TIMEOUT has a negative value, ignoring...'); + return defaultTimeout; + } + + return envValue; +} + +function isValidOrigin(accessor: string): accessor is (typeof ALLOWED_ACCESSOR_METHODS)[number] { + return ALLOWED_ACCESSOR_METHODS.includes(accessor as any); +} + +/** + * Resolves the absolute path to @rocket.chat/apps-engine's src/ directory. + * Uses require.resolve so it works regardless of the runtime environment + * (monorepo dev, Meteor bundle, standalone node_modules). + */ +function getAppsEngineDir(): string { + return path.dirname(require.resolve('@rocket.chat/apps-engine/package.json')); +} + +type AbortFunction = (reason?: any) => void; + +export class NodeRuntimeSubprocessController extends EventEmitter implements IRuntimeController { + private node: child_process.ChildProcess | undefined; + + private state: 'uninitialized' | 'ready' | 'invalid' | 'restarting' | 'unknown' | 'stopped'; + + /** + * Incremental id that keeps track of how many times we've spawned a process for this app + */ + private spawnId = 0; + + private readonly debug: debug.Debugger; + + private readonly options = { + timeout: getRuntimeTimeout(), + }; + + private readonly accessors: AppAccessorManager; + + private readonly api: AppApiManager; + + private readonly logStorage: AppLogStorage; + + private readonly bridges: AppBridges; + + private readonly messenger: ProcessMessenger; + + private readonly livenessManager: LivenessManager; + + private readonly tempFilePath: string; + + private readonly nodeBin = 'node'; + + private readonly scriptRuntimePath: string; + + private readonly appsEnginePath: string; + + constructor( + manager: AppManager, + // We need to keep the appSource around in case the subprocess needs to be restarted + private readonly appPackage: IParseAppPackageResult, + private readonly storageItem: IAppStorageItem, + ) { + super(); + + this.tempFilePath = manager.getTempFilePath(); + this.appsEnginePath = getAppsEngineDir(); + + + + this.scriptRuntimePath = require.resolve('../../../../node-runtime/dist/main.js'); + + this.debug = baseDebug.extend(appPackage.info.id); + this.messenger = new ProcessMessenger(); + this.livenessManager = new LivenessManager({ + controller: this, + messenger: this.messenger, + debug: this.debug, + }); + + this.state = 'uninitialized'; + + this.accessors = manager.getAccessorManager(); + this.api = manager.getApiManager(); + this.logStorage = manager.getLogStorage(); + this.bridges = manager.getBridges(); + } + + public spawnProcess(): void { + try { + const allowedDirs = [this.tempFilePath, path.resolve(path.dirname(this.scriptRuntimePath), '..', '..'), this.appsEnginePath]; + + const options = [ + '--permission', + ...allowedDirs.map((path) => `--allow-fs-read=${path}`), + this.scriptRuntimePath, + '--subprocess', + this.appPackage.info.id, + '--spawnId', + String(this.spawnId++), + ]; + + const environment = { + env: { + PATH: process.env.PATH, + }, + }; + + // SECURITY: We control the command, the arguments and the script that will be executed. + this.node = child_process.spawn(this.nodeBin, options, environment); + this.messenger.setReceiver(this.node); + this.livenessManager.attach(this.node); + + this.debug('Started subprocess %d with options %s and env %s', this.node.pid, inspect(options), inspect(environment)); + + this.setupListeners(); + } catch (e) { + this.state = 'invalid'; + console.error(`Failed to start Deno subprocess for app ${this.getAppId()}`, e); + } + } + + /** + * Attempts to kill the process currently controlled by this.deno + * + * @returns boolean - if a process has been killed or not + */ + public async killProcess(): Promise { + if (!this.node) { + this.debug('No child process reference'); + return false; + } + + let { killed } = this.node; + + // This field is not populated if the process is killed by the OS + if (killed) { + this.debug('App process was already killed'); + return killed; + } + + // What else should we do? + if (this.node.kill('SIGKILL')) { + // Let's wait until we get confirmation the process exited + await new Promise((r) => this.node.on('exit', r)); + killed = true; + } else { + this.debug('Tried killing the process but failed. Was it already dead?'); + killed = false; + } + + delete this.node; + this.messenger.clearReceiver(); + return killed; + } + + // Debug purposes, could be deleted later + emit(eventName: string | symbol, ...args: any[]): boolean { + const hadListeners = super.emit(eventName, ...args); + + if (!hadListeners) { + this.debug('Emitted but no one listened: ', eventName, args); + } + + return hadListeners; + } + + public getProcessState() { + return this.state; + } + + public async getStatus(): Promise { + // If the process has been terminated, we can't get the status + if (this.node?.exitCode !== null) { + return AppStatus.UNKNOWN; + } + + return this.sendRequest({ method: 'app:getStatus', params: [] }) as Promise; + } + + public async setupApp() { + this.debug('Setting up app subprocess'); + this.spawnProcess(); + + // If there is more than one file in the package, then it is a legacy app that has not been bundled + if (Object.keys(this.appPackage.files).length > 1) { + await bundleLegacyApp(this.appPackage); + } + + await this.waitUntilReady(); + + await this.sendRequest({ method: 'app:construct', params: [this.appPackage] }); + + this.emit('constructed'); + } + + public async stopApp() { + this.debug('Stopping app subprocess'); + + this.state = 'stopped'; + + await this.killProcess(); + } + + public async restartApp() { + this.debug('Restarting app subprocess'); + const logger = new AppConsole('runtime:restart'); + + logger.info({ msg: 'Starting restart procedure for app subprocess...', runtimeData: this.livenessManager.getRuntimeData() }); + + this.state = 'restarting'; + + try { + const pid = this.node?.pid; + + const hasKilled = await this.killProcess(); + + if (hasKilled) { + logger.debug({ msg: 'Process successfully terminated', pid }); + } else { + logger.warn({ msg: 'Could not terminate process. Maybe it was already dead?', pid }); + } + + await this.setupApp(); + logger.info({ msg: 'New subprocess successfully spawned', pid: this.node.pid }); + + // setupApp() changes the state to 'ready' - we'll need to workaround that for now + this.state = 'restarting'; + + await this.sendRequest({ method: 'app:initialize' }); + await this.sendRequest({ method: 'app:setStatus', params: [this.storageItem.status] }); + + if (AppStatusUtils.isEnabled(this.storageItem.status)) { + await this.sendRequest({ method: 'app:onEnable' }); + } + + this.state = 'ready'; + + logger.info('Successfully restarted app subprocess'); + } catch (e) { + logger.error({ msg: "Failed to restart app's subprocess", err: e }); + throw e; + } finally { + await this.logStorage.storeEntries(AppConsole.toStorageEntry(this.getAppId(), logger)); + } + } + + public getAppId(): string { + return this.appPackage.info.id; + } + + public async sendRequest(message: Pick, options = this.options): Promise { + const id = String(Math.random().toString(36)).substring(2); + + const start = Date.now(); + + const request = jsonrpc.request(id, message.method, message.params); + + const { promise, abort } = this.waitForResponse(request, options); + + try { + this.debug('Sending message to subprocess %s', inspect(message)); + this.messenger.send(request); + } catch (e) { + abort(e); + } + + return promise.finally(() => { + this.debug('Request %s for method %s took %dms', id, message.method, Date.now() - start); + }); + } + + private waitUntilReady(): Promise { + if (this.state === 'ready') { + return; + } + + return new Promise((resolve, reject) => { + let timeoutId: NodeJS.Timeout; + + const handler = () => { + clearTimeout(timeoutId); + resolve(); + }; + + timeoutId = setTimeout(() => { + this.off('ready', handler); + reject(new Error(`[${this.getAppId()}] Timeout: app process not ready`)); + }, this.options.timeout); + + this.once('ready', handler); + }); + } + + private waitForResponse(req: jsonrpc.RequestObject, options = this.options): { abort: AbortFunction; promise: Promise } { + const controller = new AbortController(); + const { abort, signal } = controller; + + return { + abort: abort.bind(controller), + promise: new Promise((resolve, reject) => { + const eventName = `result:${req.id}`; + + const responseCallback = (result: unknown, error: jsonrpc.IParsedObjectError['payload']['error'] | Error) => { + this.off(eventName, responseCallback); + clearTimeout(timeoutId); + + if (error) { + reject(error); + } + + resolve(result); + }; + + const timeoutId = setTimeout( + () => + responseCallback( + undefined, + new Error(`[${this.getAppId()}] Request "${req.id}" for method "${req.method}" timed out after ${options.timeout}ms`), + ), + options.timeout, + ); + + signal.onabort = () => + responseCallback(undefined, signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason))); + + this.once(eventName, responseCallback); + }), + }; + } + + private onReady(): void { + this.state = 'ready'; + } + + /** + * Listeners need to be setup every time the reference + * in `this.deno` changes, i.e. every time the subprocess + * is restarted + */ + private setupListeners(): void { + if (!this.node) { + return; + } + + this.node.stderr.on('data', this.parseError.bind(this)); + this.node.on('error', (err) => { + this.state = 'invalid'; + console.error(`Failed to startup Deno subprocess for app ${this.getAppId()}`, err); + }); + + this.node.once('exit', (code) => this.emit('processExit', code)); + + this.once('ready', this.onReady.bind(this)); + + void this.parseStdout(this.node.stdout); + } + + // Probable should extract this to a separate file + private async handleAccessorMessage({ payload: { method, id, params } }: jsonrpc.IParsedObjectRequest): Promise { + const accessorMethods = method.substring(9).split(':'); // First 9 characters are always 'accessor:' + + this.debug('Handling accessor message %s with params %s', inspect(accessorMethods), inspect(params)); + + const managerOrigin = accessorMethods.shift(); + const tailMethodName = accessorMethods.pop(); + + // If we're restarting the app, we can't register resources again, so we + // hijack requests for the `ConfigurationExtend` accessor and don't let them through + // This needs to be refactored ASAP + if (this.state === 'restarting' && managerOrigin === 'getConfigurationExtend') { + return jsonrpc.success(id, null); + } + + if (managerOrigin === 'api' && tailMethodName === 'listApis') { + const result = this.api.listApis(this.appPackage.info.id); + + return jsonrpc.success(id, result); + } + + /** + * At this point, the accessorMethods array will contain the path to the accessor from the origin (AppAccessorManager) + * The accessor is the one that contains the actual method the app wants to call + * + * Most of the times, it will take one step from origin to accessor + * For example, for the call AppAccessorManager.getEnvironmentRead().getServerSettings().getValueById() we'll have + * the following: + * + * ``` + * const managerOrigin = 'getEnvironmentRead' + * const tailMethod = 'getValueById' + * const accessorMethods = ['getServerSettings'] + * ``` + * + * But sometimes there can be more steps, like in the following example: + * AppAccessorManager.getReader().getEnvironmentReader().getEnvironmentVariables().getValueByName() + * In this case, we'll have: + * + * ``` + * const managerOrigin = 'getReader' + * const tailMethod = 'getValueByName' + * const accessorMethods = ['getEnvironmentReader', 'getEnvironmentVariables'] + * ``` + **/ + // Prevent app from trying to get properties from the manager that + // are not intended for public access + if (!isValidOrigin(managerOrigin)) { + throw new Error(`Invalid accessor namespace "${managerOrigin}"`); + } + + // Need to fix typing of return value + const getAccessorForOrigin = ( + accessorMethods: string[], + managerOrigin: (typeof ALLOWED_ACCESSOR_METHODS)[number], + accessorManager: AppAccessorManager, + ) => { + const origin = accessorManager[managerOrigin](this.appPackage.info.id); + + if (managerOrigin === 'getHttp' || managerOrigin === 'getPersistence') { + return origin; + } + + if (managerOrigin === 'getConfigurationExtend' || managerOrigin === 'getConfigurationModify') { + return origin[accessorMethods[0] as keyof typeof origin]; + } + + let accessor = origin; + + // Call all intermediary objects to "resolve" the accessor + accessorMethods.forEach((methodName) => { + const method = accessor[methodName as keyof typeof accessor] as unknown; + + if (typeof method !== 'function') { + throw new Error(`Invalid accessor method "${methodName}"`); + } + + accessor = method.apply(accessor); + }); + + return accessor; + }; + + const accessor = getAccessorForOrigin(accessorMethods, managerOrigin, this.accessors); + + const tailMethod = accessor[tailMethodName as keyof typeof accessor] as unknown; + + if (typeof tailMethod !== 'function') { + throw new Error(`Invalid accessor method "${tailMethodName}"`); + } + + const result = await tailMethod.apply(accessor, params); + + return jsonrpc.success(id, typeof result === 'undefined' ? null : result); + } + + private async handleBridgeMessage({ + payload: { method, id, params }, + }: jsonrpc.IParsedObjectRequest): Promise { + const [bridgeName, bridgeMethod] = method.substring(8).split(':'); + + this.debug('Handling bridge message %s().%s() with params %s', bridgeName, bridgeMethod, inspect(params)); + + const bridge = this.bridges[bridgeName as keyof typeof this.bridges]; + + if (!bridgeMethod.startsWith('do') || typeof bridge !== 'function' || !Array.isArray(params)) { + throw new Error('Invalid bridge request'); + } + + const bridgeInstance = bridge.call(this.bridges); + + const methodRef = bridgeInstance[bridgeMethod as keyof typeof bridge] as unknown; + + if (typeof methodRef !== 'function') { + throw new Error('Invalid bridge request'); + } + + let result; + try { + result = await methodRef.apply( + bridgeInstance, + // Should the protocol expect the placeholder APP_ID value or should the Deno process send the actual appId? + // If we do not expect the APP_ID, the Deno process will be able to impersonate other apps, potentially + params.map((value: unknown) => (value === 'APP_ID' ? this.appPackage.info.id : value)), + ); + } catch (error) { + this.debug('Error executing bridge method %s().%s() %s', bridgeName, bridgeMethod, inspect(error.message)); + const jsonRpcError = new jsonrpc.JsonRpcError(error.message, -32000, error); + return jsonrpc.error(id, jsonRpcError); + } + + return jsonrpc.success(id, typeof result === 'undefined' ? null : result); + } + + private async handleIncomingMessage(message: jsonrpc.IParsedObjectNotification | jsonrpc.IParsedObjectRequest): Promise { + const { method } = message.payload; + + if (method.startsWith('accessor:')) { + let result: jsonrpc.SuccessObject | jsonrpc.ErrorObject; + + try { + result = await this.handleAccessorMessage(message as jsonrpc.IParsedObjectRequest); + } catch (e) { + result = jsonrpc.error((message.payload as jsonrpc.RequestObject).id, new jsonrpc.JsonRpcError(e.message, 1000)); + } + + this.messenger.send(result); + + return; + } + + if (method.startsWith('bridges:')) { + let result: jsonrpc.SuccessObject | jsonrpc.ErrorObject; + + try { + result = await this.handleBridgeMessage(message as jsonrpc.IParsedObjectRequest); + } catch (e) { + result = jsonrpc.error((message.payload as jsonrpc.RequestObject).id, new jsonrpc.JsonRpcError(e.message, 1000)); + } + + this.messenger.send(result); + + return; + } + + switch (method) { + case 'ready': + this.emit('ready'); + break; + case 'log': + console.log('SUBPROCESS LOG', message); + break; + case 'unhandledRejection': + case 'uncaughtException': + await this.logUnhandledError(`runtime:${method}`, message); + break; + default: + console.warn('Unrecognized method from sub process'); + break; + } + } + + private async logUnhandledError( + method: `${AppMethod.RUNTIME_UNCAUGHT_EXCEPTION | AppMethod.RUNTIME_UNHANDLED_REJECTION}`, + message: jsonrpc.IParsedObjectRequest | jsonrpc.IParsedObjectNotification, + ) { + this.debug('Unhandled error of type "%s" caught in subprocess', method); + + const logger = new AppConsole(method); + logger.error(message.payload); + + await this.logStorage.storeEntries(AppConsole.toStorageEntry(this.getAppId(), logger)); + } + + private async handleResultMessage(message: jsonrpc.IParsedObjectError | jsonrpc.IParsedObjectSuccess): Promise { + const { id } = message.payload; + + let result: unknown; + let error: jsonrpc.IParsedObjectError['payload']['error'] | undefined; + let logs: ILoggerStorageEntry; + + if (message.type === 'success') { + const params = message.payload.result as { value: unknown; logs?: ILoggerStorageEntry }; + result = params.value; + logs = params.logs; + } else { + error = message.payload.error; + logs = message.payload.error.data?.logs as ILoggerStorageEntry; + } + + // Should we try to make sure all result messages have logs? + if (logs) { + await this.logStorage.storeEntries(logs); + } + + this.emit(`result:${id}`, result, error); + } + + private async parseStdout(stream: Readable): Promise { + try { + for await (const message of newDecoder().decodeStream(stream)) { + this.debug('Received message from subprocess %s', inspect(message)); + try { + // Process PONG resonse first as it is not JSON RPC + if (message === COMMAND_PONG) { + this.emit('pong'); + continue; + } + + const JSONRPCMessage = jsonrpc.parseObject(message); + + if (Array.isArray(JSONRPCMessage)) { + throw new Error('Invalid message format'); + } + + this.emit('heartbeat'); + + if (JSONRPCMessage.type === 'request' || JSONRPCMessage.type === 'notification') { + this.handleIncomingMessage(JSONRPCMessage).catch((reason) => + console.error(`[${this.getAppId()}] Error executing handler`, reason, message), + ); + continue; + } + + if (JSONRPCMessage.type === 'success' || JSONRPCMessage.type === 'error') { + this.handleResultMessage(JSONRPCMessage).catch((reason) => + console.error(`[${this.getAppId()}] Error executing handler`, reason, message), + ); + continue; + } + + console.error('Unrecognized message type', JSONRPCMessage); + } catch (e) { + // SyntaxError is thrown when the message is not a valid JSON + if (e instanceof SyntaxError) { + console.error(`[${this.getAppId()}] Failed to parse message`); + continue; + } + + console.error(`[${this.getAppId()}] Error executing handler`, e, message); + } + } + } catch (e) { + console.error(`[${this.getAppId()}]`, e); + this.emit('error', new Error('DECODE_ERROR')); + } + } + + private async parseError(chunk: Buffer): Promise { + try { + const data = JSON.parse(chunk.toString()); + + this.debug('Metrics received from subprocess (via stderr): %s', inspect(data)); + } catch { + console.error('Subprocess stderr', chunk.toString()); + } + } +} diff --git a/packages/apps/src/server/runtime/node/LivenessManager.ts b/packages/apps/src/server/runtime/node/LivenessManager.ts new file mode 100644 index 0000000000000..efec08025b108 --- /dev/null +++ b/packages/apps/src/server/runtime/node/LivenessManager.ts @@ -0,0 +1,254 @@ +import type { ChildProcess } from 'node:child_process'; +import { EventEmitter } from 'node:stream'; + +import type { NodeRuntimeSubprocessController } from './AppsEngineNodeRuntime'; +import type { ProcessMessenger } from './ProcessMessenger'; + +export const COMMAND_PING = '_zPING'; + +const defaultOptions: LivenessManager['options'] = { + pingTimeoutInMS: 1000, + pingIntervalInMS: 10000, + consecutiveTimeoutLimit: 4, + maxRestarts: Infinity, + restartAttemptDelayInMS: 1000, +}; + +/** + * Responsible for pinging the Node subprocess and for restarting it + * if something doesn't look right + */ +export class LivenessManager { + private readonly controller: NodeRuntimeSubprocessController; + + private readonly messenger: ProcessMessenger; + + private readonly debug: debug.Debugger; + + private readonly options: { + // How long should we wait for a response to the ping request + pingTimeoutInMS: number; + + // How long is the delay between ping messages + pingIntervalInMS: number; + + // Limit of times the process can timeout the ping response before we consider it as unresponsive + consecutiveTimeoutLimit: number; + + // Limit of times we can try to restart a process + maxRestarts: number; + + // Time to delay the next restart attempt after a failed one + restartAttemptDelayInMS: number; + }; + + private subprocess: ChildProcess; + + private watchdogTimeout: NodeJS.Timeout | null = null; + + private lastHeartbeatTimestamp = NaN; + + // A promise tracking the current ping process - used mostly for testing + private pendingPing: Promise | null; + + // This is the perfect use-case for an AbortController, but it's experimental in Node 14.x + private pingAbortController: EventEmitter; + + private pingTimeoutConsecutiveCount = 0; + + private restartCount = 0; + + private restartLog: Record[] = []; + + constructor( + deps: { + controller: NodeRuntimeSubprocessController; + messenger: ProcessMessenger; + debug: debug.Debugger; + }, + options: Partial = {}, + ) { + this.controller = deps.controller; + this.messenger = deps.messenger; + this.debug = deps.debug; + this.pingAbortController = new EventEmitter(); + + this.options = Object.assign({}, defaultOptions, options); + + this.controller.on('heartbeat', () => { + this.lastHeartbeatTimestamp = Date.now(); + this.pingTimeoutConsecutiveCount = 0; + }); + + this.controller.on('error', async (reason) => { + if (reason instanceof Error && reason.message.startsWith('DECODE_ERROR')) { + await this.restartProcess('Decode error', 'controller'); + } + }); + } + + public getRuntimeData() { + const { lastHeartbeatTimestamp, restartCount, pingTimeoutConsecutiveCount, restartLog } = this; + + return { + lastHeartbeatTimestamp, + restartCount, + pingTimeoutConsecutiveCount, + restartLog, + }; + } + + public attach(node: ChildProcess) { + this.subprocess = node; + + this.pingTimeoutConsecutiveCount = 0; + + this.subprocess.once('exit', this.handleExit.bind(this)); + this.subprocess.once('error', this.handleError.bind(this)); + + this.controller.once('constructed', this.start.bind(this)); + } + + public start() { + this.lastHeartbeatTimestamp = Date.now(); + + this.watchdogTimeout = setInterval(() => { + if (Date.now() - this.lastHeartbeatTimestamp < this.options.pingIntervalInMS) { + return; + } + + try { + this.ping(); + } catch { + // If the ping call fails synchronously, it's because we couldn't send the ping message + // then likely the process isn't running, so we stop everything + this.debug('[LivenessManager] Failed to send ping to subprocess, stopping watchdog...'); + this.stop(); + } + }, this.options.pingIntervalInMS); + + this.watchdogTimeout.unref(); + } + + public stop() { + this.pingAbortController.emit('abort'); + clearInterval(this.watchdogTimeout); + this.watchdogTimeout = null; + this.pendingPing = null; + } + + public getPendingPing() { + return this.pendingPing; + } + + /** + * Start up the process of ping/pong for liveness check + * + * The message exchange does not use JSON RPC as it adds a lot of overhead + * with the creation and encoding of a full object for transfer. By using a + * string the process is less intensive. + */ + private ping() { + const start = Date.now(); + + this.pendingPing = new Promise((resolve, reject) => { + const onceCallback = () => { + const now = Date.now(); + this.debug('Ping successful in %d ms', now - start); + clearTimeout(timeoutId); + this.pingTimeoutConsecutiveCount = 0; + this.lastHeartbeatTimestamp = now; + resolve(true); + }; + + const timeoutCallback = () => { + this.debug('Ping failed in %d ms (consecutive failure #%d)', Date.now() - start, this.pingTimeoutConsecutiveCount); + this.controller.off('pong', onceCallback); + this.pingTimeoutConsecutiveCount++; + reject('timeout'); + }; + + this.pingAbortController.once('abort', () => { + this.debug('Ping aborted'); + reject('abort'); + }); + + const timeoutId = setTimeout(timeoutCallback, this.options.pingTimeoutInMS); + + this.controller.once('pong', onceCallback); + }) + .catch((reason) => { + if (reason === 'abort') { + return false; + } + + if (reason === 'timeout' && this.pingTimeoutConsecutiveCount >= this.options.consecutiveTimeoutLimit) { + this.debug( + 'Subprocess failed to respond to pings %d consecutive times. Attempting restart...', + this.options.consecutiveTimeoutLimit, + ); + void this.restartProcess('Too many pings timed out'); + return false; + } + + return true; + }) + .finally(() => { + this.pingAbortController.removeAllListeners('abort'); + }); + + this.messenger.send(COMMAND_PING); + } + + private handleError(err: Error) { + this.debug('App has failed to start.`', err); + void this.restartProcess(err.message); + } + + private handleExit(exitCode: number, signal: string) { + const processState = this.controller.getProcessState(); + // If the we're restarting the process, or want to stop the process, or it exited cleanly, nothing else for us to do + if (processState === 'restarting' || processState === 'stopped' || (exitCode === 0 && !signal)) { + return; + } + + let reason: string; + + // Otherwise we attempt to restart the process + if (signal) { + this.debug('App has been killed (%s). Attempting restart #%d...', signal, this.restartCount + 1); + reason = `App has been killed with signal ${signal}`; + } else { + this.debug('App has exited with code %d. Attempting restart #%d...', exitCode, this.restartCount + 1); + reason = `App has exited with code ${exitCode}`; + } + + void this.restartProcess(reason); + } + + private async restartProcess(reason: string, source = 'liveness-manager') { + this.stop(); + + if (this.restartCount >= this.options.maxRestarts) { + this.debug('Limit of restarts reached (%d). Aborting restart...', this.options.maxRestarts); + void this.controller.stopApp(); + return; + } + + this.restartLog.push({ + reason, + source, + restartedAt: new Date(), + pid: this.subprocess.pid, + }); + + try { + await this.controller.restartApp(); + } catch { + this.debug('Restart attempt failed. Retrying in %dms', this.options.restartAttemptDelayInMS); + setTimeout(() => void this.restartProcess('Failed restart attempt'), this.options.restartAttemptDelayInMS); + } + + this.restartCount++; + } +} diff --git a/packages/apps/src/server/runtime/node/ProcessMessenger.ts b/packages/apps/src/server/runtime/node/ProcessMessenger.ts new file mode 100644 index 0000000000000..cd97ac04a0281 --- /dev/null +++ b/packages/apps/src/server/runtime/node/ProcessMessenger.ts @@ -0,0 +1,57 @@ +import type { ChildProcess } from 'node:child_process'; + +import type { JsonRpc } from 'jsonrpc-lite'; + +import type { COMMAND_PING } from './LivenessManager'; +import type { Encoder } from './codec'; +import { newEncoder } from './codec'; + +type Message = JsonRpc | typeof COMMAND_PING; + +export class ProcessMessenger { + private node: ChildProcess | undefined; + + private encoder: Encoder | undefined; + + private _sendStrategy: (message: Message) => void; + + constructor() { + this._sendStrategy = this.strategyError; + } + + public send(message: Message) { + this._sendStrategy(message); + } + + public setReceiver(node: ChildProcess) { + this.node = node; + + this.switchStrategy(); + } + + public clearReceiver() { + delete this.node; + delete this.encoder; + + this.switchStrategy(); + } + + private switchStrategy() { + if (this.node?.stdin?.writable) { + this._sendStrategy = this.strategySend.bind(this); + + // Get a clean encoder + this.encoder = newEncoder(); + } else { + this._sendStrategy = this.strategyError.bind(this); + } + } + + private strategyError(_message: Message) { + throw new Error('No process configured to receive a message'); + } + + private strategySend(message: Message) { + this.node.stdin.write(this.encoder.encode(message)); + } +} diff --git a/packages/apps/src/server/runtime/node/bundler.ts b/packages/apps/src/server/runtime/node/bundler.ts new file mode 100644 index 0000000000000..e80060ab5bb52 --- /dev/null +++ b/packages/apps/src/server/runtime/node/bundler.ts @@ -0,0 +1,90 @@ +import * as path from 'node:path'; + +import { build, type PluginBuild, type OnLoadArgs, type OnResolveArgs } from 'esbuild'; + +import type { IParseAppPackageResult } from '../../compiler'; + +/** + * Some legacy apps that might be installed in workspaces have not been bundled after compilation, + * leading to multiple files being sent to the subprocess and requiring further logic to require one another. + * This makes running the app in the Deno Runtime much more difficult, so instead we bundle the files at runtime. + */ +export async function bundleLegacyApp(appPackage: IParseAppPackageResult) { + const buildResult = await build({ + write: false, + bundle: true, + minify: true, + platform: 'node', + target: ['node10'], + define: { + 'global.Promise': 'Promise', + }, + external: ['@rocket.chat/apps-engine/*'], + stdin: { + contents: appPackage.files[appPackage.info.classFile], + sourcefile: appPackage.info.classFile, + loader: 'js', + }, + plugins: [ + { + name: 'legacy-app', + setup(build: PluginBuild) { + build.onResolve({ filter: /.*/ }, (args: OnResolveArgs) => { + if (args.namespace === 'file') { + return; + } + + const modulePath = path.join(path.dirname(args.importer), args.path).concat('.js'); + + const hasFile = !!appPackage.files[modulePath]; + + if (hasFile) { + return { + namespace: 'app-source', + path: modulePath, + }; + } + + // require('../') or require('./') are both valid, but aren't included in the files record in the same way + // we need to treat those differently + if (/\.\.?\//.test(args.path)) { + const indexModulePath = modulePath.replace(/\.js$/, `${path.sep}index.js`); + + if (appPackage.files[indexModulePath]) { + return { + namespace: 'app-source', + path: indexModulePath, + }; + } + } + + return { + path: args.path, + external: true, + }; + }); + + build.onLoad({ filter: /.*/, namespace: 'app-source' }, (args: OnLoadArgs) => { + if (!appPackage.files[args.path]) { + return { + errors: [ + { + text: `File ${args.path} could not be found`, + }, + ], + }; + } + + return { + contents: appPackage.files[args.path], + }; + }); + }, + }, + ], + }); + + const [{ text: bundle }] = buildResult.outputFiles; + + appPackage.files = { [appPackage.info.classFile]: bundle }; +} diff --git a/packages/apps/src/server/runtime/node/codec.ts b/packages/apps/src/server/runtime/node/codec.ts new file mode 100644 index 0000000000000..6bf711bf96b0a --- /dev/null +++ b/packages/apps/src/server/runtime/node/codec.ts @@ -0,0 +1,78 @@ +import { Decoder as _Decoder, Encoder as _Encoder, encode, ExtensionCodec } from '@msgpack/msgpack'; + +import { hasSecureFields } from '../../../lib/SecureFields'; + +const extensionCodec = new ExtensionCodec(); + +const FUNCTION_DISABLER_EXT = 0; +const BUFFER_HANDLER_EXT = 1; +const SECURE_FIELDS_HANDLER_EXT = 2; + +extensionCodec.register({ + type: FUNCTION_DISABLER_EXT, + encode: (object: unknown) => { + // We don't care about functions, but also don't want to throw an error + if (typeof object === 'function') { + return new Uint8Array([0]); + } + }, + + decode: (_data: Uint8Array) => undefined, +}); + +// We need to handle Buffers because Deno needs its own decoding +extensionCodec.register({ + type: BUFFER_HANDLER_EXT, + encode: (object: unknown) => { + if (object instanceof Buffer) { + return new Uint8Array(object.buffer, object.byteOffset, object.byteLength); + } + }, + + // msgpack will reuse the Uint8Array instance, so WE NEED to copy it instead of simply creating a view + decode: (data: Uint8Array) => Buffer.from(data), +}); + +extensionCodec.register({ + type: SECURE_FIELDS_HANDLER_EXT, + /** + * This extension doesn't really change the encoding process, but by + * not returning null or undefined, msgpack attributes the decoding of this + * object to this extension, allowing us to handle secure field logic on the + * subprocess side, without having to iterate through all objects in search + * of the field. + */ + encode: (object: unknown, context: { ignoreRoot?: boolean } = {}) => { + // Ignoring the root object allows msgpack to take care of encoding the object's properties, + // while we mark the root object itself as an extension type. + if (context?.ignoreRoot) { + context.ignoreRoot = false; + + return null; + } + + if (hasSecureFields(object)) { + return encode(object, { extensionCodec, context: { ignoreRoot: true } }); + } + }, + + // We don't really need to handle decoding here, as the subprocess will never send a message with secure fields + decode: (_data: Uint8Array) => undefined, +}); + +/** + * The Encoder and Decoder classes perform "stateful" operations, i.e. they read from a + * stream, store the data locally and decode it from its buffer. + * + * In practice, this affects the decoder when there is decode error. After an error, the decoder + * keeps the malformed data in its buffer, and even if we try to decode from another source (e.g. different stream) + * it will fail again as there's still data in the buffer. + * + * For that reason, we can't have a singleton instance of Encoder and Decoder, but rather one + * instance for each time we create a new subprocess + */ +export const newEncoder = () => new _Encoder({ extensionCodec }); +export const newDecoder = () => new _Decoder({ extensionCodec }); + +export type Encoder = _Encoder; +export type Decoder = _Decoder; From c0e4d0100621222fd49e1ada16d0d37c70b9c980 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Fri, 19 Jun 2026 15:49:54 -0300 Subject: [PATCH 04/11] feat(apps): adapt apps package to accept node-runtime --- packages/apps/package.json | 20 ++++- .../src/server/managers/AppRuntimeManager.ts | 10 ++- .../server/runtime/AppsEngineNodeRuntime.ts | 75 ------------------- 3 files changed, 25 insertions(+), 80 deletions(-) delete mode 100644 packages/apps/src/server/runtime/AppsEngineNodeRuntime.ts diff --git a/packages/apps/package.json b/packages/apps/package.json index d2f922ab57020..03adf9028b307 100644 --- a/packages/apps/package.json +++ b/packages/apps/package.json @@ -7,26 +7,38 @@ "files": [ "dist/", "deno-runtime/", + "node-runtime/", ".deno-cache/" ], "scripts": { - "build": "run-s build:clean build:default build:deno-cache", + "build": "run-s build:clean build:default build:node-runtime build:deno-cache", "build:clean": "rimraf dist", "build:default": "tsc -p tsconfig.json", "build:deno-cache": "node scripts/deno-cache.js", + "build:node-runtime": "tsc -p node-runtime/tsconfig.json", "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput", - "lint": "eslint .", + "lint:deno-runtime": "deno task --config=deno-runtime/deno.jsonc test", + "lint:node-runtime": "eslint --fix node-runtime", + "lint:default": "eslint --fix .", + "lint": "run-s lint:default lint:node-runtime", "test:deno": "deno task --config=deno-runtime/deno.jsonc test", "test:node": "NODE_ENV=test node --require ts-node/register/transpile-only --test-reporter spec --test \"tests/**/*.test.ts\"", - "testunit": "yarn test:node && yarn test:deno", - "typecheck": "tsc -p tsconfig.json --noEmit" + "test:node-runtime": "NODE_ENV=test node --require ts-node/register/transpile-only --test-reporter spec --test \"node-runtime/src/**/*.test.ts\"", + "testunit": "run-s test:node test:deno test:node-runtime", + "typecheck:default": "tsc -p tsconfig.json --noEmit", + "typecheck:node-runtime": "tsc -p node-runtime/tsconfig.json --noEmit", + "typecheck": "run-s typecheck:default typecheck:node-runtime" }, "dependencies": { "@msgpack/msgpack": "3.0.0-beta2", "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/model-typings": "workspace:^", + "@rocket.chat/ui-kit": "workspace:^", + "acorn": "^8.17.0", + "acorn-walk": "^8.3.5", "adm-zip": "^0.5.16", + "astring": "^1.9.0", "debug": "^4.3.7", "esbuild": "~0.28.1", "jose": "^4.15.9", diff --git a/packages/apps/src/server/managers/AppRuntimeManager.ts b/packages/apps/src/server/managers/AppRuntimeManager.ts index dd631bc8eebd0..3539f80148121 100644 --- a/packages/apps/src/server/managers/AppRuntimeManager.ts +++ b/packages/apps/src/server/managers/AppRuntimeManager.ts @@ -2,6 +2,7 @@ import type { AppManager } from '../AppManager'; import type { IParseAppPackageResult } from '../compiler'; import type { IRuntimeController } from '../runtime/IRuntimeController'; import { DenoRuntimeSubprocessController } from '../runtime/deno/AppsEngineDenoRuntime'; +import { NodeRuntimeSubprocessController } from '../runtime/node/AppsEngineNodeRuntime'; import type { IAppStorageItem } from '../storage'; export type AppRuntimeParams = { @@ -18,9 +19,16 @@ export type ExecRequestOptions = { timeout?: number; }; -const defaultRuntimeFactory = (manager: AppManager, appPackage: IParseAppPackageResult, storageItem: IAppStorageItem) => +const { APPS_ENGINE_RUNTIME_BACKEND = 'deno' } = process.env; + +export const nodeRuntimeFactory = (manager: AppManager, appPackage: IParseAppPackageResult, storageItem: IAppStorageItem) => + new NodeRuntimeSubprocessController(manager, appPackage, storageItem); + +export const denoRuntimeFactory = (manager: AppManager, appPackage: IParseAppPackageResult, storageItem: IAppStorageItem) => new DenoRuntimeSubprocessController(manager, appPackage, storageItem); +const defaultRuntimeFactory = APPS_ENGINE_RUNTIME_BACKEND === 'node' ? nodeRuntimeFactory : denoRuntimeFactory; + export class AppRuntimeManager { private readonly subprocesses: Record = {}; diff --git a/packages/apps/src/server/runtime/AppsEngineNodeRuntime.ts b/packages/apps/src/server/runtime/AppsEngineNodeRuntime.ts deleted file mode 100644 index 4dadf1588e87e..0000000000000 --- a/packages/apps/src/server/runtime/AppsEngineNodeRuntime.ts +++ /dev/null @@ -1,75 +0,0 @@ -import * as timers from 'node:timers'; -import * as vm from 'node:vm'; - -import type { App } from '@rocket.chat/apps-engine/definition/App'; - -import type { IAppsEngineRuntimeOptions } from './AppsEngineRuntime'; -import { APPS_ENGINE_RUNTIME_DEFAULT_TIMEOUT, AppsEngineRuntime, getFilenameForApp } from './AppsEngineRuntime'; - -export class AppsEngineNodeRuntime extends AppsEngineRuntime { - public static defaultRuntimeOptions = { - timeout: APPS_ENGINE_RUNTIME_DEFAULT_TIMEOUT, - }; - - public static defaultContext = { - ...timers, - Buffer, - console, - process: {}, - exports: {}, - }; - - public static async runCode(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): Promise { - return new Promise((resolve, reject) => { - process.nextTick(() => { - try { - resolve(this.runCodeSync(code, sandbox, options)); - } catch (e) { - reject(e); - } - }); - }); - } - - public static runCodeSync(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): any { - return vm.runInNewContext( - code, - { ...AppsEngineNodeRuntime.defaultContext, ...sandbox }, - { ...AppsEngineNodeRuntime.defaultRuntimeOptions, ...(options || {}) }, - ); - } - - constructor( - private readonly app: App, - private readonly customRequire: (mod: string) => any, - ) { - super(app, customRequire); - } - - public async runInSandbox(code: string, sandbox?: Record, options?: IAppsEngineRuntimeOptions): Promise { - return new Promise((resolve, reject) => { - process.nextTick(async () => { - try { - sandbox ??= {}; - - const result = await vm.runInNewContext( - code, - { - ...AppsEngineNodeRuntime.defaultContext, - ...sandbox, - require: this.customRequire, - }, - { - ...AppsEngineNodeRuntime.defaultRuntimeOptions, - filename: getFilenameForApp(options?.filename || this.app.getName()), - }, - ); - - resolve(result); - } catch (e) { - reject(e); - } - }); - }); - } -} From aef8faa398df709f80ab4b876242a81822c29272 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Fri, 19 Jun 2026 16:22:29 -0300 Subject: [PATCH 05/11] chore(ci): add dedicated CI step for node-runtime tests --- .github/workflows/ci-test-e2e.yml | 24 +++++++++++++++++++++--- .github/workflows/ci.yml | 21 +++++++++++++++++++++ apps/meteor/.mocharc.api.apps.js | 15 +++++++++++++++ apps/meteor/package.json | 1 + docker-compose-ci.yml | 1 + 5 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 apps/meteor/.mocharc.api.apps.js diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index e7993ea4d542e..d1e685c1aa0b0 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -162,7 +162,7 @@ jobs: run: echo "DEBUG_LOG_LEVEL=2" >> "$GITHUB_ENV" - name: Start httpbin container and wait for it to be ready - if: inputs.type == 'api' || inputs.type == 'api-livechat' + if: startsWith(inputs.type, 'api') run: | docker compose -f docker-compose-ci.yml up -d httpbin @@ -181,7 +181,7 @@ jobs: # behavior on (rate-limiter bypass, short cache TTLs) while letting # the deprecation logger log without throwing. Other suites use the # docker-compose default of TEST_MODE='true'. - TEST_MODE: ${{ (inputs.type == 'api' || inputs.type == 'api-livechat') && 'api' || 'true' }} + TEST_MODE: ${{ startsWith(inputs.type, 'api') && 'api' || 'true' }} run: | # when we are testing CE, we only need to start the rocketchat container DEBUG_LOG_LEVEL=${DEBUG_LOG_LEVEL:-0} docker compose -f docker-compose-ci.yml up -d rocketchat --wait @@ -192,7 +192,8 @@ jobs: ENTERPRISE_LICENSE: ${{ inputs.enterprise-license }} TRANSPORTER: ${{ inputs.transporter }} COMPOSE_PROFILES: ${{ inputs.type == 'api' && 'api' || '' }} - TEST_MODE: ${{ (inputs.type == 'api' || inputs.type == 'api-livechat') && 'api' || 'true' }} + TEST_MODE: ${{ startsWith(inputs.type, 'api') && 'api' || 'true' }} + APPS_ENGINE_RUNTIME_BACKEND: ${{ inputs.type == 'api-apps-node' && 'node' || '' }} run: | DEBUG_LOG_LEVEL=${DEBUG_LOG_LEVEL:-0} docker compose -f docker-compose-ci.yml up -d --wait @@ -229,6 +230,23 @@ jobs: ls -la "$COVERAGE_DIR" exit "${s:-0}" + # This step should be temporary, only here until we remove the deno-runtime + - name: E2E Test API (apps + node-runtime) + if: (inputs.type == 'api-apps-node' && inputs.release == 'ee') + working-directory: ./apps/meteor + env: + WEBHOOK_TEST_URL: 'http://httpbin' + IS_EE: 'true' + run: | + set -o xtrace + + npm run testapi:apps || s=$? + + docker compose -f ../../docker-compose-ci.yml stop + + ls -la "$COVERAGE_DIR" + exit "${s:-0}" + - name: E2E Test API (Livechat) if: inputs.type == 'api-livechat' working-directory: ./apps/meteor diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b4a8a9ea8eaee..e9a5073385bbe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -650,6 +650,27 @@ jobs: CR_PAT: ${{ secrets.CR_PAT }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + test-api-apps-node-ee: + name: 🔨 Test API Apps (node-runtime - EE) + needs: [checks, build-gh-docker-publish, release-versions] + + uses: ./.github/workflows/ci-test-e2e.yml + with: + type: api-apps-node + release: ee + transporter: 'nats://nats:4222' + enterprise-license: ${{ needs.release-versions.outputs.enterprise-license }} + mongodb-version: "['8.0']" + coverage: '8.0' + node-version: ${{ needs.release-versions.outputs.node-version }} + deno-version: ${{ needs.release-versions.outputs.deno-version }} + lowercase-repo: ${{ needs.release-versions.outputs.lowercase-repo }} + gh-docker-tag: ${{ needs.release-versions.outputs.gh-docker-tag }} + secrets: + CR_USER: ${{ secrets.CR_USER }} + CR_PAT: ${{ secrets.CR_PAT }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + test-ui-ee: name: 🔨 Test UI (EE) needs: [checks, build-gh-docker-publish, release-versions] diff --git a/apps/meteor/.mocharc.api.apps.js b/apps/meteor/.mocharc.api.apps.js new file mode 100644 index 0000000000000..7eea51b389f01 --- /dev/null +++ b/apps/meteor/.mocharc.api.apps.js @@ -0,0 +1,15 @@ +'use strict'; + +/* + * Mocha configuration for REST API integration tests. + */ + +module.exports = /** @satisfies {import('mocha').MochaOptions} */ ({ + ...require('./.mocharc.base.json'), // see https://github.com/mochajs/mocha/issues/3916 + timeout: 10000, + bail: false, + retries: 0, + file: 'tests/end-to-end/teardown.ts', + reporter: 'tests/end-to-end/reporter.ts', + spec: ['tests/end-to-end/apps/*'], +}); diff --git a/apps/meteor/package.json b/apps/meteor/package.json index f43fa09b3c04c..388d1b3e48090 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -56,6 +56,7 @@ "test:e2e:nyc": "nyc report --reporter=lcovonly", "testapi": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' mocha --config ./.mocharc.api.js", "testapi:livechat": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' mocha --config ./.mocharc.api.livechat.js", + "testapi:apps": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' mocha --config ./.mocharc.api.apps.js", "testunit": "yarn .testunit:definition && yarn .testunit:jest && yarn .testunit:server:cov", "testunit-watch": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' mocha --watch --config ./.mocharc.js", "typecheck": "meteor lint && cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" tsc --noEmit --skipLibCheck", diff --git a/docker-compose-ci.yml b/docker-compose-ci.yml index ffa7125b5f727..c2c23488913f6 100644 --- a/docker-compose-ci.yml +++ b/docker-compose-ci.yml @@ -14,6 +14,7 @@ services: image: ghcr.io/${LOWERCASE_REPOSITORY}/rocket.chat:${DOCKER_TAG}${DOCKER_TAG_SUFFIX_ROCKETCHAT:-} environment: - 'TEST_MODE=${TEST_MODE:-true}' + - APPS_ENGINE_RUNTIME_BACKEND=${APPS_ENGINE_RUNTIME_BACKEND:-} - DEBUG=${DEBUG:-} - EXIT_UNHANDLEDPROMISEREJECTION=true - MONGO_URL=mongodb://mongo:27017/rocketchat?replicaSet=rs0 From 04833b55740641340f02574592f1a4e025511577 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Fri, 19 Jun 2026 20:22:05 -0300 Subject: [PATCH 06/11] tests(apps): fix node-runtime tests --- .../src/handlers/tests/api-handler.test.ts | 89 ++++++++++++++++--- .../handlers/tests/scheduler-handler.test.ts | 2 +- .../tests/slashcommand-handler.test.ts | 33 +++---- .../src/handlers/tests/uikit-handler.test.ts | 88 +++++++++--------- .../tests/upload-event-handler.test.ts | 2 +- .../lib/accessors/tests/AppAccessors.test.ts | 19 +++- .../lib/accessors/tests/ModifyCreator.test.ts | 39 +++++--- .../lib/accessors/tests/ModifyUpdater.test.ts | 38 ++++---- .../tests/formatResponseErrorHandler.test.ts | 2 +- .../src/lib/ast/tests/data/ast_blocks.ts | 3 +- .../src/lib/ast/tests/operations.test.ts | 22 ++--- packages/apps/node-runtime/tsconfig.json | 3 +- 12 files changed, 219 insertions(+), 121 deletions(-) diff --git a/packages/apps/node-runtime/src/handlers/tests/api-handler.test.ts b/packages/apps/node-runtime/src/handlers/tests/api-handler.test.ts index 75a3702a1ee4e..0c9a92ed1f67a 100644 --- a/packages/apps/node-runtime/src/handlers/tests/api-handler.test.ts +++ b/packages/apps/node-runtime/src/handlers/tests/api-handler.test.ts @@ -1,6 +1,8 @@ import * as assert from 'node:assert'; import { beforeEach, describe, it, mock } from 'node:test'; +import type { IRead, IModify, IHttp, IPersistence } from '@rocket.chat/apps-engine/definition/accessors'; +import type { IApiRequest, IApiEndpointInfo, IApiResponse } from '@rocket.chat/apps-engine/definition/api'; import type { IApiEndpoint } from '@rocket.chat/apps-engine/definition/api/IApiEndpoint'; import { JsonRpcError } from 'jsonrpc-lite'; @@ -9,13 +11,71 @@ import apiHandler from '../api-handler'; import { createMockRequest } from './helpers/mod'; describe('handlers > api', () => { - const mockEndpoint: IApiEndpoint = { + const mockEndpoint: Required> = { path: '/test', - get: (request: any, endpoint: any, read: any, modify: any, http: any, persis: any) => Promise.resolve('ok'), - post: (request: any, endpoint: any, read: any, modify: any, http: any, persis: any) => Promise.resolve('ok'), - put: (request: any, endpoint: any, read: any, modify: any, http: any, persis: any) => { + examples: {}, + authRequired: false, + _availableMethods: [], + get( + _request: IApiRequest, + _endpoint: IApiEndpointInfo, + _read: IRead, + _modify: IModify, + _http: IHttp, + _persis: IPersistence, + ): Promise { + return Promise.resolve({ status: 200 }); + }, + post( + _request: IApiRequest, + _endpoint: IApiEndpointInfo, + _read: IRead, + _modify: IModify, + _http: IHttp, + _persis: IPersistence, + ): Promise { + return Promise.resolve({ status: 200 }); + }, + put( + _request: IApiRequest, + _endpoint: IApiEndpointInfo, + _read: IRead, + _modify: IModify, + _http: IHttp, + _persis: IPersistence, + ): Promise { throw new Error('Method execution error example'); }, + head( + _request: IApiRequest, + _endpoint: IApiEndpointInfo, + _read: IRead, + _modify: IModify, + _http: IHttp, + _persis: IPersistence, + ): Promise { + throw new Error('Function not implemented.'); + }, + options( + _request: IApiRequest, + _endpoint: IApiEndpointInfo, + _read: IRead, + _modify: IModify, + _http: IHttp, + _persis: IPersistence, + ): Promise { + throw new Error('Function not implemented.'); + }, + patch( + _request: IApiRequest, + _endpoint: IApiEndpointInfo, + _read: IRead, + _modify: IModify, + _http: IHttp, + _persis: IPersistence, + ): Promise { + throw new Error('Function not implemented.'); + }, }; beforeEach(() => { @@ -28,7 +88,7 @@ describe('handlers > api', () => { const result = await apiHandler(createMockRequest({ method: 'api:/test:get', params: ['request', 'endpointInfo'] })); - assert.deepStrictEqual(result, 'ok'); + assert.deepStrictEqual(result, { status: 200 }); assert.deepStrictEqual(_spy.mock.calls[0].arguments.length, 6); assert.deepStrictEqual(_spy.mock.calls[0].arguments[0], 'request'); assert.deepStrictEqual(_spy.mock.calls[0].arguments[1], 'endpointInfo'); @@ -41,7 +101,7 @@ describe('handlers > api', () => { const result = await apiHandler(createMockRequest({ method: 'api:/test:post', params: ['request', 'endpointInfo'] })); - assert.deepStrictEqual(result, 'ok'); + assert.deepStrictEqual(result, { status: 200 }); assert.deepStrictEqual(_spy.mock.calls[0].arguments.length, 6); assert.deepStrictEqual(_spy.mock.calls[0].arguments[0], 'request'); assert.deepStrictEqual(_spy.mock.calls[0].arguments[1], 'endpointInfo'); @@ -53,8 +113,8 @@ describe('handlers > api', () => { const result = await apiHandler(createMockRequest({ method: `api:/test:delete`, params: ['request', 'endpointInfo'] })); assert.ok(result instanceof JsonRpcError, `Expected instance of ${JsonRpcError.name}`); - assert.strictEqual((result as any).message, `/test's delete not exists`); - assert.strictEqual((result as any).code, -32000); + assert.strictEqual(result.message, `/test's delete not exists`); + assert.strictEqual(result.code, -32000); }); it('correctly handles an error if endpoint not exists', async () => { @@ -74,9 +134,11 @@ describe('handlers > api', () => { }); it('correctly handles dynamic paths with parameters (e.g., webhook/:event)', async () => { - const mockDynamicEndpoint: IApiEndpoint = { + const mockDynamicEndpoint = { + ...mockEndpoint, path: 'webhook/:event', - post: (request: any, endpoint: any, read: any, modify: any, http: any, persis: any) => Promise.resolve('webhook handled'), + post: (_request: any, _endpoint: any, _read: any, _modify: any, _http: any, _persis: any) => + Promise.resolve('webhook handled' as any), }; AppObjectRegistry.set('api:webhook/:event', mockDynamicEndpoint); @@ -94,9 +156,10 @@ describe('handlers > api', () => { }); it('correctly handles paths with multiple segments and colons', async () => { - const mockComplexEndpoint: IApiEndpoint = { + const mockComplexEndpoint = { + ...mockEndpoint, path: 'api/v1/:resource/:id', - get: (request: any, endpoint: any, read: any, modify: any, http: any, persis: any) => Promise.resolve('complex path'), + get: (_request: any, _endpoint: any, _read: any, _modify: any, _http: any, _persis: any) => Promise.resolve({ status: 201 }), }; AppObjectRegistry.set('api:api/v1/:resource/:id', mockComplexEndpoint); @@ -105,7 +168,7 @@ describe('handlers > api', () => { const result = await apiHandler(createMockRequest({ method: 'api:api/v1/:resource/:id:get', params: ['request', 'endpointInfo'] })); - assert.deepStrictEqual(result, 'complex path'); + assert.deepStrictEqual(result, { status: 201 }); assert.deepStrictEqual(_spy.mock.calls[0].arguments.length, 6); _spy.mock.restore(); diff --git a/packages/apps/node-runtime/src/handlers/tests/scheduler-handler.test.ts b/packages/apps/node-runtime/src/handlers/tests/scheduler-handler.test.ts index f71b2c4d03117..ed0d1a7164186 100644 --- a/packages/apps/node-runtime/src/handlers/tests/scheduler-handler.test.ts +++ b/packages/apps/node-runtime/src/handlers/tests/scheduler-handler.test.ts @@ -24,7 +24,7 @@ describe('handlers > scheduler', () => { mockAppAccessors.getConfigurationExtend().scheduler.registerProcessors([ { id: 'mockId', - processor: () => Promise.resolve('it works!'), + processor: () => Promise.resolve(), }, ]); }); diff --git a/packages/apps/node-runtime/src/handlers/tests/slashcommand-handler.test.ts b/packages/apps/node-runtime/src/handlers/tests/slashcommand-handler.test.ts index 7d7dcbc7a86b3..57a737487dcb7 100644 --- a/packages/apps/node-runtime/src/handlers/tests/slashcommand-handler.test.ts +++ b/packages/apps/node-runtime/src/handlers/tests/slashcommand-handler.test.ts @@ -1,6 +1,9 @@ import * as assert from 'node:assert'; import { beforeEach, describe, it, mock } from 'node:test'; +import type { IRead, IModify, IHttp, IPersistence } from '@rocket.chat/apps-engine/definition/accessors'; +import type { ISlashCommand, SlashCommandContext } from '@rocket.chat/apps-engine/definition/slashcommands'; + import { AppObjectRegistry } from '../../AppObjectRegistry'; import { createMockRequest } from './helpers/mod'; import type { AppAccessors } from '../../lib/accessors/mod'; @@ -16,38 +19,36 @@ describe('handlers > slashcommand', () => { getSenderFn: () => (id: string) => Promise.resolve([{ __type: 'bridgeCall' }, { id }]), } as unknown as AppAccessors; - const mockCommandExecutorOnly = { + const mockCommandExecutorOnly: ISlashCommand = { command: 'executor-only', i18nParamsExample: 'test', i18nDescription: 'test', providesPreview: false, - async executor(context: any, read: any, modify: any, http: any, persis: any): Promise {}, + executor(_context: SlashCommandContext, _read: IRead, _modify: IModify, _http: IHttp, _persis: IPersistence): Promise { + return Promise.resolve(); + }, }; - const mockCommandExecutorAndPreview = { + const mockCommandExecutorAndPreview: ISlashCommand = { command: 'executor-and-preview', i18nParamsExample: 'test', i18nDescription: 'test', providesPreview: true, - async executor(context: any, read: any, modify: any, http: any, persis: any): Promise {}, - async previewer(context: any, read: any, modify: any, http: any, persis: any): Promise {}, - async executePreviewItem(previewItem: any, context: any, read: any, modify: any, http: any, persis: any): Promise {}, - }; - - const mockCommandPreviewWithNoExecutor = { - command: 'preview-with-no-executor', - i18nParamsExample: 'test', - i18nDescription: 'test', - providesPreview: true, - async previewer(context: any, read: any, modify: any, http: any, persis: any): Promise {}, - async executePreviewItem(previewItem: any, context: any, read: any, modify: any, http: any, persis: any): Promise {}, + executor(_context: SlashCommandContext, _read: IRead, _modify: IModify, _http: IHttp, _persis: IPersistence): Promise { + return Promise.resolve(); + }, + previewer(_context, _read, _modify, _http, _persis) { + return Promise.resolve({ i18nTitle: 'test', items: [] }); + }, + executePreviewItem(_item, _context, _read, _modify, _http, _persis) { + return Promise.resolve(); + }, }; beforeEach(() => { AppObjectRegistry.clear(); AppObjectRegistry.set('slashcommand:executor-only', mockCommandExecutorOnly); AppObjectRegistry.set('slashcommand:executor-and-preview', mockCommandExecutorAndPreview); - AppObjectRegistry.set('slashcommand:preview-with-no-executor', mockCommandPreviewWithNoExecutor); }); it('correctly handles execution of a slash command', async () => { diff --git a/packages/apps/node-runtime/src/handlers/tests/uikit-handler.test.ts b/packages/apps/node-runtime/src/handlers/tests/uikit-handler.test.ts index 71bdacb408464..500bf957ecf71 100644 --- a/packages/apps/node-runtime/src/handlers/tests/uikit-handler.test.ts +++ b/packages/apps/node-runtime/src/handlers/tests/uikit-handler.test.ts @@ -1,9 +1,11 @@ import * as assert from 'node:assert'; import { after, beforeEach, describe, it } from 'node:test'; -import jsonrpc from 'jsonrpc-lite'; +import jsonrpc, { type Defined } from 'jsonrpc-lite'; import { AppObjectRegistry } from '../../AppObjectRegistry'; +import { Logger } from '../../lib/logger'; +import type { RequestContext } from '../../lib/requestContext'; import handleUIKitInteraction, { UIKitActionButtonInteractionContext, UIKitBlockInteractionContext, @@ -12,6 +14,14 @@ import handleUIKitInteraction, { UIKitViewSubmitInteractionContext, } from '../uikit/handler'; +function makeMockRequest(method: string, payload: { [k: string]: Defined }): RequestContext { + return Object.assign(jsonrpc.request(1, method, [payload]), { + context: { + logger: new Logger(method), + }, + }); +} + describe('handlers > uikit', () => { const mockApp = { getID: (): string => 'appId', @@ -31,73 +41,63 @@ describe('handlers > uikit', () => { }); it('successfully handles a call for "executeBlockActionHandler"', async () => { - const request = jsonrpc.request(1, 'app:executeBlockActionHandler', [ - { - actionId: 'actionId', - blockId: 'blockId', - value: 'value', - viewId: 'viewId', - }, - ]); + const request = makeMockRequest('app:executeBlockActionHandler', { + actionId: 'actionId', + blockId: 'blockId', + value: 'value', + viewId: 'viewId', + }); const result = await handleUIKitInteraction(request); assert.ok(result instanceof UIKitBlockInteractionContext, `Expected instance of ${UIKitBlockInteractionContext.name}`); }); it('successfully handles a call for "executeViewSubmitHandler"', async () => { - const request = jsonrpc.request(1, 'app:executeViewSubmitHandler', [ - { - viewId: 'viewId', - appId: 'appId', - userId: 'userId', - isAppUser: true, - values: {}, - }, - ]); + const request = makeMockRequest('app:executeViewSubmitHandler', { + viewId: 'viewId', + appId: 'appId', + userId: 'userId', + isAppUser: true, + values: {}, + }); const result = await handleUIKitInteraction(request); assert.ok(result instanceof UIKitViewSubmitInteractionContext, `Expected instance of ${UIKitViewSubmitInteractionContext.name}`); }); it('successfully handles a call for "executeViewClosedHandler"', async () => { - const request = jsonrpc.request(1, 'app:executeViewClosedHandler', [ - { - viewId: 'viewId', - appId: 'appId', - userId: 'userId', - isAppUser: true, - }, - ]); + const request = makeMockRequest('app:executeViewClosedHandler', { + viewId: 'viewId', + appId: 'appId', + userId: 'userId', + isAppUser: true, + }); const result = await handleUIKitInteraction(request); assert.ok(result instanceof UIKitViewCloseInteractionContext, `Expected instance of ${UIKitViewCloseInteractionContext.name}`); }); it('successfully handles a call for "executeActionButtonHandler"', async () => { - const request = jsonrpc.request(1, 'app:executeActionButtonHandler', [ - { - actionId: 'actionId', - appId: 'appId', - userId: 'userId', - isAppUser: true, - }, - ]); + const request = makeMockRequest('app:executeActionButtonHandler', { + actionId: 'actionId', + appId: 'appId', + userId: 'userId', + isAppUser: true, + }); const result = await handleUIKitInteraction(request); assert.ok(result instanceof UIKitActionButtonInteractionContext, `Expected instance of ${UIKitActionButtonInteractionContext.name}`); }); it('successfully handles a call for "executeLivechatBlockActionHandler"', async () => { - const request = jsonrpc.request(1, 'app:executeLivechatBlockActionHandler', [ - { - actionId: 'actionId', - appId: 'appId', - userId: 'userId', - visitor: {}, - isAppUser: true, - room: {}, - }, - ]); + const request = makeMockRequest('app:executeLivechatBlockActionHandler', { + actionId: 'actionId', + appId: 'appId', + userId: 'userId', + visitor: {}, + isAppUser: true, + room: {}, + }); const result = await handleUIKitInteraction(request); assert.ok(result instanceof UIKitLivechatBlockInteractionContext, `Expected instance of ${UIKitLivechatBlockInteractionContext.name}`); diff --git a/packages/apps/node-runtime/src/handlers/tests/upload-event-handler.test.ts b/packages/apps/node-runtime/src/handlers/tests/upload-event-handler.test.ts index 7cf0abdd7abed..5b5948fda8983 100644 --- a/packages/apps/node-runtime/src/handlers/tests/upload-event-handler.test.ts +++ b/packages/apps/node-runtime/src/handlers/tests/upload-event-handler.test.ts @@ -29,7 +29,7 @@ describe('handlers > upload', () => { app = { extendConfiguration: () => {}, executePreFileUpload: () => Promise.resolve(), - } as unknown as App; + } as unknown as App & IPreFileUpload; AppObjectRegistry.set('app', app); diff --git a/packages/apps/node-runtime/src/lib/accessors/tests/AppAccessors.test.ts b/packages/apps/node-runtime/src/lib/accessors/tests/AppAccessors.test.ts index 59febbbac883a..df8eae1f20e43 100644 --- a/packages/apps/node-runtime/src/lib/accessors/tests/AppAccessors.test.ts +++ b/packages/apps/node-runtime/src/lib/accessors/tests/AppAccessors.test.ts @@ -1,11 +1,16 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion -- acceptable in this test file */ import * as assert from 'node:assert'; import { after, beforeEach, describe, it } from 'node:test'; +import type { IRead, IModify, IHttp, IPersistence } from '@rocket.chat/apps-engine/definition/accessors'; +import type { SlashCommandContext } from '@rocket.chat/apps-engine/definition/slashcommands'; + import { AppObjectRegistry } from '../../../AppObjectRegistry'; import { AppAccessors } from '../mod'; describe('AppAccessors', () => { let appAccessors: AppAccessors; + const senderFn = (r: object) => Promise.resolve({ id: Math.random().toString(36).substring(2), @@ -26,8 +31,8 @@ describe('AppAccessors', () => { }); it('creates the correct format for IRead calls', async () => { - const roomRead = appAccessors.getReader().getRoomReader(); - const room = roomRead.getById('123'); + const roomRead = appAccessors.getReader()!.getRoomReader(); + const room = await roomRead.getById('123'); assert.deepStrictEqual(room, { params: ['123'], @@ -36,7 +41,7 @@ describe('AppAccessors', () => { }); it('creates the correct format for IEnvironmentRead calls from IRead', async () => { - const reader = appAccessors.getReader().getEnvironmentReader().getEnvironmentVariables(); + const reader = appAccessors.getReader()!.getEnvironmentReader().getEnvironmentVariables(); const room = await reader.getValueByName('NODE_ENV'); assert.deepStrictEqual(room, { @@ -72,8 +77,14 @@ describe('AppAccessors', () => { i18nDescription: 'test', i18nParamsExample: 'test', providesPreview: true, + executor(_context: SlashCommandContext, _read: IRead, _modify: IModify, _http: IHttp, _persis: IPersistence): Promise { + throw new Error('Function not implemented.'); + }, }); + // The function will not be serialized and sent to the main process + delete (command as any).params[0].executor; + assert.deepStrictEqual(command, { params: [ { @@ -105,7 +116,7 @@ describe('AppAccessors', () => { assert.deepStrictEqual(AppObjectRegistry.get('slashcommand:test'), slashcommand); // The function will not be serialized and sent to the main process - delete result.params[0].executor; + delete (result as any).params[0].executor; assert.deepStrictEqual(result, { method: 'accessor:getConfigurationExtend:slashCommands:provideSlashCommand', diff --git a/packages/apps/node-runtime/src/lib/accessors/tests/ModifyCreator.test.ts b/packages/apps/node-runtime/src/lib/accessors/tests/ModifyCreator.test.ts index 3834bddf289fb..44eb9a169ab93 100644 --- a/packages/apps/node-runtime/src/lib/accessors/tests/ModifyCreator.test.ts +++ b/packages/apps/node-runtime/src/lib/accessors/tests/ModifyCreator.test.ts @@ -1,6 +1,10 @@ import * as assert from 'node:assert'; import { after, beforeEach, describe, it, mock } from 'node:test'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; +import type { IUploadDescriptor } from '@rocket.chat/apps-engine/definition/uploads/IUploadDescriptor'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users'; + import { AppObjectRegistry } from '../../../AppObjectRegistry'; import { ModifyCreator } from '../modify/ModifyCreator'; @@ -31,8 +35,8 @@ describe('ModifyCreator', () => { // Importing types from the Apps-Engine is problematic, so we'll go with `any` here messageBuilder - .setRoom({ id: '123' } as any) - .setSender({ id: '456' } as any) + .setRoom({ id: '123' } as IRoom) + .setSender({ id: '456' } as IUser) .setText('Hello World') .setUsernameAlias('alias') .setAvatarUrl('https://avatars.com/123'); @@ -61,11 +65,13 @@ describe('ModifyCreator', () => { it('sends the correct payload in the request to upload a buffer', async () => { const modifyCreator = new ModifyCreator(senderFn); - const result = await modifyCreator.getUploadCreator().uploadBuffer(new Uint8Array([1, 2, 3, 4]), 'text/plain'); + const result = await modifyCreator + .getUploadCreator() + .uploadBuffer(Buffer.from([1, 2, 3, 4]), { filename: 'testfile' } as IUploadDescriptor); assert.deepStrictEqual(result, { method: 'accessor:getModifier:getCreator:getUploadCreator:uploadBuffer', - params: [new Uint8Array([1, 2, 3, 4]), 'text/plain'], + params: [Buffer.from([1, 2, 3, 4]), { filename: 'testfile' }], }); }); @@ -96,7 +102,7 @@ describe('ModifyCreator', () => { const result = modifyCreator.getLivechatCreator().createToken(); - assert.ok(!(result instanceof Promise)); + assert.ok(!((result as any) instanceof Promise)); assert.ok(typeof result === 'string', `Expected "${result}" to be of type "string", but got "${typeof result}"`); }); @@ -153,7 +159,12 @@ describe('ModifyCreator', () => { const modifyCreator = new ModifyCreator(failingSenderFn); const uploadCreator = modifyCreator.getUploadCreator(); - await assert.rejects(() => uploadCreator.uploadBuffer(new Uint8Array([9, 10, 11, 12]), 'image/png'), { message: 'Upload error' }); + await assert.rejects( + () => uploadCreator.uploadBuffer(Buffer.from([1, 2, 3, 4]), { filename: 'testfile-reject' } as IUploadDescriptor), + { + message: 'Upload error', + }, + ); }); it('throws an instance of Error when getUploadCreator fails with a specific error object', async () => { @@ -161,7 +172,12 @@ describe('ModifyCreator', () => { const modifyCreator = new ModifyCreator(failingSenderFn); const uploadCreator = modifyCreator.getUploadCreator(); - await assert.rejects(() => uploadCreator.uploadBuffer(new Uint8Array([1, 2, 3]), 'image/png'), { message: 'Upload method error' }); + await assert.rejects( + () => uploadCreator.uploadBuffer(Buffer.from([1, 2, 3, 4]), { filename: 'testfile-reject' } as IUploadDescriptor), + { + message: 'Upload method error', + }, + ); }); it('throws a default Error when getUploadCreator fails with an unknown error object', async () => { @@ -169,9 +185,12 @@ describe('ModifyCreator', () => { const modifyCreator = new ModifyCreator(failingSenderFn); const uploadCreator = modifyCreator.getUploadCreator(); - await assert.rejects(() => uploadCreator.uploadBuffer(new Uint8Array([1, 2, 3]), 'image/png'), { - message: 'An unknown error occurred', - }); + await assert.rejects( + () => uploadCreator.uploadBuffer(Buffer.from([1, 2, 3, 4]), { filename: 'testfile-reject' } as IUploadDescriptor), + { + message: 'An unknown error occurred', + }, + ); }); it('throws an error when a proxy method of getEmailCreator fails', async () => { diff --git a/packages/apps/node-runtime/src/lib/accessors/tests/ModifyUpdater.test.ts b/packages/apps/node-runtime/src/lib/accessors/tests/ModifyUpdater.test.ts index 827356bfd4b97..bebf586878def 100644 --- a/packages/apps/node-runtime/src/lib/accessors/tests/ModifyUpdater.test.ts +++ b/packages/apps/node-runtime/src/lib/accessors/tests/ModifyUpdater.test.ts @@ -1,6 +1,9 @@ import * as assert from 'node:assert'; import { after, beforeEach, describe, it, mock } from 'node:test'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users'; import jsonrpc from 'jsonrpc-lite'; import { AppObjectRegistry } from '../../../AppObjectRegistry'; @@ -31,9 +34,10 @@ describe('ModifyUpdater', () => { }); it('correctly formats requests for the update message flow', async () => { + // `as any` because it's hard to align the types for a private prop const _spy = mock.method(modifyUpdater, 'senderFn' as any); - const messageBuilder = await modifyUpdater.message('123', { id: '456' } as any); + const messageBuilder = await modifyUpdater.message('123', { id: '456' } as IUser); assert.deepStrictEqual(_spy.mock.calls[0].arguments, [ { @@ -45,13 +49,11 @@ describe('ModifyUpdater', () => { messageBuilder.setUpdateData( { id: '123', - room: { id: '123' }, - sender: { id: '456' }, + room: { id: '123' } as IRoom, + sender: { id: '456' } as IUser, text: 'Hello World', - }, - { - id: '456', - }, + } as IMessage, + { id: '456' } as IUser, ); await modifyUpdater.finish(messageBuilder); @@ -59,7 +61,7 @@ describe('ModifyUpdater', () => { assert.deepStrictEqual(_spy.mock.calls[1].arguments, [ { method: 'bridges:getMessageBridge:doUpdate', - params: [{ id: '123', ...messageBuilder.getChanges() }, 'deno-test'], + params: [{ id: '123', ...(messageBuilder as any).getChanges() }, 'deno-test'], }, ]); @@ -69,7 +71,7 @@ describe('ModifyUpdater', () => { it('correctly formats requests for the update room flow', async () => { const _spy = mock.method(modifyUpdater, 'senderFn' as any); - const roomBuilder = (await modifyUpdater.room('123', { id: '456' } as any)) as RoomBuilder; + const roomBuilder = (await modifyUpdater.room('123', { id: '456' } as IUser)) as RoomBuilder; assert.deepStrictEqual(_spy.mock.calls[0].arguments, [ { @@ -84,7 +86,7 @@ describe('ModifyUpdater', () => { displayName: 'Test Room', slugifiedName: 'test-room', creator: { id: '456' }, - }); + } as IRoom); roomBuilder.setMembersToBeAddedByUsernames(['username1', 'username2']); @@ -102,7 +104,7 @@ describe('ModifyUpdater', () => { }); it('correctly formats requests to UserUpdater methods', async () => { - const result = (await modifyUpdater.getUserUpdater().updateStatusText({ id: '123' } as any, 'Hello World')) as any; + const result = (await modifyUpdater.getUserUpdater().updateStatusText({ id: '123' } as IUser, 'Hello World')) as any; assert.deepStrictEqual(result, { method: 'accessor:getModifier:getUpdater:getUserUpdater:updateStatusText', @@ -111,7 +113,7 @@ describe('ModifyUpdater', () => { }); it('correctly formats requests to LivechatUpdater methods', async () => { - const result = (await modifyUpdater.getLivechatUpdater().closeRoom({ id: '123' } as any, 'close it!')) as any; + const result = (await modifyUpdater.getLivechatUpdater().closeRoom({ id: '123' } as IRoom, 'close it!')) as any; assert.deepStrictEqual(result, { method: 'accessor:getModifier:getUpdater:getLivechatUpdater:closeRoom', @@ -133,7 +135,7 @@ describe('ModifyUpdater', () => { it('throws an instance of Error when senderFn throws an error', async () => { const _stub = mock.method(modifyUpdater, 'senderFn' as any, () => Promise.reject(new Error('unit-test-error')) as any); - await assert.rejects(() => modifyUpdater.message('message-id', { _id: 'user-id' } as any), { message: 'unit-test-error' }); + await assert.rejects(() => modifyUpdater.message('message-id', { id: 'user-id' } as IUser), { message: 'unit-test-error' }); _stub.mock.restore(); }); @@ -145,7 +147,7 @@ describe('ModifyUpdater', () => { () => Promise.reject(jsonrpc.error('unit-test-error', new jsonrpc.JsonRpcError('unit-test-error', 1000))) as any, ); - await assert.rejects(() => modifyUpdater.message('message-id', { _id: 'user-id' } as any), { message: 'unit-test-error' }); + await assert.rejects(() => modifyUpdater.message('message-id', { id: 'user-id' } as IUser), { message: 'unit-test-error' }); _stub.mock.restore(); }); @@ -153,7 +155,7 @@ describe('ModifyUpdater', () => { it('throws an instance of Error when senderFn throws an unknown value', async () => { const _stub = mock.method(modifyUpdater, 'senderFn' as any, () => Promise.reject({}) as any); - await assert.rejects(() => modifyUpdater.message('message-id', { _id: 'user-id' } as any), { + await assert.rejects(() => modifyUpdater.message('message-id', { id: 'user-id' } as IUser), { message: 'An unknown error occurred', }); @@ -165,7 +167,7 @@ describe('ModifyUpdater', () => { it('throws an instance of Error when senderFn throws an error', async () => { const _stub = mock.method(modifyUpdater, 'senderFn' as any, () => Promise.reject(new Error('unit-test-error')) as any); - await assert.rejects(() => modifyUpdater.room('room-id', { _id: 'user-id' } as any), { message: 'unit-test-error' }); + await assert.rejects(() => modifyUpdater.room('room-id', { id: 'user-id' } as IUser), { message: 'unit-test-error' }); _stub.mock.restore(); }); @@ -177,7 +179,7 @@ describe('ModifyUpdater', () => { () => Promise.reject(jsonrpc.error('unit-test-error', new jsonrpc.JsonRpcError('unit-test-error', 1000))) as any, ); - await assert.rejects(() => modifyUpdater.room('room-id', { _id: 'user-id' } as any), { message: 'unit-test-error' }); + await assert.rejects(() => modifyUpdater.room('room-id', { id: 'user-id' } as IUser), { message: 'unit-test-error' }); _stub.mock.restore(); }); @@ -185,7 +187,7 @@ describe('ModifyUpdater', () => { it('throws an instance of Error when senderFn throws an unknown value', async () => { const _stub = mock.method(modifyUpdater, 'senderFn' as any, () => Promise.reject({}) as any); - await assert.rejects(() => modifyUpdater.room('room-id', { _id: 'user-id' } as any), { message: 'An unknown error occurred' }); + await assert.rejects(() => modifyUpdater.room('room-id', { id: 'user-id' } as IUser), { message: 'An unknown error occurred' }); _stub.mock.restore(); }); diff --git a/packages/apps/node-runtime/src/lib/accessors/tests/formatResponseErrorHandler.test.ts b/packages/apps/node-runtime/src/lib/accessors/tests/formatResponseErrorHandler.test.ts index 1fe4a1d3e7181..231da32a44151 100644 --- a/packages/apps/node-runtime/src/lib/accessors/tests/formatResponseErrorHandler.test.ts +++ b/packages/apps/node-runtime/src/lib/accessors/tests/formatResponseErrorHandler.test.ts @@ -206,6 +206,6 @@ describe('formatErrorResponse', () => { assert.ok(result instanceof Error, `Expected instance of Error`); assert.deepStrictEqual(result.message, 'An unknown error occurred'); // Ensure the message is not "[object Object]" - assert.deepStrictEqual(result.message !== '[object Object]', true); + assert.deepStrictEqual((result.message as string) !== '[object Object]', true); }); }); diff --git a/packages/apps/node-runtime/src/lib/ast/tests/data/ast_blocks.ts b/packages/apps/node-runtime/src/lib/ast/tests/data/ast_blocks.ts index 2f0d1b96fa263..50ee9d70d4e22 100644 --- a/packages/apps/node-runtime/src/lib/ast/tests/data/ast_blocks.ts +++ b/packages/apps/node-runtime/src/lib/ast/tests/data/ast_blocks.ts @@ -8,7 +8,8 @@ import type { AnyNode, ClassDeclaration, ExpressionStatement, FunctionDeclaratio type TestNodeExcerpt = { code: string; - node: N; + // start/end are omitted from test fixtures for brevity; cast to any to allow partial node objects + node: any; }; export const FunctionDeclarationFoo: TestNodeExcerpt = { diff --git a/packages/apps/node-runtime/src/lib/ast/tests/operations.test.ts b/packages/apps/node-runtime/src/lib/ast/tests/operations.test.ts index 92b83161daff1..576fd72e90b07 100644 --- a/packages/apps/node-runtime/src/lib/ast/tests/operations.test.ts +++ b/packages/apps/node-runtime/src/lib/ast/tests/operations.test.ts @@ -1,6 +1,18 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion -- acceptable for this test file */ import * as assert from 'node:assert'; import { beforeEach, describe, it } from 'node:test'; +import type { + AnyNode, + ArrowFunctionExpression, + AssignmentExpression, + AwaitExpression, + Expression, + MethodDefinition, + ReturnStatement, + VariableDeclaration, +} from 'acorn'; + import type { WalkerState } from '../operations'; import { asyncifyScope, @@ -24,16 +36,6 @@ import { SimpleCallExpressionOfFoo, SyncFunctionDeclarationWithAsyncCallExpression, } from './data/ast_blocks'; -import type { - AnyNode, - ArrowFunctionExpression, - AssignmentExpression, - AwaitExpression, - Expression, - MethodDefinition, - ReturnStatement, - VariableDeclaration, -} from '../../../acorn.d'; describe('getFunctionIdentifier', () => { it(`identifies the name "foo" for the code \`${FunctionDeclarationFoo.code}\``, () => { diff --git a/packages/apps/node-runtime/tsconfig.json b/packages/apps/node-runtime/tsconfig.json index 178705918947c..c243ca480e107 100644 --- a/packages/apps/node-runtime/tsconfig.json +++ b/packages/apps/node-runtime/tsconfig.json @@ -10,6 +10,5 @@ "target": "es2023", "declaration": false, }, - "include": ["./src/**/*"], - "exclude": ["./src/**/*.spec.ts", "./src/**/*.test.ts","./src/**/tests/*"] + "include": ["./src/**/*"] } From 4541989540d6e429728ae56a7737b859c9c0ba71 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Sat, 20 Jun 2026 01:45:35 -0300 Subject: [PATCH 07/11] fix(apps): linting and typechecking --- .../lib/accessors/tests/AppAccessors.test.ts | 1 + .../src/lib/ast/tests/data/ast_blocks.ts | 83 ++++++++++++++++++- packages/apps/package.json | 7 +- .../runtime/node/AppsEngineNodeRuntime.ts | 2 - 4 files changed, 85 insertions(+), 8 deletions(-) diff --git a/packages/apps/node-runtime/src/lib/accessors/tests/AppAccessors.test.ts b/packages/apps/node-runtime/src/lib/accessors/tests/AppAccessors.test.ts index df8eae1f20e43..a41891315685d 100644 --- a/packages/apps/node-runtime/src/lib/accessors/tests/AppAccessors.test.ts +++ b/packages/apps/node-runtime/src/lib/accessors/tests/AppAccessors.test.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion -- acceptable in this test file */ +/* eslint-disable testing-library/no-await-sync-queries */ import * as assert from 'node:assert'; import { after, beforeEach, describe, it } from 'node:test'; diff --git a/packages/apps/node-runtime/src/lib/ast/tests/data/ast_blocks.ts b/packages/apps/node-runtime/src/lib/ast/tests/data/ast_blocks.ts index 50ee9d70d4e22..079c970c44a0d 100644 --- a/packages/apps/node-runtime/src/lib/ast/tests/data/ast_blocks.ts +++ b/packages/apps/node-runtime/src/lib/ast/tests/data/ast_blocks.ts @@ -1,4 +1,3 @@ -// @deno-types="../../../../acorn.d.ts" import type { AnyNode, ClassDeclaration, ExpressionStatement, FunctionDeclaration, VariableDeclaration } from 'acorn'; /** @@ -9,9 +8,11 @@ import type { AnyNode, ClassDeclaration, ExpressionStatement, FunctionDeclaratio type TestNodeExcerpt = { code: string; // start/end are omitted from test fixtures for brevity; cast to any to allow partial node objects - node: any; + node: N; }; +const startEnd = { start: 0, end: 0 }; + export const FunctionDeclarationFoo: TestNodeExcerpt = { code: 'function foo() {}', node: { @@ -19,6 +20,7 @@ export const FunctionDeclarationFoo: TestNodeExcerpt = { id: { type: 'Identifier', name: 'foo', + ...startEnd, }, expression: false, generator: false, @@ -27,7 +29,9 @@ export const FunctionDeclarationFoo: TestNodeExcerpt = { body: { type: 'BlockStatement', body: [], + ...startEnd, }, + ...startEnd, }, }; @@ -42,6 +46,7 @@ export const ConstFooAssignedFunctionExpression: TestNodeExcerpt id: { type: 'Identifier', name: 'Bar', + ...startEnd, }, superClass: null, body: { @@ -140,6 +162,7 @@ export const MethodDefinitionOfFooInClassBar: TestNodeExcerpt key: { type: 'Identifier', name: 'foo', + ...startEnd, }, value: { type: 'FunctionExpression', @@ -151,14 +174,19 @@ export const MethodDefinitionOfFooInClassBar: TestNodeExcerpt body: { type: 'BlockStatement', body: [], + ...startEnd, }, + ...startEnd, }, kind: 'method', computed: false, static: false, + ...startEnd, }, ], + ...startEnd, }, + ...startEnd, }, }; @@ -171,10 +199,13 @@ export const SimpleCallExpressionOfFoo: TestNodeExcerpt = { callee: { type: 'Identifier', name: 'foo', + ...startEnd, }, arguments: [], optional: false, + ...startEnd, }, + ...startEnd, }, }; @@ -188,6 +219,7 @@ export const SyncFunctionDeclarationWithAsyncCallExpression: TestNodeExcerpt = { left: { type: 'Identifier', name: 'bar', + ...startEnd, }, right: { type: 'Identifier', name: 'foo', + ...startEnd, }, + ...startEnd, }, + ...startEnd, }, }; @@ -257,17 +300,23 @@ export const AssignmentOfFooToBarMemberExpression: TestNodeExcerpt = { id: { type: 'Identifier', name: 'bar', + ...startEnd, }, expression: false, generator: false, @@ -353,28 +413,37 @@ export const FixSimpleCallExpression: TestNodeExcerpt = { id: { type: 'Identifier', name: 'a', + ...startEnd, }, init: { type: 'CallExpression', callee: { type: 'Identifier', name: 'foo', + ...startEnd, }, arguments: [], optional: false, + ...startEnd, }, + ...startEnd, }, ], + ...startEnd, }, { type: 'ReturnStatement', argument: { type: 'Identifier', name: 'a', + ...startEnd, }, + ...startEnd, }, ], + ...startEnd, }, + ...startEnd, }, }; @@ -395,6 +464,7 @@ export const ArrowFunctionDerefCallExpression: TestNodeExcerpt Date: Wed, 24 Jun 2026 12:26:46 -0300 Subject: [PATCH 08/11] chore(ci): include new dir to turbo build outputs --- packages/apps/turbo.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/apps/turbo.json b/packages/apps/turbo.json index 0c2eb270418c2..76dfd395bb433 100644 --- a/packages/apps/turbo.json +++ b/packages/apps/turbo.json @@ -4,7 +4,7 @@ "build": { "dependsOn": ["^build"], "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/.tool-versions"], - "outputs": ["deno-runtime/**", "scripts/**", ".deno-cache/**", "dist/**"] + "outputs": ["node-runtime/**", "deno-runtime/**", "scripts/**", ".deno-cache/**", "dist/**"] } } } From 574963fae1d9632b22d370e0603f4c09ed43c362 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Fri, 26 Jun 2026 13:56:32 -0300 Subject: [PATCH 09/11] docs(apps): add shared base runtime proposal Document the plan to unify the near-duplicate deno-runtime and node-runtime trees behind a single base runtime with a thin per-platform adapter (RuntimePlatform interface). Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/proposals/shared-base-runtime.md | 207 ++++++++++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 docs/proposals/shared-base-runtime.md diff --git a/docs/proposals/shared-base-runtime.md b/docs/proposals/shared-base-runtime.md new file mode 100644 index 0000000000000..66b0b8f387de0 --- /dev/null +++ b/docs/proposals/shared-base-runtime.md @@ -0,0 +1,207 @@ +# Proposal: Shared Base Runtime for Apps (Deno + Node) + +## Status + +Draft + +## Problem + +The apps subprocess runtime exists today in two near-duplicate copies: + +- `packages/apps/deno-runtime/` — the Deno implementation (files at root) +- `packages/apps/node-runtime/` — the Node implementation (files under `src/`) + +A normalized diff of all 70 non-test source files (stripping import-path extensions, +`import type` vs `import`, and prettier/tabs-vs-spaces formatting) shows the two trees +are **~90% byte-identical logic**. The four largest apparent diffs (`BlockBuilder.ts`, +`lib/accessors/mod.ts`, `lib/ast/operations.ts`, `handlers/uikit/handler.ts`) each reduce +to **exactly 2 differing tokens** once sorted and whitespace-stripped — those tokens being +the relocated `require(...)` calls. + +Maintaining two copies means every bug fix, accessor change, or handler addition must be +applied twice and kept in sync by hand. The goal is a single **base runtime** that owns all +shared logic, with each platform supplying a thin adapter for the genuinely +platform-specific surface. + +## The Platform Boundary + +Every genuine divergence between the two runtimes reduces to **one of 11 capabilities**. +The base runtime depends only on a `RuntimePlatform` interface; each runtime supplies a +concrete adapter. + +```ts +interface RuntimePlatform { + // ── module resolution (the largest coupling: 19 files) ── + require(specifier: string): unknown; // apps-engine runtime classes + sandboxed app require + prepareEnvironment(): void; // deno: patch Socket.prototype._final; node: no-op + + // ── transport ── + readStdin(): AsyncIterable; // deno: Deno.stdin.readable | node: process.stdin + writeStdout(bytes: Uint8Array): Promise; // deno: writeAll(Deno.stdout) | node: process.stdout.write + writeStderr(bytes: Uint8Array): Promise; // deno: writeAll(Deno.stderr) | node: process.stderr.write + + // ── process lifecycle ── + pid: number; // Deno.pid | process.pid + argv: string[]; // Deno.args | process.argv + parseArgs(argv: string[]): ParsedArgs; // @std/cli | node:util + exit(code: number): never; // Deno.exit | process.exit + registerErrorHandlers(report): void; // addEventListener | process.on('uncaughtException') + + // ── rpc response correlation ── + createResponseObserver(): ResponseObserver; // EventTarget(+ErrorEvent/CustomEvent) | EventEmitter + + // ── file io ── + readFile(path: string): Promise; // Deno.open+toArrayBuffer | node:fs/promises readFile +} +``` + +Everything else — the handler layer, accessor layer, builders, extenders, modify/, AST, +room logic, codec, secureFields, logger — is logically identical and moves to the base +unchanged (modulo import-path/idiom normalization that converges automatically under one +toolchain). + +## Decisions Taken + +### `require` injection: init-time singleton (Option A) + +The 19 `require`-coupled files are constructed deep in the tree (builders instantiated by +`ModifyCreator`, etc.), so threading `platform` through every constructor would be +invasive. Instead: + +- The base exposes `setPlatform(platform)`, called **once** in `main()` before any handler + runs. +- `require`-coupled files read `platform.require` from a base-internal singleton. + +This matches today's module-level `require` usage (Deno imports a shim; Node uses the global +CJS `require`) and keeps churn to the shared files near zero. The alternative — constructor +threading (pure DI) — was rejected because it touches every builder/extender/modify +signature for no functional gain. + +### `construct.ts buildRequire` + +The allow-lists (`ALLOWED_NATIVE_MODULES` / `ALLOWED_EXTERNAL_MODULES`) and the eval shell +are base. Only the specifier-prefix policy (`npm:` / `node:` handling, Buffer injection) and +`prepareEnvironment` differ → both fold into `platform`. + +## Suggested Package Shape + +``` +packages/apps/ + base-runtime/ # the ~58 single-source files + RuntimePlatform interface + setPlatform() + deno-runtime/ # adapter: require.ts, parseArgs, EventTarget observer, Deno io + bootstrap + node-runtime/ # adapter: loader-hook, parseArgs, EventEmitter observer, node io + bootstrap +``` + +## Disposition Legend + +- **BASE** — moves to base unchanged (only import-path/idiom normalization). +- **BASE+require** — moves to base; the only coupling is `require()` → reads injected `platform.require`. +- **BASE+platform** — moves to base; needs a non-require capability injected. +- **SPLIT** — logic core moves to base; a thin slice stays in the adapter. +- **ADAPTER** — platform-specific implementation, one copy per runtime. + +## Full Inventory + +### `lib/` core + +| File | Disposition | Injected capability / notes | +|---|---|---| +| `lib/codec.ts` | BASE+require | `require('.../App.js')` to load `App` class | +| `lib/secureFields.ts` | BASE | identical | +| `lib/sanitizeDeprecatedUsage.ts` | BASE | — | +| `lib/requestContext.ts` | BASE | — | +| `lib/room.ts` | BASE | only `??` / strictness idiom | +| `lib/roomFactory.ts` | BASE | — | +| `lib/wrapAppForRequest.ts` | BASE | — | +| `lib/logger.ts` | BASE | use Node's `implements ILogger` superset as canonical | +| `lib/messenger.ts` | SPLIT | `writeStdout` + `createResponseObserver` injected; Queue/encode logic is base | +| `lib/metricsCollector.ts` | BASE+platform | `writeStderr`, `pid` | +| `lib/parseArgs.ts` | ADAPTER | shape differs (`@std/cli` vs `node:util`); exposed via `platform.parseArgs` | +| `lib/require.ts` | ADAPTER (deno) | becomes the Deno `require` impl | +| `lib/loader-hook.ts` | ADAPTER (node) | Node `registerHooks` resolver | + +### `lib/ast/` + +| File | Disposition | Notes | +|---|---|---| +| `lib/ast/mod.ts` | BASE | drop `@deno-types` comments; use real acorn imports | +| `lib/ast/operations.ts` | BASE | type-assertion idiom only | +| `acorn.d.ts`, `acorn-walk.d.ts` | BASE | dedupe the two copies (deno root vs node `lib/ast/`) into one | + +### `lib/accessors/` + +| File | Disposition | Notes | +|---|---|---| +| `accessors/mod.ts` | BASE | 48-line "diff" was whitespace + `WithProxy` typing | +| `accessors/http.ts` | BASE | identical after norm | +| `accessors/formatResponseErrorHandler.ts` | BASE | byte-identical today | +| `accessors/notifier.ts` | BASE+require | — | +| `accessors/builders/BlockBuilder.ts` | BASE+require | loads BlockType/ElementType/TextObjectType | +| `accessors/builders/DiscussionBuilder.ts` | BASE+require | — | +| `accessors/builders/LivechatMessageBuilder.ts` | BASE+require | — | +| `accessors/builders/MessageBuilder.ts` | BASE+require | — | +| `accessors/builders/RoomBuilder.ts` | BASE+require | — | +| `accessors/builders/UserBuilder.ts` | BASE+require | — | +| `accessors/builders/VideoConferenceBuilder.ts` | BASE+require | — | +| `accessors/extenders/HttpExtender.ts` | BASE | byte-identical today | +| `accessors/extenders/MessageExtender.ts` | BASE+require | — | +| `accessors/extenders/RoomExtender.ts` | BASE+require | — | +| `accessors/extenders/VideoConferenceExtend.ts` | BASE+require | — | +| `accessors/modify/ModifyCreator.ts` | BASE+require | — | +| `accessors/modify/ModifyExtender.ts` | BASE+require | — | +| `accessors/modify/ModifyUpdater.ts` | BASE+require | — | + +### `handlers/` + +| File | Disposition | Notes | +|---|---|---| +| `handlers/api-handler.ts` | BASE | lint-directive idiom only | +| `handlers/outboundcomms-handler.ts` | BASE | — | +| `handlers/scheduler-handler.ts` | BASE | — | +| `handlers/slashcommand-handler.ts` | BASE | type-assertion idiom only | +| `handlers/videoconference-handler.ts` | BASE | — | +| `handlers/uikit/handler.ts` | BASE+require | — | +| `handlers/listener/handler.ts` | BASE+require | — | +| `handlers/lib/assertions.ts` | BASE | — | +| `handlers/app/handler.ts` | BASE | dispatch table | +| `handlers/app/handleInitialize.ts` | BASE | — | +| `handlers/app/handleGetStatus.ts` | BASE | — | +| `handlers/app/handleSetStatus.ts` | BASE+require | — | +| `handlers/app/handleOnEnable.ts` | BASE | — | +| `handlers/app/handleOnDisable.ts` | BASE | — | +| `handlers/app/handleOnInstall.ts` | BASE | — | +| `handlers/app/handleOnUninstall.ts` | BASE | — | +| `handlers/app/handleOnUpdate.ts` | BASE | — | +| `handlers/app/handleOnPreSettingUpdate.ts` | BASE | — | +| `handlers/app/handleOnSettingUpdated.ts` | BASE | — | +| `handlers/app/handleUploadEvents.ts` | BASE+platform | `readFile(path)` | +| `handlers/app/construct.ts` | SPLIT | `require`, `prepareEnvironment`, `buildRequire` specifier policy injected; sandbox-eval logic is base | + +### Top-level / entry / config + +| File | Disposition | Notes | +|---|---|---| +| `AppObjectRegistry.ts` | BASE | IIFE-paren idiom only | +| `error-handlers.ts` | SPLIT | notification-shape builder is base; `registerErrorHandlers` hook is adapter | +| `main.ts` | SPLIT | message loop is base; `readStdin` / `argv` / `exit` / observer-dispatch injected | +| `globals.d.ts` (node) | ADAPTER (node) | ambient types | +| `deno.jsonc`, `deno.runtime.jsonc`, `deno.lock`, `.gitignore` | ADAPTER (deno) | + import map for adapter wiring | +| `tsconfig.json` (node) | ADAPTER (node) | — | + +## Tally + +- **BASE (pure)**: 33 files +- **BASE+require**: 19 files +- **BASE+platform**: 2 files (`metricsCollector`, `handleUploadEvents`) +- **SPLIT**: 4 files (`main`, `messenger`, `construct`, `error-handlers`) +- **ADAPTER**: `require.ts` / `loader-hook.ts`, `parseArgs.ts`, `globals.d.ts`, config + +→ **~58 of 62 logic files become single-source.** Only 4 files split, and the per-runtime +adapter is roughly ~250 lines total. + +## Out of Scope (this proposal) + +- Step-by-step migration ordering and the strategy for keeping both runtimes green during + the move (to be covered in a follow-up implementation plan). +- Test consolidation: both trees carry parallel `tests/` suites that would also collapse to + a single base suite running against each adapter. From 1a87551c2fc709c1ff4c62e3c0c77a54761893c2 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Fri, 26 Jun 2026 13:56:41 -0300 Subject: [PATCH 10/11] refactor(apps): use direct ESM imports in node-runtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the relocated require(...) helper pattern with direct ESM 'import { X } from "….js"' statements for apps-engine runtime classes. Mechanical, no behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/handlers/app/handleSetStatus.ts | 6 +---- .../src/handlers/listener/handler.ts | 6 +---- .../src/handlers/uikit/handler.ts | 27 +++++++------------ .../lib/accessors/builders/BlockBuilder.ts | 6 +---- .../accessors/builders/DiscussionBuilder.ts | 12 +++------ .../builders/LivechatMessageBuilder.ts | 12 +++------ .../lib/accessors/builders/MessageBuilder.ts | 8 ++---- .../src/lib/accessors/builders/RoomBuilder.ts | 8 ++---- .../src/lib/accessors/builders/UserBuilder.ts | 8 ++---- .../builders/VideoConferenceBuilder.ts | 8 ++---- .../accessors/extenders/MessageExtender.ts | 8 ++---- .../lib/accessors/extenders/RoomExtender.ts | 8 ++---- .../extenders/VideoConferenceExtend.ts | 8 ++---- .../src/lib/accessors/modify/ModifyCreator.ts | 12 +++------ .../lib/accessors/modify/ModifyExtender.ts | 6 +---- .../src/lib/accessors/modify/ModifyUpdater.ts | 9 ++----- .../src/lib/accessors/notifier.ts | 7 ++--- packages/apps/node-runtime/src/lib/codec.ts | 6 +---- 18 files changed, 41 insertions(+), 124 deletions(-) diff --git a/packages/apps/node-runtime/src/handlers/app/handleSetStatus.ts b/packages/apps/node-runtime/src/handlers/app/handleSetStatus.ts index 042bb5369044f..c750f36854030 100644 --- a/packages/apps/node-runtime/src/handlers/app/handleSetStatus.ts +++ b/packages/apps/node-runtime/src/handlers/app/handleSetStatus.ts @@ -1,14 +1,10 @@ import type { App } from '@rocket.chat/apps-engine/definition/App'; -import type { AppStatus as _AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; +import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus.js'; import { AppObjectRegistry } from '../../AppObjectRegistry'; import type { RequestContext } from '../../lib/requestContext'; import { wrapAppForRequest } from '../../lib/wrapAppForRequest'; -const { AppStatus } = require('@rocket.chat/apps-engine/definition/AppStatus.js') as { - AppStatus: typeof _AppStatus; -}; - export default async function handleSetStatus(request: RequestContext): Promise { const { params } = request; diff --git a/packages/apps/node-runtime/src/handlers/listener/handler.ts b/packages/apps/node-runtime/src/handlers/listener/handler.ts index cdbaa766d63fb..d12aeb13be01c 100644 --- a/packages/apps/node-runtime/src/handlers/listener/handler.ts +++ b/packages/apps/node-runtime/src/handlers/listener/handler.ts @@ -1,5 +1,5 @@ import type { App } from '@rocket.chat/apps-engine/definition/App'; -import type { AppsEngineException as _AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions/AppsEngineException'; +import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions/AppsEngineException.js'; import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; import type { Defined } from 'jsonrpc-lite'; @@ -17,10 +17,6 @@ import { Room } from '../../lib/room'; import createRoom from '../../lib/roomFactory'; import { wrapAppForRequest } from '../../lib/wrapAppForRequest'; -const { AppsEngineException } = require('@rocket.chat/apps-engine/definition/exceptions/AppsEngineException.js') as { - AppsEngineException: typeof _AppsEngineException; -}; - export default async function handleListener(request: RequestContext): Promise { const { method, params } = request; const [, evtInterface] = method.split(':'); diff --git a/packages/apps/node-runtime/src/handlers/uikit/handler.ts b/packages/apps/node-runtime/src/handlers/uikit/handler.ts index 4d70db3ad8d8a..599b710897b9c 100644 --- a/packages/apps/node-runtime/src/handlers/uikit/handler.ts +++ b/packages/apps/node-runtime/src/handlers/uikit/handler.ts @@ -5,14 +5,14 @@ import type { IUIKitViewCloseIncomingInteraction, IUIKitActionButtonIncomingInteraction, } from '@rocket.chat/apps-engine/definition/uikit/UIKitIncomingInteractionTypes'; -import type { - UIKitBlockInteractionContext as _UIKitBlockInteractionContext, - UIKitViewSubmitInteractionContext as _UIKitViewSubmitInteractionContext, - UIKitViewCloseInteractionContext as _UIKitViewCloseInteractionContext, - UIKitActionButtonInteractionContext as _UIKitActionButtonInteractionContext, -} from '@rocket.chat/apps-engine/definition/uikit/UIKitInteractionContext'; +import { + UIKitBlockInteractionContext, + UIKitViewSubmitInteractionContext, + UIKitViewCloseInteractionContext, + UIKitActionButtonInteractionContext, +} from '@rocket.chat/apps-engine/definition/uikit/UIKitInteractionContext.js'; import type { IUIKitLivechatBlockIncomingInteraction } from '@rocket.chat/apps-engine/definition/uikit/livechat/UIKitLivechatIncomingInteractionType'; -import type { UIKitLivechatBlockInteractionContext as _UIKitLivechatBlockInteractionContext } from '@rocket.chat/apps-engine/definition/uikit/livechat/UIKitLivechatInteractionContext'; +import { UIKitLivechatBlockInteractionContext } from '@rocket.chat/apps-engine/definition/uikit/livechat/UIKitLivechatInteractionContext.js'; import type { Defined } from 'jsonrpc-lite'; import { JsonRpcError } from 'jsonrpc-lite'; @@ -30,23 +30,14 @@ export const uikitInteractions = [ 'executeLivechatBlockActionHandler', ] as const; -export const { +export { UIKitBlockInteractionContext, UIKitViewSubmitInteractionContext, UIKitViewCloseInteractionContext, UIKitActionButtonInteractionContext, -} = require('@rocket.chat/apps-engine/definition/uikit/UIKitInteractionContext.js') as { - UIKitBlockInteractionContext: typeof _UIKitBlockInteractionContext; - UIKitViewSubmitInteractionContext: typeof _UIKitViewSubmitInteractionContext; - UIKitViewCloseInteractionContext: typeof _UIKitViewCloseInteractionContext; - UIKitActionButtonInteractionContext: typeof _UIKitActionButtonInteractionContext; + UIKitLivechatBlockInteractionContext, }; -export const { UIKitLivechatBlockInteractionContext } = - require('@rocket.chat/apps-engine/definition/uikit/livechat/UIKitLivechatInteractionContext.js') as { - UIKitLivechatBlockInteractionContext: typeof _UIKitLivechatBlockInteractionContext; - }; - export default async function handleUIKitInteraction(request: RequestContext): Promise { const { method: reqMethod, params } = request; const [, method] = reqMethod.split(':'); diff --git a/packages/apps/node-runtime/src/lib/accessors/builders/BlockBuilder.ts b/packages/apps/node-runtime/src/lib/accessors/builders/BlockBuilder.ts index 2af8fd4570e6d..6af59c3e7f429 100644 --- a/packages/apps/node-runtime/src/lib/accessors/builders/BlockBuilder.ts +++ b/packages/apps/node-runtime/src/lib/accessors/builders/BlockBuilder.ts @@ -1,11 +1,7 @@ -import type { BlockBuilder as _AppsEngineBlockBuilder } from '@rocket.chat/apps-engine/definition/uikit/blocks/BlockBuilder'; +import { BlockBuilder as AppsEngineBlockBuilder } from '@rocket.chat/apps-engine/definition/uikit/blocks/BlockBuilder.js'; import { AppObjectRegistry } from '../../../AppObjectRegistry'; -const { BlockBuilder: AppsEngineBlockBuilder } = require('@rocket.chat/apps-engine/definition/uikit/blocks/BlockBuilder.js') as { - BlockBuilder: typeof _AppsEngineBlockBuilder; -}; - /** * Local BlockBuilder that extends the apps-engine BlockBuilder. * It overrides the constructor to source the appId from the registry diff --git a/packages/apps/node-runtime/src/lib/accessors/builders/DiscussionBuilder.ts b/packages/apps/node-runtime/src/lib/accessors/builders/DiscussionBuilder.ts index 385418ab63e86..ce9515c472754 100644 --- a/packages/apps/node-runtime/src/lib/accessors/builders/DiscussionBuilder.ts +++ b/packages/apps/node-runtime/src/lib/accessors/builders/DiscussionBuilder.ts @@ -1,21 +1,15 @@ import type { IDiscussionBuilder as _IDiscussionBuilder } from '@rocket.chat/apps-engine/definition/accessors/IDiscussionBuilder'; import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage'; -import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; -import type { RoomType as _RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType'; +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType.js'; import { RoomBuilder } from './RoomBuilder'; -const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { - RocketChatAssociationModel: typeof _RocketChatAssociationModel; -}; - -const { RoomType } = require('@rocket.chat/apps-engine/definition/rooms/RoomType.js') as { RoomType: typeof _RoomType }; - export type IDiscussionBuilder = _IDiscussionBuilder; export class DiscussionBuilder extends RoomBuilder implements IDiscussionBuilder { - public declare kind: _RocketChatAssociationModel.DISCUSSION; + public declare kind: RocketChatAssociationModel.DISCUSSION; private reply?: string; diff --git a/packages/apps/node-runtime/src/lib/accessors/builders/LivechatMessageBuilder.ts b/packages/apps/node-runtime/src/lib/accessors/builders/LivechatMessageBuilder.ts index f2938e76bc6b5..e4ac54dad53c2 100644 --- a/packages/apps/node-runtime/src/lib/accessors/builders/LivechatMessageBuilder.ts +++ b/packages/apps/node-runtime/src/lib/accessors/builders/LivechatMessageBuilder.ts @@ -4,23 +4,17 @@ import type { ILivechatMessage as EngineLivechatMessage } from '@rocket.chat/app import type { IVisitor } from '@rocket.chat/apps-engine/definition/livechat/IVisitor'; import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage'; import type { IMessageAttachment } from '@rocket.chat/apps-engine/definition/messages/IMessageAttachment'; -import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; -import type { RoomType as _RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType'; +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType.js'; import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser'; import { MessageBuilder } from './MessageBuilder'; -const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { - RocketChatAssociationModel: typeof _RocketChatAssociationModel; -}; - -const { RoomType } = require('@rocket.chat/apps-engine/definition/rooms/RoomType.js') as { RoomType: typeof _RoomType }; - export interface ILivechatMessage extends EngineLivechatMessage, IMessage {} export class LivechatMessageBuilder implements ILivechatMessageBuilder { - public kind: _RocketChatAssociationModel.LIVECHAT_MESSAGE; + public kind: RocketChatAssociationModel.LIVECHAT_MESSAGE; private msg: ILivechatMessage; diff --git a/packages/apps/node-runtime/src/lib/accessors/builders/MessageBuilder.ts b/packages/apps/node-runtime/src/lib/accessors/builders/MessageBuilder.ts index 4e204a70b0289..f31d5c98f48fe 100644 --- a/packages/apps/node-runtime/src/lib/accessors/builders/MessageBuilder.ts +++ b/packages/apps/node-runtime/src/lib/accessors/builders/MessageBuilder.ts @@ -1,7 +1,7 @@ import type { IMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/IMessageBuilder'; import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage'; import type { IMessageAttachment } from '@rocket.chat/apps-engine/definition/messages/IMessageAttachment'; -import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; import type { IBlock } from '@rocket.chat/apps-engine/definition/uikit/blocks/Blocks'; import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser'; @@ -9,12 +9,8 @@ import type { LayoutBlock } from '@rocket.chat/ui-kit'; import { BlockBuilder } from './BlockBuilder'; -const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { - RocketChatAssociationModel: typeof _RocketChatAssociationModel; -}; - export class MessageBuilder implements IMessageBuilder { - public kind: _RocketChatAssociationModel.MESSAGE; + public kind: RocketChatAssociationModel.MESSAGE; private msg: IMessage; diff --git a/packages/apps/node-runtime/src/lib/accessors/builders/RoomBuilder.ts b/packages/apps/node-runtime/src/lib/accessors/builders/RoomBuilder.ts index 3a62a8bb62ad4..c3236afeff4b9 100644 --- a/packages/apps/node-runtime/src/lib/accessors/builders/RoomBuilder.ts +++ b/packages/apps/node-runtime/src/lib/accessors/builders/RoomBuilder.ts @@ -1,15 +1,11 @@ import type { IRoomBuilder } from '@rocket.chat/apps-engine/definition/accessors/IRoomBuilder'; -import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; import type { RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType'; import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser'; -const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { - RocketChatAssociationModel: typeof _RocketChatAssociationModel; -}; - export class RoomBuilder implements IRoomBuilder { - public kind: _RocketChatAssociationModel.ROOM | _RocketChatAssociationModel.DISCUSSION; + public kind: RocketChatAssociationModel.ROOM | RocketChatAssociationModel.DISCUSSION; protected room: IRoom; diff --git a/packages/apps/node-runtime/src/lib/accessors/builders/UserBuilder.ts b/packages/apps/node-runtime/src/lib/accessors/builders/UserBuilder.ts index ad78b2c82b767..c3e66c494461d 100644 --- a/packages/apps/node-runtime/src/lib/accessors/builders/UserBuilder.ts +++ b/packages/apps/node-runtime/src/lib/accessors/builders/UserBuilder.ts @@ -1,15 +1,11 @@ import type { IUserBuilder } from '@rocket.chat/apps-engine/definition/accessors/IUserBuilder'; -import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js'; import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser'; import type { IUserEmail } from '@rocket.chat/apps-engine/definition/users/IUserEmail'; import type { IUserSettings } from '@rocket.chat/apps-engine/definition/users/IUserSettings'; -const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { - RocketChatAssociationModel: typeof _RocketChatAssociationModel; -}; - export class UserBuilder implements IUserBuilder { - public kind: _RocketChatAssociationModel.USER; + public kind: RocketChatAssociationModel.USER; private user: Partial; diff --git a/packages/apps/node-runtime/src/lib/accessors/builders/VideoConferenceBuilder.ts b/packages/apps/node-runtime/src/lib/accessors/builders/VideoConferenceBuilder.ts index 28e5db7043f4d..66c121cdd12dd 100644 --- a/packages/apps/node-runtime/src/lib/accessors/builders/VideoConferenceBuilder.ts +++ b/packages/apps/node-runtime/src/lib/accessors/builders/VideoConferenceBuilder.ts @@ -1,17 +1,13 @@ import type { IVideoConferenceBuilder } from '@rocket.chat/apps-engine/definition/accessors/IVideoConferenceBuilder'; -import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js'; import type { IGroupVideoConference } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConference'; -const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { - RocketChatAssociationModel: typeof _RocketChatAssociationModel; -}; - export type AppVideoConference = Pick & { createdBy: IGroupVideoConference['createdBy']['_id']; }; export class VideoConferenceBuilder implements IVideoConferenceBuilder { - public kind: _RocketChatAssociationModel.VIDEO_CONFERENCE = RocketChatAssociationModel.VIDEO_CONFERENCE; + public kind: RocketChatAssociationModel.VIDEO_CONFERENCE = RocketChatAssociationModel.VIDEO_CONFERENCE; protected call: AppVideoConference; diff --git a/packages/apps/node-runtime/src/lib/accessors/extenders/MessageExtender.ts b/packages/apps/node-runtime/src/lib/accessors/extenders/MessageExtender.ts index 29e84b53a7e66..8eed82bf29921 100644 --- a/packages/apps/node-runtime/src/lib/accessors/extenders/MessageExtender.ts +++ b/packages/apps/node-runtime/src/lib/accessors/extenders/MessageExtender.ts @@ -1,14 +1,10 @@ import type { IMessageExtender } from '@rocket.chat/apps-engine/definition/accessors/IMessageExtender'; import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage'; import type { IMessageAttachment } from '@rocket.chat/apps-engine/definition/messages/IMessageAttachment'; -import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; - -const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { - RocketChatAssociationModel: typeof _RocketChatAssociationModel; -}; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js'; export class MessageExtender implements IMessageExtender { - public readonly kind: _RocketChatAssociationModel.MESSAGE; + public readonly kind: RocketChatAssociationModel.MESSAGE; constructor(private msg: IMessage) { this.kind = RocketChatAssociationModel.MESSAGE; diff --git a/packages/apps/node-runtime/src/lib/accessors/extenders/RoomExtender.ts b/packages/apps/node-runtime/src/lib/accessors/extenders/RoomExtender.ts index 0fa1637c36bb8..1a59081151c0c 100644 --- a/packages/apps/node-runtime/src/lib/accessors/extenders/RoomExtender.ts +++ b/packages/apps/node-runtime/src/lib/accessors/extenders/RoomExtender.ts @@ -1,14 +1,10 @@ import type { IRoomExtender } from '@rocket.chat/apps-engine/definition/accessors/IRoomExtender'; -import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser'; -const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { - RocketChatAssociationModel: typeof _RocketChatAssociationModel; -}; - export class RoomExtender implements IRoomExtender { - public kind: _RocketChatAssociationModel.ROOM; + public kind: RocketChatAssociationModel.ROOM; private members: Array; diff --git a/packages/apps/node-runtime/src/lib/accessors/extenders/VideoConferenceExtend.ts b/packages/apps/node-runtime/src/lib/accessors/extenders/VideoConferenceExtend.ts index 2442d1133c237..84552f3f4a92a 100644 --- a/packages/apps/node-runtime/src/lib/accessors/extenders/VideoConferenceExtend.ts +++ b/packages/apps/node-runtime/src/lib/accessors/extenders/VideoConferenceExtend.ts @@ -1,14 +1,10 @@ import type { IVideoConferenceExtender } from '@rocket.chat/apps-engine/definition/accessors/IVideoConferenceExtend'; -import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js'; import type { VideoConference, VideoConferenceMember } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConference'; import type { IVideoConferenceUser } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConferenceUser'; -const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { - RocketChatAssociationModel: typeof _RocketChatAssociationModel; -}; - export class VideoConferenceExtender implements IVideoConferenceExtender { - public kind: _RocketChatAssociationModel.VIDEO_CONFERENCE; + public kind: RocketChatAssociationModel.VIDEO_CONFERENCE; constructor(private videoConference: VideoConference) { this.kind = RocketChatAssociationModel.VIDEO_CONFERENCE; diff --git a/packages/apps/node-runtime/src/lib/accessors/modify/ModifyCreator.ts b/packages/apps/node-runtime/src/lib/accessors/modify/ModifyCreator.ts index 8b877419accea..9fa06cbe04ba2 100644 --- a/packages/apps/node-runtime/src/lib/accessors/modify/ModifyCreator.ts +++ b/packages/apps/node-runtime/src/lib/accessors/modify/ModifyCreator.ts @@ -12,12 +12,12 @@ import type { IUploadCreator } from '@rocket.chat/apps-engine/definition/accesso import type { IUserBuilder } from '@rocket.chat/apps-engine/definition/accessors/IUserBuilder'; import type { IVideoConferenceBuilder } from '@rocket.chat/apps-engine/definition/accessors/IVideoConferenceBuilder'; import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage'; -import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; -import type { RoomType as _RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType'; +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType.js'; import type { IBotUser } from '@rocket.chat/apps-engine/definition/users/IBotUser'; import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser'; -import type { UserType as _UserType } from '@rocket.chat/apps-engine/definition/users/UserType'; +import { UserType } from '@rocket.chat/apps-engine/definition/users/UserType.js'; import { AppObjectRegistry } from '../../../AppObjectRegistry'; import type * as Messenger from '../../messenger'; @@ -33,12 +33,6 @@ import type { AppVideoConference } from '../builders/VideoConferenceBuilder'; import { VideoConferenceBuilder } from '../builders/VideoConferenceBuilder'; import { formatErrorResponse } from '../formatResponseErrorHandler'; -const { RoomType } = require('@rocket.chat/apps-engine/definition/rooms/RoomType.js') as { RoomType: typeof _RoomType }; -const { UserType } = require('@rocket.chat/apps-engine/definition/users/UserType.js') as { UserType: typeof _UserType }; -const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { - RocketChatAssociationModel: typeof _RocketChatAssociationModel; -}; - export class ModifyCreator implements IModifyCreator { constructor(private readonly senderFn: typeof Messenger.sendRequest) {} diff --git a/packages/apps/node-runtime/src/lib/accessors/modify/ModifyExtender.ts b/packages/apps/node-runtime/src/lib/accessors/modify/ModifyExtender.ts index 0f94deacbd626..8ee4762666bdc 100644 --- a/packages/apps/node-runtime/src/lib/accessors/modify/ModifyExtender.ts +++ b/packages/apps/node-runtime/src/lib/accessors/modify/ModifyExtender.ts @@ -3,7 +3,7 @@ import type { IModifyExtender } from '@rocket.chat/apps-engine/definition/access import type { IRoomExtender } from '@rocket.chat/apps-engine/definition/accessors/IRoomExtender'; import type { IVideoConferenceExtender } from '@rocket.chat/apps-engine/definition/accessors/IVideoConferenceExtend'; import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage'; -import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser'; import type { VideoConference } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConference'; @@ -15,10 +15,6 @@ import { RoomExtender } from '../extenders/RoomExtender'; import { VideoConferenceExtender } from '../extenders/VideoConferenceExtend'; import { formatErrorResponse } from '../formatResponseErrorHandler'; -const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { - RocketChatAssociationModel: typeof _RocketChatAssociationModel; -}; - export class ModifyExtender implements IModifyExtender { constructor(private readonly senderFn: typeof Messenger.sendRequest) {} diff --git a/packages/apps/node-runtime/src/lib/accessors/modify/ModifyUpdater.ts b/packages/apps/node-runtime/src/lib/accessors/modify/ModifyUpdater.ts index cb20e061189ba..c4353b878a319 100644 --- a/packages/apps/node-runtime/src/lib/accessors/modify/ModifyUpdater.ts +++ b/packages/apps/node-runtime/src/lib/accessors/modify/ModifyUpdater.ts @@ -6,9 +6,9 @@ import type { IModifyUpdater } from '@rocket.chat/apps-engine/definition/accesso import type { IRoomBuilder } from '@rocket.chat/apps-engine/definition/accessors/IRoomBuilder'; import type { IUserUpdater } from '@rocket.chat/apps-engine/definition/accessors/IUserUpdater'; import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage'; -import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; -import type { RoomType as _RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType'; +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType.js'; import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser'; import { AppObjectRegistry } from '../../../AppObjectRegistry'; @@ -17,11 +17,6 @@ import { MessageBuilder } from '../builders/MessageBuilder'; import { RoomBuilder } from '../builders/RoomBuilder'; import { formatErrorResponse } from '../formatResponseErrorHandler'; -const { RoomType } = require('@rocket.chat/apps-engine/definition/rooms/RoomType.js') as { RoomType: typeof _RoomType }; -const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { - RocketChatAssociationModel: typeof _RocketChatAssociationModel; -}; - export class ModifyUpdater implements IModifyUpdater { private readonly livechatUpdater: ILivechatUpdater; diff --git a/packages/apps/node-runtime/src/lib/accessors/notifier.ts b/packages/apps/node-runtime/src/lib/accessors/notifier.ts index a53f01ea1ebdf..bfec20353afe5 100644 --- a/packages/apps/node-runtime/src/lib/accessors/notifier.ts +++ b/packages/apps/node-runtime/src/lib/accessors/notifier.ts @@ -1,5 +1,6 @@ import type { IMessageBuilder, INotifier } from '@rocket.chat/apps-engine/definition/accessors'; -import type { ITypingOptions, TypingScope as _TypingScope } from '@rocket.chat/apps-engine/definition/accessors/INotifier'; +import type { ITypingOptions } from '@rocket.chat/apps-engine/definition/accessors/INotifier'; +import { TypingScope } from '@rocket.chat/apps-engine/definition/accessors/INotifier.js'; import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; import type { IUser } from '@rocket.chat/apps-engine/definition/users'; @@ -9,10 +10,6 @@ import { AppObjectRegistry } from '../../AppObjectRegistry'; import type * as Messenger from '../messenger'; import { formatErrorResponse } from './formatResponseErrorHandler'; -const { TypingScope } = require('@rocket.chat/apps-engine/definition/accessors/INotifier.js') as { - TypingScope: typeof _TypingScope; -}; - export class Notifier implements INotifier { private senderFn: typeof Messenger.sendRequest; diff --git a/packages/apps/node-runtime/src/lib/codec.ts b/packages/apps/node-runtime/src/lib/codec.ts index 6319f2fc76fbb..0ca0ecb0c9c49 100644 --- a/packages/apps/node-runtime/src/lib/codec.ts +++ b/packages/apps/node-runtime/src/lib/codec.ts @@ -1,7 +1,7 @@ import { Buffer } from 'node:buffer'; import { decode, Decoder, Encoder, ExtensionCodec } from '@msgpack/msgpack'; -import type { App as _App } from '@rocket.chat/apps-engine/definition/App'; +import { App } from '@rocket.chat/apps-engine/definition/App.js'; import { applySecureFields, type WithSecureFields } from './secureFields'; @@ -9,10 +9,6 @@ const FUNCTION_DISABLER_EXT = 0; const BUFFER_HANDLER_EXT = 1; const SECURE_FIELDS_HANDLER_EXT = 2; -const { App } = require('@rocket.chat/apps-engine/definition/App.js') as { - App: typeof _App; -}; - const extensionCodec = new ExtensionCodec(); extensionCodec.register({ From f7661f3a2e499d238bbe2a0a01a47ad9c1c8ddc9 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Fri, 26 Jun 2026 19:19:50 -0300 Subject: [PATCH 11/11] refactor(apps): drop .js extension on deno-runtime --- .../handlers/app/handleSetStatus.ts | 4 ++-- .../deno-runtime/handlers/listener/handler.ts | 4 ++-- .../deno-runtime/handlers/uikit/handler.ts | 23 ++++++++++++------- .../lib/accessors/builders/BlockBuilder.ts | 7 +++--- .../accessors/builders/DiscussionBuilder.ts | 11 ++++----- .../builders/LivechatMessageBuilder.ts | 10 ++++---- .../lib/accessors/builders/MessageBuilder.ts | 4 ++-- .../lib/accessors/builders/RoomBuilder.ts | 2 +- .../lib/accessors/builders/UserBuilder.ts | 4 ++-- .../builders/VideoConferenceBuilder.ts | 4 ++-- .../accessors/extenders/MessageExtender.ts | 2 +- .../lib/accessors/extenders/RoomExtender.ts | 2 +- .../extenders/VideoConferenceExtend.ts | 3 ++- .../lib/accessors/modify/ModifyCreator.ts | 19 +++++++++------ .../lib/accessors/modify/ModifyExtender.ts | 2 +- .../lib/accessors/modify/ModifyUpdater.ts | 4 ++-- .../deno-runtime/lib/accessors/notifier.ts | 8 +++---- packages/apps/deno-runtime/lib/codec.ts | 2 +- packages/apps/deno-runtime/lib/roomFactory.ts | 4 ++-- 19 files changed, 65 insertions(+), 54 deletions(-) diff --git a/packages/apps/deno-runtime/handlers/app/handleSetStatus.ts b/packages/apps/deno-runtime/handlers/app/handleSetStatus.ts index b9d6e76f6e3f7..fdae2a87f5b4d 100644 --- a/packages/apps/deno-runtime/handlers/app/handleSetStatus.ts +++ b/packages/apps/deno-runtime/handlers/app/handleSetStatus.ts @@ -1,5 +1,5 @@ import type { App } from '@rocket.chat/apps-engine/definition/App'; -import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus.js'; +import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; import { AppObjectRegistry } from '../../AppObjectRegistry'; import { RequestContext } from '../../lib/requestContext'; @@ -12,7 +12,7 @@ export default async function handleSetStatus(request: RequestContext): Promise< throw new Error('Invalid params', { cause: 'invalid_param_type' }); } - const [status] = params as [typeof AppStatus]; + const [status] = params as [AppStatus]; const app = AppObjectRegistry.get('app'); diff --git a/packages/apps/deno-runtime/handlers/listener/handler.ts b/packages/apps/deno-runtime/handlers/listener/handler.ts index 2ca138a3770c2..59da5f7e0e5e5 100644 --- a/packages/apps/deno-runtime/handlers/listener/handler.ts +++ b/packages/apps/deno-runtime/handlers/listener/handler.ts @@ -1,7 +1,7 @@ import type { App } from '@rocket.chat/apps-engine/definition/App'; import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; -import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions/AppsEngineException.js'; +import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions/AppsEngineException'; import { Defined, JsonRpcError } from 'jsonrpc-lite'; import { AppObjectRegistry } from '../../AppObjectRegistry'; @@ -44,7 +44,7 @@ export default async function handleListener(request: RequestContext): Promise { - return this.user.settings; + return this.user.settings!; } public getUser(): Partial { diff --git a/packages/apps/deno-runtime/lib/accessors/builders/VideoConferenceBuilder.ts b/packages/apps/deno-runtime/lib/accessors/builders/VideoConferenceBuilder.ts index a4326ce2304de..77bfafda84d12 100644 --- a/packages/apps/deno-runtime/lib/accessors/builders/VideoConferenceBuilder.ts +++ b/packages/apps/deno-runtime/lib/accessors/builders/VideoConferenceBuilder.ts @@ -1,7 +1,7 @@ import type { IVideoConferenceBuilder } from '@rocket.chat/apps-engine/definition/accessors/IVideoConferenceBuilder'; import type { IGroupVideoConference } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConference'; -import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js'; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; export type AppVideoConference = Pick & { createdBy: IGroupVideoConference['createdBy']['_id']; @@ -19,7 +19,7 @@ export class VideoConferenceBuilder implements IVideoConferenceBuilder { public setData(data: Partial): IVideoConferenceBuilder { this.call = { rid: data.rid!, - createdBy: data.createdBy, + createdBy: data.createdBy!, providerName: data.providerName!, title: data.title!, discussionRid: data.discussionRid, diff --git a/packages/apps/deno-runtime/lib/accessors/extenders/MessageExtender.ts b/packages/apps/deno-runtime/lib/accessors/extenders/MessageExtender.ts index 55b62c207dcc8..ca251bc541dcf 100644 --- a/packages/apps/deno-runtime/lib/accessors/extenders/MessageExtender.ts +++ b/packages/apps/deno-runtime/lib/accessors/extenders/MessageExtender.ts @@ -1,5 +1,5 @@ import type { IMessageExtender } from '@rocket.chat/apps-engine/definition/accessors/IMessageExtender'; -import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js'; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage'; import type { IMessageAttachment } from '@rocket.chat/apps-engine/definition/messages/IMessageAttachment'; diff --git a/packages/apps/deno-runtime/lib/accessors/extenders/RoomExtender.ts b/packages/apps/deno-runtime/lib/accessors/extenders/RoomExtender.ts index 1a59081151c0c..3a22c9277c626 100644 --- a/packages/apps/deno-runtime/lib/accessors/extenders/RoomExtender.ts +++ b/packages/apps/deno-runtime/lib/accessors/extenders/RoomExtender.ts @@ -1,5 +1,5 @@ import type { IRoomExtender } from '@rocket.chat/apps-engine/definition/accessors/IRoomExtender'; -import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js'; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser'; diff --git a/packages/apps/deno-runtime/lib/accessors/extenders/VideoConferenceExtend.ts b/packages/apps/deno-runtime/lib/accessors/extenders/VideoConferenceExtend.ts index ba11b818ce271..bd28fd12c179f 100644 --- a/packages/apps/deno-runtime/lib/accessors/extenders/VideoConferenceExtend.ts +++ b/packages/apps/deno-runtime/lib/accessors/extenders/VideoConferenceExtend.ts @@ -1,7 +1,7 @@ import type { IVideoConferenceExtender } from '@rocket.chat/apps-engine/definition/accessors/IVideoConferenceExtend'; import type { VideoConference, VideoConferenceMember } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConference'; import type { IVideoConferenceUser } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConferenceUser'; -import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js'; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; export class VideoConferenceExtender implements IVideoConferenceExtender { public kind: RocketChatAssociationModel.VIDEO_CONFERENCE; @@ -42,6 +42,7 @@ export class VideoConferenceExtender implements IVideoConferenceExtender { public addUser(userId: VideoConferenceMember['_id'], ts?: VideoConferenceMember['ts']): IVideoConferenceExtender { this.videoConference.users.push({ _id: userId, + // @ts-expect-error - Original typing is frail ts, // Name and username will be loaded automatically by the bridge username: '', diff --git a/packages/apps/deno-runtime/lib/accessors/modify/ModifyCreator.ts b/packages/apps/deno-runtime/lib/accessors/modify/ModifyCreator.ts index 9422f093f2e21..7c0f7cb01402b 100644 --- a/packages/apps/deno-runtime/lib/accessors/modify/ModifyCreator.ts +++ b/packages/apps/deno-runtime/lib/accessors/modify/ModifyCreator.ts @@ -9,20 +9,22 @@ import type { ILivechatCreator } from '@rocket.chat/apps-engine/definition/acces import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; import type { IBotUser } from '@rocket.chat/apps-engine/definition/users/IBotUser'; -import { UserType } from '@rocket.chat/apps-engine/definition/users/UserType.js'; -import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js'; +import { UserType } from '@rocket.chat/apps-engine/definition/users/UserType'; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; import type { IMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/IMessageBuilder'; import type { IRoomBuilder } from '@rocket.chat/apps-engine/definition/accessors/IRoomBuilder'; import type { IUserBuilder } from '@rocket.chat/apps-engine/definition/accessors/IUserBuilder'; import type { IVideoConferenceBuilder } from '@rocket.chat/apps-engine/definition/accessors/IVideoConferenceBuilder'; -import { RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType.js'; +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType'; import type { ILivechatMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/ILivechatMessageBuilder'; +import type { IDiscussionBuilder } from '@rocket.chat/apps-engine/definition/accessors/IDiscussionBuilder'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser'; import * as Messenger from '../../messenger'; import { BlockBuilder } from '../builders/BlockBuilder'; import { MessageBuilder } from '../builders/MessageBuilder'; -import { DiscussionBuilder, IDiscussionBuilder } from '../builders/DiscussionBuilder'; +import { DiscussionBuilder } from '../builders/DiscussionBuilder'; import { ILivechatMessage, LivechatMessageBuilder } from '../builders/LivechatMessageBuilder'; import { RoomBuilder } from '../builders/RoomBuilder'; import { UserBuilder } from '../builders/UserBuilder'; @@ -200,7 +202,7 @@ export class ModifyCreator implements IModifyCreator { case RocketChatAssociationModel.ROOM: return this._finishRoom(builder as IRoomBuilder); case RocketChatAssociationModel.DISCUSSION: - return this._finishDiscussion(builder as IDiscussionBuilder); + return this._finishDiscussion(builder as DiscussionBuilder); case RocketChatAssociationModel.VIDEO_CONFERENCE: return this._finishVideoConference(builder as IVideoConferenceBuilder); case RocketChatAssociationModel.USER: @@ -228,7 +230,7 @@ export class ModifyCreator implements IModifyCreator { throw new Error('Invalid sender assigned to the message.'); } - result.sender = appUser; + result.sender = appUser as IUser; } if (result.blocks?.length) { @@ -272,6 +274,7 @@ export class ModifyCreator implements IModifyCreator { private async _finishRoom(builder: IRoomBuilder): Promise { const result = builder.getRoom(); + // @ts-expect-error - can't conciliate delete result.id; if (!result.type) { @@ -306,8 +309,10 @@ export class ModifyCreator implements IModifyCreator { return String(response.result); } - private async _finishDiscussion(builder: IDiscussionBuilder): Promise { + private async _finishDiscussion(builder: DiscussionBuilder): Promise { const room = builder.getRoom(); + + // @ts-expect-error - can't conciliate delete room.id; if (!room.creator || !room.creator.id) { diff --git a/packages/apps/deno-runtime/lib/accessors/modify/ModifyExtender.ts b/packages/apps/deno-runtime/lib/accessors/modify/ModifyExtender.ts index e35315d1e765e..c8ba7994638f9 100644 --- a/packages/apps/deno-runtime/lib/accessors/modify/ModifyExtender.ts +++ b/packages/apps/deno-runtime/lib/accessors/modify/ModifyExtender.ts @@ -6,7 +6,7 @@ import type { IVideoConferenceExtender } from '@rocket.chat/apps-engine/definiti import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser'; import type { VideoConference } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConference'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; -import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js'; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; import * as Messenger from '../../messenger'; import { AppObjectRegistry } from '../../../AppObjectRegistry'; diff --git a/packages/apps/deno-runtime/lib/accessors/modify/ModifyUpdater.ts b/packages/apps/deno-runtime/lib/accessors/modify/ModifyUpdater.ts index 0903d1c1506f5..1c1ced9be5b59 100644 --- a/packages/apps/deno-runtime/lib/accessors/modify/ModifyUpdater.ts +++ b/packages/apps/deno-runtime/lib/accessors/modify/ModifyUpdater.ts @@ -9,8 +9,8 @@ import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser'; import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; -import { RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType.js'; -import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js'; +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType'; +import { RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations'; import * as Messenger from '../../messenger'; diff --git a/packages/apps/deno-runtime/lib/accessors/notifier.ts b/packages/apps/deno-runtime/lib/accessors/notifier.ts index d88dc82493c84..7dd317d3573d7 100644 --- a/packages/apps/deno-runtime/lib/accessors/notifier.ts +++ b/packages/apps/deno-runtime/lib/accessors/notifier.ts @@ -1,6 +1,6 @@ import type { IMessageBuilder, INotifier } from '@rocket.chat/apps-engine/definition/accessors'; import type { ITypingOptions } from '@rocket.chat/apps-engine/definition/accessors/INotifier'; -import { TypingScope } from '@rocket.chat/apps-engine/definition/accessors/INotifier.js'; +import { TypingScope } from '@rocket.chat/apps-engine/definition/accessors/INotifier'; import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; import type { IUser } from '@rocket.chat/apps-engine/definition/users'; @@ -20,7 +20,7 @@ export class Notifier implements INotifier { if (!message.sender || !message.sender.id) { const appUser = await this.getAppUser(); - message.sender = appUser; + message.sender = appUser!; } await this.callMessageBridge('doNotifyUser', [user, message, AppObjectRegistry.get('id')]); @@ -30,7 +30,7 @@ export class Notifier implements INotifier { if (!message.sender || !message.sender.id) { const appUser = await this.getAppUser(); - message.sender = appUser; + message.sender = appUser!; } await this.callMessageBridge('doNotifyRoom', [room, message, AppObjectRegistry.get('id')]); @@ -74,6 +74,6 @@ export class Notifier implements INotifier { throw formatErrorResponse(err); }); - return response.result; + return response.result as IUser | undefined; } } diff --git a/packages/apps/deno-runtime/lib/codec.ts b/packages/apps/deno-runtime/lib/codec.ts index 0ca0ecb0c9c49..300ea2f60ff03 100644 --- a/packages/apps/deno-runtime/lib/codec.ts +++ b/packages/apps/deno-runtime/lib/codec.ts @@ -1,7 +1,7 @@ import { Buffer } from 'node:buffer'; import { decode, Decoder, Encoder, ExtensionCodec } from '@msgpack/msgpack'; -import { App } from '@rocket.chat/apps-engine/definition/App.js'; +import { App } from '@rocket.chat/apps-engine/definition/App'; import { applySecureFields, type WithSecureFields } from './secureFields'; diff --git a/packages/apps/deno-runtime/lib/roomFactory.ts b/packages/apps/deno-runtime/lib/roomFactory.ts index be643ec774e49..ba3490268a091 100644 --- a/packages/apps/deno-runtime/lib/roomFactory.ts +++ b/packages/apps/deno-runtime/lib/roomFactory.ts @@ -22,8 +22,8 @@ const getMockAppManager = (senderFn: AppAccessors['senderFn']) => ({ }), }); -export default function createRoom(room: IRoom, senderFn: AppAccessors['senderFn']) { +export default function createRoom(room: IRoom, senderFn: AppAccessors['senderFn']): IRoom { const mockAppManager = getMockAppManager(senderFn); - return new Room(room, mockAppManager as unknown as AppManager); + return new Room(room, mockAppManager as unknown as AppManager) as unknown as IRoom; }