From 5000cc897050327fe5da578686122420e6d6c7d4 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Tue, 16 Apr 2024 16:12:59 -0300 Subject: [PATCH] chore: function patches (#31899) --- .../app/api/server/helpers/getInstanceList.ts | 4 + apps/meteor/app/api/server/v1/instances.ts | 8 +- apps/meteor/ee/server/index.ts | 1 + .../ee/server/patches/getInstanceList.ts | 4 + apps/meteor/ee/server/patches/index.ts | 1 + apps/meteor/package.json | 1 + packages/patch-injection/.eslintrc.json | 4 + packages/patch-injection/jest.config.ts | 3 + packages/patch-injection/package.json | 24 ++ packages/patch-injection/src/addPatch.ts | 17 ++ packages/patch-injection/src/data.ts | 4 + packages/patch-injection/src/definition.ts | 9 + .../patch-injection/src/getFunctionPatches.ts | 15 ++ packages/patch-injection/src/index.ts | 2 + packages/patch-injection/src/makeFunction.ts | 33 +++ .../patch-injection/tests/patches.test.ts | 240 ++++++++++++++++++ packages/patch-injection/tsconfig.json | 9 + yarn.lock | 13 + 18 files changed, 388 insertions(+), 4 deletions(-) create mode 100644 apps/meteor/app/api/server/helpers/getInstanceList.ts create mode 100644 apps/meteor/ee/server/patches/getInstanceList.ts create mode 100644 apps/meteor/ee/server/patches/index.ts create mode 100644 packages/patch-injection/.eslintrc.json create mode 100644 packages/patch-injection/jest.config.ts create mode 100644 packages/patch-injection/package.json create mode 100644 packages/patch-injection/src/addPatch.ts create mode 100644 packages/patch-injection/src/data.ts create mode 100644 packages/patch-injection/src/definition.ts create mode 100644 packages/patch-injection/src/getFunctionPatches.ts create mode 100644 packages/patch-injection/src/index.ts create mode 100644 packages/patch-injection/src/makeFunction.ts create mode 100644 packages/patch-injection/tests/patches.test.ts create mode 100644 packages/patch-injection/tsconfig.json diff --git a/apps/meteor/app/api/server/helpers/getInstanceList.ts b/apps/meteor/app/api/server/helpers/getInstanceList.ts new file mode 100644 index 000000000000..d63cb1ef750e --- /dev/null +++ b/apps/meteor/app/api/server/helpers/getInstanceList.ts @@ -0,0 +1,4 @@ +import { makeFunction } from '@rocket.chat/patch-injection'; +import type { BrokerNode } from 'moleculer'; + +export const getInstanceList = makeFunction(async (): Promise => []); diff --git a/apps/meteor/app/api/server/v1/instances.ts b/apps/meteor/app/api/server/v1/instances.ts index 7b3482e0a7b8..e5404ab3e53c 100644 --- a/apps/meteor/app/api/server/v1/instances.ts +++ b/apps/meteor/app/api/server/v1/instances.ts @@ -1,16 +1,16 @@ import { InstanceStatus } from '@rocket.chat/models'; -import { Instance as InstanceService } from '../../../../ee/server/sdk'; import { isRunningMs } from '../../../../server/lib/isRunningMs'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { API } from '../api'; +import { getInstanceList } from '../helpers/getInstanceList'; -const getMatrixInstances = (() => { +const getConnections = (() => { if (isRunningMs()) { return () => []; } - return () => InstanceService.getInstances(); + return () => getInstanceList(); })(); API.v1.addRoute( @@ -24,7 +24,7 @@ API.v1.addRoute( const instanceRecords = await InstanceStatus.find().toArray(); - const connections = await getMatrixInstances(); + const connections = await getConnections(); const result = instanceRecords.map((instanceRecord) => { const connection = connections.find((c) => c.id === instanceRecord._id); diff --git a/apps/meteor/ee/server/index.ts b/apps/meteor/ee/server/index.ts index 0a9ad57cb00f..744de6f0c3ca 100644 --- a/apps/meteor/ee/server/index.ts +++ b/apps/meteor/ee/server/index.ts @@ -13,5 +13,6 @@ import './configuration/index'; import './local-services/ldap/service'; import './methods/getReadReceipts'; import './apps/startup'; +import './patches'; export { registerEEBroker } from './startup'; diff --git a/apps/meteor/ee/server/patches/getInstanceList.ts b/apps/meteor/ee/server/patches/getInstanceList.ts new file mode 100644 index 000000000000..e458c3e57992 --- /dev/null +++ b/apps/meteor/ee/server/patches/getInstanceList.ts @@ -0,0 +1,4 @@ +import { getInstanceList } from '../../../app/api/server/helpers/getInstanceList'; +import { Instance } from '../sdk'; + +getInstanceList.patch(() => Instance.getInstances()); diff --git a/apps/meteor/ee/server/patches/index.ts b/apps/meteor/ee/server/patches/index.ts new file mode 100644 index 000000000000..a477c01e5a35 --- /dev/null +++ b/apps/meteor/ee/server/patches/index.ts @@ -0,0 +1 @@ +import './getInstanceList'; diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 444a56dd836f..34a7e6f0abd5 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -266,6 +266,7 @@ "@rocket.chat/omnichannel-services": "workspace:^", "@rocket.chat/onboarding-ui": "~0.33.3", "@rocket.chat/password-policies": "workspace:^", + "@rocket.chat/patch-injection": "workspace:^", "@rocket.chat/pdf-worker": "workspace:^", "@rocket.chat/poplib": "workspace:^", "@rocket.chat/presence": "workspace:^", diff --git a/packages/patch-injection/.eslintrc.json b/packages/patch-injection/.eslintrc.json new file mode 100644 index 000000000000..a83aeda48e66 --- /dev/null +++ b/packages/patch-injection/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "extends": ["@rocket.chat/eslint-config"], + "ignorePatterns": ["**/dist"] +} diff --git a/packages/patch-injection/jest.config.ts b/packages/patch-injection/jest.config.ts new file mode 100644 index 000000000000..959a31a7c6bf --- /dev/null +++ b/packages/patch-injection/jest.config.ts @@ -0,0 +1,3 @@ +export default { + preset: 'ts-jest', +}; diff --git a/packages/patch-injection/package.json b/packages/patch-injection/package.json new file mode 100644 index 000000000000..e90f662ac202 --- /dev/null +++ b/packages/patch-injection/package.json @@ -0,0 +1,24 @@ +{ + "name": "@rocket.chat/patch-injection", + "version": "0.0.1", + "private": true, + "devDependencies": { + "@types/jest": "~29.5.7", + "eslint": "~8.45.0", + "jest": "~29.6.4", + "ts-jest": "~29.1.1", + "typescript": "~5.3.3" + }, + "scripts": { + "lint": "eslint --ext .js,.jsx,.ts,.tsx .", + "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "test": "jest", + "build": "rm -rf dist && tsc -p tsconfig.json", + "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput" + }, + "main": "./dist/index.js", + "typings": "./dist/index.d.ts", + "files": [ + "/dist" + ] +} diff --git a/packages/patch-injection/src/addPatch.ts b/packages/patch-injection/src/addPatch.ts new file mode 100644 index 000000000000..80b39cd73ff6 --- /dev/null +++ b/packages/patch-injection/src/addPatch.ts @@ -0,0 +1,17 @@ +import type { BaseFunction, PatchData, PatchFunction } from './definition'; +import { getFunctionPatches } from './getFunctionPatches'; + +export const addPatch = (baseFunction: T, patch: PatchFunction, condition?: () => boolean) => { + const patches = getFunctionPatches(baseFunction); + + const patchData: PatchData = { + patchFunction: patch, + condition, + }; + + patches.add(patchData); + + return () => { + patches.delete(patchData); + }; +}; diff --git a/packages/patch-injection/src/data.ts b/packages/patch-injection/src/data.ts new file mode 100644 index 000000000000..b0c9c371dbd8 --- /dev/null +++ b/packages/patch-injection/src/data.ts @@ -0,0 +1,4 @@ +import type { BaseFunction, PatchData } from './definition'; + +export const functions = new Map>>(); +export const calledFunctions = new Set(); diff --git a/packages/patch-injection/src/definition.ts b/packages/patch-injection/src/definition.ts new file mode 100644 index 000000000000..6a3b56c0c308 --- /dev/null +++ b/packages/patch-injection/src/definition.ts @@ -0,0 +1,9 @@ +export type BaseFunction = (...args: any[]) => any; +export type PatchFunction = (next: T, ...args: Parameters) => ReturnType; +export type PatchData = { + patchFunction: PatchFunction; + condition?: () => boolean; +}; +export type PatchedFunction = T & { + patch: (patch: PatchFunction, condition?: () => boolean) => () => void; +}; diff --git a/packages/patch-injection/src/getFunctionPatches.ts b/packages/patch-injection/src/getFunctionPatches.ts new file mode 100644 index 000000000000..ad92d6f3cbaa --- /dev/null +++ b/packages/patch-injection/src/getFunctionPatches.ts @@ -0,0 +1,15 @@ +import { calledFunctions, functions } from './data'; +import type { BaseFunction, PatchData } from './definition'; + +export const getFunctionPatches = (baseFunction: T): Set> => { + if (calledFunctions.has(baseFunction)) { + throw new Error('Patching a function that was already used.'); + } + + const patches = functions.get(baseFunction) as Set> | undefined; + if (!patches) { + throw new Error('Specified function can not be patched'); + } + + return patches; +}; diff --git a/packages/patch-injection/src/index.ts b/packages/patch-injection/src/index.ts new file mode 100644 index 000000000000..405790427177 --- /dev/null +++ b/packages/patch-injection/src/index.ts @@ -0,0 +1,2 @@ +export * from './definition'; +export * from './makeFunction'; diff --git a/packages/patch-injection/src/makeFunction.ts b/packages/patch-injection/src/makeFunction.ts new file mode 100644 index 000000000000..a32ff8bc5b20 --- /dev/null +++ b/packages/patch-injection/src/makeFunction.ts @@ -0,0 +1,33 @@ +import { addPatch } from './addPatch'; +import { calledFunctions, functions } from './data'; +import type { BaseFunction, PatchData, PatchFunction, PatchedFunction } from './definition'; + +export const makeFunction = (fn: T): PatchedFunction => { + const patches = new Set>(); + + patches.add({ + patchFunction: (_next, ...args) => fn(...args), + }); + + const result = ((...args: Parameters): ReturnType => { + let newFn: T = fn; + + for (const patch of patches) { + if (patch.condition && !patch.condition()) { + continue; + } + + const nextFn = newFn; + newFn = ((...args: Parameters) => patch.patchFunction(nextFn, ...args)) as T; + } + + calledFunctions.add(result); + return newFn(...args); + }) as PatchedFunction; + + functions.set(result, patches as Set>); + + result.patch = (patch: PatchFunction, condition?: () => boolean) => addPatch(result, patch, condition); + + return result; +}; diff --git a/packages/patch-injection/tests/patches.test.ts b/packages/patch-injection/tests/patches.test.ts new file mode 100644 index 000000000000..74e3da93268a --- /dev/null +++ b/packages/patch-injection/tests/patches.test.ts @@ -0,0 +1,240 @@ +import { expect } from 'chai'; + +import { makeFunction } from '../src/makeFunction'; + +describe('PatchCoordinator', () => { + describe('Make a simple function', () => { + it('should execute the function passed as argument', () => { + const fn = makeFunction(() => { + throw new Error('function was called'); + }); + + expect(fn).to.throw('function was called'); + }); + + it('should return the result of the internal function', () => { + const fn = makeFunction(() => 15); + + expect(fn()).to.be.equal(15); + }); + }); + + describe('Replace a simple function', () => { + it('should execute the patched function', () => { + const fn = makeFunction(() => { + throw new Error('function was called'); + }); + + fn.patch(() => { + throw new Error('patch was called'); + }); + + expect(fn).to.throw('patch was called'); + }); + + it('should return the result of the patched function', () => { + const fn = makeFunction(() => 15); + + fn.patch(() => 20); + + expect(fn()).to.be.equal(20); + }); + }); + + describe('Remove a patch', () => { + it('should execute the function passed as argument', () => { + const fn = makeFunction(() => { + throw new Error('function was called'); + }); + + const remove = fn.patch(() => { + throw new Error('patch was called'); + }); + + expect(fn).to.throw('patch was called'); + + remove(); + + expect(fn).to.throw('function was called'); + }); + + it('should return the result of the internal function', () => { + const fn = makeFunction(() => 15); + + const remove = fn.patch(() => 20); + + expect(fn()).to.be.equal(20); + + remove(); + + expect(fn()).to.be.equal(15); + }); + }); + + describe('Patch Condition', () => { + it('should execute either function depending if the patch is enabled or not', () => { + const fn = makeFunction(() => { + throw new Error('function was called'); + }); + + let enabled = false; + + fn.patch( + () => { + throw new Error('patch was called'); + }, + () => enabled, + ); + + expect(fn).to.throw('function was called'); + enabled = true; + expect(fn).to.throw('patch was called'); + enabled = false; + expect(fn).to.throw('function was called'); + }); + + it('should return the value of the right function based on the condition', () => { + const fn = makeFunction(() => 15); + + let enabled = false; + + fn.patch( + () => 20, + () => enabled, + ); + + expect(fn()).to.be.equal(15); + enabled = true; + expect(fn()).to.be.equal(20); + enabled = false; + expect(fn()).to.be.equal(15); + }); + }); + + describe('Chained calls', () => { + it('Should call the inner function', () => { + const fn = makeFunction(() => { + throw new Error('function was called'); + }); + + fn.patch((next) => { + next(); + throw new Error('patch was called'); + }); + + expect(fn).to.throw('function was called'); + }); + + it('should return the sum of values', () => { + const fn = makeFunction(() => 15); + + fn.patch((next) => 20 + next()); + + expect(fn()).to.be.equal(35); + }); + + it('should send the parameters in the correct order every time', () => { + const fn = makeFunction((a: string, b: string) => `3=${[a, b].join('')}`); + + fn.patch((next, a, b) => `2=${[a, b].join('')},${next('E', 'F')}`); + fn.patch((next, a, b) => `1=${[a, b].join('')},${next('C', 'D')}`); + + expect(fn('A', 'B')).to.be.equal('1=AB,2=CD,3=EF'); + }); + }); + + describe('Multiple patches', () => { + it('Should call the right version of the function', () => { + const fn = makeFunction(() => { + throw new Error('function was called'); + }); + + const remove = fn.patch(() => { + throw new Error('patch was called'); + }); + + const remove2 = fn.patch(() => { + throw new Error('second patch was called'); + }); + + expect(fn).to.throw('second patch was called'); + remove2(); + expect(fn).to.throw('patch was called'); + remove(); + expect(fn).to.throw('function was called'); + }); + + it('Should respect conditions and removals', () => { + const fn = makeFunction(() => { + throw new Error('function was called'); + }); + + let enabled = true; + let enabled2 = true; + + const remove = fn.patch( + () => { + throw new Error('patch was called'); + }, + () => enabled, + ); + + const remove2 = fn.patch( + () => { + throw new Error('second patch was called'); + }, + () => enabled2, + ); + + expect(fn).to.throw('second patch was called'); + enabled2 = false; + expect(fn).to.throw('patch was called'); + enabled = false; + expect(fn).to.throw('function was called'); + enabled2 = true; + expect(fn).to.throw('second patch was called'); + enabled = true; + remove2(); + expect(fn).to.throw('patch was called'); + remove(); + expect(fn).to.throw('function was called'); + }); + + it('should chain on the right order', () => { + const fn = makeFunction(() => [1]); + + let enabled2 = true; + let enabled3 = true; + let enabled4 = true; + + fn.patch( + (next) => [2].concat(next()), + () => enabled2, + ); + const remove3 = fn.patch( + (next) => [3].concat(next()), + () => enabled3, + ); + fn.patch( + (next) => [4].concat(next()), + () => enabled4, + ); + + expect(fn()).to.be.an('array').deep.equal([4, 3, 2, 1]); + + enabled2 = false; + expect(fn()).to.be.an('array').deep.equal([4, 3, 1]); + enabled4 = false; + expect(fn()).to.be.an('array').deep.equal([3, 1]); + enabled3 = false; + expect(fn()).to.be.an('array').deep.equal([1]); + enabled3 = true; + expect(fn()).to.be.an('array').deep.equal([3, 1]); + remove3(); + expect(fn()).to.be.an('array').deep.equal([1]); + enabled2 = true; + enabled4 = true; + expect(fn()).to.be.an('array').deep.equal([4, 2, 1]); + }); + }); +}); diff --git a/packages/patch-injection/tsconfig.json b/packages/patch-injection/tsconfig.json new file mode 100644 index 000000000000..52e9dd8c4976 --- /dev/null +++ b/packages/patch-injection/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.server.json", + "compilerOptions": { + "declaration": true, + "rootDir": "./src", + "outDir": "./dist" + }, + "include": ["./src/**/*"] +} diff --git a/yarn.lock b/yarn.lock index 1caf8e658f17..86e7f4ae9ed9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9219,6 +9219,7 @@ __metadata: "@rocket.chat/omnichannel-services": "workspace:^" "@rocket.chat/onboarding-ui": ~0.33.3 "@rocket.chat/password-policies": "workspace:^" + "@rocket.chat/patch-injection": "workspace:^" "@rocket.chat/pdf-worker": "workspace:^" "@rocket.chat/poplib": "workspace:^" "@rocket.chat/presence": "workspace:^" @@ -9704,6 +9705,18 @@ __metadata: languageName: unknown linkType: soft +"@rocket.chat/patch-injection@workspace:^, @rocket.chat/patch-injection@workspace:packages/patch-injection": + version: 0.0.0-use.local + resolution: "@rocket.chat/patch-injection@workspace:packages/patch-injection" + dependencies: + "@types/jest": ~29.5.7 + eslint: ~8.45.0 + jest: ~29.6.4 + ts-jest: ~29.1.1 + typescript: ~5.3.3 + languageName: unknown + linkType: soft + "@rocket.chat/pdf-worker@workspace:^, @rocket.chat/pdf-worker@workspace:ee/packages/pdf-worker": version: 0.0.0-use.local resolution: "@rocket.chat/pdf-worker@workspace:ee/packages/pdf-worker"