Spaces:
Running
Running
/** | |
* Copyright (c) 2023 MERCENARIES.AI PTE. LTD. | |
* All rights reserved. | |
*/ | |
import { type ValidateFunction } from 'ajv'; | |
import merge from 'deepmerge'; | |
// #v-ifdef MERCENARIES_SERVER=1 | |
// ------------------------------------ SERVER ------------------------------------ | |
import Exp from 'jsonata'; | |
// #v-endif | |
import * as Rete from 'rete'; | |
import { type WorkerContext } from '../BaseComponent.js'; | |
import SocketManager from '../SocketManager.js'; | |
import { | |
OmniComponentMacroTypes, | |
type OmniComponentFormat, | |
type OmniComponentMeta, | |
type OmniComponentPatch, | |
type OmniControl, | |
type OmniIO, | |
type OmniAPIAuthenticationScheme | |
} from './types.js'; | |
import { ComponentComposer, PatchComposer } from './Composers.js'; | |
import OAIControl31 from './OAIControl.js'; | |
// #v-ifdef MERCENARIES_SERVER=1 | |
// ------------------------------------ SERVER ------------------------------------ | |
const deserializeValidator = function (jsString: string): ValidateFunction { | |
const eval2 = eval; // https://esbuild.github.io/content-types/#direct-eval | |
return eval2('(' + jsString + ')'); | |
}; | |
// #v-endif | |
// ------------------------------------ SERVER ------------------------------------ | |
abstract class OAIBaseComponent extends Rete.Component { | |
data: OmniComponentFormat; | |
// #v-ifdef MERCENARIES_SERVER=1 | |
_validator?: ValidateFunction; | |
// #v-endif | |
constructor(config: OmniComponentFormat, patch?: OmniComponentPatch) { | |
const data = merge(config, patch ?? {}); | |
super(`${data.displayNamespace}.${data.displayOperationId}`); | |
this.data = data; | |
this.data.macros ??= {}; | |
this.data.flags ??= 0; | |
this.data.errors ??= []; | |
this.data.xOmniEnabled ??= true; | |
for (const key in this.data.inputs) { | |
this.data.inputs[key].source ??= { sourceType: 'requestBody' }; | |
} | |
for (const key in this.data.outputs) { | |
this.data.outputs[key].source ??= { sourceType: 'responseBody' }; | |
} | |
// #v-ifdef MERCENARIES_SERVER=1 | |
this._validator = config.validator != null ? deserializeValidator(config.validator) : undefined; | |
// #v-endif | |
} | |
static create(displayNamespace: string, displayOperationId: string) { | |
const composer = new ComponentComposer(); | |
return composer.create(displayNamespace, displayOperationId); | |
} | |
static createPatch(displayNamespace: string, displayOperationId: string) { | |
const composer = new PatchComposer(); | |
return composer.create(displayNamespace, displayOperationId); | |
} | |
get validator() { | |
return this._validator ?? null; | |
} | |
get title() { | |
return this.data.title; | |
} | |
get description() { | |
return this.data.description; | |
} | |
get scripts() { | |
return this.data.scripts; | |
} | |
get flags() { | |
return this.data.flags ?? 0; | |
} | |
get summary() { | |
return this.data.description ?? this.meta.source?.summary; | |
} | |
get category() { | |
return this.data.category ?? 'Base API'; | |
} | |
get custom() { | |
return this.data.customData || {}; | |
} | |
get tags() { | |
return this.data.tags; | |
} | |
get apiKey() { | |
return `${this.data.apiNamespace}.${this.data.apiOperationId}`; | |
} | |
get apiNamespace() { | |
return this.data.apiNamespace; | |
} | |
get renderTemplate() { | |
return this.data.renderTemplate || 'default'; | |
} | |
get type() { | |
return this.data.type ?? 'OAIComponent31'; | |
} | |
get method() { | |
return this.data.method; | |
} | |
get inputs() { | |
return this.data.inputs; | |
} | |
get meta() { | |
return this.data.meta ?? {}; | |
} | |
get outputs() { | |
return this.data.outputs; | |
} | |
get macros() { | |
return this.data.macros; | |
} | |
get hash(): string | undefined { | |
return this.data.hash; | |
} | |
get controls() { | |
return this.data.controls; | |
} | |
get xOmniEnabled() { | |
return this.data.xOmniEnabled ?? true; | |
} | |
set xOmniEnabled(enabled: boolean) { | |
this.data.xOmniEnabled = enabled; | |
} | |
setType(type: string): this { | |
this.data.type = type; | |
return this; | |
} | |
setApiOperationId(apiOperationId: string): this { | |
this.data.apiOperationId = apiOperationId; | |
return this; | |
} | |
setApiNamespace(apiNamespace: string): this { | |
this.data.apiNamespace = apiNamespace; | |
return this; | |
} | |
setDisplayNamespace(displayNamespace: string): this { | |
this.data.displayNamespace = displayNamespace; | |
return this; | |
} | |
setDisplayOperationId(displayOperationId: string): this { | |
this.data.displayOperationId = displayOperationId; | |
return this; | |
} | |
setTitle(title: string): this { | |
this.data.title = title; | |
return this; | |
} | |
setMethod(method: string): this { | |
this.data.method = method; | |
return this; | |
} | |
setDescription(description: string): this { | |
this.data.description = description; | |
return this; | |
} | |
setUrlPath(urlPath: string): this { | |
this.data.urlPath = urlPath; | |
return this; | |
} | |
setMeta(meta: OmniComponentMeta): this { | |
this.data.meta = meta; | |
return this; | |
} | |
addInput(name: string, input: OmniIO): this { | |
this.data.inputs[name] = input; | |
return this; | |
} | |
addControl(name: string, control: OmniControl): this { | |
this.data.controls[name] = control; | |
return this; | |
} | |
addOutput(name: string, output: OmniIO): this { | |
this.data.outputs[name] = output; | |
return this; | |
} | |
addTag(tag: string): this { | |
this.data.tags.push(tag); | |
return this; | |
} | |
setCategory(category: string): this { | |
this.data.category = category; | |
return this; | |
} | |
setRequestContentType(requestContentType: string): this { | |
this.data.requestContentType = requestContentType; | |
return this; | |
} | |
setResponseContentType(responseContentType: string): this { | |
this.data.responseContentType = responseContentType; | |
return this; | |
} | |
setCredentials(credentials: string): this { | |
this.data.credentials = credentials; | |
return this; | |
} | |
setValidator(validator: string): this { | |
this.data.validator = validator; | |
return this; | |
} | |
addSecurity(spec: OmniAPIAuthenticationScheme): this { | |
this.data.security = this.data.security ?? []; | |
this.data.security.push(spec); | |
return this; | |
} | |
setMacro(macro: OmniComponentMacroTypes, fn: Function): this { | |
this.data.macros![macro] = fn; | |
return this; | |
} | |
pickDefaultControl(obj: { | |
control?: { controlType?: string }; | |
controlType?: string; | |
choices?: any; | |
step?: number; | |
type?: string; | |
minimum?: number; | |
maximum?: number; | |
customSocket?: string; | |
}): string { | |
if (obj.choices != null) { | |
return 'AlpineSelectComponent'; | |
} | |
if (obj.control?.controlType != null) { | |
return obj.control?.controlType; | |
} | |
if (obj.controlType != null) { | |
return obj.controlType; | |
} | |
if ( | |
obj.step != null || | |
(obj.minimum != null && obj.maximum != null && Math.abs(obj.maximum - obj.minimum) <= 100) | |
) { | |
if (obj.type === 'float') { | |
obj.step ??= 0.1; | |
} | |
return 'AlpineNumWithSliderComponent'; | |
} | |
const objType = obj.type; | |
if (objType === null) { | |
omnilog.warn('Null Object Type'); | |
} | |
const customSocket = obj.customSocket; | |
if ( | |
customSocket && | |
['imageArray', 'image', 'document', 'documentArray', 'audio', 'file', 'audioArray', 'fileArray'].includes( | |
customSocket | |
) | |
) { | |
return 'AlpineLabelComponent'; | |
} | |
if (objType === 'number' || objType === 'integer' || objType === 'float') { | |
return 'AlpineNumComponent'; | |
} else if (objType === 'boolean') { | |
return 'AlpineToggleComponent'; | |
} else if (objType === 'error') { | |
return 'AlpineTextComponent'; | |
} else if (objType === 'object') { | |
return 'AlpineCodeMirrorComponent'; | |
} | |
if (objType === 'string' || customSocket === 'text') { | |
return 'AlpineTextComponent'; | |
} | |
return 'AlpineLabelComponent'; | |
} | |
abstract toJSON(): any; | |
abstract _builder(node: CustomReteNode): Promise<void>; | |
async builder(node: CustomReteNode): Promise<void> { | |
node.title = this.title; | |
node.description = this.description; | |
await this._builder?.(node); | |
} | |
abstract _workerStart(inputData: any, ctx: WorkerContext): Promise<void>; | |
async workerStart(inputData: any, ctx: WorkerContext) { | |
// #v-ifdef MERCENARIES_SERVER=1 | |
// ------------------------------------ SERVER ------------------------------------ | |
try { | |
await this._workerStart?.(inputData, ctx); | |
} catch (error: any) { | |
omnilog.error('Error in component worker', error); | |
// @ts-ignore | |
const payload = { | |
type: 'error', | |
node_id: ctx.node.id, | |
error: error?.message || 'Error', | |
componentKey: this.name, | |
sessionId: ctx.sessionId | |
}; | |
// ctx.app.verbose('SSE:control:setValue', payload) | |
await ctx.app.emit('sse_message', payload); | |
ctx.outputs.error = error; | |
return ctx.outputs; | |
} | |
return ctx.outputs; | |
// ------------------------------------ SERVER ------------------------------------ | |
// #v-endif | |
} | |
async setControlValue(controlId: string, value: any, ctx: any) { | |
if (this.data.controls[controlId] == null) { | |
omnilog.warn( | |
this.name, | |
'tried to update non existing control', | |
controlId, | |
' - suppressed.\nPlease check your component for a setComponentValue call that passes in a non existing control key.' | |
); | |
return; | |
} | |
// On the client, we update the display (editor does not exist on server context) | |
if (this.editor != null) { | |
// @ts-ignore | |
const ctl = this.editor?.nodes.find((n) => n.id === ctx.node.id).controls.get(controlId) ?? null; | |
if (ctl != null) { | |
(ctl as OAIControl31).setValue(value); | |
} | |
} | |
// #v-ifdef MERCENARIES_SERVER=1 | |
// ------------------------------------ SERVER ------------------------------------ | |
// On the server, we trigger a message to the client. | |
/* server */ | |
else { | |
// TODO: [session-management] Raise this as an event. | |
if (ctx?.app && ctx.node && ctx.sessionId) { | |
const payload = { | |
type: 'control:setvalue', | |
node_id: ctx.node.id, | |
controlId, | |
value, | |
componentKey: this.name, | |
sessionId: ctx.sessionId | |
}; | |
// ctx.app.verbose('SSE:control:setValue', payload) | |
await ctx.app.emit('sse_message', payload); | |
} | |
} | |
// ------------------------------------ SERVER ------------------------------------ | |
// #v-endif | |
} | |
async sendStatusUpdate(message: string, scope: string, ctx: any) { | |
// #v-ifdef MERCENARIES_SERVER=1 | |
// ------------------------------------ SERVER ------------------------------------ | |
const payload = { node_id: ctx.node.id, block: this.name, message, scope }; | |
const msg = ctx.app.io.composeMessage('block:status').from('server').to(ctx.sessionId).body(payload).toMessage(); | |
await ctx.app.io.send(ctx.sessionId, msg); | |
// ------------------------------------ SERVER ------------------------------------ | |
// #v-endif | |
} | |
} | |
class CustomReteNode extends Rete.Node { | |
public title?: string; | |
public description?: string; | |
public renderTemplate?: string; | |
public category?: string; | |
public namespace?: string; | |
public summary?: string; | |
public enabled?: boolean; | |
public xOmniEnabled?: boolean; | |
public errors?: string[]; | |
constructor(name: string) { | |
super(name); | |
} | |
} | |
class OAIComponent31 extends OAIBaseComponent { | |
constructor(config: OmniComponentFormat, patch?: OmniComponentPatch, fns?: any) { | |
if (fns) { | |
omnilog.warn('fns not implemented'); | |
} | |
super(config, patch); | |
} | |
getSocketForIO(io: OmniIO): Rete.Socket { | |
let socket = 'object'; | |
if (io.customSocket) { | |
socket = io.customSocket; | |
} else if (io.type != null && typeof io.type === 'string') { | |
socket = io.type; | |
} else if (io.dataTypes?.length > 0) { | |
socket = io.dataTypes[0]; | |
} else if (io.step != null || io.maximum != null || io.minimum != null) { | |
socket = 'number'; | |
} | |
// return SocketManager.getSingleton().getSocketFromString(socket) | |
return SocketManager.getSingleton().getOrCreateSocket(socket, io.socketOpts || {}); | |
} | |
async _redraw(node: CustomReteNode): Promise<void> {} | |
enumerateInputs(node: CustomReteNode): Record<string, OmniIO> { | |
return Object.assign({}, node.data['x-omni-dynamicInputs'] || {}, this.data.inputs); | |
} | |
enumerateOutputs(node: CustomReteNode): Record<string, OmniIO> { | |
return Object.assign({}, node.data['x-omni-dynamicOutputs'] || {}, this.data.outputs); | |
} | |
async _builder(node: CustomReteNode): Promise<void> { | |
node.category = this.category; | |
node.renderTemplate = this.renderTemplate; | |
node.title = (node.data['x-omni-title'] as string) || this.title; | |
node.summary = (node.data['x-omni-summary'] as string) || this.summary; | |
node.namespace = this.data.displayNamespace ?? this.data.apiNamespace; | |
node.meta = this.meta as Record<string, unknown>; | |
node.meta.title = this.title ?? this.summary; | |
node.errors = this.data.errors; | |
const inputs = this.enumerateInputs(node); | |
// create all inputs | |
for (const key in inputs) { | |
const io: OmniIO = inputs[key]; | |
io.name ??= key; | |
io.title ??= key; | |
const ctlType = this.pickDefaultControl(io); | |
const control = await OAIControl31.fromIO(ctlType, io, this.editor); | |
if (!io.hidden) { | |
if (io.readonly) { | |
// ReadOnly nodes get changed to controls | |
node.addControl(control); | |
} else { | |
const input: Rete.Input = new Rete.Input(key, io.title || io.name, this.getSocketForIO(io), io.allowMultiple); | |
input.name ??= key; | |
input.addControl(control); | |
node.addInput(input); | |
} | |
} | |
} | |
// create all controls | |
for (const key in this.controls) { | |
const ctl: OmniControl = this.controls[key]; | |
if (!ctl.hidden) { | |
ctl.name ??= key; | |
ctl.controlType ??= this.pickDefaultControl(ctl); | |
const control = await OAIControl31.fromControl(ctl, this.editor); | |
node.addControl(control); | |
} | |
} | |
const outputs = this.enumerateOutputs(node); | |
// create all outputs | |
for (const key in outputs) { | |
const io: OmniIO = { ...outputs[key], name: key, title: outputs[key].title ?? key }; | |
if (!io.hidden) { | |
const output: Rete.Output = new Rete.Output(key, io.title || io.name, this.getSocketForIO(io)); | |
output.name ??= key; | |
node.addOutput(output); | |
} | |
} | |
} | |
async runXFunction(ctx: WorkerContext, method: string, payload: any): Promise<any> { | |
let response: any = null; | |
// #v-ifdef MERCENARIES_SERVER=1 | |
// ------------------------------------ SERVER ------------------------------------ | |
if (method === 'X-CUSTOM') { | |
const exec = ctx.app.blocks.getMacro(this, OmniComponentMacroTypes.EXEC); | |
if (!exec) { | |
throw new Error('Block Error: X-CUSTOM macro is not defined for block' + this.name); | |
} | |
try { | |
response = exec != null ? await exec.apply(this, [payload, ctx, this]) : null; | |
} catch (ex: any) { | |
if (ex.message.includes('Free time limit reached')) { | |
// replicate.com, 2023-10 | |
throw ex; | |
} | |
throw new Error(`Error executing X-CUSTOM for block ${this.name}: ${ex.message}`); | |
} | |
} | |
// X-NOOP: Do nothing | |
else if (this.method === 'X-NOOP') { | |
response = {}; | |
} | |
// X-PASSTHROUGH: Pass the payload through | |
else if (this.method === 'X-PASSTHROUGH') { | |
response = JSON.parse(JSON.stringify(payload)); | |
} | |
// #v-endif | |
return response; | |
} | |
async _workerStart(inputData: any, ctx: WorkerContext): Promise<any> { | |
// #v-ifdef MERCENARIES_SERVER=1 | |
// ------------------------------------ SERVER ------------------------------------ | |
let payload: any = await this.getPayload(ctx); | |
payload = await this.runInputScripts(payload, ctx); | |
const { requestBody, parameters } = this.getRequestComponents(payload, ctx); | |
omnilog.log(this.name, 'requestBody', requestBody); | |
omnilog.log(this.name, 'parameters', parameters); | |
// Input Validation: If a validator function exists, execute it | |
if (this.validator != null) { | |
const isValid = this._validator?.(payload); | |
const errors = this._validator?.errors ?? []; | |
return { isValid, errors }; | |
} | |
let response; | |
// Custom Functions | |
if (this.method.startsWith('X-')) { | |
response = await this.runXFunction(ctx, this.method, payload); | |
if (response === undefined) { | |
response = { error: 'Internal error, `runXFunction` did not return a valid response object.' }; | |
} | |
} | |
// Everything Else is a standard API call | |
else { | |
response = await ctx.app.api2.execute( | |
this.apiKey, | |
requestBody, | |
{ params: parameters, responseContentType: this.data.responseContentType }, | |
{ user: ctx.userId, sessionId: ctx.sessionId, jobId: ctx.jobId } | |
); | |
if (response === undefined) { | |
response = { error: 'Internal error, `api2.execute` did not return a valid response object.' }; | |
} | |
} | |
if (typeof response === 'string') { | |
response = { result: response }; | |
} | |
//TODO: If there were errors, should we still run output scripts? | |
response = await this.runOutputScripts(response, ctx); | |
ctx.setOutputs(response); | |
return response; | |
// #v-endif | |
} | |
async runInputScripts(payload: any, ctx: WorkerContext): Promise<any> { | |
// #v-ifdef MERCENARIES_SERVER=1 | |
// ------------------------------------ SERVER ------------------------------------ | |
// Controls that are macro'd with a .display property | |
for (const key in this.controls) { | |
const control = this.controls[key]; | |
if (control.displays?.startsWith('input:')) { | |
const content = control.displays.replace('input:', ''); | |
await this.setControlValue(key, payload[content], ctx); | |
} | |
} | |
const inputs = this.enumerateInputs(ctx.node); | |
for (const key in inputs) { | |
const input = inputs[key]; | |
// run jsonata script | |
if (input.scripts) { | |
if (input.scripts.jsonata) { | |
const expression = Exp(input.scripts.jsonata); | |
try { | |
payload[key] = await expression.evaluate(payload); | |
} catch (ex: any) { | |
throw new Error(`Error evaluating jsonata expression: ${input.scripts.jsonata} - ${ex.message}`); | |
} | |
} | |
// delete script - remove fields from payload | |
if (input.scripts.delete) { | |
for (const field of input.scripts.delete) { | |
delete payload[field]; | |
} | |
} | |
} | |
} | |
// JSONATA global transform | |
const transforms = this.scripts?.['transform:input']; | |
if (transforms) { | |
if (Array.isArray(transforms) && transforms.length > 0) { | |
transforms.forEach((script: string) => { | |
omnilog.log('global jsonata'); | |
const expression = Exp(script); | |
payload = expression.evaluate(payload); | |
}); | |
} | |
} | |
//#v-endif | |
// -------------------------------------------------------------------------------------------- | |
return payload; | |
} | |
async runOutputScripts(payload: any, ctx: WorkerContext): Promise<any> { | |
// -------------------------------------------------------------------------------------------- | |
// #v-ifdef MERCENARIES_SERVER=1 | |
const socketManager = SocketManager.getSingleton(); | |
// TODO: Custom Socket Processing | |
if (this.outputs?._omni_result) { | |
payload = { _omni_result: payload }; | |
} | |
const outputs = this.enumerateOutputs(ctx.node); | |
for (const key in outputs) { | |
const output = outputs[key]; | |
// run jsonata script | |
if (output.scripts) { | |
if (output.scripts.jsonata) { | |
omnilog.log('running jsonata', output.scripts.jsonata); | |
const expression = Exp(output.scripts.jsonata); | |
try { | |
payload[key] = await expression.evaluate(payload); | |
} catch (ex: any) { | |
throw new Error(`Error evaluating jsonata expression: ${output.scripts.jsonata} - ${ex.message}`); | |
} | |
} | |
// delete script - remove fields from payload | |
if (output.scripts.delete) { | |
omnilog.log('running delete', output.scripts.jsonata); | |
for (const field of output.scripts.delete) { | |
delete payload[field]; | |
} | |
} | |
} | |
if (payload[key]) { | |
if (output.customSocket) { | |
const sock = socketManager.getOrCreateSocket(output.customSocket, output.socketOpts || {}); | |
payload[key] = sock ? await sock?.handleOutput?.(ctx, payload[key]) : payload[key]; | |
} | |
} | |
} | |
// Controls that are macro'd with a .display property | |
for (const key in this.controls) { | |
const control = this.controls[key]; | |
if (control.displays?.startsWith('output:')) { | |
const content = control.displays.replace('output:', ''); | |
await this.setControlValue(key, payload[content], ctx); | |
} | |
} | |
//#v-endif | |
// -------------------------------------------------------------------------------------------- | |
return payload; | |
} | |
getRequestComponents(payload: any, ctx: WorkerContext): { requestBody: any; parameters: any[] } { | |
const requestBody: any = {}; | |
const parameters = []; | |
// #v-ifdef MERCENARIES_SERVER=1 | |
// ------------------------------------ SERVER ------------------------------------ | |
const inputs = this.enumerateInputs(ctx.node); | |
for (const key in payload) { | |
const value = payload[key]; | |
const source = inputs[key]?.source; | |
if (!source || source.sourceType === 'requestBody') { | |
requestBody[key] = value; | |
} else { | |
if (source.sourceType === 'parameter') { | |
const param = { | |
name: key, | |
in: source.in, | |
value | |
}; | |
parameters.push(param); | |
} | |
} | |
} | |
// ------------------------------------ SERVER ------------------------------------ | |
// #v-endif | |
return { requestBody, parameters }; | |
} | |
prunePayload(input: OmniIO, payload: any, key: string) { | |
// #v-ifdef MERCENARIES_SERVER=1 | |
// ------------------------------------ SERVER ------------------------------------ | |
const value = payload[key]; | |
if (value === undefined || value === null) { | |
delete payload[key]; | |
return; | |
} | |
if (input.type === 'string' && value.length === 0 && input.required !== true) { | |
delete payload[key]; | |
} else if (input.type === 'array' && value.length === 0 && input.required !== true) { | |
delete payload[key]; | |
} else if (input.type === 'object' && Object.keys(value).length === 0 && input.required !== true) { | |
delete payload[key]; | |
} else if ((input.type === 'number' || input.type === 'integer') && value === 'inf') { | |
payload[key] = Infinity; | |
} | |
// ------------------------------------ SERVER ------------------------------------ | |
// #v-endif | |
} | |
async getPayload(ctx: WorkerContext): Promise<any> { | |
const payload: any = {}; | |
// #v-ifdef MERCENARIES_SERVER=1 | |
// ------------------------------------ SERVER ------------------------------------ | |
const inputs = this.enumerateInputs(ctx.node); | |
for (const key in inputs) { | |
const input = inputs[key]; | |
const inputValue = ctx.inputs[key] as any[]; | |
let value; | |
if (input.allowMultiple) { | |
value = inputValue?.flat?.() ?? ctx.node.data[key] ?? input.default; | |
} else { | |
value = inputValue?.[0] ?? ctx.node.data[key] ?? input.default; | |
if (input.dataTypes?.[0] === 'integer' && value === '') { | |
value = input.default; // replicate.com, 2023-10 | |
} | |
} | |
const socketManager = SocketManager.getSingleton(); | |
payload[key] = value; | |
this.prunePayload(input, payload, key); | |
if (payload[key] !== null && payload[key] !== undefined) { | |
if (input.customSocket) { | |
const sock = socketManager.getOrCreateSocket(input.customSocket, input.socketOpts || {}); | |
payload[key] = sock ? await sock?.handleInput?.(ctx, payload[key]) : payload[key]; | |
} | |
} | |
} | |
for (const key in this.controls) { | |
const value = ctx.node.data[key] ?? this.controls[key].default; | |
if (value != null && !this.controls[key].displays) { | |
payload[key] = value; | |
} | |
} | |
// ------------------------------------ SERVER ------------------------------------ | |
// #v-endif | |
return JSON.parse(JSON.stringify(payload)); | |
} | |
worker() { | |
throw new Error('This should never be called'); | |
} | |
static fromJSON(json: any, patch?: any): OAIComponent31 { | |
const comp = new OAIComponent31(json, patch); | |
if (!comp.name) { | |
throw new Error(); | |
} | |
return comp; | |
} | |
toJSON() { | |
return JSON.parse(JSON.stringify({ ...this.data, name: this.name })); | |
} | |
} | |
export { OAIBaseComponent, OAIComponent31, OAIControl31 }; | |