@objectstack/connector-openapi 15.0.0 → 15.1.1
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 +10 -10
- package/CHANGELOG.md +118 -0
- package/dist/index.d.mts +68 -6
- package/dist/index.d.ts +68 -6
- package/dist/index.js +121 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +118 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/connector-openapi-plugin.ts +90 -5
- package/src/index.ts +7 -0
- package/src/openapi-provider.test.ts +98 -0
- package/src/openapi-provider.ts +135 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
ConnectorProviderContext,
|
|
5
|
+
ConnectorProviderFactory,
|
|
6
|
+
ResolvedConnectorAuth,
|
|
7
|
+
} from '@objectstack/spec/integration';
|
|
8
|
+
import {
|
|
9
|
+
createOpenApiConnector,
|
|
10
|
+
type OpenApiDocument,
|
|
11
|
+
type RestAuth,
|
|
12
|
+
} from './openapi-connector.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The provider key this package contributes (ADR-0097). A declarative
|
|
16
|
+
* `connectors:` entry with `provider: 'openapi'` is materialized by this factory.
|
|
17
|
+
*/
|
|
18
|
+
export const OPENAPI_PROVIDER_KEY = 'openapi';
|
|
19
|
+
|
|
20
|
+
/** Injectable dependencies for {@link createOpenApiProviderFactory} (tests). */
|
|
21
|
+
export interface OpenApiProviderDeps {
|
|
22
|
+
/** Injected fetch implementation (spec fetch + request transport); defaults to global `fetch`. */
|
|
23
|
+
fetchImpl?: typeof fetch;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Shape of `providerConfig` for a `provider: 'openapi'` declarative instance. */
|
|
27
|
+
interface OpenApiProviderConfig {
|
|
28
|
+
/**
|
|
29
|
+
* The OpenAPI 3.x document: an inline object, an http(s) URL to fetch at
|
|
30
|
+
* boot, or a file path resolved relative to the declaring stack/package root
|
|
31
|
+
* (`'./billing-openapi.json'`, #3016).
|
|
32
|
+
*/
|
|
33
|
+
spec?: unknown;
|
|
34
|
+
/** Override the base URL (else the document's `servers[0].url`). */
|
|
35
|
+
baseUrl?: unknown;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolve `providerConfig.spec` into a parsed OpenAPI document (ADR-0097;
|
|
40
|
+
* union per #3016): an inline document object (the reliable, no-I/O-at-boot
|
|
41
|
+
* form used by the showcase), an http(s) URL fetched at materialization, or a
|
|
42
|
+
* **file path** read through the host's `ctx.loadPackageFile` — which resolves
|
|
43
|
+
* it relative to the declaring stack/package root and confines the read to
|
|
44
|
+
* that root (absolute / `..`-escaping paths are rejected there). Every failure
|
|
45
|
+
* throws, so the materializer's reconcile policy applies: fatal at boot, the
|
|
46
|
+
* entry is skipped on reload.
|
|
47
|
+
*/
|
|
48
|
+
async function loadOpenApiDocument(
|
|
49
|
+
spec: unknown,
|
|
50
|
+
fetchImpl: typeof fetch | undefined,
|
|
51
|
+
ctx: ConnectorProviderContext,
|
|
52
|
+
): Promise<OpenApiDocument> {
|
|
53
|
+
const connectorName = ctx.name;
|
|
54
|
+
if (spec && typeof spec === 'object' && !Array.isArray(spec)) {
|
|
55
|
+
return spec as OpenApiDocument;
|
|
56
|
+
}
|
|
57
|
+
if (typeof spec === 'string' && spec.length > 0) {
|
|
58
|
+
if (/^https?:\/\//i.test(spec)) {
|
|
59
|
+
const doFetch = fetchImpl ?? fetch;
|
|
60
|
+
const res = await doFetch(spec);
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`connector-openapi provider: connector '${connectorName}' failed to fetch spec '${spec}' (HTTP ${res.status}).`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
return (await res.json()) as OpenApiDocument;
|
|
67
|
+
}
|
|
68
|
+
// File path — dereferenced through the host capability so resolution stays
|
|
69
|
+
// anchored to (and confined within) the declaring stack/package root.
|
|
70
|
+
if (!ctx.loadPackageFile) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`connector-openapi provider: connector '${connectorName}' providerConfig.spec '${spec}' is a file path, ` +
|
|
73
|
+
`but this host provides no package file access — inline the OpenAPI document or use an http(s) URL.`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
let text: string;
|
|
77
|
+
try {
|
|
78
|
+
text = await ctx.loadPackageFile(spec);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`connector-openapi provider: connector '${connectorName}' failed to read providerConfig.spec '${spec}': ` +
|
|
82
|
+
`${(err as Error).message}`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const parsed: unknown = JSON.parse(text);
|
|
87
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
88
|
+
throw new Error('not a JSON object');
|
|
89
|
+
}
|
|
90
|
+
return parsed as OpenApiDocument;
|
|
91
|
+
} catch (err) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`connector-openapi provider: connector '${connectorName}' providerConfig.spec '${spec}' is not a parseable ` +
|
|
94
|
+
`OpenAPI JSON document: ${(err as Error).message}`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
throw new Error(
|
|
99
|
+
`connector-openapi provider: connector '${connectorName}' requires providerConfig.spec — an inline OpenAPI 3.x ` +
|
|
100
|
+
`document object, an http(s) URL, or a package-relative file path.`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Build the `openapi` {@link ConnectorProviderFactory} (ADR-0097 / ADR-0023). At
|
|
106
|
+
* boot the automation service invokes it for each `provider: 'openapi'`
|
|
107
|
+
* declarative instance: it loads the OpenAPI document from `providerConfig.spec`,
|
|
108
|
+
* then produces the same `{ def, handlers }` bundle {@link createOpenApiConnector}
|
|
109
|
+
* generates for a hand-wired OpenAPI connector — one action per operation over a
|
|
110
|
+
* static-auth HTTP transport, with the resolved `auth` applied.
|
|
111
|
+
*
|
|
112
|
+
* Hard-fails on invalid config (missing/unfetchable spec, no base URL), so a
|
|
113
|
+
* misconfigured instance fails boot loudly.
|
|
114
|
+
*/
|
|
115
|
+
export function createOpenApiProviderFactory(deps: OpenApiProviderDeps = {}): ConnectorProviderFactory {
|
|
116
|
+
return async (ctx) => {
|
|
117
|
+
const cfg = (ctx.providerConfig ?? {}) as OpenApiProviderConfig;
|
|
118
|
+
if (cfg.baseUrl !== undefined && typeof cfg.baseUrl !== 'string') {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`connector-openapi provider: connector '${ctx.name}' providerConfig.baseUrl must be a string when set.`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
const document = await loadOpenApiDocument(cfg.spec, deps.fetchImpl, ctx);
|
|
124
|
+
const auth = ctx.auth as ResolvedConnectorAuth | undefined as RestAuth | undefined;
|
|
125
|
+
return createOpenApiConnector({
|
|
126
|
+
name: ctx.name,
|
|
127
|
+
label: ctx.label,
|
|
128
|
+
description: ctx.description,
|
|
129
|
+
document,
|
|
130
|
+
baseUrl: typeof cfg.baseUrl === 'string' ? cfg.baseUrl : undefined,
|
|
131
|
+
auth,
|
|
132
|
+
fetchImpl: deps.fetchImpl,
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
}
|