Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Apps-Engine services #28964

Open
wants to merge 37 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
a3cc5c9
Import services from apps-to-service branch
d-gubert Apr 18, 2023
5b84b57
Merge remote-tracking branch 'origin/develop' into feat/apps-engine-s…
d-gubert Apr 18, 2023
fe7df73
Merge AppsService into AppsEngineService
d-gubert Apr 18, 2023
0f33e60
Adapt api service to microservice serialization
d-gubert Apr 19, 2023
255e54a
Fix types and usages
d-gubert Apr 19, 2023
170b2d3
Merge branch 'develop' into feat/apps-engine-services
d-gubert Apr 19, 2023
68f4d71
Fix startup lock
d-gubert Apr 19, 2023
af4f5a7
Improve apps statistics service types
d-gubert Apr 20, 2023
ecfdc3a
Deserializing AppFabricationFulfillment
d-gubert Apr 20, 2023
4eae25a
Improve comments in AppApiService
d-gubert Apr 20, 2023
6f803ae
Rename apps services
d-gubert Apr 25, 2023
f004770
Register services
d-gubert Apr 25, 2023
9d0cfe4
Merge remote-tracking branch 'origin/develop' into feat/apps-engine-s…
d-gubert Apr 25, 2023
4d3557b
Merge branch 'develop' into feat/apps-engine-services
d-gubert Apr 26, 2023
fbf92cb
Merge branch 'develop' into feat/apps-engine-services
d-gubert Apr 26, 2023
44d733b
Merge branch 'develop' into feat/apps-engine-services
d-gubert Apr 28, 2023
f91052a
Merge branch 'develop' into feat/apps-engine-services
d-gubert May 2, 2023
e3c5183
Merge branch 'develop' into feat/apps-engine-services
d-gubert May 10, 2023
6af3553
Merge branch 'develop' into feat/apps-engine-services
d-gubert May 10, 2023
92abdc6
Merge branch 'develop' into feat/apps-engine-services
d-gubert May 12, 2023
06607df
Merge branch 'develop' into feat/apps-engine-services
tapiarafael May 26, 2023
534748a
Merge branch 'develop' into feat/apps-engine-services
tapiarafael May 31, 2023
74e4de1
Merge branch 'develop' into feat/apps-engine-services
tapiarafael May 31, 2023
7557790
Merge branch 'develop' into feat/apps-engine-services
tapiarafael Jun 5, 2023
ba404cb
Merge branch 'develop' into feat/apps-engine-services
d-gubert Jun 5, 2023
9c1ba65
Merge branch 'develop' into feat/apps-engine-services
tapiarafael Jun 6, 2023
052d76c
Merge branch 'develop' into feat/apps-engine-services
tapiarafael Jun 7, 2023
1673d94
feat: register apps engine services
tapiarafael Jun 14, 2023
c9a7a02
Merge branch 'develop' into feat/apps-engine-services
tapiarafael Jun 14, 2023
98d7c4c
feat: get statistics from apps-engine service
tapiarafael Jun 16, 2023
2075a78
Merge remote-tracking branch 'origin/develop' into feat/apps-engine-s…
d-gubert Jun 25, 2023
0ebe968
Apply change request
d-gubert Jun 25, 2023
8b0cc7b
Merge remote-tracking branch 'origin/develop' into feat/apps-engine-s…
d-gubert Jun 29, 2023
7c694fe
Merge remote-tracking branch 'origin/develop' into feat/apps-engine-s…
d-gubert Jul 26, 2023
2a2f1db
Fix linting errors
d-gubert Jul 27, 2023
b630bb3
Typecheck hiccup
d-gubert Jul 27, 2023
fccb9cf
Further fix linting
d-gubert Jul 27, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions apps/meteor/ee/app/license/server/lib/isUnderAppLimits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ export async function isUnderAppLimits(licenseAppsConfig: NonNullable<ILicense['
return true;
}

const storageItems = await Promise.all(apps.map((app) => Apps.getAppStorageItemById(app.id)));
const activeAppsFromSameSource = storageItems.filter((item) => item && getInstallationSourceFromAppStorageItem(item) === source);
const activeAppsFromSameSource = apps.filter((item) => item && getInstallationSourceFromAppStorageItem(item.storageItem) === source);

const configKey = `max${source.charAt(0).toUpperCase()}${source.slice(1)}Apps` as keyof typeof licenseAppsConfig;
const configLimit = licenseAppsConfig[configKey];
Expand Down
8 changes: 6 additions & 2 deletions apps/meteor/ee/server/apps/orchestrator.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ export class AppServerOrchestrator {
}

getProvidedComponents() {
if (!this.isLoaded()) {
return [];
}

return this._manager.getExternalComponentManager().getProvidedComponents();
}

Expand All @@ -134,7 +138,7 @@ export class AppServerOrchestrator {
}

isLoaded() {
return this.getManager().areAppsLoaded();
return this.isInitialized() && this.getManager().areAppsLoaded();
}

isDebugging() {
Expand All @@ -161,7 +165,7 @@ export class AppServerOrchestrator {
async load() {
// Don't try to load it again if it has
// already been loaded
if (this.isLoaded()) {
if (!this.isInitialized() || this.isLoaded()) {
return;
}

Expand Down
182 changes: 182 additions & 0 deletions apps/meteor/ee/server/apps/services/apiService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import type { RequestMethod } from '@rocket.chat/apps-engine/definition/accessors';
import type { IApiEndpoint, IApiRequest } from '@rocket.chat/apps-engine/definition/api';
import { Router } from 'express';
import type { Request, NextFunction } from 'express';
import type { AppsApiServiceResponse, IAppsApiService, IRequestWithPrivateHash } from '@rocket.chat/core-services';
import { ServiceClass } from '@rocket.chat/core-services';
import type { Serialized } from '@rocket.chat/core-typings';

import { OrchestratorFactory } from './orchestratorFactory';
import type { AppServerOrchestrator } from '../orchestrator';

/* eslint-disable @typescript-eslint/ban-types -- The details of the function are not important here */

/**
* This type is used to replace the Express Response object, as in the service it won't be
* possible to get an instance of the original Response generated by Express.
*
* We use the `resolve` function to return the response to the caller.
*/
type PromiseResponse = {
resolve: (response: AppsApiServiceResponse) => void;
};
/* eslint-enable @typescript-eslint/ban-types */

type IAppsApiRequestHandler = (req: IRequestWithPrivateHash, res: PromiseResponse, next: NextFunction) => void;

interface IAppsApiRouter {
(req: Serialized<Request> | IRequestWithPrivateHash, res: PromiseResponse, next: NextFunction): void;
all(path: string, ...handlers: IAppsApiRequestHandler[]): IAppsApiRouter;
}

export class AppsApiService extends ServiceClass implements IAppsApiService {
protected name = 'apps';

private apps: AppServerOrchestrator;

protected appRouters: Map<string, IAppsApiRouter>;

constructor() {
super();
this.appRouters = new Map();
this.apps = OrchestratorFactory.getOrchestrator();
}

/* ---- ENDPOINT COMMUNICATION METHODS ---- */

/**
* This method triggers the execution of a public route registered by an app.
*
* It is supposed to be called by the ENDPOINT COMMUNICATOR in the core, as it is
* the component that interfaces directly with the Express server.
*
* The returning promise will ALWAYS resolve, even if the route is not found.
d-gubert marked this conversation as resolved.
Show resolved Hide resolved
*
* The way we indicate an error to the caller is by returning a status code.
*
* We expect the caller to appropriately respond to their HTTP request based on
* the status code, headers and body returned
*
* @param req A dry request object, containing only information and no functions
* @returns A promise that resolve to AppsApiServiceResponse type
*/
async handlePublicRequest(req: Serialized<Request>): Promise<AppsApiServiceResponse> {
return new Promise<AppsApiServiceResponse>((resolve) => {
const notFound = () => resolve({ statusCode: 404, body: 'Not found' });

const router = this.appRouters.get(req.params.appId);

if (router) {
return router(req, { resolve }, notFound);
}

notFound();
});
}

/**
* This method triggers the execution of a private route registered by an app.
*
* It is supposed to be called by the ENDPOINT COMMUNICATOR in the core, as it is
* the component that interfaces directly with the Express server.
*
d-gubert marked this conversation as resolved.
Show resolved Hide resolved
* The way we indicate an error to the caller is by returning a status code.
*
* We expect the caller to appropriately respond to their HTTP request based on
* the status code, headers and body returned
*
* @param req A dry request object, containing only information and no functions
* @returns A promise that resolves when the request is done
*/
handlePrivateRequest(req: IRequestWithPrivateHash): Promise<AppsApiServiceResponse> {
return new Promise((resolve) => {
const notFound = () => resolve({ statusCode: 404, body: 'Not found' });

const router = this.appRouters.get(req.params.appId);

if (router) {
req._privateHash = req.params.hash;
return router(req, { resolve }, notFound);
}

notFound();
});
}

/* ---- BRIDGE METHODS ---- */

async registerApi(endpoint: IApiEndpoint, appId: string): Promise<void> {
let router = this.appRouters.get(appId);

if (!router) {
// eslint-disable-next-line new-cap
router = Router() as unknown as IAppsApiRouter;
this.appRouters.set(appId, router);
}

const method = 'all';

let routePath = endpoint.path.trim();
if (!routePath.startsWith('/')) {
routePath = `/${routePath}`;
}

if (router[method] instanceof Function) {
router[method](routePath, this.authMiddleware(!!endpoint.authRequired), this._appApiExecutor(endpoint, appId));
}
}

async unregisterApi(appId: string): Promise<void> {
if (this.appRouters.get(appId)) {
this.appRouters.delete(appId);
}
}

/* ---- PRIVATE METHODS ---- */

private authMiddleware(authRequired: boolean) {
return (req: IRequestWithPrivateHash, res: PromiseResponse, next: NextFunction): void => {
if (!req.user && authRequired) {
return res.resolve({
statusCode: 401,
body: 'Unauthorized',
});
}

next();
};
}

private _appApiExecutor(endpoint: IApiEndpoint, appId: string) {
return (req: IRequestWithPrivateHash, { resolve }: PromiseResponse): void => {
const request: IApiRequest = {
method: req.method.toLowerCase() as RequestMethod,
headers: req.headers as { [key: string]: string },
query: (req.query as { [key: string]: string }) || {},
params: req.params || {},
content: req.body,
privateHash: req._privateHash,
user: req.user && this.apps.getConverters()?.get('users')?.convertToApp(req.user),
d-gubert marked this conversation as resolved.
Show resolved Hide resolved
};

this.apps
.getManager()
?.getApiManager()
.executeApi(appId, endpoint.path, request)
.then(({ status, headers = {}, content }) => {
resolve({
statusCode: status,
headers,
body: content,
});
})
.catch((reason) => {
// Should we handle this as an error?
resolve({
statusCode: 500,
body: reason.message,
});
});
};
}
}
41 changes: 41 additions & 0 deletions apps/meteor/ee/server/apps/services/converterService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms';
import type { IMessage } from '@rocket.chat/apps-engine/definition/messages';
import type { IUser } from '@rocket.chat/apps-engine/definition/users';
import type { IVisitor } from '@rocket.chat/apps-engine/definition/livechat';
import { ServiceClass } from '@rocket.chat/core-services';
import type { IAppsConverterService } from '@rocket.chat/core-services';

import { OrchestratorFactory } from './orchestratorFactory';
import type { AppServerOrchestrator } from '../orchestrator';

export class AppsConverterService extends ServiceClass implements IAppsConverterService {
protected name = 'apps';

private apps: AppServerOrchestrator;

constructor() {
super();

this.apps = OrchestratorFactory.getOrchestrator();
}

async convertRoomById(id: string): Promise<IRoom> {
return this.apps.getConverters()?.get('rooms').convertById(id);
}

async convertMessageById(id: string): Promise<IMessage> {
return this.apps.getConverters()?.get('messages').convertById(id);
}

async convertVistitorByToken(token: string): Promise<IVisitor> {
return this.apps.getConverters()?.get('visitors').convertByToken(token);
}

async convertUserToApp(user: any): Promise<IUser> {
return this.apps.getConverters()?.get('users').convertToApp(user);
}

async convertUserById(id: string): Promise<IUser> {
return this.apps.getConverters()?.get('users').convertById(id);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { AppFabricationFulfillment as AppsEngineAppFabricationFulfillment } from '@rocket.chat/apps-engine/server/compiler';
import type { AppFabricationFulfillment, AppsEngineAppResult } from '@rocket.chat/core-services';

import { transformProxiedAppToAppResult } from './transformProxiedAppToAppResult';

export function transformAppFabricationFulfillment(fulfillment: AppsEngineAppFabricationFulfillment): AppFabricationFulfillment {
return {
appId: fulfillment.getApp().getID(),
appsEngineResult: transformProxiedAppToAppResult(fulfillment.getApp()) as AppsEngineAppResult,
licenseValidationResult: {
errors: fulfillment.getLicenseValidationResult().getErrors() as Record<string, string>,
warnings: fulfillment.getLicenseValidationResult().getWarnings() as Record<string, string>,
},
storageError: fulfillment.getStorageError(),
appUserError: fulfillment.getAppUserError() as { username: string; message: string },
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { ProxiedApp } from '@rocket.chat/apps-engine/server/ProxiedApp';
import type { AppsEngineAppResult } from '@rocket.chat/core-services';

export function transformProxiedAppToAppResult(app?: ProxiedApp): AppsEngineAppResult | undefined {
if (!app) {
return;
}

return {
appId: app.getID(),
currentStatus: app.getStatus(),
storageItem: app.getStorageItem(),
};
}
Loading