@ara-web/sds
Version:
Write code faster without worry about future debts or scalability in the future
241 lines (210 loc) • 9.85 kB
text/typescript
/**
* @module restful-sds
*
* This module provides the `RestfulExtensionOperator` class and related interfaces
* to enable runtime management of SDS (Service Discovery System) extensions via a RESTful API.
*
* It replaces `ExtensionOperator` public methods with Restful
* interface, to dynamically
* add, update, or remove extensions through RESTful operations, and ensures that any custom
* REST handlers provided by extensions are also registered and managed accordingly.
*/
import { OkResult } from "@ara-web/p-hintjens";
import { ModuleLink, type ModuleURL } from "./links/module-link.js";
import { RestHandler as RestHandler, type SubRestfulHandler, RestfulMethod } from "./rest.js";
import type { Meta, ExtensionOperator, ExtendableOperator, Setup, Extendable } from "./sds.js";
import { DOCUMENT_SELECTOR, ObjectNode } from "./tree.js";
export interface RestfulSetup extends Setup {
rootNodeTag: string,
}
/**
* Any Extension must implement the following interface.
* It adds the sub restful handler to handle extension's handlers
*/
export interface RestfulExtension extends Meta, SubRestfulHandler {}
/**
* Wraps the ExtensionOperator to provide
* the synchronization with the rest through RestSynchronizer and RestDispatcher.
*
* Purpose is to allow an SDS Service to have a RESTful API
* to manage the extensions in run-time.
*
* It uses the RestDispatcher to handle the RESTful setters
* and RestSynchronizer to synchronize the data with the rest when this object is updated.
*/
export class RestfulExtensionOperator implements ExtendableOperator {
private _extensionOperator: ExtensionOperator;
private _restHandler: RestHandler;
// To register the extension's own restful data if extension has custom handler.
// private _restDispatcherOperator?: RestDispatcher<any>;
constructor(
serviceLink: ModuleLink,
extTag: string = 'memop',
extOp: ExtensionOperator
) {
if (!serviceLink.isPkgURL) {
throw `Only package url is allowed as the service link`
}
this._extensionOperator = extOp;
const restDispatcherLink = ModuleLink.fromModuleURL(serviceLink.url, {class: `RestfulExtensionOperator`, tag: extTag}).getValue();
this._restHandler = new RestHandler(restDispatcherLink, extTag);
this._restHandler.handlePost = this.handleExtensionAddition.bind(this);
this._restHandler.handlePut = this.handleExtensionUpdate.bind(this);
this._restHandler.handleDelete = this.handleExtensionDeletion.bind(this);
}
public get restHandler(): RestHandler {
return this._restHandler;
}
// public setRestDispatcherOperator(rest: Restful<any>): OkResult {
// const documentElement = rest.rootNode!;
// if (documentElement === undefined) {
// return OkResult.fail(`No document element found, are you sure element exist?`, `Please make sure element exist`);
// }
// if (documentElement.selector !== DOCUMENT_SELECTOR) {
// return OkResult.fail(`The element isn't document selector`, `Can not put element node`);
// }
// this._restDispatcherOperator = rest.dispatcher;
// return OkResult.ok();
// }
/*********************************************************************
*
* Operator's public methods
*
*********************************************************************/
public get extensions(): Readonly<RestfulExtension>[] {
return this._extensionOperator.extensions;
}
public get extensionAmount(): number {
return this._extensionOperator.extensionAmount;
}
("Call rest.post('*')")
public async addExtension(_: RestfulExtension): Promise<OkResult> {return OkResult.ok()}
public getExtension(moduleURL: ModuleURL): RestfulExtension|undefined {
return this._extensionOperator.getExtension(moduleURL);
}
("Call rest.put('*')")
public async updateExtension(_: RestfulExtension): Promise<OkResult> {return OkResult.ok()}
("Call rest.delete('*')")
public async removeExtension(_: RestfulExtension[]): Promise<OkResult> {return OkResult.ok()}
/**
* Registering a new extension in run-time.
* If extension exists, then it throws error asking to use Put.
* @param parentOrBigBro
* @param node
* @param options
* @returns
*/
private async _addExtension(ext: RestfulExtension): Promise<OkResult> {
// if (this._restDispatcherOperator === undefined) {
// return OkResult.fail(`Please pass the rest dispatcher`, `call extensionOperator.setRestDispatcherOperator`);
// }
const added = await this._extensionOperator.addExtension(ext);
if (added.isFailure) {
return OkResult.fail(`super.add(): ${added.errorTitle}`, added.errorDescription!);
}
return OkResult.ok();
}
private async _updateExtension(ext: RestfulExtension): Promise<OkResult> {
const removed = await this._removeExtension([ext]);
if (removed.isFailure) {
return OkResult.fail(`delete('${ext.packageLink}'): ${removed.errorTitle}`, removed.errorDescription!);
}
const added = await this._addExtension(ext);
if (added.isFailure) {
return OkResult.fail(`delete('${ext.packageLink}'): ${added.errorTitle}`, added.errorDescription!);
}
return OkResult.ok();
}
private async _removeExtension(
exts: RestfulExtension[]
): Promise<OkResult> {
const removed = await this._extensionOperator.removeExtension(exts);
if (removed.isFailure) {
return OkResult.fail(`super.delete(): ${removed.errorTitle}`, removed.errorDescription!);
}
return OkResult.ok();
}
/***************************************************
*
* Rest dispatching methods
*
***************************************************/
/**
* Registering a new extension in run-time.
* If extension exists, then it throws error asking to use Put.
* @param parent
* @param node
* @param options
* @returns
*/
private async handleExtensionAddition<DataType>(
parent: ObjectNode<DataType>,
node: ObjectNode<DataType>,
): Promise<OkResult> {
// Filter out the posts
if (parent.selector !== DOCUMENT_SELECTOR) {
return OkResult.ok();
}
// Now, let's make sure it exists
if (!this._restHandler.isMatchingTag(node.selector)) {
return OkResult.fail(`The node is in the root, but it's tag isn't ${this._restHandler.tag}`, `The ${node.selector} expected to be an extension`);
}
const ext = node.data!;
if (ext === null) {
return OkResult.fail(`The packageLink attribute doesn't exist in the data`, `Please update it`);
} else if (!("packageLink" in (ext! as unknown as RestfulExtension))) {
return OkResult.fail(`The packageLink attribute doesn't exist in the data`, `Please update it`);
}
// Explicitly add pending keys, since rest already synchronized.
// Because this function comes from the rest.
const added = await this._addExtension(ext! as unknown as RestfulExtension);
return added;
}
private async handleExtensionUpdate<DataType>(
_selector: string,
node: ObjectNode<DataType>,
data: DataType
): Promise<OkResult> {
// Only children of DOCUMENT_SELECTOR are considered to be extensions.
if (node.parent === undefined || node.parent?.selector !== DOCUMENT_SELECTOR) {
return OkResult.ok();
}
// Now, let's make sure it exists
if (!this._restHandler.isMatchingTag(node.selector)) {
return OkResult.fail(`The node is in the root, but it's tag isn't ${this._restHandler.tag}`, `The ${node.selector} expected to be an extension`);
}
const ext = node.data!
if (ext === null) {
return OkResult.fail(`The element is null`, `Please update the node argument`);
} else if (!("packageLink" in (ext as unknown as RestfulExtension))) {
return OkResult.fail(`The packageLink attribute doesn't exist in the node element`, `Please update the node argument`);
}
if (!("packageLink" in (data as unknown as RestfulExtension))) {
return OkResult.fail(`The packageLink in the putting data`, `Please update the 'data' argument`);
}
const dataPkgLink = (data as unknown as RestfulExtension).packageLink;
const extPkgLink = (ext as unknown as RestfulExtension).packageLink;
if (extPkgLink.isEqual(dataPkgLink)) {
return OkResult.fail(
`The data that you are trying to put has incorrect module url`,
`The extension you are trying to implement has '${extPkgLink}', while data to put has '${dataPkgLink}', please update your data's package link.`
)
}
return await this._updateExtension(data as unknown as RestfulExtension);
}
private async handleExtensionDeletion<DataType>(
_selector: string, nodes: ObjectNode<DataType>[]
): Promise<OkResult> {
const exts = nodes
.filter(node => this._restHandler.isMatchingTag(node.selector))
.map(node => node.data)
.filter(el => el !== null)
.map(el => el as unknown as Extendable)
.filter(el => el.packageLink !== undefined);
if (exts.length === 0) {
return OkResult.ok();
}
const removed = await this._removeExtension(exts);
return removed;
}
}