@optimizely-opal/opal-tool-ocp-sdk 1.0.0-beta.5 → 1.0.0-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@
2
2
  import { AuthRequirement, IslandResponse, Parameter } from '../types/Models';
3
3
  import { ToolError } from '../types/ToolError';
4
4
  import * as App from '@zaiusinc/app-sdk';
5
- import { logger, Headers } from '@zaiusinc/app-sdk';
5
+ import { logger, Headers, getAppContext } from '@zaiusinc/app-sdk';
6
6
  import { ToolFunction } from '../function/ToolFunction';
7
7
  import { GlobalToolFunction } from '../function/GlobalToolFunction';
8
8
  import { ParameterValidator } from '../validation/ParameterValidator';
@@ -17,6 +17,27 @@ export class NestedInteractions {
17
17
  }
18
18
  }
19
19
 
20
+ /**
21
+ * Interface for tool description override data (compatible with KV store)
22
+ */
23
+ interface ToolOverride extends App.ValueHash {
24
+ name: string;
25
+ description: string;
26
+ parameters: ParameterOverride[];
27
+ }
28
+
29
+ interface ParameterOverride extends App.ValueHash {
30
+ name: string;
31
+ description: string;
32
+ }
33
+
34
+ /**
35
+ * Interface for stored override data in KV store
36
+ */
37
+ interface StoredOverrides extends App.ValueHash {
38
+ [tool_name: string]: ToolOverride;
39
+ }
40
+
20
41
  /**
21
42
  * Result type for interaction handlers
22
43
  */
@@ -96,6 +117,107 @@ export class ToolsService {
96
117
  private functions: Map<string, Tool<any>> = new Map();
97
118
  private interactions: Map<string, Interaction<any>> = new Map();
98
119
 
120
+ /**
121
+ * Generate KV store key for tool overrides
122
+ * @param appVersionId App version ID
123
+ * @param functionName Function name
124
+ * @returns KV store key
125
+ */
126
+ private getOverrideKey(appVersionId: string, functionName: string): string {
127
+ return `${appVersionId}:${functionName}:opal-tools-overrides`;
128
+ }
129
+
130
+ /**
131
+ * Get tool overrides from KV store
132
+ * @param appVersionId App version ID
133
+ * @param functionName Function name
134
+ * @returns Stored overrides or null if not found
135
+ */
136
+ private async getOverrides(appVersionId: string, functionName: string): Promise<StoredOverrides> {
137
+ const key = this.getOverrideKey(appVersionId, functionName);
138
+ return await App.storage.kvStore.get<StoredOverrides>(key);
139
+ }
140
+
141
+ /**
142
+ * Apply overrides to tool definitions
143
+ * @param tools Original tool definitions
144
+ * @param overrides Override data
145
+ * @returns Tools with overrides applied
146
+ */
147
+ private applyOverrides(tools: Array<Tool<any>>, overrides: StoredOverrides): Array<Tool<any>> {
148
+ return tools.map((tool) => {
149
+ const override = overrides[tool.name];
150
+ if (!override) {
151
+ return tool;
152
+ }
153
+
154
+ // Clone the tool and apply overrides
155
+ const overriddenTool = new Tool(
156
+ tool.name,
157
+ override.description,
158
+ tool.parameters.map((param) => {
159
+ const paramOverride = override.parameters?.find((p) => p.name === param.name);
160
+ if (paramOverride) {
161
+ return new Parameter(
162
+ param.name,
163
+ param.type,
164
+ paramOverride.description,
165
+ param.required
166
+ );
167
+ }
168
+ return param;
169
+ }),
170
+ tool.endpoint,
171
+ tool.handler,
172
+ tool.authRequirements
173
+ );
174
+ overriddenTool.httpMethod = tool.httpMethod;
175
+ return overriddenTool;
176
+ });
177
+ }
178
+
179
+ /**
180
+ * Save tool description overrides
181
+ * @param appVersionId App version ID
182
+ * @param functionName Function name
183
+ * @param overrideData Override data from request
184
+ */
185
+ public async saveToolOverrides(
186
+ appVersionId: string,
187
+ functionName: string,
188
+ overrides: StoredOverrides
189
+ ): Promise<void> {
190
+ const key = this.getOverrideKey(appVersionId, functionName);
191
+ await App.storage.kvStore.patch(key, overrides);
192
+ }
193
+
194
+ /**
195
+ * Delete tool description overrides
196
+ * @param appVersionId App version ID
197
+ * @param functionName Function name
198
+ */
199
+ public async deleteToolOverrides(appVersionId: string, functionName: string): Promise<void> {
200
+ const key = this.getOverrideKey(appVersionId, functionName);
201
+ await App.storage.kvStore.delete(key);
202
+ }
203
+
204
+ /**
205
+ * Get tool definitions with overrides applied
206
+ * @param appVersionId App version ID
207
+ * @param functionName Function name
208
+ * @returns Tool definitions with overrides applied
209
+ */
210
+ public async getToolsWithOverrides(appVersionId: string, functionName: string): Promise<Array<Tool<any>>> {
211
+ const tools = Array.from(this.functions.values());
212
+ const overrides = await this.getOverrides(appVersionId, functionName);
213
+
214
+ if (overrides) {
215
+ return this.applyOverrides(tools, overrides);
216
+ }
217
+
218
+ return tools;
219
+ }
220
+
99
221
  /**
100
222
  * Enforce OptiID authentication for tools by ensuring OptiID auth requirement is present
101
223
  * @param authRequirements Original authentication requirements
@@ -200,59 +322,141 @@ export class ToolsService {
200
322
  req: App.Request,
201
323
  functionContext: ToolFunction | GlobalToolFunction
202
324
  ): Promise<App.Response> {
325
+ // Handle discovery endpoint with overrides
203
326
  if (req.path === '/discovery') {
204
- return new App.Response(200, { functions: Array.from(this.functions.values()).map((f) => f.toJSON()) });
205
- } else {
206
- const func = this.functions.get(req.path);
207
- if (func) {
208
- try {
209
- let params;
210
- if (req.bodyJSON && req.bodyJSON.parameters) {
211
- params = req.bodyJSON.parameters;
212
- } else {
213
- params = req.bodyJSON;
214
- }
327
+ return await this.handleDiscoveryRequest(functionContext);
328
+ }
215
329
 
216
- // Validate parameters before calling the handler (only if tool has parameter definitions)
217
- // ParameterValidator.validate() throws ToolError if validation fails
218
- if (func.parameters && func.parameters.length > 0) {
219
- ParameterValidator.validate(params, func.parameters, func.endpoint);
220
- }
330
+ // Handle overrides endpoint
331
+ if (req.path === '/overrides') {
332
+ return await this.handleOverridesRequest(req, functionContext);
333
+ }
221
334
 
222
- // Extract auth data from body JSON
223
- const authData = req.bodyJSON ? req.bodyJSON.auth : undefined;
335
+ // Handle regular tool functions
336
+ const func = this.functions.get(req.path);
337
+ if (func) {
338
+ try {
339
+ let params;
340
+ if (req.bodyJSON && req.bodyJSON.parameters) {
341
+ params = req.bodyJSON.parameters;
342
+ } else {
343
+ params = req.bodyJSON;
344
+ }
224
345
 
225
- const result = await func.handler(functionContext, params, authData);
226
- return new App.Response(200, result);
227
- } catch (error: any) {
228
- logger.error(`Error in function ${func.name}:`, error);
229
- return this.formatErrorResponse(error, func.endpoint);
346
+ // Validate parameters before calling the handler (only if tool has parameter definitions)
347
+ // ParameterValidator.validate() throws ToolError if validation fails
348
+ if (func.parameters && func.parameters.length > 0) {
349
+ ParameterValidator.validate(params, func.parameters, func.endpoint);
230
350
  }
351
+
352
+ // Extract auth data from body JSON
353
+ const authData = req.bodyJSON ? req.bodyJSON.auth : undefined;
354
+ const result = await func.handler(functionContext, params, authData);
355
+ return new App.Response(200, result);
356
+ } catch (error: any) {
357
+ logger.error(`Error in function ${func.name}:`, error);
358
+ return this.formatErrorResponse(error, func.endpoint);
231
359
  }
360
+ }
232
361
 
233
- const interaction = this.interactions.get(req.path);
234
- if (interaction) {
235
- try {
236
- let params;
237
- if (req.bodyJSON && req.bodyJSON.data) {
238
- params = req.bodyJSON.data;
239
- } else {
240
- params = req.bodyJSON;
241
- }
362
+ // Handle interactions
363
+ const interaction = this.interactions.get(req.path);
364
+ if (interaction) {
365
+ try {
366
+ let params;
367
+ if (req.bodyJSON && req.bodyJSON.data) {
368
+ params = req.bodyJSON.data;
369
+ } else {
370
+ params = req.bodyJSON;
371
+ }
372
+
373
+ // Extract auth data from body JSON
374
+ const authData = req.bodyJSON ? req.bodyJSON.auth : undefined;
375
+
376
+ const result = await interaction.handler(functionContext, params, authData);
377
+ return new App.Response(200, result);
378
+ } catch (error: any) {
379
+ logger.error(`Error in function ${interaction.name}:`, error);
380
+ return this.formatErrorResponse(error, interaction.endpoint);
381
+
382
+ }
383
+ }
384
+ return new App.Response(404, 'Function not found');
385
+ }
386
+
387
+ /**
388
+ * Handle discovery endpoint with overrides applied
389
+ * @param functionContext The function context to get function name from
390
+ * @returns Response with tool definitions
391
+ */
392
+ private async handleDiscoveryRequest(functionContext: ToolFunction | GlobalToolFunction): Promise<App.Response> {
393
+ try {
394
+ // Get app version from app context
395
+ const appVersionId = getAppContext().manifest.meta.version;
396
+ // Get function name from function context
397
+ const functionName = functionContext.constructor.name;
398
+
399
+ const toolsWithOverrides = await this.getToolsWithOverrides(appVersionId, functionName);
400
+ return new App.Response(200, { functions: toolsWithOverrides.map((f) => f.toJSON()) });
401
+ } catch (error: any) {
402
+ logger.error('Error getting tools with overrides:', error);
403
+ // Fallback to original tools if override fetch fails
404
+ return new App.Response(200, { functions: Array.from(this.functions.values()).map((f) => f.toJSON()) });
405
+ }
406
+ }
242
407
 
243
- // Extract auth data from body JSON
244
- const authData = req.bodyJSON ? req.bodyJSON.auth : undefined;
408
+ /**
409
+ * Handle overrides endpoint for saving and deleting tool description overrides
410
+ * @param req The request object
411
+ * @param functionContext The function context to get function name from
412
+ * @returns Response indicating success or failure
413
+ */
414
+ private async handleOverridesRequest(
415
+ req: App.Request,
416
+ functionContext: ToolFunction | GlobalToolFunction
417
+ ): Promise<App.Response> {
418
+ if (req.method === 'PATCH') {
419
+ try {
420
+ // Get app version from app context
421
+ const appVersionId = getAppContext().manifest.meta.version;
422
+ // Get function name from function context
423
+ const functionName = functionContext.constructor.name;
245
424
 
246
- const result = await interaction.handler(functionContext, params, authData);
247
- return new App.Response(200, result);
248
- } catch (error: any) {
249
- logger.error(`Error in function ${interaction.name}:`, error);
250
- return this.formatErrorResponse(error, interaction.endpoint);
425
+ if (!req.bodyJSON?.functions || !Array.isArray(req.bodyJSON.functions)) {
426
+ return new App.Response(400, { error: 'Invalid request body. Expected functions array.' });
251
427
  }
428
+
429
+ // Convert array format to map format for storage
430
+ const overrideData: StoredOverrides = (req.bodyJSON.functions as ToolOverride[]).reduce(
431
+ (map: StoredOverrides, tool: ToolOverride) => {
432
+ map[tool.name] = tool;
433
+ return map;
434
+ },
435
+ {}
436
+ );
437
+
438
+ await this.saveToolOverrides(appVersionId, functionName, overrideData);
439
+ return new App.Response(200, { success: true });
440
+ } catch (error: any) {
441
+ logger.error('Error saving tool overrides:', error);
442
+ return new App.Response(500, { error: error.message || 'Unknown error' });
252
443
  }
444
+ } else if (req.method === 'DELETE') {
445
+ try {
446
+ // Get app version from app context
447
+ const appVersionId = getAppContext().manifest.meta.version;
448
+ // Get function name from function context
449
+ const functionName = functionContext.constructor.name;
253
450
 
254
- return new App.Response(404, 'Function not found');
451
+ await this.deleteToolOverrides(appVersionId, functionName);
452
+ return new App.Response(200, { success: true });
453
+ } catch (error: any) {
454
+ logger.error('Error deleting tool overrides:', error);
455
+ return new App.Response(500, { error: error.message || 'Unknown error' });
456
+ }
255
457
  }
458
+
459
+ return new App.Response(405, { error: 'Method not allowed' });
256
460
  }
257
461
  }
258
462