@optimizely-opal/opal-tools-sdk 0.1.5-dev → 0.1.8-dev

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.
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "@optimizely-opal/opal-tools-sdk",
3
- "version": "0.1.5-dev",
3
+ "version": "0.1.8-dev",
4
4
  "description": "SDK for creating Opal-compatible tools services",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "scripts": {
8
- "build": "tsc",
9
- "test": "jest",
10
- "prepublishOnly": "npm run build"
8
+ "build": "tsc -p tsconfig.build.json",
9
+ "generate:proteus": "ts-node scripts/generate-proteus.ts",
10
+ "lint": "bash scripts/lint.sh",
11
+ "prepublishOnly": "npm run build",
12
+ "test": "vitest run"
11
13
  },
12
14
  "keywords": [
13
15
  "opal",
@@ -22,16 +24,24 @@
22
24
  "reflect-metadata": "^0.1.13"
23
25
  },
24
26
  "devDependencies": {
25
- "@types/jest": "^29.5.3",
26
- "jest": "^29.6.2",
27
- "ts-jest": "^29.1.1"
27
+ "@types/node": "^20.4.5",
28
+ "@types/supertest": "^6.0.2",
29
+ "eslint": "^9.39.2",
30
+ "eslint-plugin-perfectionist": "^5.4.0",
31
+ "json-schema-to-typescript": "^13.1.1",
32
+ "prettier": "^3.8.1",
33
+ "supertest": "^7.0.0",
34
+ "ts-node": "^10.9.2",
35
+ "typescript-eslint": "^8.54.0",
36
+ "vitest": "^1.2.0"
28
37
  },
29
38
  "peerDependencies": {
30
- "typescript": "^5.1.6",
31
- "@types/node": "^20.4.5",
32
39
  "@types/express": "^4.17.17",
40
+ "@types/node": "^20.4.5",
33
41
  "axios": "^1.6.0",
34
- "express": "^4.18.2"
42
+ "express": "^4.18.2",
43
+ "typescript": "^5.1.6",
44
+ "zod": "^3.25.0 || ^4.0.0"
35
45
  },
36
46
  "repository": {
37
47
  "type": "git",
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env ts-node
2
+ /**
3
+ * Generate Adaptive Proteus Document types from JSON schema.
4
+ *
5
+ * This script uses json-schema-to-typescript to generate TypeScript interfaces
6
+ * from the proteus-document-spec.json schema. The generated types will replace the
7
+ * manual Proteus builders once we're ready to switch over.
8
+ *
9
+ * Usage:
10
+ * npm run generate:proteus
11
+ */
12
+
13
+ import * as fs from "fs";
14
+ import { compile } from "json-schema-to-typescript";
15
+ import * as path from "path";
16
+
17
+ /**
18
+ * Generate builder functions and ProteusResponse class.
19
+ *
20
+ * The generated TypeScript interfaces are great for validation, but we want to
21
+ * keep the builder API (UI.Document(), etc.) for ease of use.
22
+ */
23
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
+ function generateBuilderCode(schema: any): string {
25
+ // Extract all Proteus components from definitions
26
+ const proteusComponents: Array<{ fullName: string; shortName: string }> = [];
27
+ const skipTypes = new Set([
28
+ "ProteusAtomicCondition",
29
+ "ProteusCondition",
30
+ "ProteusElement",
31
+ "ProteusEventHandler",
32
+ "ProteusNode",
33
+ ]);
34
+
35
+ if (schema.definitions) {
36
+ for (const componentName of Object.keys(schema.definitions)) {
37
+ if (
38
+ componentName.startsWith("Proteus") &&
39
+ !skipTypes.has(componentName)
40
+ ) {
41
+ // Extract the short name (e.g., "Document" from "ProteusDocument")
42
+ const shortName = componentName.substring(7); // Remove "Proteus" prefix
43
+ proteusComponents.push({ fullName: componentName, shortName });
44
+ }
45
+ }
46
+ }
47
+
48
+ // Sort components for consistent output
49
+ proteusComponents.sort((a, b) => a.shortName.localeCompare(b.shortName));
50
+
51
+ // Generate factory functions that return properly typed objects
52
+ const builderMethods = proteusComponents
53
+ .map(
54
+ ({ fullName, shortName }) =>
55
+ ` ${shortName}: (props: Omit<${fullName}, '$type'>): ${fullName} => ({ $type: '${shortName}' as const, ...props }),`,
56
+ )
57
+ .join("\n");
58
+
59
+ return `
60
+ /**
61
+ * Builder namespace for Adaptive UI Document components.
62
+ *
63
+ * Usage:
64
+ * UI.Document({ children: [...] })
65
+ * UI.Heading({ children: "Title", level: "2" })
66
+ * UI.Input({ name: "field_name", placeholder: "Enter..." })
67
+ */
68
+ export const UI = {
69
+ MIME_TYPE: "application/vnd.opal.proteus+json" as const,
70
+ ${builderMethods}
71
+ };
72
+ `;
73
+ }
74
+
75
+ async function main() {
76
+ // Paths
77
+ const scriptDir = __dirname;
78
+ const sdkRoot = path.join(scriptDir, "..");
79
+ const schemaFile = path.join(sdkRoot, "..", "proteus-document-spec.json");
80
+ const outputFile = path.join(sdkRoot, "src", "proteus.ts");
81
+
82
+ if (!fs.existsSync(schemaFile)) {
83
+ console.error(`Error: Schema file not found at ${schemaFile}`);
84
+ process.exit(1);
85
+ }
86
+
87
+ try {
88
+ // Read the schema
89
+ const schema = JSON.parse(fs.readFileSync(schemaFile, "utf-8"));
90
+
91
+ // Create a new schema that references all definitions to force their generation
92
+ const schemaWithExports = {
93
+ ...schema,
94
+ definitions: schema.definitions,
95
+ // Export each definition by creating a oneOf at the root
96
+ oneOf: Object.keys(schema.definitions || {})
97
+ .filter((key) => key.startsWith("Proteus"))
98
+ .map((key) => ({ $ref: `#/definitions/${key}` })),
99
+ };
100
+
101
+ // Generate TypeScript types
102
+ const ts = await compile(schemaWithExports, "ProteusDocumentSchema", {
103
+ bannerComment: `/**
104
+ * Generated by json-schema-to-typescript
105
+ * DO NOT MODIFY - This file is auto-generated from proteus-document-spec.json
106
+ * Run 'npm run generate:proteus' to regenerate
107
+ */`,
108
+ declareExternallyReferenced: true,
109
+ strictIndexSignatures: true,
110
+ style: {
111
+ semi: true,
112
+ singleQuote: true,
113
+ },
114
+ unknownAny: true,
115
+ unreachableDefinitions: false,
116
+ });
117
+
118
+ // Post-process to add builder functions
119
+ const finalContent = ts + "\n" + generateBuilderCode(schema);
120
+
121
+ // Write the output file
122
+ fs.writeFileSync(outputFile, finalContent, "utf-8");
123
+
124
+ console.log(`Generated: ${outputFile}`);
125
+ } catch (error) {
126
+ console.error("Error: Generation failed");
127
+ console.error(error);
128
+ process.exit(1);
129
+ }
130
+ }
131
+
132
+ main().catch((error) => {
133
+ console.error(error);
134
+ process.exit(1);
135
+ });
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+
3
+ if [[ "$*" == *"--no-fix"* ]]; then
4
+ prettier --check . && eslint . && tsc --noEmit
5
+ else
6
+ prettier --write . && eslint --fix . && tsc --noEmit
7
+ fi
package/src/auth.ts CHANGED
@@ -1,3 +1,10 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ interface AuthOptions {
3
+ provider: string;
4
+ required?: boolean;
5
+ scopeBundle: string;
6
+ }
7
+
1
8
  /**
2
9
  * Middleware to handle authentication requirements
3
10
  * @param req Express request
@@ -7,46 +14,44 @@
7
14
  export function authMiddleware(req: any, res: any, next: any) {
8
15
  const authHeader = req.headers.authorization;
9
16
  if (!authHeader && req.authRequired) {
10
- return res.status(401).json({ error: 'Authentication required' });
17
+ return res.status(401).json({ error: "Authentication required" });
11
18
  }
12
-
19
+
13
20
  // The Tools Management Service will provide the appropriate token
14
21
  // in the Authorization header
15
22
  next();
16
23
  }
17
24
 
18
- interface AuthOptions {
19
- provider: string;
20
- scopeBundle: string;
21
- required?: boolean;
22
- }
23
-
24
25
  /**
25
26
  * Decorator to indicate that a tool requires authentication
26
27
  * @param options Authentication options
27
28
  */
28
29
  export function requiresAuth(options: AuthOptions) {
29
- return function(target: any, propertyKey?: string, descriptor?: PropertyDescriptor) {
30
+ return function (
31
+ target: any,
32
+ propertyKey?: string,
33
+ descriptor?: PropertyDescriptor,
34
+ ) {
30
35
  const isMethod = propertyKey && descriptor;
31
-
36
+
32
37
  if (isMethod && descriptor) {
33
38
  const originalMethod = descriptor.value;
34
-
35
- descriptor.value = function(...args: any[]) {
39
+
40
+ descriptor.value = function (...args: any[]) {
36
41
  // Store auth requirements in function metadata
37
42
  const fn = originalMethod as any;
38
43
  fn.__authRequirements__ = {
39
44
  provider: options.provider,
45
+ required: options.required ?? true,
40
46
  scopeBundle: options.scopeBundle,
41
- required: options.required ?? true
42
47
  };
43
-
48
+
44
49
  return originalMethod.apply(this, args);
45
50
  };
46
-
51
+
47
52
  return descriptor;
48
53
  }
49
-
54
+
50
55
  return target;
51
56
  };
52
57
  }
package/src/decorators.ts CHANGED
@@ -1,70 +1,26 @@
1
- import 'reflect-metadata';
2
- import { ParameterType, Parameter, AuthRequirement } from './models';
3
- import { registry } from './registry';
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import "reflect-metadata";
3
+
4
+ import { AuthRequirement, Parameter, ParameterType } from "./models";
5
+ import { registry } from "./registry";
4
6
 
5
7
  interface ParameterDefinition {
6
- name: string;
7
- type: ParameterType;
8
8
  description: string;
9
+ name: string;
9
10
  required: boolean;
11
+ type: ParameterType;
10
12
  }
11
13
 
12
14
  interface ToolOptions {
13
- name: string;
14
- description: string;
15
- parameters?: ParameterDefinition[];
16
15
  authRequirements?: {
17
16
  provider: string;
18
- scopeBundle: string;
19
17
  required?: boolean;
18
+ scopeBundle: string;
20
19
  };
21
- }
22
-
23
- /**
24
- * Map a TypeScript type to a ParameterType
25
- * @param type TypeScript type
26
- */
27
- function mapTypeToParameterType(type: any): ParameterType {
28
- if (type === String || type.name === 'String') {
29
- return ParameterType.String;
30
- } else if (type === Number || type.name === 'Number') {
31
- return ParameterType.Number;
32
- } else if (type === Boolean || type.name === 'Boolean') {
33
- return ParameterType.Boolean;
34
- } else if (type === Array || type.name === 'Array') {
35
- return ParameterType.List;
36
- } else if (type === Object || type.name === 'Object') {
37
- return ParameterType.Dictionary;
38
- }
39
-
40
- // Default to string
41
- return ParameterType.String;
42
- }
43
-
44
- /**
45
- * Extract parameters from a TypeScript interface
46
- * @param paramType Parameter type object
47
- */
48
- function extractParameters(paramType: any): Parameter[] {
49
- const parameters: Parameter[] = [];
50
-
51
- // This is very basic and doesn't handle complex types
52
- // For production use, this would need to be more sophisticated
53
- for (const key in paramType) {
54
- if (paramType.hasOwnProperty(key)) {
55
- const type = typeof paramType[key] === 'undefined' ? String : paramType[key].constructor;
56
- const required = true; // In a real implementation, we'd detect optional parameters
57
-
58
- parameters.push(new Parameter(
59
- key,
60
- mapTypeToParameterType(type),
61
- '', // Description - in a real impl we'd use TypeDoc or similar
62
- required
63
- ));
64
- }
65
- }
66
-
67
- return parameters;
20
+ description: string;
21
+ name: string;
22
+ parameters?: ParameterDefinition[];
23
+ uiResource?: string;
68
24
  }
69
25
 
70
26
  /**
@@ -75,6 +31,7 @@ function extractParameters(paramType: any): Parameter[] {
75
31
  * - authRequirements: (Optional) Authentication requirements
76
32
  * Format: { provider: "oauth_provider", scopeBundle: "permissions_scope", required: true }
77
33
  * Example: { provider: "google", scopeBundle: "calendar", required: true }
34
+ * - uiResource: (Optional) URI of associated UI resource for dynamic rendering (e.g., "ui://my-app/create-form")
78
35
  *
79
36
  * Note: If your tool requires authentication, define your handler function with two parameters:
80
37
  * ```
@@ -84,24 +41,30 @@ function extractParameters(paramType: any): Parameter[] {
84
41
  * ```
85
42
  */
86
43
  export function tool(options: ToolOptions) {
87
- return function(target: any, propertyKey?: string, descriptor?: PropertyDescriptor) {
44
+ return function (
45
+ target: any,
46
+ propertyKey?: string,
47
+ descriptor?: PropertyDescriptor,
48
+ ) {
88
49
  const isMethod = propertyKey && descriptor;
89
50
  const handler = isMethod ? descriptor.value : target;
90
51
 
91
52
  // Generate endpoint from name - ensure hyphens instead of underscores
92
- const endpoint = `/tools/${options.name.replace(/_/g, '-')}`;
53
+ const endpoint = `/tools/${options.name.replace(/_/g, "-")}`;
93
54
 
94
55
  // Convert parameter definitions to Parameter objects
95
56
  const parameters: Parameter[] = [];
96
57
  if (options.parameters && options.parameters.length > 0) {
97
58
  // Use the explicitly provided parameter definitions
98
59
  for (const paramDef of options.parameters) {
99
- parameters.push(new Parameter(
100
- paramDef.name,
101
- paramDef.type,
102
- paramDef.description,
103
- paramDef.required
104
- ));
60
+ parameters.push(
61
+ new Parameter(
62
+ paramDef.name,
63
+ paramDef.type,
64
+ paramDef.description,
65
+ paramDef.required,
66
+ ),
67
+ );
105
68
  }
106
69
  }
107
70
 
@@ -112,8 +75,8 @@ export function tool(options: ToolOptions) {
112
75
  new AuthRequirement(
113
76
  options.authRequirements.provider,
114
77
  options.authRequirements.scopeBundle,
115
- options.authRequirements.required ?? true
116
- )
78
+ options.authRequirements.required ?? true,
79
+ ),
117
80
  ];
118
81
  }
119
82
 
@@ -125,7 +88,9 @@ export function tool(options: ToolOptions) {
125
88
  handler,
126
89
  parameters,
127
90
  endpoint,
128
- authRequirements
91
+ authRequirements,
92
+ false,
93
+ options.uiResource,
129
94
  );
130
95
  }
131
96
 
package/src/index.ts CHANGED
@@ -1,6 +1,11 @@
1
- import 'reflect-metadata';
1
+ import "reflect-metadata";
2
2
 
3
- export { ToolsService } from './service';
4
- export { tool } from './decorators';
5
- export { requiresAuth } from './auth';
6
- export * from './models';
3
+ export { requiresAuth } from "./auth";
4
+ export { tool } from "./decorators";
5
+ export * from "./models";
6
+ export { UI } from "./proteus";
7
+ export type * from "./proteus";
8
+ export { registerResource } from "./registerResource";
9
+ export { registerTool } from "./registerTool";
10
+ export type { RequestHandlerExtra } from "./registerTool";
11
+ export { ToolsService } from "./service";