manu-sapiens commited on
Commit
74bd902
·
1 Parent(s): fadd78e

removing check on Data. Added support for Patches. Added serial openapi patch

Browse files
Dockerfile CHANGED
@@ -1,20 +1,16 @@
1
- FROM node:20.6.1
2
-
3
- USER root
4
- RUN mkdir -p /data
5
- RUN chmod 0777 /data
6
 
7
  USER node
8
  WORKDIR /app
9
 
10
- RUN chown -Rh $user:$user /data
11
  RUN git clone https://github.com/omnitool-ai/omnitool.git
12
  RUN cd omnitool && yarn install
13
 
14
  RUN mkdir -p /app/omnitool/node_modules
15
- RUN chmod 0777 /app
16
- RUN chown -Rh $user:$user /app
17
  COPY --chown=node . /app
18
- EXPOSE 4444
19
 
 
20
  CMD ["node", "hf_server.js"]
 
1
+ FROM node:21.2.0
 
 
 
 
2
 
3
  USER node
4
  WORKDIR /app
5
 
 
6
  RUN git clone https://github.com/omnitool-ai/omnitool.git
7
  RUN cd omnitool && yarn install
8
 
9
  RUN mkdir -p /app/omnitool/node_modules
10
+ RUN chmod -R 0777 /app
11
+ RUN chown -Rh node:node /app
12
  COPY --chown=node . /app
13
+ COPY --chown=node ./patches /app/omnitool/
14
 
15
+ EXPOSE 4444
16
  CMD ["node", "hf_server.js"]
hf_server.js CHANGED
@@ -3,7 +3,7 @@
3
  * All rights reserved.
4
  */
5
  //@ts-check
6
- const VERSION = '0.6.0.hf.016.e';
7
 
8
  const express = require('express');
9
  const http = require('http');
 
3
  * All rights reserved.
4
  */
5
  //@ts-check
6
+ const VERSION = '0.6.0.hf.018.a';
7
 
8
  const express = require('express');
9
  const http = require('http');
omnitool_init.sh CHANGED
@@ -1,15 +1,15 @@
1
  #!/bin/bash
2
- echo "[->] START "
3
 
4
- echo "[->] CHECKING EXISTING /DATA "
5
  # was: /data
6
- if [ -d "../data" ]; then
7
- echo "$(ls -l ../data)"
8
- else
9
- echo "[<] NO persistent /DATA DETECTED. You can add 20Gig of persistent storage as a paid option to Hugging Face"
10
- echo "$(mkdir -p ../data)"
11
- echo "$(ls -l ../data)"
12
- fi
13
 
14
  echo "[->] UPDATE OMNITOOL if needed"
15
  cd ./omnitool
@@ -36,4 +36,4 @@ fi
36
  #rm ./.mercs.yaml.original
37
 
38
  echo "[->] YARN START "
39
- yarn start -u -rb -ll 3 -R blocks
 
1
  #!/bin/bash
2
+ echo "[->] START v0000.61"
3
 
4
+ # echo "[->] CHECKING EXISTING /DATA "
5
  # was: /data
6
+ #if [ -d "../data" ]; then
7
+ # echo "$(ls -l ../data)"
8
+ #else
9
+ # echo "[<] NO persistent /DATA DETECTED. You can add 20Gig of persistent storage as a paid option to Hugging Face"
10
+ # echo "$(mkdir -p ../data)"
11
+ # echo "$(ls -l ../data)"
12
+ #fi
13
 
14
  echo "[->] UPDATE OMNITOOL if needed"
15
  cd ./omnitool
 
36
  #rm ./.mercs.yaml.original
37
 
38
  echo "[->] YARN START "
39
+ yarn start -u -rb -R blocks
patches/packages/omni-server/src/core/BlockManager.ts ADDED
@@ -0,0 +1,1178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Copyright (c) 2023 MERCENARIES.AI PTE. LTD.
3
+ * All rights reserved.
4
+ */
5
+
6
+ // ----------------------------------------------------------------------------------------------
7
+ // BlockManager.ts
8
+ //
9
+ // Purpose: Manage the blocks that are available to the server.
10
+ // Provide on demand block composition.
11
+ // ----------------------------------------------------------------------------------------------
12
+
13
+ import MurmurHash3 from 'imurmurhash';
14
+ import path from 'path';
15
+ import type MercsServer from './Server.js';
16
+
17
+ import { existsSync } from 'fs';
18
+ import { access, readFile, readdir, stat } from 'fs/promises';
19
+ // import { existsSync, promises as fs } from 'fs';
20
+ import yaml from 'js-yaml';
21
+ import {
22
+ OAIBaseComponent,
23
+ OAIComponent31,
24
+ WorkerContext,
25
+ type OmniAPIAuthenticationScheme,
26
+ type OmniAPIKey,
27
+ type OmniComponentFormat,
28
+ type OmniComponentMacroTypes,
29
+ type OmniComponentPatch,
30
+ type OmniNamespaceDefinition
31
+ } from 'omni-sockets';
32
+ import { Manager, omnilog, type IApp, type IBlockOrPatchSummary } from 'omni-shared';
33
+
34
+ import SwaggerClient from 'swagger-client';
35
+ import { type AmqpService } from '../services/AmqpService.js';
36
+ import { OpenAPIReteAdapter } from '../services/ComponentService/OpenAPIReteAdapter.js';
37
+ import { KVStorage, type IKVStorageConfig } from './KVStorage.js';
38
+ import { KNOWN_EXTENSION_METHODS} from './ServerExtensionsManager.js';
39
+ import { type NodeData } from 'rete/types/core/data.js';
40
+ import { OmniDefaultBlocks } from '../blocks/DefaultBlocks.js';
41
+ import { StorageAdapter } from './StorageAdapter.js';
42
+ import { type CredentialService } from 'services/CredentialsService/CredentialService.js';
43
+
44
+ interface IBlockManagerConfig {
45
+ preload: boolean;
46
+ kvStorage: IKVStorageConfig;
47
+ }
48
+ const PRELOAD_REGISTRY_IN_PARALLEL = false;
49
+
50
+ type UndefinedPruned<T> = T extends object ? { [P in keyof T]: UndefinedPruned<T[P]> } : T;
51
+
52
+ function removeUndefinedValues<T>(obj: T): UndefinedPruned<T> {
53
+ if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
54
+ return obj as UndefinedPruned<T>;
55
+ }
56
+
57
+ const result: Partial<UndefinedPruned<T>> = {};
58
+
59
+ for (const key in obj) {
60
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
61
+ const value = (obj as any)[key];
62
+ if (value !== undefined) {
63
+ result[key as keyof T] = removeUndefinedValues(value);
64
+ }
65
+ }
66
+ }
67
+
68
+ return result as UndefinedPruned<T>;
69
+ }
70
+
71
+ class BlockManager extends Manager {
72
+ ReteAdapter: typeof OpenAPIReteAdapter = OpenAPIReteAdapter;
73
+ BaseComponent: typeof OAIBaseComponent = OAIBaseComponent;
74
+
75
+ private readonly factories: Map<string, Function>; // This holds component factories
76
+ private readonly namespaces: StorageAdapter<OmniNamespaceDefinition>; // This holds namespaces
77
+ private readonly patches: StorageAdapter<OmniComponentPatch>; // This holds patches
78
+ private readonly blocks: StorageAdapter<OmniComponentFormat>; // This holds blocks
79
+ private readonly blocksAndPatches: StorageAdapter<OmniComponentFormat | OmniComponentPatch>; // This holds blocks and patches
80
+ private readonly macros: Map<string, Function>; // This holds functions attached to blocks
81
+ private readonly cache: StorageAdapter<any>; // This holds cached entries
82
+
83
+ private readonly config: IBlockManagerConfig;
84
+ public _kvStorage?: KVStorage;
85
+
86
+ constructor(app: IApp, config: IBlockManagerConfig) {
87
+ super(app);
88
+ this.config = config;
89
+
90
+ this.blocks = new StorageAdapter<OmniComponentFormat>('block:'); /* ) new Map<string, OmniComponentFormat>() */
91
+ this.patches = new StorageAdapter<OmniComponentPatch>('patch:');
92
+ this.blocksAndPatches = new StorageAdapter<OmniComponentFormat | OmniComponentPatch>();
93
+ this.namespaces = new StorageAdapter<OmniNamespaceDefinition>('ns:');
94
+ this.cache = new StorageAdapter<any>('cache:', undefined, 60 * 60 * 24);
95
+
96
+ // Type factories and macros
97
+ this.factories = new Map<string, Function>();
98
+ this.macros = new Map<string, Function>();
99
+
100
+ this.registerType('OAIComponent31', OAIComponent31.fromJSON);
101
+
102
+ app.events.on('credential_change', (e: any) => {
103
+ this.cache.clearWithPrefix();
104
+ });
105
+ }
106
+
107
+ get kvStorage(): KVStorage {
108
+ if (this._kvStorage == null) {
109
+ throw new Error('BlockManager kvStorage accessed before load');
110
+ }
111
+ return this._kvStorage;
112
+ }
113
+
114
+ async init() {
115
+ const kvConfig = this.config.kvStorage;
116
+ if (kvConfig) {
117
+ this._kvStorage = new KVStorage(this.app, kvConfig);
118
+ // Need to register view before init() call
119
+ this._kvStorage.registerView(
120
+ 'BlocksAndPatches',
121
+ `CREATE VIEW IF NOT EXISTS BlocksAndPatches (key,
122
+ value,
123
+ valueType,
124
+ blob,
125
+ expiry,
126
+ tags,
127
+ deleted,
128
+ seq) AS
129
+ SELECT
130
+ key,
131
+ value,
132
+ valueType,
133
+ blob,
134
+ expiry,
135
+ tags,
136
+ deleted,
137
+ ROW_NUMBER() OVER (ORDER BY seq DESC) AS new_seq
138
+ FROM
139
+ kvstore
140
+ WHERE
141
+ (key LIKE 'block:%' OR key LIKE 'patch:%')
142
+ AND deleted = 0;`
143
+ );
144
+ if (!(await this.kvStorage.init())) {
145
+ throw new Error('KVStorage failed to start');
146
+ }
147
+
148
+ const resetDB = (this.app as MercsServer).options.resetDB;
149
+ if (resetDB?.split(',').includes('blocks')) {
150
+ this.info('Resetting blocks storage');
151
+ this.kvStorage.clear();
152
+ }
153
+ await this.kvStorage.vacuum();
154
+
155
+ this.app.events.on('register_blocks', (blocks: OmniComponentFormat[]) => {
156
+ blocks.forEach((block) => {
157
+ this.addBlock(block);
158
+ });
159
+ });
160
+
161
+ this.app.events.on('register_patches', (patches: OmniComponentPatch[]) => {
162
+ patches.forEach((patch) => {
163
+ this.addPatch(patch);
164
+ });
165
+ });
166
+
167
+ this.app.events.on('register_macros', (macros: Record<string, Function>) => {
168
+ Object.entries(macros || {}).forEach(([key, value]) => {
169
+ this.registerMacro(key, value);
170
+ });
171
+ });
172
+
173
+ this.blocks.bindStorage(this.kvStorage);
174
+ this.namespaces.bindStorage(this.kvStorage);
175
+ this.patches.bindStorage(this.kvStorage);
176
+ this.cache.bindStorage(this.kvStorage);
177
+ this.blocksAndPatches.bindStorage(this.kvStorage);
178
+ }
179
+
180
+ OmniDefaultBlocks.forEach((block) => {
181
+ this.addBlock(block);
182
+ });
183
+
184
+ this.registerExecutors();
185
+
186
+ if (this.config.preload) {
187
+ await this.preload();
188
+ }
189
+
190
+ this.info('BlockManager initialized');
191
+ return true;
192
+ }
193
+
194
+ formatHeader(blockOrPatch: {
195
+ title?: string;
196
+ description?: string;
197
+ category?: string;
198
+ displayNamespace: string;
199
+ displayOperationId: string;
200
+ tags?: string[];
201
+ }): IBlockOrPatchSummary {
202
+ return {
203
+ title: blockOrPatch.title ?? `${blockOrPatch.displayNamespace + '.' + blockOrPatch.displayOperationId}`,
204
+ description: blockOrPatch.description ?? '',
205
+ category: blockOrPatch.category,
206
+ name: `${blockOrPatch.displayNamespace + '.' + blockOrPatch.displayOperationId}`,
207
+ tags: blockOrPatch.tags ?? []
208
+ };
209
+ }
210
+
211
+ async stop() {
212
+ await this.kvStorage.stop();
213
+ await super.stop();
214
+
215
+ this.info('BlockManager stopped');
216
+ return true;
217
+ }
218
+
219
+ private registerExecutors() {
220
+ const amqpService = this.app.services.get('amqp') as AmqpService;
221
+
222
+ // @ts-ignore
223
+ this.app.api2 ??= {};
224
+ // @ts-ignore
225
+ this.app.api2.execute = async (
226
+ api: string,
227
+ body?: any,
228
+ requestConfig?: {
229
+ headers?: any;
230
+ params?: any;
231
+ responseType?: string;
232
+ responseEncoding?: string;
233
+ timeout: 0;
234
+ },
235
+ ctx?: any
236
+ ) => {
237
+ if (!ctx.userId || !ctx.sessionId) {
238
+ this.debug('execute() called without ctx.userId or ctx.sessionId');
239
+ }
240
+
241
+ const oid = api.split('.');
242
+ const integrationId = oid.shift();
243
+ const opKey = oid.join('.');
244
+
245
+ await this.app.events.emit('pre_request_execute', [ctx, api, { body, params: requestConfig?.params ?? {} }]);
246
+ omnilog.log('Executing', integrationId, opKey, body, requestConfig, ctx);
247
+
248
+ let result: any;
249
+ try {
250
+
251
+ result = await amqpService.publishAwaitable(
252
+ 'omni_tasks',
253
+ undefined,
254
+ Object.assign({}, { integration: { key: integrationId, operationId: opKey, block: api } }, { body }, requestConfig, {
255
+ job_ctx: ctx
256
+ })
257
+ );
258
+ } catch (e: unknown) {
259
+ this.error('Error executing', api, e);
260
+ result = { error: e };
261
+ } finally {
262
+ try {
263
+ await this.app.events.emit('post_request_execute', [
264
+ ctx,
265
+ api,
266
+ { body, params: requestConfig?.params ?? {}, result }
267
+ ]);
268
+ } catch (ex) {
269
+ omnilog.error(ex);
270
+ }
271
+ }
272
+ if (result.error) {
273
+ throw result.error;
274
+ }
275
+ return result;
276
+ };
277
+ }
278
+
279
+
280
+ private async preloadDir(registryDir:string, prefix?: string)
281
+ {
282
+ // check if directory exists
283
+ if (!await this.checkDirectory(registryDir)) {
284
+ return;
285
+ }
286
+
287
+ const registryFiles = await readdir(registryDir);
288
+ this.debug(`Scanning registry folder ${registryDir}, containing ${registryFiles.length} files.`);
289
+
290
+ if (PRELOAD_REGISTRY_IN_PARALLEL)
291
+ {
292
+ const tasks = registryFiles.map(async (file) => {
293
+ if (file.startsWith('.')) {
294
+ return null;
295
+ }
296
+ const filePath = path.join(registryDir, file);
297
+ const s = await stat(filePath);
298
+ if (s.isDirectory()) {
299
+ try {
300
+ await this.registerFromFolder(filePath, prefix, (this.app as MercsServer).options.refreshBlocks);
301
+ } catch (error) {
302
+ this.warn(`Failed to register from ${filePath}`, error);
303
+ }
304
+ }
305
+ });
306
+
307
+ await Promise.all(tasks);
308
+ }
309
+ else
310
+ {
311
+ this.warn("Loading registry files in sequence for maximum compatibility.");
312
+ for (const file of registryFiles)
313
+ {
314
+ if (file.startsWith('.'))
315
+ {
316
+ continue;
317
+ }
318
+ const filePath = path.join(registryDir, file);
319
+ const s = await stat(filePath);
320
+ if (s.isDirectory())
321
+ {
322
+ try
323
+ {
324
+ await this.registerFromFolder(filePath, prefix, (this.app as MercsServer).options.refreshBlocks);
325
+ }
326
+ catch(error)
327
+ {
328
+ this.error(`Failed to preloadDir from ${filePath}. Error = ${error}. Skipping...`);
329
+ }
330
+ }
331
+ }
332
+ }
333
+ }
334
+
335
+ // Preload APIS
336
+ private async preload() {
337
+ const start = performance.now(); // Start timer
338
+
339
+ //const testDir = process.cwd() + '/data.local/apis-testing/';
340
+ //@ts-ignore
341
+ const apisTestingPath = this.app.config.settings.paths?.apisTestingPath || 'data.local/apis-testing';
342
+ const testDir = path.join(process.cwd(), apisTestingPath)
343
+ await this.preloadDir(testDir, 'test')
344
+
345
+ // First load the local apis defined by the user
346
+ //const localDir = process.cwd() + '/data.local/apis-local/';
347
+ //@ts-ignore
348
+ const apisLocalPath = this.app.config.settings.paths?.apisLocalPath || 'data.local/apis-local';
349
+ const localDir = path.join(process.cwd(), apisLocalPath)
350
+
351
+ await this.preloadDir(localDir, 'local')
352
+
353
+ const registryDir = process.cwd() + '/extensions/omni-core-blocks/server/apis/';
354
+ await this.preloadDir(registryDir)
355
+
356
+ const end = performance.now(); // End timer
357
+ this.info(`BlockManager preload completed in ${(end - start).toFixed()}ms`);
358
+ }
359
+
360
+ async uninstallNamespace(ns: string, prefix: string = 'local') {
361
+ // sanitize
362
+ ns = ns.replace(/[^a-zA-Z0-9-_]/g, '');
363
+ if (ns.length <3) {
364
+ throw new Error('Namespace too short');
365
+ }
366
+ const name = `${prefix}-${ns}`
367
+ if (!this.namespaces.get(name)) {
368
+ throw new Error('Namespace '+name+'not found');
369
+ }
370
+ this.info(`Uninstalling namespace ${name}`);
371
+ this._kvStorage?.runSQL(`DELETE FROM kvstore WHERE key LIKE ?`, `%:${name}%`);
372
+ }
373
+
374
+ async registerFromFolder(dirPath: string, prefix?:string, forceRefresh:boolean=false): Promise<void> {
375
+ const start = performance.now(); // Start timer
376
+ const files = await readdir(dirPath);
377
+
378
+ await Promise.all(
379
+ files.map(async (file) => {
380
+ if (file.endsWith('.yaml')) {
381
+ try {
382
+ // load the yaml file
383
+ const nsData = yaml.load(await readFile(path.join(dirPath, file), 'utf8')) as OmniNamespaceDefinition;
384
+
385
+ if (!nsData.title) {
386
+ nsData.title = nsData.namespace;
387
+ }
388
+
389
+ if (prefix)
390
+ {
391
+ nsData.namespace = `${prefix}-${nsData.namespace}`;
392
+ nsData.title = `$${nsData.title} (${prefix})`;
393
+ nsData.prefix = prefix
394
+ }
395
+
396
+ // get the namespace
397
+ const ns = nsData.namespace;
398
+ const url = nsData.api?.url ?? nsData.api?.spec ?? nsData.api?.json;
399
+ if (!ns || !url) {
400
+ this.error(`Skipping ${dirPath}\\${file} as it does not have a valid namespace or api field`);
401
+ return;
402
+ }
403
+
404
+ if (this.namespaces.has(ns) && !forceRefresh) {
405
+ this.debug('Skipping namespace ' + ns + " as it's already registered");
406
+ await Promise.resolve();
407
+ return;
408
+ }
409
+
410
+ await this.addNamespace(ns, nsData, true);
411
+ const opIds: string[] = [];
412
+ const patches: OmniComponentPatch[] = [];
413
+
414
+ const cDir = path.join(dirPath, 'blocks');
415
+
416
+ if (await this.checkDirectory(cDir)) {
417
+ const components = await readdir(cDir);
418
+ await Promise.all(
419
+ components.map(async (component) => {
420
+ if (component.endsWith('.yaml')) {
421
+ // load the yaml file
422
+ const patch = yaml.load(await readFile(cDir + '/' + component, 'utf8')) as any;
423
+ if (nsData.prefix)
424
+ {
425
+ patch.title = `${patch.title} (${nsData.prefix})`;
426
+ patch.apiNamespace = `${nsData.prefix}-${patch.apiNamespace}`;
427
+ patch.displayNamespace = `${nsData.prefix}-${patch.displayNamespace}`;
428
+ patch.tags = patch.tags ?? [];
429
+ patch.tags.push(nsData.prefix);
430
+ }
431
+ opIds.push(patch.apiOperationId);
432
+ patches.push(patch);
433
+ }
434
+ })
435
+ );
436
+ }
437
+
438
+ this.info(`Loading ${url} as ${ns}`);
439
+
440
+ try {
441
+ await this.blocksFromNamespace(nsData, dirPath, opIds, patches);
442
+ await this.processPatches(patches);
443
+ } catch (e) {
444
+ this.error(`Failed to process ${ns} ${url}`, e);
445
+ throw e;
446
+ }
447
+ } catch (error) {
448
+ this.warn(`Failed to register from ${path.join(dirPath, file)}`, error);
449
+ throw error;
450
+ }
451
+ }
452
+ })
453
+ );
454
+
455
+ const end = performance.now(); // Start timer
456
+ this.info(`BlockManager registerFromFolder from ${dirPath} in ${(end - start).toFixed()}ms`);
457
+ }
458
+
459
+ private async loadAPISpec(currDir: string, api: { url?: string; json?: string; spec?: string }): Promise<any> {
460
+ const start = performance.now(); // Start timer
461
+
462
+ let parsedSchema = null;
463
+
464
+ if (api.url != null)
465
+ {
466
+ this.info('Loading API from URL', api.url);
467
+ let response;
468
+ try {
469
+ // Download the spec using fetch
470
+ response = await fetch(api.url);
471
+ }
472
+ catch (error)
473
+ {
474
+ this.error(error);
475
+ throw new Error(`Failed to fetch spec from ${api.url}`);
476
+ }
477
+
478
+ const spec = await response.text();
479
+ try
480
+ {
481
+ if (api.url.endsWith('.yaml') || api.url.endsWith('.yml')) {
482
+ // @ts-ignore
483
+ parsedSchema = await SwaggerClient.resolve({ spec: yaml.load(spec) });
484
+ }
485
+ else
486
+ {
487
+ // @ts-ignore
488
+ parsedSchema = await SwaggerClient.resolve({ spec: JSON.parse(spec) });
489
+ }
490
+
491
+ // @ts-ignore
492
+
493
+ } catch (error) {
494
+ this.error(error);
495
+ throw new Error(`Failed to resolve spec from ${api.url}`);
496
+ }
497
+ } else if (api.json) {
498
+ this.info('Loading API from JSON', api.json);
499
+ // @ts-ignore
500
+ parsedSchema = await SwaggerClient.resolve({ spec: api.json });
501
+ } else if (api.spec != null) {
502
+ this.info('Loading API from SPEC', api.spec);
503
+ const specPath = path.join(currDir, api.spec);
504
+ if (existsSync(specPath)) {
505
+ const spec = yaml.load(await readFile(specPath, 'utf8')) as any;
506
+ // @ts-ignore
507
+ parsedSchema = await SwaggerClient.resolve({ spec });
508
+ } else {
509
+ this.error(`Spec file ${specPath} not found`);
510
+ throw new Error(`Spec file ${specPath} not found`);
511
+ }
512
+ } else {
513
+ throw new Error('No url or spec provided');
514
+ }
515
+
516
+ const end = performance.now(); // End timer
517
+ this.info(`loadAPISpec ${currDir} completed in ${(end - start).toFixed(1)} milliseconds`);
518
+ return parsedSchema?.spec ?? parsedSchema;
519
+ }
520
+
521
+ private async checkDirectory(path: string) {
522
+ try {
523
+ await access(path);
524
+ return true;
525
+ } catch {
526
+ return false;
527
+ }
528
+ }
529
+
530
+ private async blocksFromNamespace(
531
+ nsData: OmniNamespaceDefinition,
532
+ dir: string,
533
+ filterOpIds: string[],
534
+ patches: OmniComponentPatch[]
535
+ ) {
536
+ const ns = nsData.namespace;
537
+ this.info(`Processing API ${ns}`, filterOpIds, patches?.length ?? 0);
538
+ const specDoc = await this.loadAPISpec(dir, nsData.api ?? {});
539
+
540
+ if (!specDoc) {
541
+ this.error(`Error: Could not fetch OpenAPI spec for ${ns}`);
542
+ return;
543
+ }
544
+
545
+ const adapter = new OpenAPIReteAdapter(ns, specDoc, nsData.api?.auth);
546
+ const blocks = adapter.getReteComponentDefs(/* filterOpIds */);
547
+
548
+ this.info('------ Adding Blocks ------');
549
+
550
+ for (const c of blocks) {
551
+ const key = `${c.displayNamespace}.${c.displayOperationId}`;
552
+
553
+ // Add to new blocks manager
554
+ if (!this.hasBlock(key)) {
555
+ try {
556
+ this.addBlock(c);
557
+ await this.app.events.emit('block_added', [{block: c}]);
558
+ this.verbose(`Added Block "${key}"`);
559
+ } catch (e) {
560
+ this.error(`Failed to add block "${key}"`, e);
561
+ await this.app.events.emit('block_added', [{error: e}]);
562
+ return;
563
+ }
564
+ }
565
+ }
566
+ }
567
+
568
+ private async processPatches(patches: OmniComponentPatch[]) {
569
+ this.info('------ Adding Patches ------');
570
+
571
+ for (const p of patches) {
572
+ const key = `${p.displayNamespace}.${p.displayOperationId}`;
573
+ try {
574
+ if (!this.blocks.has(`${p.apiNamespace}.${p.apiOperationId}`)) {
575
+ this.warn(
576
+ `Patch ${p.displayNamespace}.${p.displayOperationId} skipped as base block ${p.apiNamespace}.${p.apiOperationId} was not found`
577
+ );
578
+ } else {
579
+ if (this.patches.has(key)) {
580
+ this.verbose(`Patch ${key} already registered, overwriting`);
581
+ }
582
+ const allowOverwrite = true;
583
+ this.addPatch(p, allowOverwrite);
584
+ }
585
+ } catch (e) {
586
+ this.error(`Failed to add patch ${key}`, e);
587
+ }
588
+
589
+ this.info(`Adding patch ${key}`);
590
+ }
591
+ }
592
+
593
+ getBlock(key: string): OmniComponentFormat | undefined {
594
+ return this.blocks.get(key);
595
+ }
596
+
597
+ public async addNamespace(key: string, namespace: OmniNamespaceDefinition, allowOverwrite?: boolean) {
598
+ if (!key) throw new Error('addNamespace(): key cannot be undefined');
599
+ if (!namespace) {
600
+ throw new Error('addNamespace(): namespace cannot be undefined');
601
+ }
602
+ if (this.namespaces.has(key) && !allowOverwrite) {
603
+ throw new Error(`addNamespace(): namespace ${key} already registered`);
604
+ }
605
+ this.namespaces.set(key, namespace);
606
+
607
+ await this.app.events.emit('register_namespace', namespace);
608
+
609
+ return this;
610
+ }
611
+
612
+ public addPatch(patch: OmniComponentPatch, allowOverwrite?: boolean) {
613
+ const key = `${patch.displayNamespace}.${patch.displayOperationId}`;
614
+ if (!key) throw new Error('addPatch(): key cannot be undefined');
615
+ if (!patch) throw new Error('addPatch(): patch cannot be undefined');
616
+ if (this.patches.has(key) && !allowOverwrite) {
617
+ throw new Error(`addPatch(): patch ${key} already registered`);
618
+ }
619
+
620
+ // We use these to identify the base blocks, so patches without them are invalid
621
+ if (!patch.apiNamespace) {
622
+ throw new Error(`addPatch(): patch ${key} is missing apiNamespace`);
623
+ }
624
+ if (!patch.apiOperationId) {
625
+ throw new Error(`addPatch(): patch ${key} is missing apiOperationId`);
626
+ }
627
+
628
+ this.info('Registering patch', key);
629
+ patch = removeUndefinedValues(patch);
630
+ patch.hash = BlockManager.hashObject(patch);
631
+ this.patches.set(key, patch);
632
+ }
633
+
634
+ getMacro(component: OAIBaseComponent, macroType: OmniComponentMacroTypes): Function | undefined {
635
+ let macro = component?.macros?.[macroType];
636
+
637
+ if (typeof macro === 'string') {
638
+ macro = this.macros.get(macro);
639
+ }
640
+
641
+ if (typeof macro === 'function') {
642
+ return macro.bind(component);
643
+ }
644
+
645
+ return undefined;
646
+ }
647
+
648
+ hashObject(obj: any) {
649
+ return BlockManager.hashObject(obj);
650
+ }
651
+
652
+ static hashObject(obj: any) {
653
+ if (obj.patch) delete obj.patch;
654
+ const hashState = new MurmurHash3();
655
+ const hash = hashState.hash(JSON.stringify(obj)).result().toString(16);
656
+ return hash;
657
+ }
658
+
659
+ public registerMacro(key: string, macro: Function) {
660
+ this.macros.set(key, macro);
661
+ }
662
+
663
+ public addBlock(block: OmniComponentFormat) {
664
+ const key = `${block.apiNamespace}.${block.apiOperationId}`;
665
+ if (!block) throw new Error(`Block ${key} is undefined`);
666
+ if (!block.type) throw new Error(`Block ${key} is missing type`);
667
+ if (!this.factories.has(block.type)) {
668
+ throw new Error(`Block ${key} has unknown type ${block.type}` + Array.from(this.factories.keys()).toString());
669
+ }
670
+
671
+ if (block.displayNamespace !== block.apiNamespace || block.displayOperationId !== block.apiOperationId) {
672
+ throw new Error(
673
+ `addBlock(): Block ${key} has mismatched display and api namespaces, indicating it is a patch. Use addPatch() instead`
674
+ );
675
+ }
676
+
677
+ this.debug('Registering block', key);
678
+
679
+ /*
680
+ Macros
681
+
682
+ Macros are functions attached to a blocks' macro collection. Since we don't want to serialize functions into the database, we rely on them getting
683
+ registered on every startup, from 2 potential sources:
684
+
685
+ 1. The block itself, which can have a macro collection with functions. These are registered as macro://<block_key>:<namespace>.<operationId>
686
+ 2. Extensions, which can export macros along with the createComponent function. These are picked up by the extension manager and fired as register_macros event
687
+ which is picked up here. This is useful to allow many blocks to use the same exec function for example
688
+ */
689
+ const macros = block.macros;
690
+
691
+ if (macros && Object.keys(macros).length > 0) {
692
+ for (const m in macros) {
693
+ // @ts-ignore
694
+ const macro = macros[m];
695
+ this.verbose('Registering macro', m);
696
+
697
+ if (typeof macro === 'function') {
698
+ const macroKey = 'macro://' + m + ':' + block.displayNamespace + '.' + block.displayOperationId;
699
+
700
+ this.registerMacro(macroKey, macro);
701
+ // @ts-ignore
702
+ macros[m] = macroKey;
703
+ } else if (typeof macro === 'string') {
704
+ // @ts-ignore
705
+ if (!this.macros.has(macro)) {
706
+ throw new Error(`Block ${key} has unknown macro ${m}. The Macro has to be registered before the block`);
707
+ }
708
+ }
709
+ }
710
+ }
711
+
712
+ block = removeUndefinedValues(block) as OmniComponentFormat;
713
+ block.hash = BlockManager.hashObject(block);
714
+
715
+ this.blocks.set(key, block);
716
+ }
717
+
718
+ public hasBlock(key: string): boolean {
719
+ if (!key) throw new Error('hasBlock(): key cannot be undefined');
720
+ return this.blocks.has(key);
721
+ }
722
+
723
+ public async canRunBlock(block: OAIBaseComponent, userId: string): Promise<boolean> {
724
+ if (!block) throw new Error('canRunBlock(): block cannot be undefined');
725
+
726
+ const credentialsService = this.app.services.get('credentials') as CredentialService | undefined;
727
+ if (!credentialsService) {
728
+ throw new Error('Credentials service unavailable');
729
+ }
730
+
731
+ return await credentialsService.hasSecret(userId, block.apiNamespace);
732
+ }
733
+
734
+ public registerType(key: string, Factory: Function): void {
735
+ if (!key) throw new Error('registerType(): key cannot be undefined');
736
+ if (!Factory || typeof Factory !== 'function') {
737
+ throw new Error(`Factory ${key} must be a function`);
738
+ }
739
+ if (this.factories.has(key)) {
740
+ throw new Error(`Block type ${key} already registered`);
741
+ }
742
+
743
+ this.factories.set(key, Factory);
744
+ }
745
+
746
+ // return a composed block. If the key responds to a patch, the patch is applied to the underlying block
747
+ public async getInstance(key: string, userId?: string): Promise<OAIBaseComponent | undefined> {
748
+ const patch = this.patches.get(key);
749
+
750
+ const baseKey = patch ? `${patch.apiNamespace}.${patch.apiOperationId}` : key;
751
+
752
+ const block = this.blocks.get(baseKey);
753
+
754
+ if (!block) {
755
+ return undefined;
756
+ }
757
+
758
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
759
+ const Factory = this.factories.get(block.type)!; // Block types are guaranteed on insert so we can ! here
760
+ const ret = Factory(block, patch) as OAIComponent31;
761
+
762
+ if (block.dependsOn) {
763
+ const check = block.dependsOn.filter((d: string) => !this.hasBlock(d) && !this.patches.has(d));
764
+
765
+ if (check.length > 0) {
766
+ ret.data.errors.push(`Missing dependencies: ${check.join(',')}`);
767
+ }
768
+ }
769
+
770
+ ret.data.tags.push(patch ? 'patch' : 'base-api');
771
+
772
+ const hideInputs = ret.scripts?.['hideExcept:inputs'];
773
+ if (hideInputs?.length) {
774
+ for (const k in ret.inputs) {
775
+ ret.inputs[k].hidden = ret.inputs[k].hidden ?? !hideInputs.includes(k);
776
+ }
777
+ }
778
+
779
+ const hideOutputs = ret.scripts?.['hideExcept:outputs'];
780
+ if (hideOutputs?.length) {
781
+ for (const k in ret.outputs) {
782
+ ret.outputs[k].hidden = ret.outputs[k].hidden ?? !hideOutputs.includes(k);
783
+ }
784
+ }
785
+
786
+ // TODO: Do not hide _omni_result socket for now as it is easier to debug and surface issue
787
+ // Once there is patch, hide _omni_result socket in outputs if it exists
788
+ // if (patch && Object.keys(ret.outputs ?? {}).length > 1 && ret.outputs?._omni_result) {
789
+ // ret.outputs['_omni_result'].hidden = true;
790
+ // }
791
+
792
+ if (userId && !(await this.canRunBlock(ret, userId))) {
793
+ ret.data.errors.push('Block cannot run');
794
+ }
795
+
796
+ return ret as OAIBaseComponent;
797
+ }
798
+
799
+ public async tryResolveExtensionBlock(ctx: any, key: string): Promise<OAIBaseComponent|undefined>
800
+ {
801
+ // if an extension is involved, the block key is in the form of <extension_name>:<block_key>
802
+ if (key.indexOf(':') > 0)
803
+ {
804
+ const server = (this.app as MercsServer)
805
+ const [extensionId, blockKey] = key.split(':');
806
+
807
+ if (server.extensions.has(extensionId))
808
+ {
809
+ const extension = server.extensions.get(extensionId);
810
+ if (extension)
811
+ {
812
+ const block = await extension.invokeKnownMethod(KNOWN_EXTENSION_METHODS.resolveMissingBlock, ctx, blockKey)
813
+
814
+ if (block)
815
+ {
816
+ return block;
817
+ }
818
+ else
819
+ {
820
+ return undefined;
821
+ }
822
+ }
823
+ }
824
+ }
825
+ }
826
+
827
+
828
+ public async getInstances(
829
+ keys: string[],
830
+ userId?: string,
831
+ failBehavior: 'throw' | 'filter' | 'missing_block' = 'throw'
832
+ ): Promise<{blocks: OAIBaseComponent[], missing: string[]}> {
833
+ if (!keys || !Array.isArray(keys)) {
834
+ throw new Error('getInstances(keys): keys must be string[]');
835
+ }
836
+ const missing:string[] = []
837
+
838
+ const promises = keys.map(async (key) => {
839
+ const block = await this.getInstance(key, userId);
840
+ if (block) {
841
+ return block;
842
+ }
843
+ missing.push(key)
844
+ if (failBehavior === 'throw') {
845
+ const patch = this.patches.get(key);
846
+ if (patch) {
847
+ throw new Error(`Unable to compose patched block "${key}" / "${patch.apiNamespace}.${patch.apiOperationId}"`);
848
+ }
849
+ throw new Error(`Unable to find block "${key}"`);
850
+ }
851
+
852
+ if (failBehavior === 'missing_block') {
853
+ omnilog.warn(`[getInstances] Unable to compose block "${key}"`); // Caution: `key` may differ from patched key
854
+ const result = await this.getInstance('omnitool._block_missing', userId);
855
+ if (result)
856
+ {
857
+ result.data.errors.push(`Unable to compose block "${key}"`);
858
+ //@ts-ignore
859
+ result.data._missingKey = key;
860
+ }
861
+ return result;
862
+ }
863
+ return undefined;
864
+ });
865
+
866
+ let result = await Promise.all(promises);
867
+
868
+ if (failBehavior === 'filter') {
869
+ result = result.filter((r) => r);
870
+ }
871
+ return {blocks: (result ?? []) as OAIBaseComponent[], missing}
872
+ }
873
+
874
+ public getAllNamespaces(opts?: { filter: any }): OmniNamespaceDefinition[] {
875
+ let all = Array.from(this.namespaces.values());
876
+ if (opts?.filter) {
877
+ all = all.filter((n: OmniNamespaceDefinition) => n.namespace === opts.filter);
878
+ }
879
+ return all;
880
+ }
881
+
882
+ private orderByTitle(a: IBlockOrPatchSummary, b: IBlockOrPatchSummary) {
883
+ const aKey = a?.title ?? a?.name ?? '';
884
+ const bKey = b?.title ?? b?.name ?? '';
885
+ const locale = 'en-US-POSIX'; // Ensure consistent string comparison, similar to the "C" locale.
886
+ return aKey.toLowerCase().localeCompare(bKey.toLowerCase(), locale);
887
+ }
888
+
889
+ public getFilteredBlocksAndPatches(
890
+ limit: number,
891
+ cursor: number,
892
+ keyword: string,
893
+ opts?: { contentMatch: string; tags: string }
894
+ ): Array<[number, IBlockOrPatchSummary]> {
895
+ const maxLimit = 9999;
896
+ const filter = keyword.replace(/ /g, '').toLowerCase() ?? '';
897
+ const blockAndPatches = this.blocksAndPatches.search(
898
+ maxLimit,
899
+ 0,
900
+ filter,
901
+ opts?.contentMatch,
902
+ opts?.tags,
903
+ 'BlocksAndPatches'
904
+ );
905
+ const all: Array<[number, IBlockOrPatchSummary]> = [];
906
+ if (blockAndPatches) {
907
+ for (const item of blockAndPatches) {
908
+ const itemFormatHeader = this.formatHeader(item[1]);
909
+ if (itemFormatHeader.tags?.includes('base-api')) continue;
910
+ all.push([item[2], itemFormatHeader]);
911
+ }
912
+ }
913
+ all.sort((a, b) => this.orderByTitle(a[1], b[1]));
914
+ return all.slice(cursor, cursor + limit);
915
+ }
916
+
917
+ public getNamespace(key: string): OmniNamespaceDefinition | undefined {
918
+ return this.namespaces.get(key);
919
+ }
920
+
921
+ public getBlocksForNamespace(ns: string): OmniComponentFormat[] {
922
+ // TODO: Gezo: This doesn't actually work, it ignores anything that's a patch.
923
+ // We first need to retrieve all patches for the namespace, then get blocks that do not have patches
924
+ // GetInstance handles this for example
925
+
926
+ return Array.from(this.blocks.values()).filter((block: OmniComponentFormat) => block.apiNamespace === ns);
927
+ }
928
+
929
+ public async getAllBlocks(
930
+ includeDefinitions: boolean = true,
931
+ filter?: any
932
+ ): Promise<Array<OAIBaseComponent | string>> {
933
+ const patches = Array.from(this.patches.keys());
934
+
935
+ if (includeDefinitions) {
936
+ // TODO: We actually want to return header information here, keys alone are not so useful
937
+ let blocks = Array.from(this.blocks.keys());
938
+
939
+ // Build a set from the keys in patches.
940
+ const patchSet = new Set(patches);
941
+
942
+ // Filter blocks if they alias a patch.
943
+ blocks = blocks.filter((key) => !patchSet.has(key));
944
+
945
+ return [...blocks, ...patches];
946
+ }
947
+
948
+ const patchInstances = (await Promise.all(patches.map(async (key) => await this.getInstance(key)))).filter(
949
+ Boolean
950
+ ) as OAIBaseComponent[];
951
+
952
+ const blocks = Array.from(this.blocks.keys()).filter(
953
+ (key) => !patchInstances.find((p: OAIBaseComponent) => p.name === key)
954
+ );
955
+ const blockInstances = (await Promise.all(blocks.map(async (key) => await this.getInstance(key)))).filter(
956
+ Boolean
957
+ ) as OAIBaseComponent[];
958
+
959
+ return [...blockInstances, ...patchInstances];
960
+ }
961
+
962
+ public getRequiredCredentialsForBlock(key: string): OmniAPIKey[] {
963
+ const block = this.blocks.get(key);
964
+ if (!block) throw new Error(`Block ${key} not found`);
965
+
966
+ const securitySchemes = block.security;
967
+
968
+ if (!securitySchemes || securitySchemes.length <= 0) {
969
+ return [];
970
+ }
971
+
972
+ const requiredCredentials: OmniAPIKey[] = [];
973
+ // For each security scheme, parse the required credentials
974
+ for (const scheme of securitySchemes) {
975
+ scheme.requireKeys?.forEach((key: OmniAPIKey) => {
976
+ const existing = requiredCredentials.find((k) => k.id === key.id);
977
+ if (!existing) {
978
+ requiredCredentials.push(key);
979
+ }
980
+ });
981
+ }
982
+
983
+ return Array.from(requiredCredentials);
984
+ }
985
+
986
+ public getRequiredCredentials(namespace: string, includeOptional: boolean = true): OmniAPIKey[] {
987
+ // Get the security schemes from the API spec
988
+ const securitySchemes: OmniAPIAuthenticationScheme[] = [];
989
+ const components = this.getBlocksForNamespace(namespace);
990
+ if (components != null) {
991
+ components.forEach((component) => {
992
+ if (component.security != null) {
993
+ securitySchemes.push(...component.security);
994
+ }
995
+ });
996
+ }
997
+ if (securitySchemes.length <= 0) {
998
+ return []; // `No security schemes found for namespace ${namespace}`
999
+ }
1000
+
1001
+ const requiredCredentials: OmniAPIKey[] = [];
1002
+ // For each security scheme, parse the required credentials
1003
+ for (const scheme of securitySchemes) {
1004
+ if (!includeOptional && scheme.isOptional) {
1005
+ continue;
1006
+ }
1007
+
1008
+ scheme.requireKeys?.forEach((key: OmniAPIKey) => {
1009
+ const existing = requiredCredentials.find((k) => k.id === key.id);
1010
+ if (!existing) {
1011
+ requiredCredentials.push(key);
1012
+ }
1013
+ });
1014
+ }
1015
+
1016
+ return Array.from(requiredCredentials);
1017
+ }
1018
+
1019
+ async getSecurityScheme(apiNamespace: string, version?: string): Promise<OmniAPIAuthenticationScheme[]> {
1020
+ // Get the security schemes from the API spec
1021
+ const securitySchemes: OmniAPIAuthenticationScheme[] = [];
1022
+ const components = this.getBlocksForNamespace(apiNamespace);
1023
+ if (components != null) {
1024
+ components.forEach((component) => {
1025
+ if (component.security != null) {
1026
+ securitySchemes.push(...component.security);
1027
+ }
1028
+ });
1029
+ }
1030
+
1031
+ return securitySchemes;
1032
+ }
1033
+
1034
+ async searchSecurityScheme(apiNamespace: string, version?: string, schemeType?: string, oauthFlowType?: string) {
1035
+ const securitySchemes = await this.getSecurityScheme(apiNamespace, version);
1036
+ const filteredSecuritySchemes = securitySchemes.filter((securityScheme) => {
1037
+ if (schemeType != null) {
1038
+ // Filter by the scheme type
1039
+ if (securityScheme.type !== schemeType) {
1040
+ return false;
1041
+ }
1042
+
1043
+ // Filter by the oauth flow type
1044
+ if (schemeType === 'oauth2' && oauthFlowType != null) {
1045
+ if (Object.hasOwnProperty.call(securityScheme.oauth, oauthFlowType)) {
1046
+ return true;
1047
+ } else {
1048
+ return false;
1049
+ }
1050
+ }
1051
+ }
1052
+
1053
+ return true;
1054
+ });
1055
+ return filteredSecuritySchemes;
1056
+ }
1057
+
1058
+ public getAPISignature(namespace: string, operationId: string) {
1059
+ const ns = this.getNamespace(namespace);
1060
+ if (!ns) {
1061
+ throw new Error(`Namespace ${namespace} not found`);
1062
+ }
1063
+
1064
+ const component = this.getBlock(`${namespace}.${operationId}`);
1065
+ if (!component) {
1066
+ throw new Error(`BlockManager: Component ${operationId} not found`);
1067
+ }
1068
+
1069
+ const signature = {
1070
+ method: component.method,
1071
+ url: ns.api?.basePath + component.urlPath,
1072
+ contentType: component.responseContentType,
1073
+ requestContentType: component.requestContentType,
1074
+ security: component.security
1075
+ };
1076
+ this.debug(`getAPISignature ${namespace} ${operationId} ${JSON.stringify(signature, null, 2)}`);
1077
+ return signature;
1078
+ }
1079
+
1080
+ public async runBlock(
1081
+ ctx: WorkerContext,
1082
+ blockName: string,
1083
+ args: any,
1084
+ outputs?: any,
1085
+ opts?: {
1086
+ cacheType?: 'global' | 'user' | 'session';
1087
+ cacheKey?: string;
1088
+ cacheTTLInSeconds?: number;
1089
+ bustCache?: false;
1090
+ timeout?: number;
1091
+ }
1092
+ ) {
1093
+ opts ??= {};
1094
+
1095
+ this.info('runblock', blockName, args, outputs, opts);
1096
+
1097
+ if (!ctx.sessionId) {
1098
+ this.error('Invalid session');
1099
+ return { error: 'Invalid session' };
1100
+ }
1101
+
1102
+ const block = await this.getInstance(blockName);
1103
+ this.info(`Running block ${blockName}`);
1104
+ if (!block) {
1105
+ this.error('Invalid block', blockName);
1106
+ return { error: 'Invalid block' };
1107
+ }
1108
+
1109
+ const inputs: Record<string, any> = {};
1110
+
1111
+ for (const key in args) {
1112
+ if (args[key] !== null && args[key] !== undefined) {
1113
+ inputs[key] = Array.isArray(args[key]) ? args[key] : [args[key]];
1114
+ }
1115
+ }
1116
+
1117
+ outputs ??= { text: '' };
1118
+
1119
+ const node = {
1120
+ id: 1,
1121
+ name: blockName,
1122
+ type: 'component',
1123
+ component: blockName,
1124
+ inputs,
1125
+ outputs,
1126
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
1127
+ data: {} as NodeData,
1128
+ position: [0, 0] as [number, number]
1129
+ };
1130
+
1131
+ const workerContext = WorkerContext.create(ctx.app, null, node, {
1132
+ ...ctx.getData()
1133
+ });
1134
+
1135
+ let cKey = '';
1136
+ const ttl = (opts.cacheTTLInSeconds ? opts.cacheTTLInSeconds * 1000 : 60 * 60 * 24 * 1000) + Date.now();
1137
+ if (opts?.cacheType) {
1138
+ const hashState = new MurmurHash3();
1139
+ if (opts.cacheType === 'session') {
1140
+ cKey = ctx.sessionId;
1141
+ } else if (opts.cacheType === 'global') {
1142
+ cKey = 'global';
1143
+ } else if (opts.cacheType === 'user') {
1144
+ cKey = ctx.userId;
1145
+ }
1146
+ const hash = hashState.hash(JSON.stringify(inputs)).result().toString(16);
1147
+ cKey = cKey + ':' + blockName + ':' + block.hash + hash;
1148
+ }
1149
+
1150
+ if (opts?.bustCache) {
1151
+ this.info('Busting cache for ' + cKey);
1152
+ this.cache.delete(cKey);
1153
+ } else if (cKey.length && this.cache.get(cKey)) {
1154
+ this.info('Cache hit for ' + cKey);
1155
+ return this.cache.get(cKey);
1156
+ }
1157
+
1158
+ let result = (await block.workerStart(inputs, workerContext)) as any;
1159
+
1160
+ // Let's throw on errors
1161
+ if (!result || result.error) {
1162
+ if (!result) {
1163
+ result = { error: 'Unknown error' };
1164
+ }
1165
+ this.error('Error running block', result.error);
1166
+ return result;
1167
+ }
1168
+
1169
+ if (cKey.length) {
1170
+ this.info('Cache miss for ' + cKey);
1171
+ this.cache.set(cKey, result, ttl);
1172
+ }
1173
+
1174
+ return result;
1175
+ }
1176
+ }
1177
+
1178
+ export { BlockManager };
patches/packages/omni-server/src/core/BlockManager.ts.good ADDED
@@ -0,0 +1,1223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Copyright (c) 2023 MERCENARIES.AI PTE. LTD.
3
+ * All rights reserved.
4
+ */
5
+
6
+ // ----------------------------------------------------------------------------------------------
7
+ // BlockManager.ts
8
+ //
9
+ // Purpose: Manage the blocks that are available to the server.
10
+ // Provide on demand block composition.
11
+ // ----------------------------------------------------------------------------------------------
12
+ import MurmurHash3 from 'imurmurhash';
13
+ import path from 'path';
14
+ import type MercsServer from './Server.js';
15
+
16
+ import { existsSync } from 'fs';
17
+ import { access, readFile, readdir, stat } from 'fs/promises';
18
+ // import { existsSync, promises as fs } from 'fs';
19
+ import yaml from 'js-yaml';
20
+ import {
21
+ OAIBaseComponent,
22
+ OAIComponent31,
23
+ WorkerContext,
24
+ type OmniAPIAuthenticationScheme,
25
+ type OmniAPIKey,
26
+ type OmniComponentFormat,
27
+ type OmniComponentMacroTypes,
28
+ type OmniComponentPatch,
29
+ type OmniNamespaceDefinition
30
+ } from 'omni-sockets';
31
+ import { Manager, omnilog, type IApp, type IBlockOrPatchSummary } from 'omni-shared';
32
+
33
+ import SwaggerClient from 'swagger-client';
34
+ import { type AmqpService } from '../services/AmqpService.js';
35
+ import { OpenAPIReteAdapter } from '../services/ComponentService/OpenAPIReteAdapter.js';
36
+ import { KVStorage, type IKVStorageConfig } from './KVStorage.js';
37
+ import { KNOWN_EXTENSION_METHODS} from './ServerExtensionsManager.js';
38
+ import { type NodeData } from 'rete/types/core/data.js';
39
+ import { OmniDefaultBlocks } from '../blocks/DefaultBlocks.js';
40
+ import { StorageAdapter } from './StorageAdapter.js';
41
+
42
+ import { type CredentialService } from 'services/CredentialsService/CredentialService.js';
43
+ import axios from 'axios';
44
+ interface IBlockManagerConfig {
45
+ preload: boolean;
46
+ kvStorage: IKVStorageConfig;
47
+ }
48
+ const PRELOAD_REGISTRY_IN_PARALLEL = false;
49
+
50
+ type UndefinedPruned<T> = T extends object ? { [P in keyof T]: UndefinedPruned<T[P]> } : T;
51
+
52
+ function removeUndefinedValues<T>(obj: T): UndefinedPruned<T> {
53
+ if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
54
+ return obj as UndefinedPruned<T>;
55
+ }
56
+
57
+ const result: Partial<UndefinedPruned<T>> = {};
58
+
59
+ for (const key in obj) {
60
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
61
+ const value = (obj as any)[key];
62
+ if (value !== undefined) {
63
+ result[key as keyof T] = removeUndefinedValues(value);
64
+ }
65
+ }
66
+ }
67
+
68
+ return result as UndefinedPruned<T>;
69
+ }
70
+
71
+ class BlockManager extends Manager {
72
+ ReteAdapter: typeof OpenAPIReteAdapter = OpenAPIReteAdapter;
73
+ BaseComponent: typeof OAIBaseComponent = OAIBaseComponent;
74
+
75
+ private readonly factories: Map<string, Function>; // This holds component factories
76
+ private readonly namespaces: StorageAdapter<OmniNamespaceDefinition>; // This holds namespaces
77
+ private readonly patches: StorageAdapter<OmniComponentPatch>; // This holds patches
78
+ private readonly blocks: StorageAdapter<OmniComponentFormat>; // This holds blocks
79
+ private readonly blocksAndPatches: StorageAdapter<OmniComponentFormat | OmniComponentPatch>; // This holds blocks and patches
80
+ private readonly macros: Map<string, Function>; // This holds functions attached to blocks
81
+ private readonly cache: StorageAdapter<any>; // This holds cached entries
82
+
83
+ private readonly config: IBlockManagerConfig;
84
+ public _kvStorage?: KVStorage;
85
+
86
+ constructor(app: IApp, config: IBlockManagerConfig) {
87
+ super(app);
88
+ this.config = config;
89
+
90
+ this.blocks = new StorageAdapter<OmniComponentFormat>('block:'); /* ) new Map<string, OmniComponentFormat>() */
91
+ this.patches = new StorageAdapter<OmniComponentPatch>('patch:');
92
+ this.blocksAndPatches = new StorageAdapter<OmniComponentFormat | OmniComponentPatch>();
93
+ this.namespaces = new StorageAdapter<OmniNamespaceDefinition>('ns:');
94
+ this.cache = new StorageAdapter<any>('cache:', undefined, 60 * 60 * 24);
95
+
96
+ // Type factories and macros
97
+ this.factories = new Map<string, Function>();
98
+ this.macros = new Map<string, Function>();
99
+
100
+ this.registerType('OAIComponent31', OAIComponent31.fromJSON);
101
+
102
+ app.events.on('credential_change', (e: any) => {
103
+ this.cache.clearWithPrefix();
104
+ });
105
+ }
106
+
107
+ get kvStorage(): KVStorage {
108
+ if (this._kvStorage == null) {
109
+ throw new Error('BlockManager kvStorage accessed before load');
110
+ }
111
+ return this._kvStorage;
112
+ }
113
+
114
+ async init() {
115
+ const kvConfig = this.config.kvStorage;
116
+ if (kvConfig) {
117
+ this._kvStorage = new KVStorage(this.app, kvConfig);
118
+ // Need to register view before init() call
119
+ this._kvStorage.registerView(
120
+ 'BlocksAndPatches',
121
+ `CREATE VIEW IF NOT EXISTS BlocksAndPatches (key,
122
+ value,
123
+ valueType,
124
+ blob,
125
+ expiry,
126
+ tags,
127
+ deleted,
128
+ seq) AS
129
+ SELECT
130
+ key,
131
+ value,
132
+ valueType,
133
+ blob,
134
+ expiry,
135
+ tags,
136
+ deleted,
137
+ ROW_NUMBER() OVER (ORDER BY seq DESC) AS new_seq
138
+ FROM
139
+ kvstore
140
+ WHERE
141
+ (key LIKE 'block:%' OR key LIKE 'patch:%')
142
+ AND deleted = 0;`
143
+ );
144
+ if (!(await this.kvStorage.init())) {
145
+ throw new Error('KVStorage failed to start');
146
+ }
147
+
148
+ const resetDB = (this.app as MercsServer).options.resetDB;
149
+ if (resetDB?.split(',').includes('blocks')) {
150
+ this.info('Resetting blocks storage');
151
+ this.kvStorage.clear();
152
+ }
153
+ await this.kvStorage.vacuum();
154
+
155
+ this.app.events.on('register_blocks', (blocks: OmniComponentFormat[]) => {
156
+ blocks.forEach((block) => {
157
+ this.addBlock(block);
158
+ });
159
+ });
160
+
161
+ this.app.events.on('register_patches', (patches: OmniComponentPatch[]) => {
162
+ patches.forEach((patch) => {
163
+ this.addPatch(patch);
164
+ });
165
+ });
166
+
167
+ this.app.events.on('register_macros', (macros: Record<string, Function>) => {
168
+ Object.entries(macros || {}).forEach(([key, value]) => {
169
+ this.registerMacro(key, value);
170
+ });
171
+ });
172
+
173
+ this.blocks.bindStorage(this.kvStorage);
174
+ this.namespaces.bindStorage(this.kvStorage);
175
+ this.patches.bindStorage(this.kvStorage);
176
+ this.cache.bindStorage(this.kvStorage);
177
+ this.blocksAndPatches.bindStorage(this.kvStorage);
178
+ }
179
+
180
+ OmniDefaultBlocks.forEach((block) => {
181
+ this.addBlock(block);
182
+ });
183
+
184
+ this.registerExecutors();
185
+
186
+ if (this.config.preload) {
187
+ await this.preload();
188
+ }
189
+
190
+ this.info('BlockManager initialized');
191
+ return true;
192
+ }
193
+
194
+ formatHeader(blockOrPatch: {
195
+ title?: string;
196
+ description?: string;
197
+ category?: string;
198
+ displayNamespace: string;
199
+ displayOperationId: string;
200
+ tags?: string[];
201
+ }): IBlockOrPatchSummary {
202
+ return {
203
+ title: blockOrPatch.title ?? `${blockOrPatch.displayNamespace + '.' + blockOrPatch.displayOperationId}`,
204
+ description: blockOrPatch.description ?? '',
205
+ category: blockOrPatch.category,
206
+ name: `${blockOrPatch.displayNamespace + '.' + blockOrPatch.displayOperationId}`,
207
+ tags: blockOrPatch.tags ?? []
208
+ };
209
+ }
210
+
211
+ async stop() {
212
+ await this.kvStorage.stop();
213
+ await super.stop();
214
+
215
+ this.info('BlockManager stopped');
216
+ return true;
217
+ }
218
+
219
+ private registerExecutors() {
220
+ const amqpService = this.app.services.get('amqp') as AmqpService;
221
+
222
+ // @ts-ignore
223
+ this.app.api2 ??= {};
224
+ // @ts-ignore
225
+ this.app.api2.execute = async (
226
+ api: string,
227
+ body?: any,
228
+ requestConfig?: {
229
+ headers?: any;
230
+ params?: any;
231
+ responseType?: string;
232
+ responseEncoding?: string;
233
+ timeout: 0;
234
+ },
235
+ ctx?: any
236
+ ) => {
237
+ if (!ctx.userId || !ctx.sessionId) {
238
+ this.debug('execute() called without ctx.userId or ctx.sessionId');
239
+ }
240
+
241
+ const oid = api.split('.');
242
+ const integrationId = oid.shift();
243
+ const opKey = oid.join('.');
244
+
245
+ await this.app.events.emit('pre_request_execute', [ctx, api, { body, params: requestConfig?.params ?? {} }]);
246
+ omnilog.log('Executing', integrationId, opKey, body, requestConfig, ctx);
247
+
248
+ let result: any;
249
+ try {
250
+
251
+ result = await amqpService.publishAwaitable(
252
+ 'omni_tasks',
253
+ undefined,
254
+ Object.assign({}, { integration: { key: integrationId, operationId: opKey, block: api } }, { body }, requestConfig, {
255
+ job_ctx: ctx
256
+ })
257
+ );
258
+ } catch (e: unknown) {
259
+ this.error('Error executing', api, e);
260
+ result = { error: e };
261
+ } finally {
262
+ try {
263
+ await this.app.events.emit('post_request_execute', [
264
+ ctx,
265
+ api,
266
+ { body, params: requestConfig?.params ?? {}, result }
267
+ ]);
268
+ } catch (ex) {
269
+ omnilog.error(ex);
270
+ }
271
+ }
272
+ if (result.error) {
273
+ throw result.error;
274
+ }
275
+ return result;
276
+ };
277
+ }
278
+
279
+
280
+ private async preloadDir(registryDir:string, prefix?: string)
281
+ {
282
+ // check if directory exists
283
+ if (!await this.checkDirectory(registryDir)) {
284
+ return;
285
+ }
286
+
287
+ const registryFiles = await readdir(registryDir);
288
+ this.debug(`Scanning registry folder ${registryDir}, containing ${registryFiles.length} files.`);
289
+
290
+ if (PRELOAD_REGISTRY_IN_PARALLEL)
291
+ {
292
+ const tasks = registryFiles.map(async (file) => {
293
+ if (file.startsWith('.')) {
294
+ return null;
295
+ }
296
+ const filePath = path.join(registryDir, file);
297
+ const s = await stat(filePath);
298
+ if (s.isDirectory()) {
299
+ await this.registerFromFolder(filePath, prefix, (this.app as MercsServer).options.refreshBlocks);
300
+ }});
301
+ await Promise.all(tasks);
302
+ }
303
+ else
304
+ {
305
+ for (const file of registryFiles)
306
+ {
307
+ if (file.startsWith('.'))
308
+ {
309
+ continue;
310
+ }
311
+ const filePath = path.join(registryDir, file);
312
+ const s = await stat(filePath);
313
+ if (s.isDirectory())
314
+ {
315
+ await this.registerFromFolder(filePath, prefix, (this.app as MercsServer).options.refreshBlocks);
316
+ }
317
+ }
318
+ }
319
+ }
320
+
321
+ // Preload APIS
322
+ private async preload() {
323
+ const start = performance.now(); // Start timer
324
+
325
+ //const testDir = process.cwd() + '/data.local/apis-testing/';
326
+ //@ts-ignore
327
+ const apisTestingPath = this.app.config.settings.paths?.apisTestingPath || 'data.local/apis-testing';
328
+ const testDir = path.join(process.cwd(), apisTestingPath)
329
+ await this.preloadDir(testDir, 'test')
330
+
331
+ // First load the local apis defined by the user
332
+ //const localDir = process.cwd() + '/data.local/apis-local/';
333
+ //@ts-ignore
334
+ const apisLocalPath = this.app.config.settings.paths?.apisLocalPath || 'data.local/apis-local';
335
+ const localDir = path.join(process.cwd(), apisLocalPath)
336
+
337
+ await this.preloadDir(localDir, 'local')
338
+
339
+ const registryDir = process.cwd() + '/extensions/omni-core-blocks/server/apis/';
340
+ await this.preloadDir(registryDir)
341
+
342
+ const end = performance.now(); // End timer
343
+ this.info(`BlockManager preload completed in ${(end - start).toFixed()}ms`);
344
+ }
345
+
346
+ async uninstallNamespace(ns: string, prefix: string = 'local') {
347
+ // sanitize
348
+ ns = ns.replace(/[^a-zA-Z0-9-_]/g, '');
349
+ if (ns.length <3) {
350
+ throw new Error('Namespace too short');
351
+ }
352
+ const name = `${prefix}-${ns}`
353
+ if (!this.namespaces.get(name)) {
354
+ throw new Error('Namespace '+name+'not found');
355
+ }
356
+ this.info(`Uninstalling namespace ${name}`);
357
+ this._kvStorage?.runSQL(`DELETE FROM kvstore WHERE key LIKE ?`, `%:${name}%`);
358
+ }
359
+
360
+ async registerFromFolder(dirPath: string, prefix?:string, forceRefresh:boolean=false): Promise<void> {
361
+ const start = performance.now(); // Start timer
362
+ const files = await readdir(dirPath);
363
+
364
+ await Promise.all(
365
+ files.map(async (file) => {
366
+ if (file.endsWith('.yaml')) {
367
+ try {
368
+ // load the yaml file
369
+ const nsData = yaml.load(await readFile(path.join(dirPath, file), 'utf8')) as OmniNamespaceDefinition;
370
+
371
+ if (!nsData.title) {
372
+ nsData.title = nsData.namespace;
373
+ }
374
+
375
+ if (prefix)
376
+ {
377
+ nsData.namespace = `${prefix}-${nsData.namespace}`;
378
+ nsData.title = `$${nsData.title} (${prefix})`;
379
+ nsData.prefix = prefix
380
+ }
381
+
382
+ // get the namespace
383
+ const ns = nsData.namespace;
384
+ const url = nsData.api?.url ?? nsData.api?.spec ?? nsData.api?.json;
385
+ if (!ns || !url) {
386
+ this.error(`Skipping ${dirPath}\\${file} as it does not have a valid namespace or api field`);
387
+ return;
388
+ }
389
+
390
+ if (this.namespaces.has(ns) && !forceRefresh) {
391
+ this.debug('Skipping namespace ' + ns + " as it's already registered");
392
+ await Promise.resolve();
393
+ return;
394
+ }
395
+
396
+ await this.addNamespace(ns, nsData, true);
397
+ const opIds: string[] = [];
398
+ const patches: OmniComponentPatch[] = [];
399
+
400
+ const cDir = path.join(dirPath, 'blocks');
401
+
402
+ if (await this.checkDirectory(cDir)) {
403
+ const components = await readdir(cDir);
404
+ await Promise.all(
405
+ components.map(async (component) => {
406
+ if (component.endsWith('.yaml')) {
407
+ // load the yaml file
408
+ const patch = yaml.load(await readFile(cDir + '/' + component, 'utf8')) as any;
409
+ if (nsData.prefix)
410
+ {
411
+ patch.title = `${patch.title} (${nsData.prefix})`;
412
+ patch.apiNamespace = `${nsData.prefix}-${patch.apiNamespace}`;
413
+ patch.displayNamespace = `${nsData.prefix}-${patch.displayNamespace}`;
414
+ patch.tags = patch.tags ?? [];
415
+ patch.tags.push(nsData.prefix);
416
+ }
417
+ opIds.push(patch.apiOperationId);
418
+ patches.push(patch);
419
+ }
420
+ })
421
+ );
422
+ }
423
+
424
+ this.info(`Loading ${url} as ${ns}`);
425
+
426
+ try {
427
+ await this.blocksFromNamespace(nsData, dirPath, opIds, patches);
428
+ await this.processPatches(patches);
429
+ } catch (e) {
430
+ this.error(`Failed to process ${ns} ${url}`, e);
431
+ }
432
+ } catch (error) {
433
+ this.warn(`Failed to register from ${path.join(dirPath, file)}`, error);
434
+ }
435
+ }
436
+ })
437
+ );
438
+
439
+ const end = performance.now(); // Start timer
440
+ this.info(`BlockManager registerFromFolder from ${dirPath} in ${(end - start).toFixed()}ms`);
441
+ }
442
+
443
+ private async loadAPISpec(currDir: string, api: { url?: string; json?: string; spec?: string }): Promise<any> {
444
+ const start = performance.now(); // Start timer
445
+
446
+ let parsedSchema = null;
447
+
448
+ if (api.url != null) {
449
+ const api_url = api.url.toLocaleLowerCase();
450
+
451
+ this.info('Loading API from URL', api.url);
452
+ this.warn(`[${api_url}] -------START --------- `)
453
+ let schema_text;
454
+ let result;
455
+
456
+ try {
457
+ //result = await fetch(api.url);
458
+ result = await axios.get(api.url);
459
+ this.warn(`[${api_url}] SUCCESS: FETCH `);
460
+ }
461
+ catch (error) {
462
+ this.error(error);
463
+ this.warn(`[${api_url}] FAILURE: AXIOS.GET with error ${error}`);
464
+ throw new Error(`Failed to load spec from ${api.url}`);
465
+ }
466
+
467
+ try{
468
+ schema_text = result.data
469
+ this.warn(`[${api_url}] SUCCESS: READ DATA`);
470
+ }
471
+ catch (error) {
472
+ this.error(error);
473
+ this.warn(`[${api_url}] FAILURE: READ DATA with error = ${error}`);
474
+ //throw new Error(`Failed to get json from result from ${api.url}`);
475
+ schema_text = undefined;
476
+ }
477
+
478
+ if (schema_text)
479
+ {
480
+
481
+ try {
482
+ if (api_url.endsWith('.yaml') || api_url.endsWith('.yml'))
483
+ {
484
+ // @ts-ignore
485
+ parsedSchema = await SwaggerClient.resolve({ spec: yaml.load(schema_text) });
486
+ this.warn(`[${api_url}] SUCCESS: RESOLVE FROM YAML`)
487
+ }
488
+ else
489
+ {
490
+ if (typeof schema_text === 'string')
491
+ {
492
+ try
493
+ {
494
+ schema_text = JSON.parse(schema_text);
495
+ this.warn(`[${api_url}] SUCCESS: PARSE JSON`)
496
+ }
497
+ catch (error) {
498
+ this.error(error);
499
+ this.warn(`[${api_url}] FAILURE: PARSE JSON with error = ${error}`)
500
+ throw new Error(`Failed to parse json from ${api.url}`);
501
+ }
502
+ }
503
+ // @ts-ignore
504
+ parsedSchema = await SwaggerClient.resolve({ spec: schema_text });
505
+ this.warn(`[${api_url}] SUCCESS: RESOLVE FROM YAML`)
506
+ }
507
+
508
+ let parsed_spec = JSON.stringify(parsedSchema?.spec);
509
+ if (parsed_spec)
510
+ {
511
+ parsed_spec = parsed_spec.substring(0,256);
512
+ this.warn(`[${api_url}] READ SPEC = ${parsed_spec}`);
513
+ }
514
+ else
515
+ throw new Error(`Failed to parse spec from ${api.url}`);
516
+
517
+ this.warn(`[${api_url}] SUCCESS: RESOLVE`);
518
+ } catch (error) {
519
+ this.error(error);
520
+ this.warn(`[${api_url}] FAILURE RESOLVE with error = ${error}`)
521
+ throw new Error(`Failed to parse spec from ${api.url}`);
522
+ }
523
+ }
524
+ else
525
+ {
526
+ this.warn(`[${api_url}] FAILURE: NO SCHEMA TEXT. Using DIRECT RESOLVE FROM URL INSTEAD`)
527
+
528
+ try{
529
+ // @ts-ignore
530
+ parsedSchema = await SwaggerClient.resolve({ url: api.url });
531
+ const parsed_error = parsedSchema.error;
532
+ const parsed_spec = parsedSchema.spec;
533
+ this.warn(`[${api_url}] error read from resolve = ${parsed_error}`);
534
+ this.warn(`[${api_url}] READ SPEC = ${parsed_spec.substring(0,256)}`);
535
+ this.warn(`[${api_url}] SUCCESS: RESOLVE FROM URL`);
536
+
537
+ }
538
+ catch (error) {
539
+ this.error(error);
540
+ this.warn(`[${api_url}] FAILURE: RESOLVE FROM URL with error = ${error}`)
541
+ throw new Error(`Failed to parse spec from ${api.url}`);
542
+ }
543
+ }
544
+ } else if (api.json) {
545
+ this.info('Loading API from JSON', api.json);
546
+ // @ts-ignore
547
+ parsedSchema = await SwaggerClient.resolve({ spec: api.json });
548
+ } else if (api.spec != null) {
549
+ this.info('Loading API from SPEC', api.spec);
550
+ const specPath = path.join(currDir, api.spec);
551
+ if (existsSync(specPath)) {
552
+ const spec = yaml.load(await readFile(specPath, 'utf8')) as any;
553
+ // @ts-ignore
554
+ parsedSchema = await SwaggerClient.resolve({ spec });
555
+ } else {
556
+ this.error(`Spec file ${specPath} not found`);
557
+ throw new Error(`Spec file ${specPath} not found`);
558
+ }
559
+ } else {
560
+ throw new Error('No url or spec provided');
561
+ }
562
+
563
+ const end = performance.now(); // End timer
564
+ this.info(`loadAPISpec ${currDir} completed in ${(end - start).toFixed(1)} milliseconds`);
565
+ return parsedSchema?.spec ?? parsedSchema;
566
+ }
567
+
568
+ private async checkDirectory(path: string) {
569
+ try {
570
+ await access(path);
571
+ return true;
572
+ } catch {
573
+ return false;
574
+ }
575
+ }
576
+
577
+ private async blocksFromNamespace(
578
+ nsData: OmniNamespaceDefinition,
579
+ dir: string,
580
+ filterOpIds: string[],
581
+ patches: OmniComponentPatch[]
582
+ ) {
583
+ const ns = nsData.namespace;
584
+ this.info(`Processing API ${ns}`, filterOpIds, patches?.length ?? 0);
585
+ const specDoc = await this.loadAPISpec(dir, nsData.api ?? {});
586
+
587
+ if (!specDoc) {
588
+ this.error(`Error: Could not fetch OpenAPI spec for ${ns}`);
589
+ return;
590
+ }
591
+
592
+ const adapter = new OpenAPIReteAdapter(ns, specDoc, nsData.api?.auth);
593
+ const blocks = adapter.getReteComponentDefs(/* filterOpIds */);
594
+
595
+ this.info('------ Adding Blocks ------');
596
+
597
+ for (const c of blocks) {
598
+ const key = `${c.displayNamespace}.${c.displayOperationId}`;
599
+
600
+ // Add to new blocks manager
601
+ if (!this.hasBlock(key)) {
602
+ try {
603
+ this.addBlock(c);
604
+ this.verbose(`Added Block "${key}"`);
605
+ } catch (e) {
606
+ this.error(`Failed to add block "${key}"`, e);
607
+ return;
608
+ }
609
+ }
610
+ }
611
+ }
612
+
613
+ private async processPatches(patches: OmniComponentPatch[]) {
614
+ this.info('------ Adding Patches ------');
615
+
616
+ for (const p of patches) {
617
+ const key = `${p.displayNamespace}.${p.displayOperationId}`;
618
+ try {
619
+ if (!this.blocks.has(`${p.apiNamespace}.${p.apiOperationId}`)) {
620
+ this.warn(
621
+ `Patch ${p.displayNamespace}.${p.displayOperationId} skipped as base block ${p.apiNamespace}.${p.apiOperationId} was not found`
622
+ );
623
+ } else {
624
+ if (this.patches.has(key)) {
625
+ this.verbose(`Patch ${key} already registered, overwriting`);
626
+ }
627
+ const allowOverwrite = true;
628
+ this.addPatch(p, allowOverwrite);
629
+ }
630
+ } catch (e) {
631
+ this.error(`Failed to add patch ${key}`, e);
632
+ }
633
+
634
+ this.info(`Adding patch ${key}`);
635
+ }
636
+ }
637
+
638
+ getBlock(key: string): OmniComponentFormat | undefined {
639
+ return this.blocks.get(key);
640
+ }
641
+
642
+ public async addNamespace(key: string, namespace: OmniNamespaceDefinition, allowOverwrite?: boolean) {
643
+ if (!key) throw new Error('addNamespace(): key cannot be undefined');
644
+ if (!namespace) {
645
+ throw new Error('addNamespace(): namespace cannot be undefined');
646
+ }
647
+ if (this.namespaces.has(key) && !allowOverwrite) {
648
+ throw new Error(`addNamespace(): namespace ${key} already registered`);
649
+ }
650
+ this.namespaces.set(key, namespace);
651
+
652
+ await this.app.events.emit('register_namespace', namespace);
653
+
654
+ return this;
655
+ }
656
+
657
+ public addPatch(patch: OmniComponentPatch, allowOverwrite?: boolean) {
658
+ const key = `${patch.displayNamespace}.${patch.displayOperationId}`;
659
+ if (!key) throw new Error('addPatch(): key cannot be undefined');
660
+ if (!patch) throw new Error('addPatch(): patch cannot be undefined');
661
+ if (this.patches.has(key) && !allowOverwrite) {
662
+ throw new Error(`addPatch(): patch ${key} already registered`);
663
+ }
664
+
665
+ // We use these to identify the base blocks, so patches without them are invalid
666
+ if (!patch.apiNamespace) {
667
+ throw new Error(`addPatch(): patch ${key} is missing apiNamespace`);
668
+ }
669
+ if (!patch.apiOperationId) {
670
+ throw new Error(`addPatch(): patch ${key} is missing apiOperationId`);
671
+ }
672
+
673
+ this.info('Registering patch', key);
674
+ patch = removeUndefinedValues(patch);
675
+ patch.hash = BlockManager.hashObject(patch);
676
+ this.patches.set(key, patch);
677
+ }
678
+
679
+ getMacro(component: OAIBaseComponent, macroType: OmniComponentMacroTypes): Function | undefined {
680
+ let macro = component?.macros?.[macroType];
681
+
682
+ if (typeof macro === 'string') {
683
+ macro = this.macros.get(macro);
684
+ }
685
+
686
+ if (typeof macro === 'function') {
687
+ return macro.bind(component);
688
+ }
689
+
690
+ return undefined;
691
+ }
692
+
693
+ hashObject(obj: any) {
694
+ return BlockManager.hashObject(obj);
695
+ }
696
+
697
+ static hashObject(obj: any) {
698
+ if (obj.patch) delete obj.patch;
699
+ const hashState = new MurmurHash3();
700
+ const hash = hashState.hash(JSON.stringify(obj)).result().toString(16);
701
+ return hash;
702
+ }
703
+
704
+ public registerMacro(key: string, macro: Function) {
705
+ this.macros.set(key, macro);
706
+ }
707
+
708
+ public addBlock(block: OmniComponentFormat) {
709
+ const key = `${block.apiNamespace}.${block.apiOperationId}`;
710
+ if (!block) throw new Error(`Block ${key} is undefined`);
711
+ if (!block.type) throw new Error(`Block ${key} is missing type`);
712
+ if (!this.factories.has(block.type)) {
713
+ throw new Error(`Block ${key} has unknown type ${block.type}` + Array.from(this.factories.keys()).toString());
714
+ }
715
+
716
+ if (block.displayNamespace !== block.apiNamespace || block.displayOperationId !== block.apiOperationId) {
717
+ throw new Error(
718
+ `addBlock(): Block ${key} has mismatched display and api namespaces, indicating it is a patch. Use addPatch() instead`
719
+ );
720
+ }
721
+
722
+ this.debug('Registering block', key);
723
+
724
+ /*
725
+ Macros
726
+
727
+ Macros are functions attached to a blocks' macro collection. Since we don't want to serialize functions into the database, we rely on them getting
728
+ registered on every startup, from 2 potential sources:
729
+
730
+ 1. The block itself, which can have a macro collection with functions. These are registered as macro://<block_key>:<namespace>.<operationId>
731
+ 2. Extensions, which can export macros along with the createComponent function. These are picked up by the extension manager and fired as register_macros event
732
+ which is picked up here. This is useful to allow many blocks to use the same exec function for example
733
+ */
734
+ const macros = block.macros;
735
+
736
+ if (macros && Object.keys(macros).length > 0) {
737
+ for (const m in macros) {
738
+ // @ts-ignore
739
+ const macro = macros[m];
740
+ this.verbose('Registering macro', m);
741
+
742
+ if (typeof macro === 'function') {
743
+ const macroKey = 'macro://' + m + ':' + block.displayNamespace + '.' + block.displayOperationId;
744
+
745
+ this.registerMacro(macroKey, macro);
746
+ // @ts-ignore
747
+ macros[m] = macroKey;
748
+ } else if (typeof macro === 'string') {
749
+ // @ts-ignore
750
+ if (!this.macros.has(macro)) {
751
+ throw new Error(`Block ${key} has unknown macro ${m}. The Macro has to be registered before the block`);
752
+ }
753
+ }
754
+ }
755
+ }
756
+
757
+ block = removeUndefinedValues(block) as OmniComponentFormat;
758
+ block.hash = BlockManager.hashObject(block);
759
+
760
+ this.blocks.set(key, block);
761
+ }
762
+
763
+ public hasBlock(key: string): boolean {
764
+ if (!key) throw new Error('hasBlock(): key cannot be undefined');
765
+ return this.blocks.has(key);
766
+ }
767
+
768
+ public async canRunBlock(block: OAIBaseComponent, userId: string): Promise<boolean> {
769
+ if (!block) throw new Error('canRunBlock(): block cannot be undefined');
770
+
771
+ const credentialsService = this.app.services.get('credentials') as CredentialService | undefined;
772
+ if (!credentialsService) {
773
+ throw new Error('Credentials service unavailable');
774
+ }
775
+
776
+ return await credentialsService.hasSecret(userId, block.apiNamespace);
777
+ }
778
+
779
+ public registerType(key: string, Factory: Function): void {
780
+ if (!key) throw new Error('registerType(): key cannot be undefined');
781
+ if (!Factory || typeof Factory !== 'function') {
782
+ throw new Error(`Factory ${key} must be a function`);
783
+ }
784
+ if (this.factories.has(key)) {
785
+ throw new Error(`Block type ${key} already registered`);
786
+ }
787
+
788
+ this.factories.set(key, Factory);
789
+ }
790
+
791
+ // return a composed block. If the key responds to a patch, the patch is applied to the underlying block
792
+ public async getInstance(key: string, userId?: string): Promise<OAIBaseComponent | undefined> {
793
+ const patch = this.patches.get(key);
794
+
795
+ const baseKey = patch ? `${patch.apiNamespace}.${patch.apiOperationId}` : key;
796
+
797
+ const block = this.blocks.get(baseKey);
798
+
799
+ if (!block) {
800
+ return undefined;
801
+ }
802
+
803
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
804
+ const Factory = this.factories.get(block.type)!; // Block types are guaranteed on insert so we can ! here
805
+ const ret = Factory(block, patch) as OAIComponent31;
806
+
807
+ if (block.dependsOn) {
808
+ const check = block.dependsOn.filter((d: string) => !this.hasBlock(d) && !this.patches.has(d));
809
+
810
+ if (check.length > 0) {
811
+ ret.data.errors.push(`Missing dependencies: ${check.join(',')}`);
812
+ }
813
+ }
814
+
815
+ ret.data.tags.push(patch ? 'patch' : 'base-api');
816
+
817
+ const hideInputs = ret.scripts?.['hideExcept:inputs'];
818
+ if (hideInputs?.length) {
819
+ for (const k in ret.inputs) {
820
+ ret.inputs[k].hidden = ret.inputs[k].hidden ?? !hideInputs.includes(k);
821
+ }
822
+ }
823
+
824
+ const hideOutputs = ret.scripts?.['hideExcept:outputs'];
825
+ if (hideOutputs?.length) {
826
+ for (const k in ret.outputs) {
827
+ ret.outputs[k].hidden = ret.outputs[k].hidden ?? !hideOutputs.includes(k);
828
+ }
829
+ }
830
+
831
+ // TODO: Do not hide _omni_result socket for now as it is easier to debug and surface issue
832
+ // Once there is patch, hide _omni_result socket in outputs if it exists
833
+ // if (patch && Object.keys(ret.outputs ?? {}).length > 1 && ret.outputs?._omni_result) {
834
+ // ret.outputs['_omni_result'].hidden = true;
835
+ // }
836
+
837
+ if (userId && !(await this.canRunBlock(ret, userId))) {
838
+ ret.data.errors.push('Block cannot run');
839
+ }
840
+
841
+ return ret as OAIBaseComponent;
842
+ }
843
+
844
+ public async tryResolveExtensionBlock(ctx: any, key: string): Promise<OAIBaseComponent|undefined>
845
+ {
846
+ // if an extension is involved, the block key is in the form of <extension_name>:<block_key>
847
+ if (key.indexOf(':') > 0)
848
+ {
849
+ const server = (this.app as MercsServer)
850
+ const [extensionId, blockKey] = key.split(':');
851
+
852
+ if (server.extensions.has(extensionId))
853
+ {
854
+ const extension = server.extensions.get(extensionId);
855
+ if (extension)
856
+ {
857
+ const block = await extension.invokeKnownMethod(KNOWN_EXTENSION_METHODS.resolveMissingBlock, ctx, blockKey)
858
+
859
+ if (block)
860
+ {
861
+ return block;
862
+ }
863
+ else
864
+ {
865
+ return undefined;
866
+ }
867
+ }
868
+ }
869
+ }
870
+ }
871
+
872
+
873
+ public async getInstances(
874
+ keys: string[],
875
+ userId?: string,
876
+ failBehavior: 'throw' | 'filter' | 'missing_block' = 'throw'
877
+ ): Promise<{blocks: OAIBaseComponent[], missing: string[]}> {
878
+ if (!keys || !Array.isArray(keys)) {
879
+ throw new Error('getInstances(keys): keys must be string[]');
880
+ }
881
+ const missing:string[] = []
882
+
883
+ const promises = keys.map(async (key) => {
884
+ const block = await this.getInstance(key, userId);
885
+ if (block) {
886
+ return block;
887
+ }
888
+ missing.push(key)
889
+ if (failBehavior === 'throw') {
890
+ const patch = this.patches.get(key);
891
+ if (patch) {
892
+ throw new Error(`Unable to compose patched block "${key}" / "${patch.apiNamespace}.${patch.apiOperationId}"`);
893
+ }
894
+ throw new Error(`Unable to find block "${key}"`);
895
+ }
896
+
897
+ if (failBehavior === 'missing_block') {
898
+ omnilog.warn(`[getInstances] Unable to compose block "${key}"`); // Caution: `key` may differ from patched key
899
+ const result = await this.getInstance('omnitool._block_missing', userId);
900
+ if (result)
901
+ {
902
+ result.data.errors.push(`Unable to compose block "${key}"`);
903
+ //@ts-ignore
904
+ result.data._missingKey = key;
905
+ }
906
+ return result;
907
+ }
908
+ return undefined;
909
+ });
910
+
911
+ let result = await Promise.all(promises);
912
+
913
+ if (failBehavior === 'filter') {
914
+ result = result.filter((r) => r);
915
+ }
916
+ return {blocks: (result ?? []) as OAIBaseComponent[], missing}
917
+ }
918
+
919
+ public getAllNamespaces(opts?: { filter: any }): OmniNamespaceDefinition[] {
920
+ let all = Array.from(this.namespaces.values());
921
+ if (opts?.filter) {
922
+ all = all.filter((n: OmniNamespaceDefinition) => n.namespace === opts.filter);
923
+ }
924
+ return all;
925
+ }
926
+
927
+ private orderByTitle(a: IBlockOrPatchSummary, b: IBlockOrPatchSummary) {
928
+ const aKey = a?.title ?? a?.name ?? '';
929
+ const bKey = b?.title ?? b?.name ?? '';
930
+ const locale = 'en-US-POSIX'; // Ensure consistent string comparison, similar to the "C" locale.
931
+ return aKey.toLowerCase().localeCompare(bKey.toLowerCase(), locale);
932
+ }
933
+
934
+ public getFilteredBlocksAndPatches(
935
+ limit: number,
936
+ cursor: number,
937
+ keyword: string,
938
+ opts?: { contentMatch: string; tags: string }
939
+ ): Array<[number, IBlockOrPatchSummary]> {
940
+ const maxLimit = 9999;
941
+ const filter = keyword.replace(/ /g, '').toLowerCase() ?? '';
942
+ const blockAndPatches = this.blocksAndPatches.search(
943
+ maxLimit,
944
+ 0,
945
+ filter,
946
+ opts?.contentMatch,
947
+ opts?.tags,
948
+ 'BlocksAndPatches'
949
+ );
950
+ const all: Array<[number, IBlockOrPatchSummary]> = [];
951
+ if (blockAndPatches) {
952
+ for (const item of blockAndPatches) {
953
+ const itemFormatHeader = this.formatHeader(item[1]);
954
+ if (itemFormatHeader.tags?.includes('base-api')) continue;
955
+ all.push([item[2], itemFormatHeader]);
956
+ }
957
+ }
958
+ all.sort((a, b) => this.orderByTitle(a[1], b[1]));
959
+ return all.slice(cursor, cursor + limit);
960
+ }
961
+
962
+ public getNamespace(key: string): OmniNamespaceDefinition | undefined {
963
+ return this.namespaces.get(key);
964
+ }
965
+
966
+ public getBlocksForNamespace(ns: string): OmniComponentFormat[] {
967
+ // TODO: Gezo: This doesn't actually work, it ignores anything that's a patch.
968
+ // We first need to retrieve all patches for the namespace, then get blocks that do not have patches
969
+ // GetInstance handles this for example
970
+
971
+ return Array.from(this.blocks.values()).filter((block: OmniComponentFormat) => block.apiNamespace === ns);
972
+ }
973
+
974
+ public async getAllBlocks(
975
+ includeDefinitions: boolean = true,
976
+ filter?: any
977
+ ): Promise<Array<OAIBaseComponent | string>> {
978
+ const patches = Array.from(this.patches.keys());
979
+
980
+ if (includeDefinitions) {
981
+ // TODO: We actually want to return header information here, keys alone are not so useful
982
+ let blocks = Array.from(this.blocks.keys());
983
+
984
+ // Build a set from the keys in patches.
985
+ const patchSet = new Set(patches);
986
+
987
+ // Filter blocks if they alias a patch.
988
+ blocks = blocks.filter((key) => !patchSet.has(key));
989
+
990
+ return [...blocks, ...patches];
991
+ }
992
+
993
+ const patchInstances = (await Promise.all(patches.map(async (key) => await this.getInstance(key)))).filter(
994
+ Boolean
995
+ ) as OAIBaseComponent[];
996
+
997
+ const blocks = Array.from(this.blocks.keys()).filter(
998
+ (key) => !patchInstances.find((p: OAIBaseComponent) => p.name === key)
999
+ );
1000
+ const blockInstances = (await Promise.all(blocks.map(async (key) => await this.getInstance(key)))).filter(
1001
+ Boolean
1002
+ ) as OAIBaseComponent[];
1003
+
1004
+ return [...blockInstances, ...patchInstances];
1005
+ }
1006
+
1007
+ public getRequiredCredentialsForBlock(key: string): OmniAPIKey[] {
1008
+ const block = this.blocks.get(key);
1009
+ if (!block) throw new Error(`Block ${key} not found`);
1010
+
1011
+ const securitySchemes = block.security;
1012
+
1013
+ if (!securitySchemes || securitySchemes.length <= 0) {
1014
+ return [];
1015
+ }
1016
+
1017
+ const requiredCredentials: OmniAPIKey[] = [];
1018
+ // For each security scheme, parse the required credentials
1019
+ for (const scheme of securitySchemes) {
1020
+ scheme.requireKeys?.forEach((key: OmniAPIKey) => {
1021
+ const existing = requiredCredentials.find((k) => k.id === key.id);
1022
+ if (!existing) {
1023
+ requiredCredentials.push(key);
1024
+ }
1025
+ });
1026
+ }
1027
+
1028
+ return Array.from(requiredCredentials);
1029
+ }
1030
+
1031
+ public getRequiredCredentials(namespace: string, includeOptional: boolean = true): OmniAPIKey[] {
1032
+ // Get the security schemes from the API spec
1033
+ const securitySchemes: OmniAPIAuthenticationScheme[] = [];
1034
+ const components = this.getBlocksForNamespace(namespace);
1035
+ if (components != null) {
1036
+ components.forEach((component) => {
1037
+ if (component.security != null) {
1038
+ securitySchemes.push(...component.security);
1039
+ }
1040
+ });
1041
+ }
1042
+ if (securitySchemes.length <= 0) {
1043
+ return []; // `No security schemes found for namespace ${namespace}`
1044
+ }
1045
+
1046
+ const requiredCredentials: OmniAPIKey[] = [];
1047
+ // For each security scheme, parse the required credentials
1048
+ for (const scheme of securitySchemes) {
1049
+ if (!includeOptional && scheme.isOptional) {
1050
+ continue;
1051
+ }
1052
+
1053
+ scheme.requireKeys?.forEach((key: OmniAPIKey) => {
1054
+ const existing = requiredCredentials.find((k) => k.id === key.id);
1055
+ if (!existing) {
1056
+ requiredCredentials.push(key);
1057
+ }
1058
+ });
1059
+ }
1060
+
1061
+ return Array.from(requiredCredentials);
1062
+ }
1063
+
1064
+ async getSecurityScheme(apiNamespace: string, version?: string): Promise<OmniAPIAuthenticationScheme[]> {
1065
+ // Get the security schemes from the API spec
1066
+ const securitySchemes: OmniAPIAuthenticationScheme[] = [];
1067
+ const components = this.getBlocksForNamespace(apiNamespace);
1068
+ if (components != null) {
1069
+ components.forEach((component) => {
1070
+ if (component.security != null) {
1071
+ securitySchemes.push(...component.security);
1072
+ }
1073
+ });
1074
+ }
1075
+
1076
+ return securitySchemes;
1077
+ }
1078
+
1079
+ async searchSecurityScheme(apiNamespace: string, version?: string, schemeType?: string, oauthFlowType?: string) {
1080
+ const securitySchemes = await this.getSecurityScheme(apiNamespace, version);
1081
+ const filteredSecuritySchemes = securitySchemes.filter((securityScheme) => {
1082
+ if (schemeType != null) {
1083
+ // Filter by the scheme type
1084
+ if (securityScheme.type !== schemeType) {
1085
+ return false;
1086
+ }
1087
+
1088
+ // Filter by the oauth flow type
1089
+ if (schemeType === 'oauth2' && oauthFlowType != null) {
1090
+ if (Object.hasOwnProperty.call(securityScheme.oauth, oauthFlowType)) {
1091
+ return true;
1092
+ } else {
1093
+ return false;
1094
+ }
1095
+ }
1096
+ }
1097
+
1098
+ return true;
1099
+ });
1100
+ return filteredSecuritySchemes;
1101
+ }
1102
+
1103
+ public getAPISignature(namespace: string, operationId: string) {
1104
+ const ns = this.getNamespace(namespace);
1105
+ if (!ns) {
1106
+ throw new Error(`Namespace ${namespace} not found`);
1107
+ }
1108
+
1109
+ const component = this.getBlock(`${namespace}.${operationId}`);
1110
+ if (!component) {
1111
+ throw new Error(`BlockManager: Component ${operationId} not found`);
1112
+ }
1113
+
1114
+ const signature = {
1115
+ method: component.method,
1116
+ url: ns.api?.basePath + component.urlPath,
1117
+ contentType: component.responseContentType,
1118
+ requestContentType: component.requestContentType,
1119
+ security: component.security
1120
+ };
1121
+ this.debug(`getAPISignature ${namespace} ${operationId} ${JSON.stringify(signature, null, 2)}`);
1122
+ return signature;
1123
+ }
1124
+
1125
+ public async runBlock(
1126
+ ctx: WorkerContext,
1127
+ blockName: string,
1128
+ args: any,
1129
+ outputs?: any,
1130
+ opts?: {
1131
+ cacheType?: 'global' | 'user' | 'session';
1132
+ cacheKey?: string;
1133
+ cacheTTLInSeconds?: number;
1134
+ bustCache?: false;
1135
+ timeout?: number;
1136
+ }
1137
+ ) {
1138
+ opts ??= {};
1139
+
1140
+ this.info('runblock', blockName, args, outputs, opts);
1141
+
1142
+ if (!ctx.sessionId) {
1143
+ this.error('Invalid session');
1144
+ return { error: 'Invalid session' };
1145
+ }
1146
+
1147
+ const block = await this.getInstance(blockName);
1148
+ this.info(`Running block ${blockName}`);
1149
+ if (!block) {
1150
+ this.error('Invalid block', blockName);
1151
+ return { error: 'Invalid block' };
1152
+ }
1153
+
1154
+ const inputs: Record<string, any> = {};
1155
+
1156
+ for (const key in args) {
1157
+ if (args[key] !== null && args[key] !== undefined) {
1158
+ inputs[key] = Array.isArray(args[key]) ? args[key] : [args[key]];
1159
+ }
1160
+ }
1161
+
1162
+ outputs ??= { text: '' };
1163
+
1164
+ const node = {
1165
+ id: 1,
1166
+ name: blockName,
1167
+ type: 'component',
1168
+ component: blockName,
1169
+ inputs,
1170
+ outputs,
1171
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
1172
+ data: {} as NodeData,
1173
+ position: [0, 0] as [number, number]
1174
+ };
1175
+
1176
+ const workerContext = WorkerContext.create(ctx.app, null, node, {
1177
+ ...ctx.getData()
1178
+ });
1179
+
1180
+ let cKey = '';
1181
+ const ttl = (opts.cacheTTLInSeconds ? opts.cacheTTLInSeconds * 1000 : 60 * 60 * 24 * 1000) + Date.now();
1182
+ if (opts?.cacheType) {
1183
+ const hashState = new MurmurHash3();
1184
+ if (opts.cacheType === 'session') {
1185
+ cKey = ctx.sessionId;
1186
+ } else if (opts.cacheType === 'global') {
1187
+ cKey = 'global';
1188
+ } else if (opts.cacheType === 'user') {
1189
+ cKey = ctx.userId;
1190
+ }
1191
+ const hash = hashState.hash(JSON.stringify(inputs)).result().toString(16);
1192
+ cKey = cKey + ':' + blockName + ':' + block.hash + hash;
1193
+ }
1194
+
1195
+ if (opts?.bustCache) {
1196
+ this.info('Busting cache for ' + cKey);
1197
+ this.cache.delete(cKey);
1198
+ } else if (cKey.length && this.cache.get(cKey)) {
1199
+ this.info('Cache hit for ' + cKey);
1200
+ return this.cache.get(cKey);
1201
+ }
1202
+
1203
+ let result = (await block.workerStart(inputs, workerContext)) as any;
1204
+
1205
+ // Let's throw on errors
1206
+ if (!result || result.error) {
1207
+ if (!result) {
1208
+ result = { error: 'Unknown error' };
1209
+ }
1210
+ this.error('Error running block', result.error);
1211
+ return result;
1212
+ }
1213
+
1214
+ if (cKey.length) {
1215
+ this.info('Cache miss for ' + cKey);
1216
+ this.cache.set(cKey, result, ttl);
1217
+ }
1218
+
1219
+ return result;
1220
+ }
1221
+ }
1222
+
1223
+ export { BlockManager };
patches/packages/omni-server/test_api.cjs ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const axios = require('axios');
2
+ const yaml = require('js-yaml');
3
+
4
+ async function testUrl(apiUrl) {
5
+ try {
6
+ const result = await axios.get(apiUrl);
7
+ console.log(`SUCCESS: FETCH ${apiUrl}`);
8
+
9
+ let schemaText = result.data;
10
+ console.log(`SUCCESS: READ DATA from ${apiUrl}`);
11
+
12
+ let parsedSchema;
13
+ if (apiUrl.endsWith('.yaml') || apiUrl.endsWith('.yml')) {
14
+ parsedSchema = yaml.load(schemaText);
15
+ console.log(`SUCCESS: RESOLVE FROM YAML ${apiUrl}`);
16
+ } else {
17
+ if (typeof schemaText === 'string') {
18
+ try {
19
+ schemaText = JSON.parse(schemaText);
20
+ console.log(`SUCCESS: PARSE JSON from ${apiUrl}`);
21
+ } catch (error) {
22
+ console.error(`FAILURE: PARSE JSON from ${apiUrl} with error = ${error}`);
23
+ throw error;
24
+ }
25
+ }
26
+ parsedSchema = schemaText;
27
+ console.log(`SUCCESS: RESOLVE FROM JSON ${apiUrl}`);
28
+ }
29
+
30
+ console.log(`Parsed schema from ${apiUrl}:`, parsedSchema);
31
+ } catch (error) {
32
+ console.error(`Failed to load spec from ${apiUrl}`, error);
33
+ }
34
+ }
35
+
36
+ // Replace with the URL you want to test
37
+ testUrl('https:api.elevenlabs.io/openapi.json');
38
+ testUrl('https:api.replicate.com/openapi.json');