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 @@
1import { HttpApiBuilder } from "@effect/platform";
2import { Effect, Option } from "effect";
3import type { SecretId, ScopeId } from "@executor/sdk";
4
5import { ExecutorApi } from "@executor/api";
6import { ExecutorService } from "../services/executor";
7
8const 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
24export 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 @@
1import { HttpApiBuilder } from "@effect/platform";
2import { Effect } from "effect";
3
4import { ExecutorApi } from "@executor/api";
5import { ExecutorService } from "../services/executor";
6
7export 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 @@
1export { createApiHandler, type ApiHandler } from "./main";
2export { ExecutorServiceLive } from "./services/executor";
apps/server/src/main.ts+40−0
@@ -0,0 +1,40 @@
1import {
2 HttpApiBuilder,
3 HttpApiSwagger,
4 HttpMiddleware,
5 HttpServer,
6} from "@effect/platform";
7import { Layer } from "effect";
8
9import { ExecutorApi } from "@executor/api";
10import { ToolsHandlers } from "./handlers/tools";
11import { SecretsHandlers } from "./handlers/secrets";
12import { ExecutorServiceLive } from "./services/executor";
13
14// ---------------------------------------------------------------------------
15// API layer — wire handlers
16// ---------------------------------------------------------------------------
17
18const 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
27export 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
38export type ApiHandler = ReturnType<typeof createApiHandler>;
39
40export { ExecutorServiceLive } from "./services/executor";
apps/server/src/services/executor.ts+22−0
@@ -0,0 +1,22 @@
1import { Context, Effect, Layer } from "effect";
2import { createExecutor, makeTestConfig } from "@executor/sdk";
3
4import type { Executor } from "@executor/sdk";
5
6// ---------------------------------------------------------------------------
7// Service tag — provides the executor instance to HTTP handlers
8// ---------------------------------------------------------------------------
9
10export class ExecutorService extends Context.Tag("ExecutorService")<
11 ExecutorService,
12 Executor
13>() {}
14
15// ---------------------------------------------------------------------------
16// Default layer — creates an in-memory executor
17// ---------------------------------------------------------------------------
18
19export 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