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 @@
1import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform";
2import { Schema } from "effect";
3import { ScopeId } from "@executor/sdk";
4
5import {
6 OpenApiParseError,
7 OpenApiExtractionError,
8} from "../errors";
9import { SpecPreview } from "../preview";
10
11// ---------------------------------------------------------------------------
12// Params
13// ---------------------------------------------------------------------------
14
15const scopeIdParam = HttpApiSchema.param("scopeId", ScopeId);
16
17// ---------------------------------------------------------------------------
18// Payloads
19// ---------------------------------------------------------------------------
20
21const 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
30const PreviewSpecPayload = Schema.Struct({
31 spec: Schema.String,
32});
33
34// ---------------------------------------------------------------------------
35// Responses
36// ---------------------------------------------------------------------------
37
38const AddSpecResponse = Schema.Struct({
39 toolCount: Schema.Number,
40 namespace: Schema.String,
41});
42
43const 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
54const ParseError = OpenApiParseError.annotations(
55 HttpApiSchema.annotations({ status: 400 }),
56);
57const ExtractionError = OpenApiExtractionError.annotations(
58 HttpApiSchema.annotations({ status: 400 }),
59);
60
61// ---------------------------------------------------------------------------
62// Group
63// ---------------------------------------------------------------------------
64
65const namespaceParam = HttpApiSchema.param("namespace", Schema.String);
66
67export 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 @@
1import { HttpApiBuilder } from "@effect/platform";
2import { Context, Effect } from "effect";
3
4import { addGroup } from "@executor/api";
5import type { OpenApiPluginExtension } from "../plugin";
6import { OpenApiGroup } from "./group";
7
8// ---------------------------------------------------------------------------
9// Service tag — the server provides the OpenAPI extension
10// ---------------------------------------------------------------------------
11
12export class OpenApiExtensionService extends Context.Tag(
13 "OpenApiExtensionService",
14)<OpenApiExtensionService, OpenApiPluginExtension>() {}
15
16// ---------------------------------------------------------------------------
17// Composed API — core + openapi group
18// ---------------------------------------------------------------------------
19
20const ExecutorApiWithOpenApi = addGroup(OpenApiGroup);
21
22// ---------------------------------------------------------------------------
23// Handlers
24// ---------------------------------------------------------------------------
25
26export 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 @@
1export { OpenApiGroup } from "./group";
2export { OpenApiHandlers, OpenApiExtensionService } from "./handlers";