From 68e8993fb649a51fbf27309d7a87d3a1d0f55532 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Tue, 31 Oct 2023 20:57:20 -0300 Subject: [PATCH 1/5] wip --- deno-runtime/lib/accessors/mod.ts | 64 +++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 7 deletions(-) diff --git a/deno-runtime/lib/accessors/mod.ts b/deno-runtime/lib/accessors/mod.ts index a8bd88df3..4b2f519f3 100644 --- a/deno-runtime/lib/accessors/mod.ts +++ b/deno-runtime/lib/accessors/mod.ts @@ -1,11 +1,61 @@ +import { IAppAccessors } from '@rocket.chat/apps-engine/definition/accessors/IAppAccessors.ts'; +import { IEnvironmentWrite } from '@rocket.chat/apps-engine/definition/accessors/IEnvironmentWrite.ts'; +import { IEnvironmentRead } from '@rocket.chat/apps-engine/definition/accessors/IEnvironmentRead.ts'; export function proxify(namespace: string) { - return new Proxy({}, { - get(target: unknown, prop: string): unknown { - return (...args: unknown[]) => { - return {}; - }; - } - }) + return new Proxy( + {}, + { + get(target: unknown, prop: string): unknown { + return (...args: unknown[]) => { + return {}; + }; + }, + }, + ); } +export class AppAccessors { + private environmentReader?: IEnvironmentRead; + private defaultAppAccessors?: IAppAccessors; + private environmentWriter?: IEnvironmentWrite; + + constructor(private readonly appId: string) {} + + public getEnvironmentReader() { + if (!this.environmentReader) { + this.environmentReader = { + getSettings: this.getSettingsReader(), + getServerSettings: this.getServerSettingsReader(), + getEnvironmentVariables: this.getEnvironmentVariablesReader(), + } + } + + return this.environmentReader; + } + + public getEnvironmentWriter() { + if (!this.environmentWriter) { + this.environmentWriter = { + getSettings: this.getSettingsUpdater(), + getServerSettings: this.getServerSettingsUpdater(), + } + } + + return this.environmentWriter; + } + + public getDefaultAppAccessors() { + if (!this.defaultAppAccessors) { + this.defaultAppAccessors = { + environmentReader: this.getEnvironmentReader(), + environmentWriter: this.getEnvironmentWriter(), + reader: this.getReader(), + http: this.getHttp(), + providedApiEndpoints: this.getProvidedApiEndpoints(), + } + } + + return this.defaultAppAccessors; + } +} From 37e71bb67d86a53858d63a8ddc325a939ab39d63 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Thu, 2 Nov 2023 17:49:09 -0300 Subject: [PATCH 2/5] wip --- deno-runtime/deno.jsonc | 1 + deno-runtime/lib/accessors/mod.ts | 115 +++++++++++++++----- deno-runtime/lib/messenger.ts | 18 ++- deno-runtime/main.ts | 18 +-- package-lock.json | 5 + package.json | 1 + src/server/runtime/AppsEngineDenoRuntime.ts | 14 ++- 7 files changed, 132 insertions(+), 40 deletions(-) diff --git a/deno-runtime/deno.jsonc b/deno-runtime/deno.jsonc index 56764e1ae..ed778589e 100644 --- a/deno-runtime/deno.jsonc +++ b/deno-runtime/deno.jsonc @@ -1,5 +1,6 @@ { "imports": { + "@rocket.chat/apps-engine/": "./../src/", "acorn": "npm:acorn@8.10.0", "acorn-walk": "npm:acorn-walk@8.2.0", "astring": "npm:astring@1.8.6" diff --git a/deno-runtime/lib/accessors/mod.ts b/deno-runtime/lib/accessors/mod.ts index 4b2f519f3..bcd35d381 100644 --- a/deno-runtime/lib/accessors/mod.ts +++ b/deno-runtime/lib/accessors/mod.ts @@ -1,61 +1,122 @@ import { IAppAccessors } from '@rocket.chat/apps-engine/definition/accessors/IAppAccessors.ts'; import { IEnvironmentWrite } from '@rocket.chat/apps-engine/definition/accessors/IEnvironmentWrite.ts'; import { IEnvironmentRead } from '@rocket.chat/apps-engine/definition/accessors/IEnvironmentRead.ts'; +import { IConfigurationModify } from '@rocket.chat/apps-engine/definition/accessors/IConfigurationModify.ts'; +import { IRead } from '@rocket.chat/apps-engine/definition/accessors/IRead.ts'; +import { IConfigurationExtend } from '@rocket.chat/apps-engine/definition/accessors/IConfigurationExtend.ts'; -export function proxify(namespace: string) { +import * as Messenger from '../messenger.ts'; + +export function proxify(namespace: string): T { return new Proxy( {}, { - get(target: unknown, prop: string): unknown { - return (...args: unknown[]) => { - return {}; - }; - }, + get: + (_target: unknown, prop: string) => + (...params: unknown[]) => + Messenger.sendRequest({ + method: `accessor:${namespace}.${prop}`, + params, + }), }, - ); + ) as T; } export class AppAccessors { - private environmentReader?: IEnvironmentRead; private defaultAppAccessors?: IAppAccessors; private environmentWriter?: IEnvironmentWrite; + private configModifier?: IConfigurationModify; + private configExtender?: IConfigurationExtend; + private reader?: IRead; - constructor(private readonly appId: string) {} + public getEnvironmentRead(namespacePrefix = ''): IEnvironmentRead { + // Not worth it to "cache" this one because of the prefix + return { + getSettings: () => proxify(namespacePrefix + 'environmentRead.getSettings'), + getServerSettings: () => proxify(namespacePrefix + 'environmentRead.getServerSettings'), + getEnvironmentVariables: () => proxify(namespacePrefix + 'environmentRead.getEnvironmentVariables'), + }; + } - public getEnvironmentReader() { - if (!this.environmentReader) { - this.environmentReader = { - getSettings: this.getSettingsReader(), - getServerSettings: this.getServerSettingsReader(), - getEnvironmentVariables: this.getEnvironmentVariablesReader(), - } + public getEnvironmentWrite() { + if (!this.environmentWriter) { + this.environmentWriter = { + getSettings: () => proxify('environmentWrite.getSettings'), + getServerSettings: () => proxify('environmentWrite.getServerSettings'), + }; } - return this.environmentReader; + return this.environmentWriter; } - public getEnvironmentWriter() { - if (!this.environmentWriter) { - this.environmentWriter = { - getSettings: this.getSettingsUpdater(), - getServerSettings: this.getServerSettingsUpdater(), + public getConfigurationModify() { + if (!this.configModifier) { + this.configModifier = { + scheduler: proxify('configurationModify.scheduler'), + slashCommands: proxify('configurationModify.slashCommands'), + serverSettings: proxify('configurationModify.serverSettings'), + }; + } + + return this.configModifier; + } + + public getConifgurationExtend() { + if (!this.configExtender) { + this.configExtender = { + ui: proxify('configurationExtend.ui'), + api: proxify('configurationExtend.api'), + http: proxify('configurationExtend.http'), + settings: proxify('configurationExtend.settings'), + scheduler: proxify('configurationExtend.scheduler'), + slashCommands: proxify('configurationExtend.slashCommands'), + externalComponents: proxify('configurationExtend.externalComponents'), + videoConfProviders: proxify('configurationExtend.videoConfProviders'), } } - return this.environmentWriter; + return this.configExtender; } public getDefaultAppAccessors() { if (!this.defaultAppAccessors) { this.defaultAppAccessors = { - environmentReader: this.getEnvironmentReader(), - environmentWriter: this.getEnvironmentWriter(), + environmentReader: this.getEnvironmentRead(), + environmentWriter: this.getEnvironmentWrite(), reader: this.getReader(), http: this.getHttp(), - providedApiEndpoints: this.getProvidedApiEndpoints(), - } + providedApiEndpoints: proxify('providedApiEndpoints'), + }; } return this.defaultAppAccessors; } + + public getReader() { + if (!this.reader) { + this.reader = { + getEnvironmentReader: () => this.getEnvironmentRead('reader.'), + getMessageReader: () => proxify('reader.getMessageReader'), + getPersistenceReader: () => proxify('reader.getPersistenceReader'), + getRoomReader: () => proxify('reader.getRoomReader'), + getUserReader: () => proxify('reader.getUserReader'), + getNotifier: () => proxify('reader.getNotifier'), + getLivechatReader: () => proxify('reader.getLivechatReader'), + getUploadReader: () => proxify('reader.getUploadReader'), + getCloudWorkspaceReader: () => proxify('reader.getCloudWorkspaceReader'), + getVideoConferenceReader: () => proxify('reader.getVideoConferenceReader'), + getOAuthAppsReader: () => proxify('reader.getOAuthAppsReader'), + getThreadReader: () => proxify('reader.getThreadReader'), + getRoleReader: () => proxify('reader.getRoleReader'), + }; + } + + return this.reader; + } + + public getHttp() { + return proxify('http'); + } } + +export const AppAccessorsInstance = new AppAccessors(); diff --git a/deno-runtime/lib/messenger.ts b/deno-runtime/lib/messenger.ts index 98e50028e..2c64a689c 100644 --- a/deno-runtime/lib/messenger.ts +++ b/deno-runtime/lib/messenger.ts @@ -4,7 +4,7 @@ export type JSONRPC_Message = { export type RequestDescriptor = { method: string; - params: any[]; + params: unknown[]; }; export type Request = JSONRPC_Message & @@ -14,7 +14,7 @@ export type Request = JSONRPC_Message & export type SuccessResponseDescriptor = { id: string; - result: any; + result: unknown; }; export type SuccessResponse = JSONRPC_Message & SuccessResponseDescriptor; @@ -32,6 +32,8 @@ export type ErrorResponse = JSONRPC_Message & ErrorResponseDescriptor; export type Response = SuccessResponse | ErrorResponse; +export type NotificationDescriptor = RequestDescriptor; + export function isJSONRPCMessage(message: object): message is JSONRPC_Message { return 'jsonrpc' in message && message['jsonrpc'] === '2.0-rc'; } @@ -99,7 +101,7 @@ export async function successResponse({ id, result }: SuccessResponseDescriptor) await Deno.stdout.write(encoded); } -export async function sendRequest(requestDescriptor: RequestDescriptor): Promise { +export async function sendRequest(requestDescriptor: RequestDescriptor): Promise { const request: Request = { jsonrpc: '2.0-rc', id: Math.random().toString(36).slice(2), @@ -125,3 +127,13 @@ export async function sendRequest(requestDescriptor: RequestDescriptor): Promise RPCResponseObserver.addEventListener(`response:${request.id}`, handler); }); } + +export function sendNotification(notification: NotificationDescriptor) { + const request = { + jsonrpc: '2.0-rc', + ...notification, + } + + const encoded = encoder.encode(JSON.stringify(request)); + Deno.stdout.write(encoded); +} diff --git a/deno-runtime/main.ts b/deno-runtime/main.ts index 9e50220a8..418c58949 100644 --- a/deno-runtime/main.ts +++ b/deno-runtime/main.ts @@ -10,7 +10,7 @@ if (!Deno.args.includes('--subprocess')) { import { createRequire } from 'node:module'; import { sanitizeDeprecatedUsage } from "./lib/sanitizeDeprecatedUsage.ts"; -import { proxify } from "./lib/accessors/mod.ts"; +import { AppAccessorsInstance, proxify } from "./lib/accessors/mod.ts"; import * as Messenger from "./lib/messenger.ts"; const require = createRequire(import.meta.url); @@ -66,7 +66,7 @@ async function handlInitializeApp({ id, source }: { id: string; source: string } const exports = await wrapAppCode(source)(require); // This is the same naive logic we've been using in the App Compiler const appClass = Object.values(exports)[0] as typeof App; - const app = new appClass({ author: {} }, proxify('logger'), proxify('AppAccessors')); + const app = new appClass({ author: {} }, proxify('logger'), AppAccessorsInstance.getDefaultAppAccessors()); if (typeof app.getName !== 'function') { throw new Error('App must contain a getName function'); @@ -98,9 +98,9 @@ async function handlInitializeApp({ id, source }: { id: string; source: string } async function handleRequest({ method, params, id }: Messenger.Request): Promise { switch (method) { case 'construct': { - const [appId, source] = params; - app = await handlInitializeApp({ id: appId, source }) - Messenger.successResponse(id, { result: "hooray!" }); + const [appId, source] = params as [string, string]; + const app = await handlInitializeApp({ id: appId, source }) + Messenger.successResponse({ id, result: 'ok'}); break; } default: { @@ -113,7 +113,7 @@ async function handleRequest({ method, params, id }: Messenger.Request): Promise } } -async function handleResponse(response: Messenger.Response): Promise { +function handleResponse(response: Messenger.Response) { let event: Event; if (Messenger.isErrorResponse(response)) { @@ -126,14 +126,14 @@ async function handleResponse(response: Messenger.Response): Promise { } async function main() { - setTimeout(() => notifyEngine({ method: 'ready' }), 1_780); + setTimeout(() => Messenger.sendNotification({ method: 'ready', params: null }), 1_780); const decoder = new TextDecoder(); let app: typeof App; for await (const chunk of Deno.stdin.readable) { const message = decoder.decode(chunk); - let JSONRPCMessage + let JSONRPCMessage; try { JSONRPCMessage = JSON.parse(message); @@ -146,7 +146,7 @@ async function main() { } if (Messenger.isResponse(JSONRPCMessage)) { - await handleResponse(JSONRPCMessage); + handleResponse(JSONRPCMessage); } } } diff --git a/package-lock.json b/package-lock.json index 60e3df93f..055288bc9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6845,6 +6845,11 @@ "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", "dev": true }, + "jsonrpc-lite": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/jsonrpc-lite/-/jsonrpc-lite-2.2.0.tgz", + "integrity": "sha512-/cbbSxtZWs1O7R4tWqabrCM/t3N8qKUZMAg9IUqpPvUs6UyRvm6pCNYkskyKN/XU0UgffW+NY2ZRr8t0AknX7g==" + }, "just-debounce": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.1.0.tgz", diff --git a/package.json b/package.json index b6e0fa22f..5b7283744 100644 --- a/package.json +++ b/package.json @@ -95,6 +95,7 @@ "cryptiles": "^4.1.3", "deno-bin": "^1.36.2", "jose": "^4.11.1", + "jsonrpc-lite": "^2.2.0", "lodash.clonedeep": "^4.5.0", "semver": "^5.7.1", "stack-trace": "0.0.10", diff --git a/src/server/runtime/AppsEngineDenoRuntime.ts b/src/server/runtime/AppsEngineDenoRuntime.ts index 743e6d77e..efd9d03df 100644 --- a/src/server/runtime/AppsEngineDenoRuntime.ts +++ b/src/server/runtime/AppsEngineDenoRuntime.ts @@ -29,6 +29,11 @@ export function getDenoWrapperPath(): string { } } +type ControllerDeps = { + accessors: AppAccessorManager; + api: AppApiManager; +}; + export class DenoRuntimeSubprocessController extends EventEmitter { private readonly deno: child_process.ChildProcess; @@ -38,8 +43,12 @@ export class DenoRuntimeSubprocessController extends EventEmitter { private state: 'uninitialized' | 'ready' | 'invalid' | 'unknown'; + private readonly accessors: AppAccessorManager; + + private readonly api: AppApiManager; + // We need to keep the appSource around in case the Deno process needs to be restarted - constructor(private readonly appId: string, private readonly appSource: string) { + constructor(private readonly appId: string, private readonly appSource: string, deps: ControllerDeps) { super(); this.state = 'uninitialized'; @@ -55,6 +64,9 @@ export class DenoRuntimeSubprocessController extends EventEmitter { } catch { this.state = 'invalid'; } + + this.accessors = deps.accessors; + this.api = deps.api; } emit(eventName: string | symbol, ...args: any[]): boolean { From 8033938ebab67441a51bc8eb6079cbdc6b3445bf Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Tue, 7 Nov 2023 14:51:38 -0300 Subject: [PATCH 3/5] Accessors proxified --- deno-runtime/lib/accessors/_test.ts | 80 ++++++++++ deno-runtime/lib/accessors/mod.ts | 139 ++++++++++++------ deno-runtime/lib/ast/tests/operations.test.ts | 2 +- deno-runtime/main.ts | 2 +- 4 files changed, 175 insertions(+), 48 deletions(-) create mode 100644 deno-runtime/lib/accessors/_test.ts diff --git a/deno-runtime/lib/accessors/_test.ts b/deno-runtime/lib/accessors/_test.ts new file mode 100644 index 000000000..903cc99a4 --- /dev/null +++ b/deno-runtime/lib/accessors/_test.ts @@ -0,0 +1,80 @@ +import { 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, getProxify } from "./mod.ts"; + +describe('AppAccessors', () => { + let appAccessors: AppAccessors; + const proxify = getProxify((r) => Promise.resolve({ + id: Math.random().toString(36).substring(2), + jsonrpc: '2.0', + result: r, + serialize() { + return JSON.stringify(this); + } + })); + + beforeEach(() => { + appAccessors = new AppAccessors(proxify); + }); + + it('creates the correct format for IRead calls', async () => { + const roomRead = appAccessors.getReader().getRoomReader(); + const room = await roomRead.getById('123'); + + assertEquals(room.result, { + 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.result, { + 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.result, { + 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.result, { + 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.result, { + params: [{ + command: 'test', + i18nDescription: 'test', + i18nParamsExample: 'test', + providesPreview: true, + }], + method: 'accessor:getConfigurationModify:slashCommands:modifySlashCommand', + }); + }); +}); diff --git a/deno-runtime/lib/accessors/mod.ts b/deno-runtime/lib/accessors/mod.ts index bcd35d381..8f313a1a1 100644 --- a/deno-runtime/lib/accessors/mod.ts +++ b/deno-runtime/lib/accessors/mod.ts @@ -1,21 +1,25 @@ -import { IAppAccessors } from '@rocket.chat/apps-engine/definition/accessors/IAppAccessors.ts'; -import { IEnvironmentWrite } from '@rocket.chat/apps-engine/definition/accessors/IEnvironmentWrite.ts'; -import { IEnvironmentRead } from '@rocket.chat/apps-engine/definition/accessors/IEnvironmentRead.ts'; -import { IConfigurationModify } from '@rocket.chat/apps-engine/definition/accessors/IConfigurationModify.ts'; -import { IRead } from '@rocket.chat/apps-engine/definition/accessors/IRead.ts'; -import { IConfigurationExtend } from '@rocket.chat/apps-engine/definition/accessors/IConfigurationExtend.ts'; +// @ts-ignore - this is a hack to make the tests work +import type { IAppAccessors } from '@rocket.chat/apps-engine/definition/accessors/IAppAccessors.ts'; +import type { IEnvironmentWrite } from '@rocket.chat/apps-engine/definition/accessors/IEnvironmentWrite.ts'; +import type { IEnvironmentRead } from '@rocket.chat/apps-engine/definition/accessors/IEnvironmentRead.ts'; +import type { IConfigurationModify } from '@rocket.chat/apps-engine/definition/accessors/IConfigurationModify.ts'; +import type { IRead } from '@rocket.chat/apps-engine/definition/accessors/IRead.ts'; +import type { IModify } from '@rocket.chat/apps-engine/definition/accessors/IModify.ts'; +import type { IPersistence } from '@rocket.chat/apps-engine/definition/accessors/IPersistence.ts'; +import type { IHttp } from '@rocket.chat/apps-engine/definition/accessors/IHttp.ts'; +import type { IConfigurationExtend } from '@rocket.chat/apps-engine/definition/accessors/IConfigurationExtend.ts'; import * as Messenger from '../messenger.ts'; -export function proxify(namespace: string): T { +export const getProxify = (call: typeof Messenger.sendRequest) => function proxify(namespace: string): T { return new Proxy( - {}, + { __kind: namespace }, // debugging purposes { get: (_target: unknown, prop: string) => (...params: unknown[]) => - Messenger.sendRequest({ - method: `accessor:${namespace}.${prop}`, + call({ + method: `accessor:${namespace}:${prop}`, params, }), }, @@ -24,25 +28,34 @@ export function proxify(namespace: string): T { 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 http?: IHttp; + + constructor(private readonly proxify: (n: string) => T) {} + + public getEnvironmentRead(): IEnvironmentRead { + if (!this.environmentRead) { + this.environmentRead = { + getSettings: () => this.proxify('getEnvironmentRead:getSettings'), + getServerSettings: () => this.proxify('getEnvironmentRead:getServerSettings'), + getEnvironmentVariables: () => this.proxify('getEnvironmentRead:getEnvironmentVariables'), + }; + } - public getEnvironmentRead(namespacePrefix = ''): IEnvironmentRead { - // Not worth it to "cache" this one because of the prefix - return { - getSettings: () => proxify(namespacePrefix + 'environmentRead.getSettings'), - getServerSettings: () => proxify(namespacePrefix + 'environmentRead.getServerSettings'), - getEnvironmentVariables: () => proxify(namespacePrefix + 'environmentRead.getEnvironmentVariables'), - }; + return this.environmentRead; } public getEnvironmentWrite() { if (!this.environmentWriter) { this.environmentWriter = { - getSettings: () => proxify('environmentWrite.getSettings'), - getServerSettings: () => proxify('environmentWrite.getServerSettings'), + getSettings: () => this.proxify('getEnvironmentWrite:getSettings'), + getServerSettings: () => this.proxify('getEnvironmentWrite:getServerSettings'), }; } @@ -52,9 +65,9 @@ export class AppAccessors { public getConfigurationModify() { if (!this.configModifier) { this.configModifier = { - scheduler: proxify('configurationModify.scheduler'), - slashCommands: proxify('configurationModify.slashCommands'), - serverSettings: proxify('configurationModify.serverSettings'), + scheduler: this.proxify('getConfigurationModify:scheduler'), + slashCommands: this.proxify('getConfigurationModify:slashCommands'), + serverSettings: this.proxify('getConfigurationModify:serverSettings'), }; } @@ -64,14 +77,14 @@ export class AppAccessors { public getConifgurationExtend() { if (!this.configExtender) { this.configExtender = { - ui: proxify('configurationExtend.ui'), - api: proxify('configurationExtend.api'), - http: proxify('configurationExtend.http'), - settings: proxify('configurationExtend.settings'), - scheduler: proxify('configurationExtend.scheduler'), - slashCommands: proxify('configurationExtend.slashCommands'), - externalComponents: proxify('configurationExtend.externalComponents'), - videoConfProviders: proxify('configurationExtend.videoConfProviders'), + ui: this.proxify('getConfigurationExtend:ui'), + api: this.proxify('getConfigurationExtend:api'), + http: this.proxify('getConfigurationExtend:http'), + settings: this.proxify('getConfigurationExtend:settings'), + scheduler: this.proxify('getConfigurationExtend:scheduler'), + slashCommands: this.proxify('getConfigurationExtend:slashCommands'), + externalComponents: this.proxify('getConfigurationExtend:externalComponents'), + videoConfProviders: this.proxify('getConfigurationExtend:videoConfProviders'), } } @@ -85,7 +98,7 @@ export class AppAccessors { environmentWriter: this.getEnvironmentWrite(), reader: this.getReader(), http: this.getHttp(), - providedApiEndpoints: proxify('providedApiEndpoints'), + providedApiEndpoints: this.proxify('providedApiEndpoints'), }; } @@ -95,28 +108,62 @@ export class AppAccessors { public getReader() { if (!this.reader) { this.reader = { - getEnvironmentReader: () => this.getEnvironmentRead('reader.'), - getMessageReader: () => proxify('reader.getMessageReader'), - getPersistenceReader: () => proxify('reader.getPersistenceReader'), - getRoomReader: () => proxify('reader.getRoomReader'), - getUserReader: () => proxify('reader.getUserReader'), - getNotifier: () => proxify('reader.getNotifier'), - getLivechatReader: () => proxify('reader.getLivechatReader'), - getUploadReader: () => proxify('reader.getUploadReader'), - getCloudWorkspaceReader: () => proxify('reader.getCloudWorkspaceReader'), - getVideoConferenceReader: () => proxify('reader.getVideoConferenceReader'), - getOAuthAppsReader: () => proxify('reader.getOAuthAppsReader'), - getThreadReader: () => proxify('reader.getThreadReader'), - getRoleReader: () => proxify('reader.getRoleReader'), + 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.proxify('getReader: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'), }; } return this.reader; } + public getModifier() { + if (!this.modifier) { + this.modifier = { + getCreator: () => this.proxify('getModifier:getCreator'), // can't be proxy + getUpdater: () => this.proxify('getModifier:getUpdater'), // can't be proxy + getDeleter: () => this.proxify('getModifier:getDeleter'), + getExtender: () => this.proxify('getModifier:getExtender'), // can't be proxy + getNotifier: () => this.proxify('getModifier: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 proxify('http'); + if (!this.http) { + this.http = this.proxify('getHttp'); + } + + return this.http; } } -export const AppAccessorsInstance = new AppAccessors(); +export const AppAccessorsInstance = new AppAccessors(getProxify(Messenger.sendRequest.bind(Messenger))); diff --git a/deno-runtime/lib/ast/tests/operations.test.ts b/deno-runtime/lib/ast/tests/operations.test.ts index 5758dc21a..2b00c271f 100644 --- a/deno-runtime/lib/ast/tests/operations.test.ts +++ b/deno-runtime/lib/ast/tests/operations.test.ts @@ -17,7 +17,7 @@ import { SimpleCallExpressionOfFoo, SyncFunctionDeclarationWithAsyncCallExpression, } from './data/ast_blocks.ts'; -import { AnyNode, ArrowFunctionExpression, AssignmentExpression, AwaitExpression, CallExpression, Expression, MethodDefinition, ReturnStatement, VariableDeclaration } from '../../../acorn.d.ts'; +import { AnyNode, ArrowFunctionExpression, AssignmentExpression, AwaitExpression, Expression, MethodDefinition, ReturnStatement, VariableDeclaration } from '../../../acorn.d.ts'; import { assertNotEquals } from 'https://deno.land/std@0.203.0/assert/assert_not_equals.ts'; describe('getFunctionIdentifier', () => { diff --git a/deno-runtime/main.ts b/deno-runtime/main.ts index 9b307962d..e19a361e3 100644 --- a/deno-runtime/main.ts +++ b/deno-runtime/main.ts @@ -10,7 +10,7 @@ if (!Deno.args.includes('--subprocess')) { import { createRequire } from 'node:module'; import { sanitizeDeprecatedUsage } from "./lib/sanitizeDeprecatedUsage.ts"; -import { AppAccessorsInstance, proxify } from "./lib/accessors/mod.ts"; +import { AppAccessorsInstance } from "./lib/accessors/mod.ts"; import * as Messenger from "./lib/messenger.ts"; const require = createRequire(import.meta.url); From cbb2a832f455684111e3f65574624a21c4e98f77 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Tue, 7 Nov 2023 19:44:10 -0300 Subject: [PATCH 4/5] Handle accessor incoming request --- src/server/runtime/AppsEngineDenoRuntime.ts | 111 +++++++++++++++++++- 1 file changed, 110 insertions(+), 1 deletion(-) diff --git a/src/server/runtime/AppsEngineDenoRuntime.ts b/src/server/runtime/AppsEngineDenoRuntime.ts index bb93072cf..b37ce9568 100644 --- a/src/server/runtime/AppsEngineDenoRuntime.ts +++ b/src/server/runtime/AppsEngineDenoRuntime.ts @@ -6,12 +6,32 @@ import * as jsonrpc from 'jsonrpc-lite'; import type { AppAccessorManager, AppApiManager } from '../managers'; import type { AppManager } from '../AppManager'; +import { IEnvironmentRead, IEnvironmentWrite, IRead } from '../../definition/accessors'; export type AppRuntimeParams = { appId: string; appSource: string; }; +const ALLOWED_ACCESSOR_METHODS = [ + 'getConfigurationExtend', + 'getEnvironmentRead', + 'getEnvironmentWrite', + 'getConfigurationModify', + 'getReader', + 'getPersistence', + 'getHttp', +] as Array< + keyof Pick< + AppAccessorManager, + 'getConfigurationExtend' | 'getEnvironmentRead' | 'getEnvironmentWrite' | 'getConfigurationModify' | 'getReader' | 'getPersistence' | 'getHttp' + > +>; + +function isValidOrigin(accessor: string): accessor is typeof ALLOWED_ACCESSOR_METHODS[number] { + return ALLOWED_ACCESSOR_METHODS.includes(accessor as any); +} + /** * Resolves the absolute path of the Deno executable * installed by deno-bin. @@ -131,8 +151,97 @@ export class DenoRuntimeSubprocessController extends EventEmitter { this.on('ready', this.onReady.bind(this)); } + // 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:' + const managerOrigin = accessorMethods.shift(); + const tailMethodName = accessorMethods.pop(); + + /** + * 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'); + } + + // Need to fix typing of return value + const getAccessorForOrigin = ( + accessorMethods: string[], + managerOrigin: typeof ALLOWED_ACCESSOR_METHODS[number], + accessorManager: AppAccessorManager, + ) => { + const origin = accessorManager[managerOrigin](this.appId); + + // These will need special treatment + if (managerOrigin === 'getConfigurationExtend' || managerOrigin === 'getConfigurationModify') { + return origin[accessorMethods[0] as keyof typeof origin]; + } + + if (managerOrigin === 'getHttp' || managerOrigin === 'getPersistence') { + return 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'); + } + + 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'); + } + + const result = await tailMethod.apply(accessor, params); + + return jsonrpc.success(id, result); + } + private async handleIncomingMessage(message: jsonrpc.IParsedObjectNotification | jsonrpc.IParsedObjectRequest): Promise { - const { method, id } = message.payload; + const { method } = message.payload; + + if (method.startsWith('accessor:')) { + const result = await this.handleAccessorMessage(message as jsonrpc.IParsedObjectRequest); + + this.deno.stdin.write(result.serialize()); + } switch (method) { case 'ready': From 72906bdcf8a17c0e7adcb6f807c64317ca171976 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Thu, 9 Nov 2023 16:50:00 -0300 Subject: [PATCH 5/5] Add DenoRuntimeSubprocessController tests --- src/server/runtime/AppsEngineDenoRuntime.ts | 16 +-- .../DenoRuntimeSubprocessController.spec.ts | 97 +++++++++++++++++++ tests/test-data/bridges/httpBridge.ts | 8 +- .../test-data/bridges/serverSettingBridge.ts | 13 ++- tests/test-data/bridges/userBridge.ts | 21 +++- tests/test-data/utilities.ts | 48 ++++++++- 6 files changed, 188 insertions(+), 15 deletions(-) create mode 100644 tests/server/runtime/DenoRuntimeSubprocessController.spec.ts diff --git a/src/server/runtime/AppsEngineDenoRuntime.ts b/src/server/runtime/AppsEngineDenoRuntime.ts index b37ce9568..a9400cc1f 100644 --- a/src/server/runtime/AppsEngineDenoRuntime.ts +++ b/src/server/runtime/AppsEngineDenoRuntime.ts @@ -6,7 +6,6 @@ import * as jsonrpc from 'jsonrpc-lite'; import type { AppAccessorManager, AppApiManager } from '../managers'; import type { AppManager } from '../AppManager'; -import { IEnvironmentRead, IEnvironmentWrite, IRead } from '../../definition/accessors'; export type AppRuntimeParams = { appId: string; @@ -103,6 +102,7 @@ export class DenoRuntimeSubprocessController extends EventEmitter { } public getState() { + console.log(this.api); return this.state; } @@ -307,21 +307,21 @@ type ExecRequestContext = { export class AppsEngineDenoRuntime { private readonly subprocesses: Record = {}; - // private readonly accessorManager: AppAccessorManager; + private readonly accessorManager: AppAccessorManager; - // private readonly apiManager: AppApiManager; + private readonly apiManager: AppApiManager; - // constructor(manager: AppManager) { - // this.accessorManager = manager.getAccessorManager(); - // this.apiManager = manager.getApiManager(); - // } + constructor(manager: AppManager) { + this.accessorManager = manager.getAccessorManager(); + this.apiManager = manager.getApiManager(); + } public async startRuntimeForApp({ appId, appSource }: AppRuntimeParams, options = { force: false }): Promise { if (appId in this.subprocesses && !options.force) { throw new Error('App already has an associated runtime'); } - this.subprocesses[appId] = new DenoRuntimeSubprocessController(appId, appSource); + this.subprocesses[appId] = new DenoRuntimeSubprocessController(appId, appSource, { accessors: this.accessorManager, api: this.apiManager }); await this.subprocesses[appId].setupApp(); } diff --git a/tests/server/runtime/DenoRuntimeSubprocessController.spec.ts b/tests/server/runtime/DenoRuntimeSubprocessController.spec.ts new file mode 100644 index 000000000..067dcdc2f --- /dev/null +++ b/tests/server/runtime/DenoRuntimeSubprocessController.spec.ts @@ -0,0 +1,97 @@ +import { TestFixture, Setup, SetupFixture, Expect, AsyncTest } from 'alsatian'; + +import { AppAccessorManager, AppApiManager } from '../../../src/server/managers'; +import { TestData, TestInfastructureSetup } from '../../test-data/utilities'; +import { DenoRuntimeSubprocessController } from '../../../src/server/runtime/AppsEngineDenoRuntime'; + +@TestFixture('DenoRuntimeSubprocessController') +export class DenuRuntimeSubprocessControllerTestFixture { + private accessors: AppAccessorManager; + + private api: AppApiManager; + + private simpleAppSource = 'module.exports={ default: new class { constructor() { this.name = "parangarico" } } };console.log("hi from app")'; + + private controller: DenoRuntimeSubprocessController; + + @SetupFixture + public fixture() { + const infrastructure = new TestInfastructureSetup(); + const manager = infrastructure.getMockManager(); + + this.accessors = new AppAccessorManager(manager); + + manager.getAccessorManager = () => this.accessors; + + this.api = new AppApiManager(manager); + + manager.getApiManager = () => this.api; + } + + @Setup + public setup() { + const app = TestData.getMockApp('deno-controller', 'Deno Controller test'); + + this.controller = new DenoRuntimeSubprocessController(app.getID(), this.simpleAppSource, { accessors: this.accessors, api: this.api }); + } + + @AsyncTest('correctly identifies a call to the HTTP accessor') + public async testHttpAccessor() { + // eslint-disable-next-line + const r = await this.controller['handleAccessorMessage']({ + type: 'request' as any, + payload: { + jsonrpc: '2.0', + id: 'test', + method: 'accessor:getHttp:get', + params: ['https://google.com', { content: "{ test: 'test' }" }], + serialize: () => '', + }, + }); + + Expect(r.result).toEqual({ + method: 'get', + url: 'https://google.com', + content: "{ test: 'test' }", + statusCode: 200, + headers: {}, + }); + } + + @AsyncTest('correctly identifies a call to the IRead accessor') + public async testIReadAccessor() { + // eslint-disable-next-line + const { id, result } = await this.controller['handleAccessorMessage']({ + type: 'request' as any, + payload: { + jsonrpc: '2.0', + id: 'test', + method: 'accessor:getReader:getUserReader:getByUsername', + params: ['rocket.cat'], + serialize: () => '', + }, + }); + + Expect(id).toBe('test'); + Expect((result as any).username).toEqual('rocket.cat'); + Expect((result as any).appId).toEqual('deno-controller'); + } + + @AsyncTest('correctly identifies a call to the IEnvironmentReader accessor via IRead') + public async testIEnvironmentReaderAccessor() { + // eslint-disable-next-line + const { id, result } = await this.controller['handleAccessorMessage']({ + type: 'request' as any, + payload: { + jsonrpc: '2.0', + id: 'requestId', + method: 'accessor:getReader:getEnvironmentReader:getServerSettings:getOneById', + params: ['setting test id'], + serialize: () => '', + }, + }); + + Expect(id).toBe('requestId'); + Expect((result as any).id).toEqual('setting test id'); + } +} diff --git a/tests/test-data/bridges/httpBridge.ts b/tests/test-data/bridges/httpBridge.ts index fc08a1ce6..396658f39 100644 --- a/tests/test-data/bridges/httpBridge.ts +++ b/tests/test-data/bridges/httpBridge.ts @@ -4,6 +4,12 @@ import { HttpBridge } from '../../../src/server/bridges'; export class TestsHttpBridge extends HttpBridge { public call(info: IHttpBridgeRequestInfo): Promise { - throw new Error('Method not implemented.'); + return Promise.resolve({ + url: info.url, + method: info.method, + statusCode: 200, + headers: info.request.headers, + content: info.request.content, + }); } } diff --git a/tests/test-data/bridges/serverSettingBridge.ts b/tests/test-data/bridges/serverSettingBridge.ts index 938f2de97..74e6e1f71 100644 --- a/tests/test-data/bridges/serverSettingBridge.ts +++ b/tests/test-data/bridges/serverSettingBridge.ts @@ -1,4 +1,4 @@ -import type { ISetting } from '../../../src/definition/settings'; +import { SettingType, type ISetting } from '../../../src/definition/settings'; import { ServerSettingBridge } from '../../../src/server/bridges'; export class TestsServerSettingBridge extends ServerSettingBridge { @@ -7,7 +7,16 @@ export class TestsServerSettingBridge extends ServerSettingBridge { } public getOneById(id: string, appId: string): Promise { - throw new Error('Method not implemented.'); + return Promise.resolve({ + id, + packageValue: 'packageValue', + value: 'value', + i18nLabel: 'i18nLabel', + i18nDescription: 'i18nDescription', + required: true, + public: true, + type: SettingType.STRING, + }); } public hideGroup(name: string): Promise { diff --git a/tests/test-data/bridges/userBridge.ts b/tests/test-data/bridges/userBridge.ts index c9399f5b6..79cf75b75 100644 --- a/tests/test-data/bridges/userBridge.ts +++ b/tests/test-data/bridges/userBridge.ts @@ -1,4 +1,5 @@ -import type { IUser, UserType } from '../../../src/definition/users'; +import type { IUser } from '../../../src/definition/users'; +import { UserStatusConnection, UserType } from '../../../src/definition/users'; import { UserBridge } from '../../../src/server/bridges'; export class TestsUserBridge extends UserBridge { @@ -7,7 +8,23 @@ export class TestsUserBridge extends UserBridge { } public getByUsername(username: string, appId: string): Promise { - throw new Error('Method not implemented.'); + return Promise.resolve({ + id: 'id', + username, + isEnabled: true, + emails: [], + name: 'name', + roles: [], + type: UserType.USER, + active: true, + appId, + utcOffset: 0, + status: 'offline', + statusConnection: UserStatusConnection.OFFLINE, + lastLoginAt: new Date(), + createdAt: new Date(), + updatedAt: new Date(), + }); } public create(user: Partial): Promise { diff --git a/tests/test-data/utilities.ts b/tests/test-data/utilities.ts index 5f3e714a0..8e4f90987 100644 --- a/tests/test-data/utilities.ts +++ b/tests/test-data/utilities.ts @@ -26,6 +26,14 @@ import type { AppManager } from '../../src/server/AppManager'; import type { AppBridges } from '../../src/server/bridges'; import { ProxiedApp } from '../../src/server/ProxiedApp'; import type { AppLogStorage, AppMetadataStorage, AppSourceStorage, IAppStorageItem } from '../../src/server/storage'; +import type { + AppExternalComponentManager, + AppSchedulerManager, + AppSettingsManager, + AppSlashCommandManager, + AppVideoConfProviderManager, +} from '../../src/server/managers'; +import type { UIActionButtonManager } from '../../src/server/managers/UIActionButtonManager'; export class TestInfastructureSetup { private appStorage: TestsAppStorage; @@ -36,11 +44,43 @@ export class TestInfastructureSetup { private sourceStorage: TestSourceStorage; + private appManager: AppManager; + constructor() { this.appStorage = new TestsAppStorage(); this.logStorage = new TestsAppLogStorage(); this.bridges = new TestsAppBridges(); this.sourceStorage = new TestSourceStorage(); + + this.appManager = { + getBridges: () => { + return this.bridges as AppBridges; + }, + getCommandManager() { + return {} as AppSlashCommandManager; + }, + getExternalComponentManager() { + return {} as AppExternalComponentManager; + }, + getOneById(appId: string): ProxiedApp { + return appId === 'failMePlease' ? undefined : TestData.getMockApp(appId, 'testing'); + }, + getLogStorage(): AppLogStorage { + return new TestsAppLogStorage(); + }, + getSchedulerManager() { + return {} as AppSchedulerManager; + }, + getUIActionButtonManager() { + return {} as UIActionButtonManager; + }, + getVideoConfProviderManager() { + return {} as AppVideoConfProviderManager; + }, + getSettingsManager() { + return {} as AppSettingsManager; + }, + } as AppManager; } public getAppStorage(): AppMetadataStorage { @@ -58,6 +98,10 @@ export class TestInfastructureSetup { public getSourceStorage(): AppSourceStorage { return this.sourceStorage; } + + public getMockManager(): AppManager { + return this.appManager; + } } const date = new Date(); @@ -377,10 +421,10 @@ export class TestData { { status: AppStatus.UNKNOWN } as IAppStorageItem, { getName() { - return 'testing'; + return name; }, getID() { - return 'testing'; + return id; }, getRuntime() { return { runInSandbox: (mod: string) => mod };