Skip to content

Commit e1d00fd

Browse files
committed
docs(spec) + showcase: demo the openapi file-path spec (#3016)
- spec: providerConfig describe/TSDoc now documents the spec union (inline object | package-relative file path | http(s) URL) so the auto-generated references pick it up - showcase: StatusOpenApiConnector — a provider: 'openapi' declarative instance whose OpenAPI document is referenced as a package-relative file path (src/system/connectors/status-openapi.json), materialized at boot by an option-less ConnectorOpenApiPlugin; getHealth dispatches GET /api/v1/health against the running server itself - coverage notes + stale connectors: comment updated (the collection has held provider-bound instances since ADR-0096, not only descriptors) Verified: booted the showcase (--fresh, random port); GET /api/v1/automation/connectors lists showcase_status_openapi (origin: declarative) with the getHealth action. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016eY7byWABTUPtJG7R2AEFU
1 parent 9ef203b commit e1d00fd

7 files changed

Lines changed: 86 additions & 14 deletions

File tree

examples/app-showcase/objectstack.config.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

33
import { defineStack } from '@objectstack/spec';
4+
import { ConnectorOpenApiPlugin } from '@objectstack/connector-openapi';
45
import { ConnectorRestPlugin } from '@objectstack/connector-rest';
56
import { ConnectorSlackPlugin } from '@objectstack/connector-slack';
67
import {
@@ -102,12 +103,17 @@ export default defineStack({
102103

103104
// Concrete connectors for the `connector_action` node. The baseline engine
104105
// ships the dispatch node + an empty registry; these plugins populate it.
105-
// • rest → points at the running server itself, so the REST connector
106-
// flow's call + response are observable on the flow run with no
107-
// external dependency. Override the target with SHOWCASE_SELF_URL.
108-
// • slack → registered so TaskCompletedSlackFlow resolves its connector;
109-
// live posting needs a real bot token (set SLACK_BOT_TOKEN).
106+
// • rest → points at the running server itself, so the REST connector
107+
// flow's call + response are observable on the flow run with no
108+
// external dependency. Override the target with SHOWCASE_SELF_URL.
109+
// • slack → registered so TaskCompletedSlackFlow resolves its connector;
110+
// live posting needs a real bot token (set SLACK_BOT_TOKEN).
111+
// • openapi → option-less: contributes only the `openapi` provider factory
112+
// (ADR-0096), which materializes the StatusOpenApiConnector
113+
// declarative instance below — its OpenAPI document is a
114+
// package-relative FILE PATH read at boot (#3016).
110115
plugins: [
116+
new ConnectorOpenApiPlugin(),
111117
new ConnectorRestPlugin({
112118
name: 'rest',
113119
baseUrl: process.env.SHOWCASE_SELF_URL ?? 'http://127.0.0.1:3000',
@@ -191,8 +197,11 @@ export default defineStack({
191197
// Declarative REST endpoints (object_operation + flow) — the metadata
192198
// counterpart of the code-mounted recalc endpoint (see src/system/apis/).
193199
apis: allApis,
194-
// Declarative connector CATALOG DESCRIPTORS (#2612) — metadata-only, never
195-
// runtime-dispatchable; the live connectors are the plugins above. See
200+
// Declarative `connectors:` — both kinds (ADR-0096): provider-bound
201+
// INSTANCES (StatusApiConnector via `rest`; StatusOpenApiConnector via
202+
// `openapi` with a package-relative file-path spec, #3016) materialized into
203+
// live, dispatchable connectors at boot, plus a CATALOG DESCRIPTOR
204+
// (ErpCatalogConnector, #2612) that stays metadata-only. See
196205
// src/system/connectors/ for the full contract.
197206
connectors: allConnectors,
198207
hooks: allHooks,

examples/app-showcase/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
},
2424
"dependencies": {
2525
"@objectstack/cloud-connection": "workspace:*",
26+
"@objectstack/connector-openapi": "workspace:*",
2627
"@objectstack/connector-rest": "workspace:*",
2728
"@objectstack/connector-slack": "workspace:*",
2829
"@objectstack/driver-sql": "workspace:*",

examples/app-showcase/src/coverage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ export const STACK_COLLECTION_COVERAGE: Record<string, KindCoverage> = {
179179
status: 'demonstrated',
180180
files: ['src/system/connectors/index.ts', 'src/automation/flows/index.ts'],
181181
notes:
182-
'Both connector kinds are demonstrated. (1) Provider-bound INSTANCE (ADR-0096 / #2977): StatusApiConnector declares `provider: rest` and is materialized into a live, dispatchable connector at boot by ConnectorRestPlugin\'s provider factory — ShowcaseDeclarativeConnectorPingFlow calls it via connector_action and it appears in GET /connectors. (2) Catalog DESCRIPTOR (#2612): ErpCatalogConnector has no provider, so it stays inert metadata; enabled:false marks the deliberate catalog entry and silences the boot audit. Plugin-registered connectors (ConnectorRestPlugin/ConnectorSlackPlugin in objectstack.config.ts) are also exercised by the connector flows.',
182+
'Both connector kinds are demonstrated. (1) Provider-bound INSTANCES (ADR-0096 / #2977): StatusApiConnector declares `provider: rest` (inline config) and StatusOpenApiConnector declares `provider: openapi` with its OpenAPI document referenced as a package-relative FILE PATH (#3016, read at boot and confined to the package root) — both are materialized into live, dispatchable connectors at boot; ShowcaseDeclarativeConnectorPingFlow calls the rest instance via connector_action and both appear in GET /connectors. (2) Catalog DESCRIPTOR (#2612): ErpCatalogConnector has no provider, so it stays inert metadata; enabled:false marks the deliberate catalog entry and silences the boot audit. Plugin-registered connectors (ConnectorRestPlugin/ConnectorSlackPlugin in objectstack.config.ts) are also exercised by the connector flows.',
183183
},
184184
};
185185

examples/app-showcase/src/system/connectors/index.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,39 @@ export const StatusApiConnector = defineConnector({
5858
},
5959
auth: { type: 'none' },
6060
});
61+
/**
62+
* ADR-0096 provider-bound instance, **file-path spec** form (#3016): the OpenAPI
63+
* document lives next to this file (`status-openapi.json`) and is referenced by
64+
* a path resolved relative to THIS package's root at materialization — reads
65+
* are confined to the package root (absolute / `..`-escaping paths are
66+
* rejected), and a missing or unparseable document fails boot loudly. The
67+
* `openapi` provider factory (ConnectorOpenApiPlugin in objectstack.config.ts)
68+
* turns the document's one operation (`getHealth` → `GET /api/v1/health`) into
69+
* a dispatchable action against the running server itself, so the materialized
70+
* connector is observable with no external dependency. Complements
71+
* {@link StatusApiConnector}, which demos the same materialization from the
72+
* `rest` provider's inline config.
73+
*/
74+
export const StatusOpenApiConnector = defineConnector({
75+
name: 'showcase_status_openapi',
76+
label: 'Status API (Declarative OpenAPI Instance, File-Path Spec)',
77+
type: 'api',
78+
description:
79+
'Provider-bound declarative connector instance (ADR-0096) whose OpenAPI document is referenced as a ' +
80+
'package-relative file path (#3016) and read at boot, confined to the package root. Materialized into a live ' +
81+
'`openapi` connector — getHealth dispatches GET /api/v1/health against the running server.',
82+
provider: 'openapi',
83+
providerConfig: {
84+
// Package-relative file ref (#3016) — resolved against the directory that
85+
// holds objectstack.config.ts (the CLI passes it as the automation
86+
// service's packageRoot). Inline documents and http(s) URLs stay valid.
87+
spec: './src/system/connectors/status-openapi.json',
88+
// Same self-pointing literal rationale as StatusApiConnector above.
89+
baseUrl: 'http://127.0.0.1:3000',
90+
},
91+
auth: { type: 'none' },
92+
});
93+
6194
export const ErpCatalogConnector = defineConnector({
6295
name: 'showcase_erp_catalog',
6396
label: 'ERP Integration (Catalog Descriptor)',
@@ -110,4 +143,4 @@ export const ErpCatalogConnector = defineConnector({
110143
enabled: false,
111144
});
112145

113-
export const allConnectors: Connector[] = [StatusApiConnector, ErpCatalogConnector];
146+
export const allConnectors: Connector[] = [StatusApiConnector, StatusOpenApiConnector, ErpCatalogConnector];
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"openapi": "3.0.3",
3+
"info": {
4+
"title": "Showcase Status API",
5+
"version": "1.0.0",
6+
"description": "Minimal OpenAPI document for the showcase's own health probe. Referenced by the StatusOpenApiConnector declarative instance (src/system/connectors/index.ts) as a package-relative file path — the #3016 / ADR-0096 spec form resolved and confined to this package's root at boot."
7+
},
8+
"servers": [{ "url": "http://127.0.0.1:3000" }],
9+
"paths": {
10+
"/api/v1/health": {
11+
"get": {
12+
"operationId": "getHealth",
13+
"summary": "Server health probe",
14+
"description": "Returns the running server's health status.",
15+
"responses": {
16+
"200": { "description": "Server is healthy" }
17+
}
18+
}
19+
}
20+
}
21+
}

packages/spec/src/integration/connector.zod.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -605,13 +605,18 @@ export const ConnectorSchema = lazySchema(() => z.object({
605605
/**
606606
* ADR-0096 — provider-specific configuration, **validated by the provider
607607
* factory** (not by this schema): the OpenAPI provider expects `{ spec,
608-
* baseUrl? }`, the MCP provider a `{ transport }`, the REST provider a
609-
* `{ baseUrl }`. Deliberately untyped here — re-modelling each provider's
610-
* inputs in the stack schema (an OpenAPI document, an MCP transport) is exactly
611-
* what ADR-0023 rejected. Ignored unless `provider` is set.
608+
* baseUrl? }` — where `spec` is an inline OpenAPI document object, a file
609+
* path resolved relative to the declaring stack/package root (reads are
610+
* confined to that root; #3016), or an http(s) URL — the MCP provider a
611+
* `{ transport }`, the REST provider a `{ baseUrl }`. Deliberately untyped
612+
* here — re-modelling each provider's inputs in the stack schema (an OpenAPI
613+
* document, an MCP transport) is exactly what ADR-0023 rejected. Ignored
614+
* unless `provider` is set.
612615
*/
613616
providerConfig: z.record(z.string(), z.unknown()).optional().describe(
614-
'Provider-specific config validated by the provider factory at boot (e.g. { spec, baseUrl } for openapi). Requires `provider`.',
617+
'Provider-specific config validated by the provider factory at boot (e.g. { spec, baseUrl } for openapi, ' +
618+
"where spec is an inline document, a package-relative file path like './billing-openapi.json', or an http(s) URL). " +
619+
'Requires `provider`.',
615620
),
616621

617622
/**

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)