History

feat: plugin API routes + frontend component registration

OpenAPI plugin now has three sub-path exports: - @executor/plugin-openapi (SDK plugin, unchanged) - @executor/plugin-openapi/api (HTTP routes + handlers) - OpenApiGroup: previewSpec, addSpec, listSpecs, removeSpec endpoints - OpenApiHandlers: Effect HttpApiBuilder handlers - OpenApiExtensionService: Context tag for wiring - @executor/plugin-openapi/react (React components + atoms) - OpenApiClient: typed AtomHttpApi client (core + openapi routes) - Atoms: openApiSpecsAtom, previewOpenApiSpec, addOpenApiSpec, removeOpenApiSpec - openApiSourcePlugin: implements SourcePlugin contract - Lazy-loaded components: AddOpenApiSource (multi-step), EditOpenApiSource, OpenApiSourceSummary Shared infrastructure: - @executor/api: addGroup() composes plugin HttpApiGroups with CoreExecutorApi - @executor/react: SourcePlugin contract type for shell↔plugin UI composition Shell owns layout/nav/common actions, plugin owns content within typed frames Pattern: plugins use addGroup() + AtomHttpApi.Tag to get typed query/mutation atoms for both core and plugin-specific routes. Peer deps with optional metadata so SDK-only consumers don't pull in React. 10 packages typecheck clean, 21 OpenAPI tests passing.

Rhys Sullivan committed 7 months ago

Commit 1ddae2a1 parent f973e3218 files · +416 −6Browse files

Showing the changes to packages/plugins/openapi/src/api (3 of 18 files). Show all

Unified · Split

3 files
  1. packages/plugins/openapi/src/api/group.ts +90 −0
  2. packages/plugins/openapi/src/api/handlers.ts +63 −0
  3. packages/plugins/openapi/src/api/index.ts +2 −0
packages/plugins/openapi/src/api/group.ts+90−0
@@ -0,0 +1,90 @@
1+import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform";
2+import { Schema } from "effect";
3+import { ScopeId } from "@executor/sdk";
4+
5+import {
6+ OpenApiParseError,
7+ OpenApiExtractionError,
8+} from "../errors";
9+import { SpecPreview } from "../preview";
10+
11+// ---------------------------------------------------------------------------
12+// Params
13+// ---------------------------------------------------------------------------
14+
15+const scopeIdParam = HttpApiSchema.param("scopeId", ScopeId);
16+
17+// ---------------------------------------------------------------------------
18+// Payloads
19+// ---------------------------------------------------------------------------
20+
21+const AddSpecPayload = Schema.Struct({
22+ spec: Schema.String,
23+ baseUrl: Schema.optional(Schema.String),
24+ namespace: Schema.optional(Schema.String),
25+ headers: Schema.optional(
26+ Schema.Record({ key: Schema.String, value: Schema.Unknown }),
27+ ),
28+});
29+
30+const PreviewSpecPayload = Schema.Struct({
31+ spec: Schema.String,
32+});
33+
34+// ---------------------------------------------------------------------------
35+// Responses
36+// ---------------------------------------------------------------------------
37+
38+const AddSpecResponse = Schema.Struct({
39+ toolCount: Schema.Number,
40+ namespace: Schema.String,
41+});
42+
43+const ListSpecsResponse = Schema.Array(
44+ Schema.Struct({
45+ namespace: Schema.String,
46+ toolCount: Schema.Number,
47+ }),
48+);
49+
50+// ---------------------------------------------------------------------------
51+// Errors with HTTP status
52+// ---------------------------------------------------------------------------
53+
54+const ParseError = OpenApiParseError.annotations(
55+ HttpApiSchema.annotations({ status: 400 }),
56+);
57+const ExtractionError = OpenApiExtractionError.annotations(
58+ HttpApiSchema.annotations({ status: 400 }),
59+);
60+
61+// ---------------------------------------------------------------------------
62+// Group
63+// ---------------------------------------------------------------------------
64+
65+const namespaceParam = HttpApiSchema.param("namespace", Schema.String);
66+
67+export class OpenApiGroup extends HttpApiGroup.make("openapi")
68+ .add(
69+ HttpApiEndpoint.post("previewSpec")`/scopes/${scopeIdParam}/openapi/preview`
70+ .setPayload(PreviewSpecPayload)
71+ .addSuccess(SpecPreview)
72+ .addError(ParseError)
73+ .addError(ExtractionError),
74+ )
75+ .add(
76+ HttpApiEndpoint.post("addSpec")`/scopes/${scopeIdParam}/openapi/specs`
77+ .setPayload(AddSpecPayload)
78+ .addSuccess(AddSpecResponse)
79+ .addError(ParseError)
80+ .addError(ExtractionError),
81+ )
82+ .add(
83+ HttpApiEndpoint.get("listSpecs")`/scopes/${scopeIdParam}/openapi/specs`
84+ .addSuccess(ListSpecsResponse),
85+ )
86+ .add(
87+ HttpApiEndpoint.del("removeSpec")`/scopes/${scopeIdParam}/openapi/specs/${namespaceParam}`
88+ .addSuccess(Schema.Struct({ removed: Schema.Boolean })),
89+ )
90+ .prefix("/v1") {}
packages/plugins/openapi/src/api/handlers.ts+63−0
@@ -0,0 +1,63 @@
1+import { HttpApiBuilder } from "@effect/platform";
2+import { Context, Effect } from "effect";
3+
4+import { addGroup } from "@executor/api";
5+import type { OpenApiPluginExtension } from "../plugin";
6+import { OpenApiGroup } from "./group";
7+
8+// ---------------------------------------------------------------------------
9+// Service tag — the server provides the OpenAPI extension
10+// ---------------------------------------------------------------------------
11+
12+export class OpenApiExtensionService extends Context.Tag(
13+ "OpenApiExtensionService",
14+)<OpenApiExtensionService, OpenApiPluginExtension>() {}
15+
16+// ---------------------------------------------------------------------------
17+// Composed API — core + openapi group
18+// ---------------------------------------------------------------------------
19+
20+const ExecutorApiWithOpenApi = addGroup(OpenApiGroup);
21+
22+// ---------------------------------------------------------------------------
23+// Handlers
24+// ---------------------------------------------------------------------------
25+
26+export const OpenApiHandlers = HttpApiBuilder.group(
27+ ExecutorApiWithOpenApi,
28+ "openapi",
29+ (handlers) =>
30+ handlers
31+ .handle("previewSpec", ({ payload }) =>
32+ Effect.gen(function* () {
33+ const ext = yield* OpenApiExtensionService;
34+ return yield* ext.previewSpec(payload.spec);
35+ }).pipe(Effect.orDie),
36+ )
37+ .handle("addSpec", ({ payload }) =>
38+ Effect.gen(function* () {
39+ const ext = yield* OpenApiExtensionService;
40+ const result = yield* ext.addSpec({
41+ spec: payload.spec,
42+ baseUrl: payload.baseUrl,
43+ namespace: payload.namespace,
44+ headers: payload.headers as Record<string, string> | undefined,
45+ });
46+ return {
47+ toolCount: result.toolCount,
48+ namespace: payload.namespace ?? "api",
49+ };
50+ }).pipe(Effect.orDie),
51+ )
52+ .handle("listSpecs", () =>
53+ // TODO: wire to operation store listByNamespace
54+ Effect.succeed([] as { namespace: string; toolCount: number }[]),
55+ )
56+ .handle("removeSpec", ({ path }) =>
57+ Effect.gen(function* () {
58+ const ext = yield* OpenApiExtensionService;
59+ yield* ext.removeSpec(path.namespace);
60+ return { removed: true };
61+ }),
62+ ),
63+);
packages/plugins/openapi/src/api/index.ts+2−0
@@ -0,0 +1,2 @@
1+export { OpenApiGroup } from "./group";
2+export { OpenApiHandlers, OpenApiExtensionService } from "./handlers";