Spaces:
Running
Running
File size: 7,657 Bytes
b39afbe |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 |
/**
* Copyright (c) 2023 MERCENARIES.AI PTE. LTD.
* All rights reserved.
*/
/* eslint-disable @typescript-eslint/ban-types */
// -------------------------------------------------------------
//
// -------------------------------------------------------------
import { type IManager } from '../core/Manager.js';
import { Service, type IServiceConfig } from '../core/Service.js';
import axios from 'axios';
interface IAPIServiceConfig extends IServiceConfig {
host: string;
integrationsUrl: string;
}
interface IAPIDefinition {
key: string;
handler: Function;
params: any[];
description?: string;
}
interface IRemoteAPI {
name: string;
description: string;
method: string;
params: Array<{
convert?: string;
name: string;
type: string;
required?: boolean;
default?: any;
min?: number;
max?: number;
}>;
endpoint: string;
namespace: string;
results?: {};
cache?: any; // TODO: [route caching]
}
class APIService extends Service {
constructor(id: string, manager: IManager, config: IAPIServiceConfig) {
super(id, manager, config || { id });
}
create() {
this.info(`${this.id} create`);
// Create a shortcut accessor on the client object
Object.defineProperty(this.app, 'api', { value: this, writable: false, enumerable: false });
return true;
}
_clampValues(remoteAPI: IRemoteAPI, args: any): void {
// Clamp values to min/max
const service = this;
for (const key in args) {
// @ts-ignore
const rp = remoteAPI.params[key];
if (rp != null && rp.type === 'number') {
if (rp.min != null && args[key] < rp.min) {
service.warn(
`Invalid parameter value for ${key} for ${remoteAPI.namespace}.${remoteAPI.name}, clamping to min value ${rp.min}`,
args
);
args[key] = rp.min;
}
if (rp.max != null && args[key] > rp.max) {
service.warn(
`Invalid parameter value for ${key} for ${remoteAPI.namespace}.${remoteAPI.name}, clamping to max value ${rp.max}`
);
args[key] = rp.max;
}
}
}
}
_validateArgs(remoteAPI: IRemoteAPI, args: any): void {
const service = this;
for (const param of remoteAPI.params) {
// Enforce required:true parameters to exist
if (param.required === true && args[param.name] == null) {
service.error(`Missing parameter ${param.name} for ${remoteAPI.namespace}.${remoteAPI.name}`, args);
throw new Error(`Missing parameter ${param.name} for ${remoteAPI.namespace}.${remoteAPI.name}`);
} else if (args[param.name] == null && param.default != null) {
// Augment default values
args[param.name] = param.default;
}
let isArray = false;
// Perform Type Validation
if (args[param.name] != null && param.type != null) {
isArray = param.type.includes('[]');
let type = param.type;
const value = args[param.name];
// Validate that arrayts are arrays
if (isArray) {
type = type.replace('[]', '');
if (!Array.isArray(value)) {
const err = `Invalid parameter type ${typeof value} for ${param.name} for ${remoteAPI.namespace}.${
remoteAPI.name
}. Expected an Array`;
service.error(err);
throw new Error(err);
}
}
if (type !== '') {
// TODO: handle arrays
if (isArray) {
// eslint-disable-next-line valid-typeof
if (!value.every((v: any) => (type === 'image' && v.ticket) || typeof v === type)) {
const err = `Invalid parameter value type ${typeof value[0]} for ${param.name} for ${
remoteAPI.namespace
}.${remoteAPI.name}. Expected an Array of ${type}`;
service.error(err);
throw new Error(err);
}
// eslint-disable-next-line valid-typeof
} else if ((type === 'image' && !value.ticket) || typeof value !== type) {
const err = `Invalid parameter type ${typeof value} for ${param.name} for ${remoteAPI.namespace}.${
remoteAPI.name
}. Expected ${type}`;
service.error(err);
throw new Error(err);
}
}
}
}
}
// Function to convert artifacts to the right representation required for this service
async _convertValues(remoteAPI: IRemoteAPI, args: any): Promise<void> {}
wrappedAxiosCall(remoteAPI: IRemoteAPI): any {
const service = this;
return async function (args: any, opts?: { headers?: any }, responseOpts?: { raw?: boolean }): Promise<any> {
// @ts-ignore
for (const key in args) {
if (args[key] == null) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete args[key];
}
}
service.verbose(`Validating ${remoteAPI.namespace}.${remoteAPI.name}`);
service._validateArgs(remoteAPI, args); // Basic validation for datatypes
service._clampValues(remoteAPI, args); //
await service._convertValues(remoteAPI, args);
const serviceConfig = service.config as IAPIServiceConfig;
let axiosConfig: any = {
// @ts-ignore
method: remoteAPI.method.toLowerCase(),
// @ts-ignore
url: serviceConfig.host + remoteAPI.endpoint,
withCredentials: true,
data: args
};
if (axiosConfig.method === 'get') {
axiosConfig.params = args;
// axiosConfig.params.cacheBust = Date.now()
}
if (opts != null && typeof opts === 'object') {
axiosConfig = { ...axiosConfig, ...opts };
}
service.info(`Invoking ${remoteAPI.namespace}.${remoteAPI.name}`);
try {
const result = await axios(axiosConfig);
service.verbose('Remote function result received');
if (responseOpts?.raw) {
return result;
} else {
// If a results object is defined, then we need to map the results to the return object as instructed
if (
remoteAPI.results != null &&
typeof remoteAPI.results === 'object' &&
Object.keys(remoteAPI.results).length > 0
) {
const ret = {};
for (const key in remoteAPI.results) {
// @ts-ignore
ret[key] = result.data[remoteAPI.results[key].prop];
}
return ret;
} else {
return result.data;
}
}
} catch (ex: any) {
// @ts-ignore
service.error(
`Error invoking ${remoteAPI.namespace}.${remoteAPI.name}`,
axiosConfig,
ex?.response?.data?.error,
ex
);
return { error: ex?.response?.data?.error || ex?.message || ex };
}
};
}
async getRemoteAPIsfromServer() {
const serviceConfig = this.config as IAPIServiceConfig;
try {
this.verbose('Registering remote functions from', serviceConfig.host, serviceConfig.integrationsUrl);
const result = await axios.get(serviceConfig.host + serviceConfig.integrationsUrl);
this.success('Received remoteAPIs from server');
// this.verbose('APIS:', result.data)
return result.data;
} catch (ex) {
this.error('Failed to load remoteAPIs from server', ex);
return [];
}
}
async start() {
this.info(`${this.id} start`);
return true;
}
async stop() {
this.info(`${this.id} stop`);
return true;
}
}
export { APIService, type IAPIDefinition, type IAPIServiceConfig, type IRemoteAPI };
|