History

feat: API contract, server handler, and frontend with effect-atom

- packages/core/api: HttpApi contract with ToolsApi + SecretsApi groups SDK tagged errors pass through directly with status annotations (ToolNotFoundError→404, PolicyDeniedError→403, etc.) - apps/server: API handler implementation via HttpApiBuilder Exports createApiHandler() for reuse (no baked-in server runtime) Handlers are pure pass-through — no manual error catching - apps/web: Vite + React frontend served on same port as API Vite plugin forwards /v1/*, /docs, /openapi.json to Effect handler effect-atom integration: AtomHttpApi.Tag typed client, query/mutation atoms RegistryProvider + useAtomValue with Result pattern matching - Catalog pins @effect/platform, platform-bun, platform-node, cluster - All 9 packages typecheck clean

Rhys Sullivan committed 7 months ago

Commit 8babea81 parent 00219a926 files · +1022 −32Browse files

Showing the changes to apps/server/src (5 of 26 files). Show all

Unified · Split

5 files
  1. apps/server/src/handlers/secrets.ts +63 −0
  2. apps/server/src/handlers/tools.ts +37 −0
  3. apps/server/src/index.ts +2 −0
  4. apps/server/src/main.ts +40 −0
  5. apps/server/src/services/executor.ts +22 −0
apps/server/src/handlers/secrets.ts+63−0
@@ -0,0 +1,63 @@
1+import { HttpApiBuilder } from "@effect/platform";
2+import { Effect, Option } from "effect";
3+import type { SecretId, ScopeId } from "@executor/sdk";
4+
5+import { ExecutorApi } from "@executor/api";
6+import { ExecutorService } from "../services/executor";
7+
8+const refToResponse = (ref: {
9+ id: SecretId;
10+ scopeId: ScopeId;
11+ name: string;
12+ provider: Option.Option<string>;
13+ purpose?: string;
14+ createdAt: Date;
15+}) => ({
16+ id: ref.id,
17+ scopeId: ref.scopeId,
18+ name: ref.name,
19+ provider: Option.getOrUndefined(ref.provider),
20+ purpose: ref.purpose,
21+ createdAt: ref.createdAt.getTime(),
22+});
23+
24+export const SecretsHandlers = HttpApiBuilder.group(
25+ ExecutorApi,
26+ "secrets",
27+ (handlers) =>
28+ handlers
29+ .handle("list", ({ path }) =>
30+ Effect.gen(function* () {
31+ const executor = yield* ExecutorService;
32+ const refs = yield* executor.secrets.list();
33+ return refs.map(refToResponse);
34+ }),
35+ )
36+ .handle("status", ({ path }) =>
37+ Effect.gen(function* () {
38+ const executor = yield* ExecutorService;
39+ const status = yield* executor.secrets.status(path.secretId);
40+ return { secretId: path.secretId, status };
41+ }),
42+ )
43+ .handle("set", ({ path, payload }) =>
44+ Effect.gen(function* () {
45+ const executor = yield* ExecutorService;
46+ const ref = yield* executor.secrets.set({
47+ id: payload.id,
48+ name: payload.name,
49+ value: payload.value,
50+ purpose: payload.purpose,
51+ provider: payload.provider,
52+ });
53+ return refToResponse(ref);
54+ }),
55+ )
56+ .handle("remove", ({ path }) =>
57+ Effect.gen(function* () {
58+ const executor = yield* ExecutorService;
59+ const removed = yield* executor.secrets.remove(path.secretId);
60+ return { removed };
61+ }),
62+ ),
63+);
apps/server/src/handlers/tools.ts+37−0
@@ -0,0 +1,37 @@
1+import { HttpApiBuilder } from "@effect/platform";
2+import { Effect } from "effect";
3+
4+import { ExecutorApi } from "@executor/api";
5+import { ExecutorService } from "../services/executor";
6+
7+export const ToolsHandlers = HttpApiBuilder.group(
8+ ExecutorApi,
9+ "tools",
10+ (handlers) =>
11+ handlers
12+ .handle("list", ({ path }) =>
13+ Effect.gen(function* () {
14+ const executor = yield* ExecutorService;
15+ const tools = yield* executor.tools.list();
16+ return tools.map((t) => ({
17+ id: t.id,
18+ name: t.name,
19+ description: t.description,
20+ tags: [...t.tags],
21+ mayElicit: t.mayElicit,
22+ }));
23+ }),
24+ )
25+ .handle("schema", ({ path }) =>
26+ Effect.gen(function* () {
27+ const executor = yield* ExecutorService;
28+ return yield* executor.tools.schema(path.toolId);
29+ }),
30+ )
31+ .handle("invoke", ({ path, payload }) =>
32+ Effect.gen(function* () {
33+ const executor = yield* ExecutorService;
34+ return yield* executor.tools.invoke(path.toolId, payload.args);
35+ }),
36+ ),
37+);
apps/server/src/index.ts+2−0
@@ -0,0 +1,2 @@
1+export { createApiHandler, type ApiHandler } from "./main";
2+export { ExecutorServiceLive } from "./services/executor";
apps/server/src/main.ts+40−0
@@ -0,0 +1,40 @@
1+import {
2+ HttpApiBuilder,
3+ HttpApiSwagger,
4+ HttpMiddleware,
5+ HttpServer,
6+} from "@effect/platform";
7+import { Layer } from "effect";
8+
9+import { ExecutorApi } from "@executor/api";
10+import { ToolsHandlers } from "./handlers/tools";
11+import { SecretsHandlers } from "./handlers/secrets";
12+import { ExecutorServiceLive } from "./services/executor";
13+
14+// ---------------------------------------------------------------------------
15+// API layer — wire handlers
16+// ---------------------------------------------------------------------------
17+
18+const ApiLive = HttpApiBuilder.api(ExecutorApi).pipe(
19+ Layer.provide([ToolsHandlers, SecretsHandlers]),
20+ Layer.provide(ExecutorServiceLive),
21+);
22+
23+// ---------------------------------------------------------------------------
24+// Web handler — usable by Vite plugin, standalone server, tests, etc.
25+// ---------------------------------------------------------------------------
26+
27+export const createApiHandler = () =>
28+ HttpApiBuilder.toWebHandler(
29+ HttpApiSwagger.layer().pipe(
30+ Layer.provideMerge(HttpApiBuilder.middlewareOpenApi()),
31+ Layer.provideMerge(HttpApiBuilder.middlewareCors()),
32+ Layer.provideMerge(ApiLive),
33+ Layer.provideMerge(HttpServer.layerContext),
34+ ),
35+ { middleware: HttpMiddleware.logger },
36+ );
37+
38+export type ApiHandler = ReturnType<typeof createApiHandler>;
39+
40+export { ExecutorServiceLive } from "./services/executor";
apps/server/src/services/executor.ts+22−0
@@ -0,0 +1,22 @@
1+import { Context, Effect, Layer } from "effect";
2+import { createExecutor, makeTestConfig } from "@executor/sdk";
3+
4+import type { Executor } from "@executor/sdk";
5+
6+// ---------------------------------------------------------------------------
7+// Service tag — provides the executor instance to HTTP handlers
8+// ---------------------------------------------------------------------------
9+
10+export class ExecutorService extends Context.Tag("ExecutorService")<
11+ ExecutorService,
12+ Executor
13+>() {}
14+
15+// ---------------------------------------------------------------------------
16+// Default layer — creates an in-memory executor
17+// ---------------------------------------------------------------------------
18+
19+export const ExecutorServiceLive = Layer.effect(
20+ ExecutorService,
21+ createExecutor(makeTestConfig()),
22+);

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t