@objectstack/connector-openapi 7.4.0
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/.turbo/turbo-build.log +22 -0
- package/CHANGELOG.md +23 -0
- package/LICENSE +93 -0
- package/dist/index.d.mts +147 -0
- package/dist/index.d.ts +147 -0
- package/dist/index.js +252 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +224 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +37 -0
- package/src/connector-openapi-plugin.test.ts +28 -0
- package/src/connector-openapi-plugin.ts +32 -0
- package/src/index.ts +36 -0
- package/src/openapi-connector.test.ts +141 -0
- package/src/openapi-connector.ts +414 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/openapi-connector.ts","../src/connector-openapi-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * @objectstack/connector-openapi\n *\n * Generate an ObjectStack {@link Connector} from a declarative OpenAPI 3.x\n * document (ADR-0023). One operation becomes one connector action; a single\n * generic handler drives a self-contained static-auth HTTP transport (mirroring\n * `@objectstack/connector-rest`). The generated connector is an ordinary\n * `type: 'api'` connector — registered via `engine.registerConnector` with no\n * new engine surface.\n *\n * Open-source scope: static auth only (`none` / `api-key` / `basic` / `bearer`),\n * credentials supplied by the caller. Managed OAuth2, credential vaulting, and\n * per-tenant lifecycle are the enterprise tier (ADR-0015 / 0022).\n */\n\nexport {\n createOpenApiConnector,\n type OpenApiConnectorBundle,\n type OpenApiConnectorConfig,\n type OpenApiDocument,\n type OpenApiPathItem,\n type OpenApiOperation,\n type OpenApiParameter,\n type OpenApiRequestBody,\n type OpenApiResponse,\n type OpenApiSecurityScheme,\n type OperationInfo,\n type RestAuth,\n type JsonSchema,\n} from './openapi-connector.js';\nexport {\n registerOpenApiConnector,\n type ConnectorRegistrySurface,\n} from './connector-openapi-plugin.js';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Connector } from '@objectstack/spec/integration';\n\n/**\n * OpenAPI connector generator — turns a declarative OpenAPI 3.x document into a\n * {@link Connector} definition + handler map (ADR-0023).\n *\n * Each OpenAPI operation maps to one connector action; a single generic handler\n * (closing over the operation's method + path template) drives one shared HTTP\n * request implementation. That transport mirrors `@objectstack/connector-rest`\n * (build URL from base+path+query, apply static auth, JSON-encode the body,\n * normalise the response to `{ status, ok, body }`) — kept inline so this package\n * stays self-contained, depending only on `@objectstack/core` + `@objectstack/spec`\n * like its sibling connectors. The output is an ordinary `type: 'api'` connector,\n * registered via `engine.registerConnector(def, handlers)` exactly like a\n * hand-written one — the registry, the `connector_action` node, the discovery\n * route, and the Studio palette never know it came from OpenAPI.\n *\n * Open-source scope: **static** auth only (`none` / `api-key` / `basic` /\n * `bearer`), with credentials supplied by the caller. Managed OAuth2, credential\n * vaulting, and per-tenant lifecycle are the enterprise tier (ADR-0015 / 0022).\n */\n\n/** Static auth understood by the generated connector (the open-source subset). */\nexport type RestAuth = Extract<Connector['authentication'], { type: 'none' | 'api-key' | 'basic' | 'bearer' }>;\n\n/** An action on a Connector definition (derived to avoid guessing export names). */\ntype ConnectorAction = NonNullable<Connector['actions']>[number];\n\n/** Handler signature accepted by the connector registry (ADR-0018 §Addendum). */\ntype ConnectorHandler = (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>;\n\n/** A connector definition paired with its action handlers, ready for registerConnector(). */\nexport interface OpenApiConnectorBundle {\n def: Connector;\n handlers: Record<string, ConnectorHandler>;\n}\n\n/** A free-form JSON Schema fragment (matches ConnectorAction input/outputSchema). */\nexport type JsonSchema = Record<string, unknown>;\n\n/** Minimal subset of an OpenAPI 3.x document consumed by the generator.\n * The caller is responsible for loading and de-referencing ($ref) the doc. */\nexport interface OpenApiDocument {\n openapi?: string;\n info?: { title?: string; description?: string; version?: string };\n servers?: { url: string }[];\n paths?: Record<string, OpenApiPathItem>;\n components?: { securitySchemes?: Record<string, OpenApiSecurityScheme> };\n}\n\nexport interface OpenApiPathItem {\n [method: string]: OpenApiOperation | unknown;\n}\n\nexport interface OpenApiOperation {\n operationId?: string;\n summary?: string;\n description?: string;\n tags?: string[];\n parameters?: OpenApiParameter[];\n requestBody?: OpenApiRequestBody;\n responses?: Record<string, OpenApiResponse>;\n}\n\nexport interface OpenApiParameter {\n name: string;\n in: 'path' | 'query' | 'header' | 'cookie';\n required?: boolean;\n description?: string;\n schema?: JsonSchema;\n}\n\nexport interface OpenApiRequestBody {\n required?: boolean;\n description?: string;\n content?: Record<string, { schema?: JsonSchema }>;\n}\n\nexport interface OpenApiResponse {\n description?: string;\n content?: Record<string, { schema?: JsonSchema }>;\n}\n\nexport interface OpenApiSecurityScheme {\n type: 'apiKey' | 'http' | 'oauth2' | 'openIdConnect';\n name?: string;\n in?: 'header' | 'query' | 'cookie';\n scheme?: string;\n}\n\n/** Flattened view of a single operation, passed to the `include` predicate. */\nexport interface OperationInfo {\n operationId?: string;\n method: string;\n path: string;\n tags?: string[];\n summary?: string;\n description?: string;\n}\n\n/** Configuration for {@link createOpenApiConnector}. */\nexport interface OpenApiConnectorConfig {\n /** Connector machine name (snake_case). Defaults to a slug of info.title. */\n name?: string;\n /** Human-friendly label. Defaults to info.title (then name). */\n label?: string;\n /** Description. Defaults to info.description. */\n description?: string;\n /** Icon identifier for the Studio palette. Defaults to `globe`. */\n icon?: string;\n /** The parsed OpenAPI 3.x document (caller loads/derefs it). */\n document: OpenApiDocument;\n /** Override the base URL (else servers[0].url). */\n baseUrl?: string;\n /** Static auth with credentials. Defaults to `{ type: 'none' }`. */\n auth?: RestAuth;\n /** Headers merged into every request (request-level headers win). */\n defaultHeaders?: Record<string, string>;\n /** Only include operations for which this predicate returns true (allowlist). */\n include?: (op: OperationInfo) => boolean;\n /** Injected fetch implementation (defaults to global `fetch`). */\n fetchImpl?: typeof fetch;\n}\n\n/** OpenAPI HTTP method keys, in a deterministic order. */\nconst HTTP_METHODS = ['get', 'put', 'post', 'delete', 'patch', 'options', 'head', 'trace'] as const;\n\n/** Input passed to the shared request transport. */\ninterface RequestInput {\n method: string;\n path: string;\n headers?: Record<string, string>;\n query?: Record<string, string>;\n body?: unknown;\n}\n\n/**\n * Build an OpenAPI connector definition and its handler map.\n *\n * @returns the `Connector` definition (`def`) and a `handlers` record keyed by\n * action key, suitable for `engine.registerConnector(def, handlers)`.\n */\nexport function createOpenApiConnector(config: OpenApiConnectorConfig): OpenApiConnectorBundle {\n const { document, include } = config;\n const auth: RestAuth = config.auth ?? { type: 'none' };\n const doFetch = config.fetchImpl ?? fetch;\n const name = config.name ?? slug(document.info?.title ?? 'openapi_connector');\n const label = config.label ?? document.info?.title ?? titleize(name);\n const description = config.description ?? document.info?.description;\n const baseUrl = config.baseUrl ?? document.servers?.[0]?.url;\n if (!baseUrl) {\n throw new Error('createOpenApiConnector: no base URL — provide config.baseUrl or document.servers[0].url');\n }\n\n // One shared transport (mirrors connector-rest) reused by every action handler.\n async function request(input: RequestInput): Promise<Record<string, unknown>> {\n const method = input.method.toUpperCase();\n const headers: Record<string, string> = { ...config.defaultHeaders, ...input.headers };\n const query: Record<string, string> = { ...input.query };\n applyAuth(auth, headers, query);\n\n const url = buildUrl(baseUrl as string, input.path, query);\n const hasBody = input.body !== undefined && method !== 'GET' && method !== 'HEAD';\n if (hasBody && headers['Content-Type'] === undefined && headers['content-type'] === undefined) {\n headers['Content-Type'] = 'application/json';\n }\n\n const response = await doFetch(url, {\n method,\n headers,\n body: hasBody ? JSON.stringify(input.body) : undefined,\n });\n\n const contentType = response.headers.get('content-type') ?? '';\n const parsed = contentType.includes('application/json') ? await response.json() : await response.text();\n return { status: response.status, ok: response.ok, body: parsed };\n }\n\n const actions: ConnectorAction[] = [];\n const handlers: Record<string, ConnectorHandler> = {};\n const seenKeys = new Set<string>();\n\n for (const op of collectOperations(document)) {\n if (include && !include(toInfo(op))) continue;\n const key = uniqueKey(op.operationId ?? slug(`${op.method}_${op.path}`), seenKeys);\n\n actions.push({\n key,\n label: op.summary ?? titleize(key),\n description: op.description,\n inputSchema: buildInputSchema(op),\n outputSchema: buildOutputSchema(op),\n });\n\n handlers[key] = async (input: Record<string, unknown>) => {\n const req = input as { path?: unknown; query?: unknown; header?: unknown; body?: unknown };\n return request({\n method: op.method,\n path: interpolatePath(op.path, asRecord(req.path)),\n query: stringifyValues(asRecord(req.query)),\n headers: stringifyValues(asRecord(req.header)),\n body: req.body,\n });\n };\n }\n\n const def: Connector = {\n name,\n label,\n type: 'api',\n description,\n icon: config.icon ?? 'globe',\n authentication: auth,\n // Defaulted by ConnectorSchema; set explicitly so the literal satisfies\n // the (post-parse) Connector output type (mirrors connector-rest/mcp).\n status: 'active',\n enabled: true,\n connectionTimeoutMs: 30000,\n requestTimeoutMs: 30000,\n actions,\n };\n\n return { def, handlers };\n}\n\ninterface Op extends OpenApiOperation {\n method: string;\n path: string;\n}\n\n/** Flatten paths × methods into a deterministic list of operations. */\nfunction collectOperations(doc: OpenApiDocument): Op[] {\n const ops: Op[] = [];\n for (const [path, item] of Object.entries(doc.paths ?? {})) {\n if (!item || typeof item !== 'object') continue;\n const record = item as Record<string, unknown>;\n for (const method of HTTP_METHODS) {\n const operation = record[method] as OpenApiOperation | undefined;\n if (!operation || typeof operation !== 'object') continue;\n ops.push({ ...operation, method, path });\n }\n }\n return ops;\n}\n\nfunction toInfo(op: Op): OperationInfo {\n return {\n operationId: op.operationId,\n method: op.method,\n path: op.path,\n tags: op.tags,\n summary: op.summary,\n description: op.description,\n };\n}\n\n/**\n * Assemble the action inputSchema from an operation's parameters + requestBody.\n * Produces { type: 'object', properties: { path, query, header, body }, required }\n * where only non-empty sections are emitted.\n */\nfunction buildInputSchema(op: OpenApiOperation): JsonSchema | undefined {\n const sections: Record<'path' | 'query' | 'header', { props: Record<string, JsonSchema>; required: string[] }> = {\n path: { props: {}, required: [] },\n query: { props: {}, required: [] },\n header: { props: {}, required: [] },\n };\n\n for (const p of op.parameters ?? []) {\n if (!p || typeof p !== 'object' || '$ref' in p) continue;\n if (p.in !== 'path' && p.in !== 'query' && p.in !== 'header') continue;\n const sec = sections[p.in];\n sec.props[p.name] = p.schema ?? (p.description ? { type: 'string', description: p.description } : { type: 'string' });\n if (p.required) sec.required.push(p.name);\n }\n\n const properties: Record<string, JsonSchema> = {};\n const required: string[] = [];\n for (const where of ['path', 'query', 'header'] as const) {\n const sec = sections[where];\n if (Object.keys(sec.props).length === 0) continue;\n const schema: JsonSchema = { type: 'object', properties: sec.props };\n if (sec.required.length) schema.required = sec.required;\n properties[where] = schema;\n // Path params are always required when present; others only if any are.\n if (where === 'path' || sec.required.length) required.push(where);\n }\n\n const bodySchema = extractRequestBodySchema(op.requestBody);\n if (bodySchema) {\n properties.body = bodySchema;\n if (op.requestBody && !('$ref' in op.requestBody) && op.requestBody.required) required.push('body');\n }\n\n if (Object.keys(properties).length === 0) return undefined;\n const schema: JsonSchema = { type: 'object', properties };\n if (required.length) schema.required = required;\n return schema;\n}\n\n/** Pick the success response's JSON schema (200 → first 2xx → default). */\nfunction buildOutputSchema(op: OpenApiOperation): JsonSchema | undefined {\n const responses = op.responses;\n if (!responses) return undefined;\n let code: string | undefined;\n if (responses['200']) code = '200';\n else code = Object.keys(responses).find((c) => /^2\\d\\d$/.test(c));\n if (!code && responses['default']) code = 'default';\n if (!code) return undefined;\n const resp = responses[code];\n if (!resp || typeof resp !== 'object' || '$ref' in resp) return undefined;\n return pickJsonSchema(resp.content);\n}\n\n/** Extract the requestBody JSON schema (prefers application/json). */\nfunction extractRequestBodySchema(rb: OpenApiRequestBody | undefined): JsonSchema | undefined {\n if (!rb || typeof rb !== 'object' || '$ref' in rb) return undefined;\n return pickJsonSchema(rb.content);\n}\n\n/** Choose the application/json schema, falling back to the first content type. */\nfunction pickJsonSchema(content: Record<string, { schema?: JsonSchema }> | undefined): JsonSchema | undefined {\n if (!content) return undefined;\n const chosen = content['application/json'] ?? Object.values(content)[0];\n return chosen?.schema;\n}\n\n/** Build the request URL from base + path + query, encoding query params. */\nfunction buildUrl(baseUrl: string, path: string, query: Record<string, string>): string {\n const base = baseUrl.replace(/\\/+$/, '');\n const suffix = path ? (path.startsWith('/') ? path : `/${path}`) : '';\n const url = new URL(base + suffix);\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined && value !== null) url.searchParams.set(key, String(value));\n }\n return url.toString();\n}\n\n/** Apply static auth to the outgoing headers / query (mirrors connector-rest). */\nfunction applyAuth(auth: RestAuth, headers: Record<string, string>, query: Record<string, string>): void {\n switch (auth.type) {\n case 'none':\n return;\n case 'bearer':\n headers['Authorization'] = `Bearer ${auth.token}`;\n return;\n case 'basic': {\n const encoded = Buffer.from(`${auth.username}:${auth.password}`).toString('base64');\n headers['Authorization'] = `Basic ${encoded}`;\n return;\n }\n case 'api-key':\n if (auth.paramName) query[auth.paramName] = auth.key;\n else headers[auth.headerName ?? 'X-API-Key'] = auth.key;\n return;\n }\n}\n\n/** Interpolate {name} path templates with encoded values from the input. */\nfunction interpolatePath(template: string, pathParams: Record<string, unknown>): string {\n return template.replace(/\\{([^}]+)\\}/g, (_match, key: string) => {\n const value = pathParams[key];\n return value === undefined || value === null ? `{${key}}` : encodeURIComponent(String(value));\n });\n}\n\n/** Coerce a record of mixed values into string values, dropping null/undefined. */\nfunction stringifyValues(rec: Record<string, unknown>): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries(rec)) {\n if (v === undefined || v === null) continue;\n out[k] = String(v);\n }\n return out;\n}\n\n/** Return v if it is a plain object, else an empty record. */\nfunction asRecord(v: unknown): Record<string, unknown> {\n return v && typeof v === 'object' && !Array.isArray(v) ? (v as Record<string, unknown>) : {};\n}\n\n/** Ensure a deterministically unique action key within the connector. */\nfunction uniqueKey(base: string, seen: Set<string>): string {\n let candidate = base;\n if (seen.has(candidate)) {\n let i = 2;\n while (seen.has(`${base}_${i}`)) i++;\n candidate = `${base}_${i}`;\n }\n seen.add(candidate);\n return candidate;\n}\n\n/** Slugify a string into a snake_case machine name (`/^[a-z_][a-z0-9_]*$/`). */\nfunction slug(s: string): string {\n const out = s\n .normalize('NFKD')\n .replace(/[^a-zA-Z0-9]+/g, '_')\n .replace(/^_+|_+$/g, '')\n .toLowerCase();\n if (!out) return 'connector';\n return /^[a-z_]/.test(out) ? out : `op_${out}`;\n}\n\n/** Title-case a snake_case key for a default label (`get_pets` → `Get Pets`). */\nfunction titleize(name: string): string {\n return name\n .split('_')\n .filter(Boolean)\n .map((w) => w.charAt(0).toUpperCase() + w.slice(1))\n .join(' ');\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Connector } from '@objectstack/spec/integration';\nimport { createOpenApiConnector, type OpenApiConnectorConfig } from './openapi-connector.js';\n\n/**\n * Minimal surface of the automation engine this helper depends on — the\n * connector registry from ADR-0018 §Addendum. Kept structural so callers need\n * no runtime dependency on `@objectstack/service-automation` (mirrors\n * connector-rest / connector-mcp).\n */\nexport interface ConnectorRegistrySurface {\n registerConnector(\n def: Connector,\n handlers: Record<\n string,\n (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>\n >,\n ): void;\n unregisterConnector(name: string): void;\n}\n\n/**\n * Generate an OpenAPI-backed connector and register it on the engine's connector\n * registry so the baseline `connector_action` node can dispatch to the generated\n * actions (ADR-0023). Returns the registered connector name.\n */\nexport function registerOpenApiConnector(registry: ConnectorRegistrySurface, config: OpenApiConnectorConfig): string {\n const { def, handlers } = createOpenApiConnector(config);\n registry.registerConnector(def, handlers);\n return def.name;\n}\n"],"mappings":"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","names":["schema"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// src/openapi-connector.ts
|
|
2
|
+
var HTTP_METHODS = ["get", "put", "post", "delete", "patch", "options", "head", "trace"];
|
|
3
|
+
function createOpenApiConnector(config) {
|
|
4
|
+
const { document, include } = config;
|
|
5
|
+
const auth = config.auth ?? { type: "none" };
|
|
6
|
+
const doFetch = config.fetchImpl ?? fetch;
|
|
7
|
+
const name = config.name ?? slug(document.info?.title ?? "openapi_connector");
|
|
8
|
+
const label = config.label ?? document.info?.title ?? titleize(name);
|
|
9
|
+
const description = config.description ?? document.info?.description;
|
|
10
|
+
const baseUrl = config.baseUrl ?? document.servers?.[0]?.url;
|
|
11
|
+
if (!baseUrl) {
|
|
12
|
+
throw new Error("createOpenApiConnector: no base URL \u2014 provide config.baseUrl or document.servers[0].url");
|
|
13
|
+
}
|
|
14
|
+
async function request(input) {
|
|
15
|
+
const method = input.method.toUpperCase();
|
|
16
|
+
const headers = { ...config.defaultHeaders, ...input.headers };
|
|
17
|
+
const query = { ...input.query };
|
|
18
|
+
applyAuth(auth, headers, query);
|
|
19
|
+
const url = buildUrl(baseUrl, input.path, query);
|
|
20
|
+
const hasBody = input.body !== void 0 && method !== "GET" && method !== "HEAD";
|
|
21
|
+
if (hasBody && headers["Content-Type"] === void 0 && headers["content-type"] === void 0) {
|
|
22
|
+
headers["Content-Type"] = "application/json";
|
|
23
|
+
}
|
|
24
|
+
const response = await doFetch(url, {
|
|
25
|
+
method,
|
|
26
|
+
headers,
|
|
27
|
+
body: hasBody ? JSON.stringify(input.body) : void 0
|
|
28
|
+
});
|
|
29
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
30
|
+
const parsed = contentType.includes("application/json") ? await response.json() : await response.text();
|
|
31
|
+
return { status: response.status, ok: response.ok, body: parsed };
|
|
32
|
+
}
|
|
33
|
+
const actions = [];
|
|
34
|
+
const handlers = {};
|
|
35
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
36
|
+
for (const op of collectOperations(document)) {
|
|
37
|
+
if (include && !include(toInfo(op))) continue;
|
|
38
|
+
const key = uniqueKey(op.operationId ?? slug(`${op.method}_${op.path}`), seenKeys);
|
|
39
|
+
actions.push({
|
|
40
|
+
key,
|
|
41
|
+
label: op.summary ?? titleize(key),
|
|
42
|
+
description: op.description,
|
|
43
|
+
inputSchema: buildInputSchema(op),
|
|
44
|
+
outputSchema: buildOutputSchema(op)
|
|
45
|
+
});
|
|
46
|
+
handlers[key] = async (input) => {
|
|
47
|
+
const req = input;
|
|
48
|
+
return request({
|
|
49
|
+
method: op.method,
|
|
50
|
+
path: interpolatePath(op.path, asRecord(req.path)),
|
|
51
|
+
query: stringifyValues(asRecord(req.query)),
|
|
52
|
+
headers: stringifyValues(asRecord(req.header)),
|
|
53
|
+
body: req.body
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const def = {
|
|
58
|
+
name,
|
|
59
|
+
label,
|
|
60
|
+
type: "api",
|
|
61
|
+
description,
|
|
62
|
+
icon: config.icon ?? "globe",
|
|
63
|
+
authentication: auth,
|
|
64
|
+
// Defaulted by ConnectorSchema; set explicitly so the literal satisfies
|
|
65
|
+
// the (post-parse) Connector output type (mirrors connector-rest/mcp).
|
|
66
|
+
status: "active",
|
|
67
|
+
enabled: true,
|
|
68
|
+
connectionTimeoutMs: 3e4,
|
|
69
|
+
requestTimeoutMs: 3e4,
|
|
70
|
+
actions
|
|
71
|
+
};
|
|
72
|
+
return { def, handlers };
|
|
73
|
+
}
|
|
74
|
+
function collectOperations(doc) {
|
|
75
|
+
const ops = [];
|
|
76
|
+
for (const [path, item] of Object.entries(doc.paths ?? {})) {
|
|
77
|
+
if (!item || typeof item !== "object") continue;
|
|
78
|
+
const record = item;
|
|
79
|
+
for (const method of HTTP_METHODS) {
|
|
80
|
+
const operation = record[method];
|
|
81
|
+
if (!operation || typeof operation !== "object") continue;
|
|
82
|
+
ops.push({ ...operation, method, path });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return ops;
|
|
86
|
+
}
|
|
87
|
+
function toInfo(op) {
|
|
88
|
+
return {
|
|
89
|
+
operationId: op.operationId,
|
|
90
|
+
method: op.method,
|
|
91
|
+
path: op.path,
|
|
92
|
+
tags: op.tags,
|
|
93
|
+
summary: op.summary,
|
|
94
|
+
description: op.description
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function buildInputSchema(op) {
|
|
98
|
+
const sections = {
|
|
99
|
+
path: { props: {}, required: [] },
|
|
100
|
+
query: { props: {}, required: [] },
|
|
101
|
+
header: { props: {}, required: [] }
|
|
102
|
+
};
|
|
103
|
+
for (const p of op.parameters ?? []) {
|
|
104
|
+
if (!p || typeof p !== "object" || "$ref" in p) continue;
|
|
105
|
+
if (p.in !== "path" && p.in !== "query" && p.in !== "header") continue;
|
|
106
|
+
const sec = sections[p.in];
|
|
107
|
+
sec.props[p.name] = p.schema ?? (p.description ? { type: "string", description: p.description } : { type: "string" });
|
|
108
|
+
if (p.required) sec.required.push(p.name);
|
|
109
|
+
}
|
|
110
|
+
const properties = {};
|
|
111
|
+
const required = [];
|
|
112
|
+
for (const where of ["path", "query", "header"]) {
|
|
113
|
+
const sec = sections[where];
|
|
114
|
+
if (Object.keys(sec.props).length === 0) continue;
|
|
115
|
+
const schema2 = { type: "object", properties: sec.props };
|
|
116
|
+
if (sec.required.length) schema2.required = sec.required;
|
|
117
|
+
properties[where] = schema2;
|
|
118
|
+
if (where === "path" || sec.required.length) required.push(where);
|
|
119
|
+
}
|
|
120
|
+
const bodySchema = extractRequestBodySchema(op.requestBody);
|
|
121
|
+
if (bodySchema) {
|
|
122
|
+
properties.body = bodySchema;
|
|
123
|
+
if (op.requestBody && !("$ref" in op.requestBody) && op.requestBody.required) required.push("body");
|
|
124
|
+
}
|
|
125
|
+
if (Object.keys(properties).length === 0) return void 0;
|
|
126
|
+
const schema = { type: "object", properties };
|
|
127
|
+
if (required.length) schema.required = required;
|
|
128
|
+
return schema;
|
|
129
|
+
}
|
|
130
|
+
function buildOutputSchema(op) {
|
|
131
|
+
const responses = op.responses;
|
|
132
|
+
if (!responses) return void 0;
|
|
133
|
+
let code;
|
|
134
|
+
if (responses["200"]) code = "200";
|
|
135
|
+
else code = Object.keys(responses).find((c) => /^2\d\d$/.test(c));
|
|
136
|
+
if (!code && responses["default"]) code = "default";
|
|
137
|
+
if (!code) return void 0;
|
|
138
|
+
const resp = responses[code];
|
|
139
|
+
if (!resp || typeof resp !== "object" || "$ref" in resp) return void 0;
|
|
140
|
+
return pickJsonSchema(resp.content);
|
|
141
|
+
}
|
|
142
|
+
function extractRequestBodySchema(rb) {
|
|
143
|
+
if (!rb || typeof rb !== "object" || "$ref" in rb) return void 0;
|
|
144
|
+
return pickJsonSchema(rb.content);
|
|
145
|
+
}
|
|
146
|
+
function pickJsonSchema(content) {
|
|
147
|
+
if (!content) return void 0;
|
|
148
|
+
const chosen = content["application/json"] ?? Object.values(content)[0];
|
|
149
|
+
return chosen?.schema;
|
|
150
|
+
}
|
|
151
|
+
function buildUrl(baseUrl, path, query) {
|
|
152
|
+
const base = baseUrl.replace(/\/+$/, "");
|
|
153
|
+
const suffix = path ? path.startsWith("/") ? path : `/${path}` : "";
|
|
154
|
+
const url = new URL(base + suffix);
|
|
155
|
+
for (const [key, value] of Object.entries(query)) {
|
|
156
|
+
if (value !== void 0 && value !== null) url.searchParams.set(key, String(value));
|
|
157
|
+
}
|
|
158
|
+
return url.toString();
|
|
159
|
+
}
|
|
160
|
+
function applyAuth(auth, headers, query) {
|
|
161
|
+
switch (auth.type) {
|
|
162
|
+
case "none":
|
|
163
|
+
return;
|
|
164
|
+
case "bearer":
|
|
165
|
+
headers["Authorization"] = `Bearer ${auth.token}`;
|
|
166
|
+
return;
|
|
167
|
+
case "basic": {
|
|
168
|
+
const encoded = Buffer.from(`${auth.username}:${auth.password}`).toString("base64");
|
|
169
|
+
headers["Authorization"] = `Basic ${encoded}`;
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
case "api-key":
|
|
173
|
+
if (auth.paramName) query[auth.paramName] = auth.key;
|
|
174
|
+
else headers[auth.headerName ?? "X-API-Key"] = auth.key;
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function interpolatePath(template, pathParams) {
|
|
179
|
+
return template.replace(/\{([^}]+)\}/g, (_match, key) => {
|
|
180
|
+
const value = pathParams[key];
|
|
181
|
+
return value === void 0 || value === null ? `{${key}}` : encodeURIComponent(String(value));
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
function stringifyValues(rec) {
|
|
185
|
+
const out = {};
|
|
186
|
+
for (const [k, v] of Object.entries(rec)) {
|
|
187
|
+
if (v === void 0 || v === null) continue;
|
|
188
|
+
out[k] = String(v);
|
|
189
|
+
}
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
function asRecord(v) {
|
|
193
|
+
return v && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
194
|
+
}
|
|
195
|
+
function uniqueKey(base, seen) {
|
|
196
|
+
let candidate = base;
|
|
197
|
+
if (seen.has(candidate)) {
|
|
198
|
+
let i = 2;
|
|
199
|
+
while (seen.has(`${base}_${i}`)) i++;
|
|
200
|
+
candidate = `${base}_${i}`;
|
|
201
|
+
}
|
|
202
|
+
seen.add(candidate);
|
|
203
|
+
return candidate;
|
|
204
|
+
}
|
|
205
|
+
function slug(s) {
|
|
206
|
+
const out = s.normalize("NFKD").replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "").toLowerCase();
|
|
207
|
+
if (!out) return "connector";
|
|
208
|
+
return /^[a-z_]/.test(out) ? out : `op_${out}`;
|
|
209
|
+
}
|
|
210
|
+
function titleize(name) {
|
|
211
|
+
return name.split("_").filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/connector-openapi-plugin.ts
|
|
215
|
+
function registerOpenApiConnector(registry, config) {
|
|
216
|
+
const { def, handlers } = createOpenApiConnector(config);
|
|
217
|
+
registry.registerConnector(def, handlers);
|
|
218
|
+
return def.name;
|
|
219
|
+
}
|
|
220
|
+
export {
|
|
221
|
+
createOpenApiConnector,
|
|
222
|
+
registerOpenApiConnector
|
|
223
|
+
};
|
|
224
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/openapi-connector.ts","../src/connector-openapi-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Connector } from '@objectstack/spec/integration';\n\n/**\n * OpenAPI connector generator — turns a declarative OpenAPI 3.x document into a\n * {@link Connector} definition + handler map (ADR-0023).\n *\n * Each OpenAPI operation maps to one connector action; a single generic handler\n * (closing over the operation's method + path template) drives one shared HTTP\n * request implementation. That transport mirrors `@objectstack/connector-rest`\n * (build URL from base+path+query, apply static auth, JSON-encode the body,\n * normalise the response to `{ status, ok, body }`) — kept inline so this package\n * stays self-contained, depending only on `@objectstack/core` + `@objectstack/spec`\n * like its sibling connectors. The output is an ordinary `type: 'api'` connector,\n * registered via `engine.registerConnector(def, handlers)` exactly like a\n * hand-written one — the registry, the `connector_action` node, the discovery\n * route, and the Studio palette never know it came from OpenAPI.\n *\n * Open-source scope: **static** auth only (`none` / `api-key` / `basic` /\n * `bearer`), with credentials supplied by the caller. Managed OAuth2, credential\n * vaulting, and per-tenant lifecycle are the enterprise tier (ADR-0015 / 0022).\n */\n\n/** Static auth understood by the generated connector (the open-source subset). */\nexport type RestAuth = Extract<Connector['authentication'], { type: 'none' | 'api-key' | 'basic' | 'bearer' }>;\n\n/** An action on a Connector definition (derived to avoid guessing export names). */\ntype ConnectorAction = NonNullable<Connector['actions']>[number];\n\n/** Handler signature accepted by the connector registry (ADR-0018 §Addendum). */\ntype ConnectorHandler = (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>;\n\n/** A connector definition paired with its action handlers, ready for registerConnector(). */\nexport interface OpenApiConnectorBundle {\n def: Connector;\n handlers: Record<string, ConnectorHandler>;\n}\n\n/** A free-form JSON Schema fragment (matches ConnectorAction input/outputSchema). */\nexport type JsonSchema = Record<string, unknown>;\n\n/** Minimal subset of an OpenAPI 3.x document consumed by the generator.\n * The caller is responsible for loading and de-referencing ($ref) the doc. */\nexport interface OpenApiDocument {\n openapi?: string;\n info?: { title?: string; description?: string; version?: string };\n servers?: { url: string }[];\n paths?: Record<string, OpenApiPathItem>;\n components?: { securitySchemes?: Record<string, OpenApiSecurityScheme> };\n}\n\nexport interface OpenApiPathItem {\n [method: string]: OpenApiOperation | unknown;\n}\n\nexport interface OpenApiOperation {\n operationId?: string;\n summary?: string;\n description?: string;\n tags?: string[];\n parameters?: OpenApiParameter[];\n requestBody?: OpenApiRequestBody;\n responses?: Record<string, OpenApiResponse>;\n}\n\nexport interface OpenApiParameter {\n name: string;\n in: 'path' | 'query' | 'header' | 'cookie';\n required?: boolean;\n description?: string;\n schema?: JsonSchema;\n}\n\nexport interface OpenApiRequestBody {\n required?: boolean;\n description?: string;\n content?: Record<string, { schema?: JsonSchema }>;\n}\n\nexport interface OpenApiResponse {\n description?: string;\n content?: Record<string, { schema?: JsonSchema }>;\n}\n\nexport interface OpenApiSecurityScheme {\n type: 'apiKey' | 'http' | 'oauth2' | 'openIdConnect';\n name?: string;\n in?: 'header' | 'query' | 'cookie';\n scheme?: string;\n}\n\n/** Flattened view of a single operation, passed to the `include` predicate. */\nexport interface OperationInfo {\n operationId?: string;\n method: string;\n path: string;\n tags?: string[];\n summary?: string;\n description?: string;\n}\n\n/** Configuration for {@link createOpenApiConnector}. */\nexport interface OpenApiConnectorConfig {\n /** Connector machine name (snake_case). Defaults to a slug of info.title. */\n name?: string;\n /** Human-friendly label. Defaults to info.title (then name). */\n label?: string;\n /** Description. Defaults to info.description. */\n description?: string;\n /** Icon identifier for the Studio palette. Defaults to `globe`. */\n icon?: string;\n /** The parsed OpenAPI 3.x document (caller loads/derefs it). */\n document: OpenApiDocument;\n /** Override the base URL (else servers[0].url). */\n baseUrl?: string;\n /** Static auth with credentials. Defaults to `{ type: 'none' }`. */\n auth?: RestAuth;\n /** Headers merged into every request (request-level headers win). */\n defaultHeaders?: Record<string, string>;\n /** Only include operations for which this predicate returns true (allowlist). */\n include?: (op: OperationInfo) => boolean;\n /** Injected fetch implementation (defaults to global `fetch`). */\n fetchImpl?: typeof fetch;\n}\n\n/** OpenAPI HTTP method keys, in a deterministic order. */\nconst HTTP_METHODS = ['get', 'put', 'post', 'delete', 'patch', 'options', 'head', 'trace'] as const;\n\n/** Input passed to the shared request transport. */\ninterface RequestInput {\n method: string;\n path: string;\n headers?: Record<string, string>;\n query?: Record<string, string>;\n body?: unknown;\n}\n\n/**\n * Build an OpenAPI connector definition and its handler map.\n *\n * @returns the `Connector` definition (`def`) and a `handlers` record keyed by\n * action key, suitable for `engine.registerConnector(def, handlers)`.\n */\nexport function createOpenApiConnector(config: OpenApiConnectorConfig): OpenApiConnectorBundle {\n const { document, include } = config;\n const auth: RestAuth = config.auth ?? { type: 'none' };\n const doFetch = config.fetchImpl ?? fetch;\n const name = config.name ?? slug(document.info?.title ?? 'openapi_connector');\n const label = config.label ?? document.info?.title ?? titleize(name);\n const description = config.description ?? document.info?.description;\n const baseUrl = config.baseUrl ?? document.servers?.[0]?.url;\n if (!baseUrl) {\n throw new Error('createOpenApiConnector: no base URL — provide config.baseUrl or document.servers[0].url');\n }\n\n // One shared transport (mirrors connector-rest) reused by every action handler.\n async function request(input: RequestInput): Promise<Record<string, unknown>> {\n const method = input.method.toUpperCase();\n const headers: Record<string, string> = { ...config.defaultHeaders, ...input.headers };\n const query: Record<string, string> = { ...input.query };\n applyAuth(auth, headers, query);\n\n const url = buildUrl(baseUrl as string, input.path, query);\n const hasBody = input.body !== undefined && method !== 'GET' && method !== 'HEAD';\n if (hasBody && headers['Content-Type'] === undefined && headers['content-type'] === undefined) {\n headers['Content-Type'] = 'application/json';\n }\n\n const response = await doFetch(url, {\n method,\n headers,\n body: hasBody ? JSON.stringify(input.body) : undefined,\n });\n\n const contentType = response.headers.get('content-type') ?? '';\n const parsed = contentType.includes('application/json') ? await response.json() : await response.text();\n return { status: response.status, ok: response.ok, body: parsed };\n }\n\n const actions: ConnectorAction[] = [];\n const handlers: Record<string, ConnectorHandler> = {};\n const seenKeys = new Set<string>();\n\n for (const op of collectOperations(document)) {\n if (include && !include(toInfo(op))) continue;\n const key = uniqueKey(op.operationId ?? slug(`${op.method}_${op.path}`), seenKeys);\n\n actions.push({\n key,\n label: op.summary ?? titleize(key),\n description: op.description,\n inputSchema: buildInputSchema(op),\n outputSchema: buildOutputSchema(op),\n });\n\n handlers[key] = async (input: Record<string, unknown>) => {\n const req = input as { path?: unknown; query?: unknown; header?: unknown; body?: unknown };\n return request({\n method: op.method,\n path: interpolatePath(op.path, asRecord(req.path)),\n query: stringifyValues(asRecord(req.query)),\n headers: stringifyValues(asRecord(req.header)),\n body: req.body,\n });\n };\n }\n\n const def: Connector = {\n name,\n label,\n type: 'api',\n description,\n icon: config.icon ?? 'globe',\n authentication: auth,\n // Defaulted by ConnectorSchema; set explicitly so the literal satisfies\n // the (post-parse) Connector output type (mirrors connector-rest/mcp).\n status: 'active',\n enabled: true,\n connectionTimeoutMs: 30000,\n requestTimeoutMs: 30000,\n actions,\n };\n\n return { def, handlers };\n}\n\ninterface Op extends OpenApiOperation {\n method: string;\n path: string;\n}\n\n/** Flatten paths × methods into a deterministic list of operations. */\nfunction collectOperations(doc: OpenApiDocument): Op[] {\n const ops: Op[] = [];\n for (const [path, item] of Object.entries(doc.paths ?? {})) {\n if (!item || typeof item !== 'object') continue;\n const record = item as Record<string, unknown>;\n for (const method of HTTP_METHODS) {\n const operation = record[method] as OpenApiOperation | undefined;\n if (!operation || typeof operation !== 'object') continue;\n ops.push({ ...operation, method, path });\n }\n }\n return ops;\n}\n\nfunction toInfo(op: Op): OperationInfo {\n return {\n operationId: op.operationId,\n method: op.method,\n path: op.path,\n tags: op.tags,\n summary: op.summary,\n description: op.description,\n };\n}\n\n/**\n * Assemble the action inputSchema from an operation's parameters + requestBody.\n * Produces { type: 'object', properties: { path, query, header, body }, required }\n * where only non-empty sections are emitted.\n */\nfunction buildInputSchema(op: OpenApiOperation): JsonSchema | undefined {\n const sections: Record<'path' | 'query' | 'header', { props: Record<string, JsonSchema>; required: string[] }> = {\n path: { props: {}, required: [] },\n query: { props: {}, required: [] },\n header: { props: {}, required: [] },\n };\n\n for (const p of op.parameters ?? []) {\n if (!p || typeof p !== 'object' || '$ref' in p) continue;\n if (p.in !== 'path' && p.in !== 'query' && p.in !== 'header') continue;\n const sec = sections[p.in];\n sec.props[p.name] = p.schema ?? (p.description ? { type: 'string', description: p.description } : { type: 'string' });\n if (p.required) sec.required.push(p.name);\n }\n\n const properties: Record<string, JsonSchema> = {};\n const required: string[] = [];\n for (const where of ['path', 'query', 'header'] as const) {\n const sec = sections[where];\n if (Object.keys(sec.props).length === 0) continue;\n const schema: JsonSchema = { type: 'object', properties: sec.props };\n if (sec.required.length) schema.required = sec.required;\n properties[where] = schema;\n // Path params are always required when present; others only if any are.\n if (where === 'path' || sec.required.length) required.push(where);\n }\n\n const bodySchema = extractRequestBodySchema(op.requestBody);\n if (bodySchema) {\n properties.body = bodySchema;\n if (op.requestBody && !('$ref' in op.requestBody) && op.requestBody.required) required.push('body');\n }\n\n if (Object.keys(properties).length === 0) return undefined;\n const schema: JsonSchema = { type: 'object', properties };\n if (required.length) schema.required = required;\n return schema;\n}\n\n/** Pick the success response's JSON schema (200 → first 2xx → default). */\nfunction buildOutputSchema(op: OpenApiOperation): JsonSchema | undefined {\n const responses = op.responses;\n if (!responses) return undefined;\n let code: string | undefined;\n if (responses['200']) code = '200';\n else code = Object.keys(responses).find((c) => /^2\\d\\d$/.test(c));\n if (!code && responses['default']) code = 'default';\n if (!code) return undefined;\n const resp = responses[code];\n if (!resp || typeof resp !== 'object' || '$ref' in resp) return undefined;\n return pickJsonSchema(resp.content);\n}\n\n/** Extract the requestBody JSON schema (prefers application/json). */\nfunction extractRequestBodySchema(rb: OpenApiRequestBody | undefined): JsonSchema | undefined {\n if (!rb || typeof rb !== 'object' || '$ref' in rb) return undefined;\n return pickJsonSchema(rb.content);\n}\n\n/** Choose the application/json schema, falling back to the first content type. */\nfunction pickJsonSchema(content: Record<string, { schema?: JsonSchema }> | undefined): JsonSchema | undefined {\n if (!content) return undefined;\n const chosen = content['application/json'] ?? Object.values(content)[0];\n return chosen?.schema;\n}\n\n/** Build the request URL from base + path + query, encoding query params. */\nfunction buildUrl(baseUrl: string, path: string, query: Record<string, string>): string {\n const base = baseUrl.replace(/\\/+$/, '');\n const suffix = path ? (path.startsWith('/') ? path : `/${path}`) : '';\n const url = new URL(base + suffix);\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined && value !== null) url.searchParams.set(key, String(value));\n }\n return url.toString();\n}\n\n/** Apply static auth to the outgoing headers / query (mirrors connector-rest). */\nfunction applyAuth(auth: RestAuth, headers: Record<string, string>, query: Record<string, string>): void {\n switch (auth.type) {\n case 'none':\n return;\n case 'bearer':\n headers['Authorization'] = `Bearer ${auth.token}`;\n return;\n case 'basic': {\n const encoded = Buffer.from(`${auth.username}:${auth.password}`).toString('base64');\n headers['Authorization'] = `Basic ${encoded}`;\n return;\n }\n case 'api-key':\n if (auth.paramName) query[auth.paramName] = auth.key;\n else headers[auth.headerName ?? 'X-API-Key'] = auth.key;\n return;\n }\n}\n\n/** Interpolate {name} path templates with encoded values from the input. */\nfunction interpolatePath(template: string, pathParams: Record<string, unknown>): string {\n return template.replace(/\\{([^}]+)\\}/g, (_match, key: string) => {\n const value = pathParams[key];\n return value === undefined || value === null ? `{${key}}` : encodeURIComponent(String(value));\n });\n}\n\n/** Coerce a record of mixed values into string values, dropping null/undefined. */\nfunction stringifyValues(rec: Record<string, unknown>): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries(rec)) {\n if (v === undefined || v === null) continue;\n out[k] = String(v);\n }\n return out;\n}\n\n/** Return v if it is a plain object, else an empty record. */\nfunction asRecord(v: unknown): Record<string, unknown> {\n return v && typeof v === 'object' && !Array.isArray(v) ? (v as Record<string, unknown>) : {};\n}\n\n/** Ensure a deterministically unique action key within the connector. */\nfunction uniqueKey(base: string, seen: Set<string>): string {\n let candidate = base;\n if (seen.has(candidate)) {\n let i = 2;\n while (seen.has(`${base}_${i}`)) i++;\n candidate = `${base}_${i}`;\n }\n seen.add(candidate);\n return candidate;\n}\n\n/** Slugify a string into a snake_case machine name (`/^[a-z_][a-z0-9_]*$/`). */\nfunction slug(s: string): string {\n const out = s\n .normalize('NFKD')\n .replace(/[^a-zA-Z0-9]+/g, '_')\n .replace(/^_+|_+$/g, '')\n .toLowerCase();\n if (!out) return 'connector';\n return /^[a-z_]/.test(out) ? out : `op_${out}`;\n}\n\n/** Title-case a snake_case key for a default label (`get_pets` → `Get Pets`). */\nfunction titleize(name: string): string {\n return name\n .split('_')\n .filter(Boolean)\n .map((w) => w.charAt(0).toUpperCase() + w.slice(1))\n .join(' ');\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Connector } from '@objectstack/spec/integration';\nimport { createOpenApiConnector, type OpenApiConnectorConfig } from './openapi-connector.js';\n\n/**\n * Minimal surface of the automation engine this helper depends on — the\n * connector registry from ADR-0018 §Addendum. Kept structural so callers need\n * no runtime dependency on `@objectstack/service-automation` (mirrors\n * connector-rest / connector-mcp).\n */\nexport interface ConnectorRegistrySurface {\n registerConnector(\n def: Connector,\n handlers: Record<\n string,\n (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>\n >,\n ): void;\n unregisterConnector(name: string): void;\n}\n\n/**\n * Generate an OpenAPI-backed connector and register it on the engine's connector\n * registry so the baseline `connector_action` node can dispatch to the generated\n * actions (ADR-0023). Returns the registered connector name.\n */\nexport function registerOpenApiConnector(registry: ConnectorRegistrySurface, config: OpenApiConnectorConfig): string {\n const { def, handlers } = createOpenApiConnector(config);\n registry.registerConnector(def, handlers);\n return def.name;\n}\n"],"mappings":"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","names":["schema"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@objectstack/connector-openapi",
|
|
3
|
+
"version": "7.4.0",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"description": "OpenAPI 3.x connector generator for ObjectStack — turns a declarative OpenAPI document into connector actions on the automation engine's registry, with a self-contained static-auth HTTP transport (ADR-0023).",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@objectstack/core": "7.4.0",
|
|
17
|
+
"@objectstack/spec": "7.4.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^25.9.1",
|
|
21
|
+
"typescript": "^6.0.3",
|
|
22
|
+
"vitest": "^4.1.7",
|
|
23
|
+
"@objectstack/service-automation": "7.4.0"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"objectstack",
|
|
27
|
+
"connector",
|
|
28
|
+
"openapi",
|
|
29
|
+
"swagger",
|
|
30
|
+
"integration",
|
|
31
|
+
"api"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsup --config ../../../tsup.config.ts",
|
|
35
|
+
"test": "vitest run --passWithNoTests"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
4
|
+
import { registerOpenApiConnector, type ConnectorRegistrySurface } from './connector-openapi-plugin.js';
|
|
5
|
+
import type { OpenApiDocument } from './openapi-connector.js';
|
|
6
|
+
|
|
7
|
+
const doc: OpenApiDocument = {
|
|
8
|
+
info: { title: 'Mini' },
|
|
9
|
+
servers: [{ url: 'https://api.mini.example.com' }],
|
|
10
|
+
paths: {
|
|
11
|
+
'/ping': { get: { operationId: 'ping', responses: { '200': { description: 'ok' } } } },
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
describe('registerOpenApiConnector', () => {
|
|
16
|
+
it('registers the generated definition + handlers on the registry', () => {
|
|
17
|
+
const registerConnector = vi.fn();
|
|
18
|
+
const registry = { registerConnector, unregisterConnector: vi.fn() } as unknown as ConnectorRegistrySurface;
|
|
19
|
+
|
|
20
|
+
const name = registerOpenApiConnector(registry, { document: doc });
|
|
21
|
+
|
|
22
|
+
expect(name).toBe('mini');
|
|
23
|
+
expect(registerConnector).toHaveBeenCalledTimes(1);
|
|
24
|
+
const [def, handlers] = registerConnector.mock.calls[0];
|
|
25
|
+
expect(def.name).toBe('mini');
|
|
26
|
+
expect(typeof handlers.ping).toBe('function');
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
import type { Connector } from '@objectstack/spec/integration';
|
|
4
|
+
import { createOpenApiConnector, type OpenApiConnectorConfig } from './openapi-connector.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Minimal surface of the automation engine this helper depends on — the
|
|
8
|
+
* connector registry from ADR-0018 §Addendum. Kept structural so callers need
|
|
9
|
+
* no runtime dependency on `@objectstack/service-automation` (mirrors
|
|
10
|
+
* connector-rest / connector-mcp).
|
|
11
|
+
*/
|
|
12
|
+
export interface ConnectorRegistrySurface {
|
|
13
|
+
registerConnector(
|
|
14
|
+
def: Connector,
|
|
15
|
+
handlers: Record<
|
|
16
|
+
string,
|
|
17
|
+
(input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>
|
|
18
|
+
>,
|
|
19
|
+
): void;
|
|
20
|
+
unregisterConnector(name: string): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Generate an OpenAPI-backed connector and register it on the engine's connector
|
|
25
|
+
* registry so the baseline `connector_action` node can dispatch to the generated
|
|
26
|
+
* actions (ADR-0023). Returns the registered connector name.
|
|
27
|
+
*/
|
|
28
|
+
export function registerOpenApiConnector(registry: ConnectorRegistrySurface, config: OpenApiConnectorConfig): string {
|
|
29
|
+
const { def, handlers } = createOpenApiConnector(config);
|
|
30
|
+
registry.registerConnector(def, handlers);
|
|
31
|
+
return def.name;
|
|
32
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @objectstack/connector-openapi
|
|
5
|
+
*
|
|
6
|
+
* Generate an ObjectStack {@link Connector} from a declarative OpenAPI 3.x
|
|
7
|
+
* document (ADR-0023). One operation becomes one connector action; a single
|
|
8
|
+
* generic handler drives a self-contained static-auth HTTP transport (mirroring
|
|
9
|
+
* `@objectstack/connector-rest`). The generated connector is an ordinary
|
|
10
|
+
* `type: 'api'` connector — registered via `engine.registerConnector` with no
|
|
11
|
+
* new engine surface.
|
|
12
|
+
*
|
|
13
|
+
* Open-source scope: static auth only (`none` / `api-key` / `basic` / `bearer`),
|
|
14
|
+
* credentials supplied by the caller. Managed OAuth2, credential vaulting, and
|
|
15
|
+
* per-tenant lifecycle are the enterprise tier (ADR-0015 / 0022).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
createOpenApiConnector,
|
|
20
|
+
type OpenApiConnectorBundle,
|
|
21
|
+
type OpenApiConnectorConfig,
|
|
22
|
+
type OpenApiDocument,
|
|
23
|
+
type OpenApiPathItem,
|
|
24
|
+
type OpenApiOperation,
|
|
25
|
+
type OpenApiParameter,
|
|
26
|
+
type OpenApiRequestBody,
|
|
27
|
+
type OpenApiResponse,
|
|
28
|
+
type OpenApiSecurityScheme,
|
|
29
|
+
type OperationInfo,
|
|
30
|
+
type RestAuth,
|
|
31
|
+
type JsonSchema,
|
|
32
|
+
} from './openapi-connector.js';
|
|
33
|
+
export {
|
|
34
|
+
registerOpenApiConnector,
|
|
35
|
+
type ConnectorRegistrySurface,
|
|
36
|
+
} from './connector-openapi-plugin.js';
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect } from 'vitest';
|
|
4
|
+
import { createOpenApiConnector, type OpenApiDocument } from './openapi-connector.js';
|
|
5
|
+
|
|
6
|
+
const doc: OpenApiDocument = {
|
|
7
|
+
openapi: '3.0.0',
|
|
8
|
+
info: { title: 'Pet Store', description: 'A sample pet API' },
|
|
9
|
+
servers: [{ url: 'https://api.pets.example.com/v1' }],
|
|
10
|
+
components: {
|
|
11
|
+
securitySchemes: { apiKey: { type: 'apiKey', name: 'X-API-Key', in: 'header' } },
|
|
12
|
+
},
|
|
13
|
+
paths: {
|
|
14
|
+
'/pets/{petId}': {
|
|
15
|
+
get: {
|
|
16
|
+
operationId: 'getPetById',
|
|
17
|
+
summary: 'Get a pet by id',
|
|
18
|
+
tags: ['pets'],
|
|
19
|
+
parameters: [
|
|
20
|
+
{ name: 'petId', in: 'path', required: true, schema: { type: 'integer' } },
|
|
21
|
+
{ name: 'detail', in: 'query', schema: { type: 'string' } },
|
|
22
|
+
],
|
|
23
|
+
responses: {
|
|
24
|
+
'200': { content: { 'application/json': { schema: { type: 'object', properties: { id: { type: 'integer' } } } } } },
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
'/pets': {
|
|
29
|
+
post: {
|
|
30
|
+
operationId: 'createPet',
|
|
31
|
+
summary: 'Create a pet',
|
|
32
|
+
tags: ['pets'],
|
|
33
|
+
requestBody: {
|
|
34
|
+
required: true,
|
|
35
|
+
content: { 'application/json': { schema: { type: 'object', properties: { name: { type: 'string' } } } } },
|
|
36
|
+
},
|
|
37
|
+
responses: { '201': { description: 'created' } },
|
|
38
|
+
},
|
|
39
|
+
get: {
|
|
40
|
+
// no operationId — exercises the slug fallback
|
|
41
|
+
summary: 'List pets',
|
|
42
|
+
tags: ['admin'],
|
|
43
|
+
responses: { '200': { description: 'ok' } },
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** A fake fetch that records calls and returns a JSON response (mirrors connector-rest tests). */
|
|
50
|
+
function fakeFetch(payload: unknown) {
|
|
51
|
+
const calls: Array<{ url: string; init: { method?: string; body?: unknown; headers?: Record<string, string> } }> = [];
|
|
52
|
+
const fetchImpl = (async (url: string, init: { method?: string; body?: unknown; headers?: Record<string, string> }) => {
|
|
53
|
+
calls.push({ url, init });
|
|
54
|
+
return {
|
|
55
|
+
status: 200,
|
|
56
|
+
ok: true,
|
|
57
|
+
headers: { get: () => 'application/json' },
|
|
58
|
+
json: async () => payload,
|
|
59
|
+
text: async () => JSON.stringify(payload),
|
|
60
|
+
};
|
|
61
|
+
}) as unknown as typeof fetch;
|
|
62
|
+
return { calls, fetchImpl };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe('createOpenApiConnector', () => {
|
|
66
|
+
it('derives connector metadata from info + servers', () => {
|
|
67
|
+
const { def } = createOpenApiConnector({ document: doc });
|
|
68
|
+
expect(def.name).toBe('pet_store');
|
|
69
|
+
expect(def.label).toBe('Pet Store');
|
|
70
|
+
expect(def.description).toBe('A sample pet API');
|
|
71
|
+
expect(def.type).toBe('api');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('maps each operation to an action and falls back to a slug key', () => {
|
|
75
|
+
const { def } = createOpenApiConnector({ document: doc });
|
|
76
|
+
const keys = (def.actions ?? []).map((a) => a.key);
|
|
77
|
+
expect(keys).toContain('getPetById');
|
|
78
|
+
expect(keys).toContain('createPet');
|
|
79
|
+
expect(keys).toContain('get_pets'); // GET /pets has no operationId
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('assembles input schema sections from parameters and requestBody', () => {
|
|
83
|
+
const { def } = createOpenApiConnector({ document: doc });
|
|
84
|
+
const get = def.actions?.find((a) => a.key === 'getPetById');
|
|
85
|
+
expect(get?.inputSchema).toMatchObject({
|
|
86
|
+
type: 'object',
|
|
87
|
+
properties: {
|
|
88
|
+
path: { type: 'object', properties: { petId: { type: 'integer' } }, required: ['petId'] },
|
|
89
|
+
query: { type: 'object', properties: { detail: { type: 'string' } } },
|
|
90
|
+
},
|
|
91
|
+
required: ['path'],
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const post = def.actions?.find((a) => a.key === 'createPet');
|
|
95
|
+
expect(post?.inputSchema).toMatchObject({
|
|
96
|
+
properties: { body: { type: 'object', properties: { name: { type: 'string' } } } },
|
|
97
|
+
required: ['body'],
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('picks the success response schema as the output schema', () => {
|
|
102
|
+
const { def } = createOpenApiConnector({ document: doc });
|
|
103
|
+
const get = def.actions?.find((a) => a.key === 'getPetById');
|
|
104
|
+
expect(get?.outputSchema).toMatchObject({ type: 'object', properties: { id: { type: 'integer' } } });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('defaults authentication to none and reflects supplied credentials', () => {
|
|
108
|
+
expect(createOpenApiConnector({ document: doc }).def.authentication).toEqual({ type: 'none' });
|
|
109
|
+
const withAuth = createOpenApiConnector({ document: doc, auth: { type: 'bearer', token: 'secret' } });
|
|
110
|
+
expect(withAuth.def.authentication).toEqual({ type: 'bearer', token: 'secret' });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('honors an include allowlist', () => {
|
|
114
|
+
const { def } = createOpenApiConnector({ document: doc, include: (op) => (op.tags ?? []).includes('pets') });
|
|
115
|
+
expect((def.actions ?? []).map((a) => a.key)).toEqual(['getPetById', 'createPet']);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('handler interpolates path params and forwards query via the REST request', async () => {
|
|
119
|
+
const { calls, fetchImpl } = fakeFetch({ id: 42 });
|
|
120
|
+
const { handlers } = createOpenApiConnector({ document: doc, fetchImpl });
|
|
121
|
+
const result = await handlers.getPetById({ path: { petId: 42 }, query: { detail: 'full' } }, {});
|
|
122
|
+
|
|
123
|
+
expect(calls).toHaveLength(1);
|
|
124
|
+
expect(calls[0].url).toBe('https://api.pets.example.com/v1/pets/42?detail=full');
|
|
125
|
+
expect(calls[0].init.method).toBe('GET');
|
|
126
|
+
expect(result).toEqual({ status: 200, ok: true, body: { id: 42 } });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('handler sends a JSON body for write operations', async () => {
|
|
130
|
+
const { calls, fetchImpl } = fakeFetch({});
|
|
131
|
+
const { handlers } = createOpenApiConnector({ document: doc, fetchImpl });
|
|
132
|
+
await handlers.createPet({ body: { name: 'Rex' } }, {});
|
|
133
|
+
|
|
134
|
+
expect(calls[0].init.method).toBe('POST');
|
|
135
|
+
expect(calls[0].init.body).toBe(JSON.stringify({ name: 'Rex' }));
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('throws when no base URL can be determined', () => {
|
|
139
|
+
expect(() => createOpenApiConnector({ document: { info: { title: 'x' }, paths: {} } })).toThrow(/base URL/);
|
|
140
|
+
});
|
|
141
|
+
});
|