Skip to content

Commit 1bba8cb

Browse files
committed
Move table collection into createExecutor
1 parent 2892fe2 commit 1bba8cb

6 files changed

Lines changed: 86 additions & 51 deletions

File tree

examples/all-plugins/src/main.ts

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,7 @@
1919

2020
import { Cause, Effect } from "effect";
2121

22-
import {
23-
SecretId,
24-
Scope,
25-
ScopeId,
26-
SetSecretInput,
27-
collectTables,
28-
createExecutor,
29-
} from "@executor-js/sdk";
22+
import { SecretId, Scope, ScopeId, SetSecretInput, createExecutor } from "@executor-js/sdk";
3023
import { createSqliteTestFumaDb } from "@executor-js/sdk/testing";
3124

3225
import { fileSecretsPlugin } from "@executor-js/plugin-file-secrets";
@@ -166,16 +159,15 @@ const program = Effect.gen(function* () {
166159
console.log("Building executor with every ported plugin");
167160
console.log("=".repeat(72));
168161

169-
const sqlite = yield* Effect.promise(() =>
170-
createSqliteTestFumaDb({
171-
tables: collectTables(plugins),
172-
namespace: "executor_example_all_plugins",
173-
}),
174-
);
175-
176162
const executor = yield* createExecutor({
177163
scopes: [scope],
178-
db: sqlite.db,
164+
db: ({ tables }) =>
165+
Effect.promise(() =>
166+
createSqliteTestFumaDb({
167+
tables,
168+
namespace: "executor_example_all_plugins",
169+
}),
170+
),
179171
plugins,
180172
onElicitation: "accept-all" as const,
181173
});
@@ -437,7 +429,6 @@ const program = Effect.gen(function* () {
437429
console.log("-".repeat(72));
438430

439431
yield* executor.close();
440-
yield* Effect.promise(() => sqlite.close());
441432
console.log("Executor closed. Done.");
442433
});
443434

examples/promise-sdk/src/main.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
* — no Effect knowledge needed. In-memory stores, runs anywhere.
44
*/
55
import { createExecutor, SecretId, SetSecretInput } from "@executor-js/sdk/promise";
6-
import { collectTables } from "@executor-js/sdk/core";
76
import { createSqliteTestFumaDb } from "@executor-js/sdk/testing";
87
import { mcpPlugin } from "@executor-js/plugin-mcp/promise";
98
import { openApiPlugin } from "@executor-js/plugin-openapi/promise";
@@ -14,13 +13,13 @@ import { graphqlPlugin } from "@executor-js/plugin-graphql/promise";
1413
// ---------------------------------------------------------------------------
1514

1615
const plugins = [mcpPlugin(), openApiPlugin(), graphqlPlugin()] as const;
17-
const db = await createSqliteTestFumaDb({
18-
tables: collectTables(plugins),
19-
namespace: "executor_promise_example",
20-
});
2116

2217
const executor = await createExecutor({
23-
db: db.db,
18+
db: ({ tables }) =>
19+
createSqliteTestFumaDb({
20+
tables,
21+
namespace: "executor_promise_example",
22+
}),
2423
scopes: [{ id: "my-app", name: "my-app" }],
2524
plugins,
2625
onElicitation: "accept-all",
@@ -120,4 +119,3 @@ const resolved = await executor.secrets.get("api-key");
120119
console.log("Secret:", resolved);
121120

122121
await executor.close();
123-
await db.close();

packages/core/sdk/src/executor.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,17 @@ export type Executor<TPlugins extends readonly AnyPlugin[] = readonly []> = {
332332
readonly close: () => Effect.Effect<void, StorageFailure>;
333333
} & PluginExtensions<TPlugins>;
334334

335+
export interface ExecutorDb {
336+
readonly db: FumaDb;
337+
readonly close?: () => Effect.Effect<void, StorageFailure> | Promise<void> | void;
338+
}
339+
340+
export type ExecutorDbInput = FumaDb | ExecutorDb;
341+
342+
export type ExecutorDbFactory = (config: {
343+
readonly tables: FumaTables;
344+
}) => ExecutorDbInput | Effect.Effect<ExecutorDbInput, StorageFailure>;
345+
335346
export interface ExecutorConfig<TPlugins extends readonly AnyPlugin[] = readonly []> {
336347
/**
337348
* Precedence-ordered scope stack. Innermost first; typical shape is
@@ -341,7 +352,7 @@ export interface ExecutorConfig<TPlugins extends readonly AnyPlugin[] = readonly
341352
* Must be non-empty.
342353
*/
343354
readonly scopes: readonly Scope[];
344-
readonly db: FumaDb;
355+
readonly db: ExecutorDbInput | ExecutorDbFactory;
345356
readonly plugins?: TPlugins;
346357
/**
347358
* How to respond when a tool requests user input mid-invocation. Pass
@@ -751,7 +762,14 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea
751762
const empty: readonly AnyPlugin[] = [];
752763
return empty as TPlugins;
753764
};
754-
const { scopes, db: rootDbUntyped, plugins = defaultPlugins() } = config;
765+
const { scopes, plugins = defaultPlugins() } = config;
766+
const dbInput = yield* Effect.suspend(() => {
767+
if (typeof config.db !== "function") return Effect.succeed(config.db);
768+
const out = config.db({ tables: collectTables(plugins) });
769+
return Effect.isEffect(out) ? out : Effect.succeed(out);
770+
});
771+
const rootDbUntyped = "db" in dbInput ? dbInput.db : dbInput;
772+
const closeDb = "db" in dbInput ? dbInput.close : undefined;
755773

756774
if (scopes.length === 0) {
757775
return yield* new StorageError({
@@ -3483,6 +3501,21 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea
34833501
yield* runtime.plugin.close();
34843502
}
34853503
}
3504+
if (closeDb) {
3505+
const out = closeDb();
3506+
if (Effect.isEffect(out)) {
3507+
yield* out;
3508+
} else if (out instanceof Promise) {
3509+
yield* Effect.tryPromise({
3510+
try: () => out,
3511+
catch: (cause) =>
3512+
new StorageError({
3513+
message: "Executor database close failed",
3514+
cause,
3515+
}),
3516+
});
3517+
}
3518+
}
34863519
});
34873520

34883521
// Public Executor surface — storage-backed methods surface

packages/core/sdk/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,9 @@ export {
305305
export {
306306
type Executor,
307307
type ExecutorConfig,
308+
type ExecutorDb,
309+
type ExecutorDbFactory,
310+
type ExecutorDbInput,
308311
type OnElicitation,
309312
type InvokeOptions,
310313
createExecutor,

packages/core/sdk/src/promise-executor.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@
1515
import { Brand, Effect } from "effect";
1616

1717
import {
18+
collectTables,
1819
createExecutor as createEffectExecutor,
1920
type Executor as EffectExecutor,
2021
type OnElicitation,
2122
} from "./executor";
22-
import type { FumaDb } from "./fuma-runtime";
23+
import type { FumaDb, FumaTables } from "./fuma-runtime";
2324
import { ScopeId } from "./ids";
2425
import type { AnyPlugin } from "./plugin";
2526
import { Scope } from "./scope";
@@ -73,12 +74,21 @@ export interface ExecutorConfig<TPlugins extends readonly AnyPlugin[] = readonly
7374
* `{ id, name }` partials to build a multi-scope executor.
7475
*/
7576
readonly scopes?: readonly { readonly id?: string; readonly name?: string }[];
77+
readonly plugins?: TPlugins;
7678
/**
77-
* FumaDB ORM handle for the configured Executor schema. Hosts choose
78-
* their database engine and FumaDB adapter, then pass the query handle here.
79+
* FumaDB ORM handle, or a factory that receives the full Executor table
80+
* map after plugins have been applied. Public consumers usually want the
81+
* factory form so `collectTables(plugins)` stays inside `createExecutor`.
7982
*/
80-
readonly db: FumaDb;
81-
readonly plugins?: TPlugins;
83+
readonly db:
84+
| FumaDb
85+
| { readonly db: FumaDb; readonly close?: () => Promise<void> | void }
86+
| ((config: {
87+
readonly tables: FumaTables;
88+
}) =>
89+
| FumaDb
90+
| { readonly db: FumaDb; readonly close?: () => Promise<void> | void }
91+
| Promise<FumaDb | { readonly db: FumaDb; readonly close?: () => Promise<void> | void }>);
8292
/**
8393
* How to respond when a tool requests user input mid-invocation. Pass
8494
* `"accept-all"` for tests / non-interactive hosts, or a handler
@@ -141,6 +151,10 @@ export const createExecutor = async <const TPlugins extends readonly AnyPlugin[]
141151
config: ExecutorConfig<TPlugins>,
142152
): Promise<Executor<TPlugins>> => {
143153
const plugins = (config?.plugins ?? []) as TPlugins;
154+
const db =
155+
typeof config.db === "function"
156+
? await config.db({ tables: collectTables(plugins) })
157+
: config.db;
144158

145159
const scopes =
146160
config.scopes && config.scopes.length > 0
@@ -161,7 +175,7 @@ export const createExecutor = async <const TPlugins extends readonly AnyPlugin[]
161175

162176
const effectConfig = {
163177
scopes,
164-
db: config.db,
178+
db,
165179
plugins,
166180
onElicitation: config.onElicitation,
167181
};

packages/core/sdk/src/promise.test.ts

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { describe, expect, it } from "@effect/vitest";
22

33
import { createExecutor } from "./promise";
4-
import { collectTables } from "./executor";
54
import { definePlugin, tool } from "./plugin";
65
import { createSqliteTestFumaDb } from "./sqlite-test-db";
76
import { Effect, Schema } from "effect";
@@ -38,12 +37,12 @@ const echoPlugin = definePlugin(() => ({
3837
describe("promise/createExecutor", () => {
3938
it("returns Promise-shaped executor and invokes static tools", async () => {
4039
const plugins = [echoPlugin()] as const;
41-
const db = await createSqliteTestFumaDb({
42-
tables: collectTables(plugins),
43-
namespace: "executor_promise_test",
44-
});
4540
const executor = await createExecutor({
46-
db: db.db,
41+
db: ({ tables }) =>
42+
createSqliteTestFumaDb({
43+
tables,
44+
namespace: "executor_promise_test",
45+
}),
4746
plugins,
4847
onElicitation: "accept-all",
4948
});
@@ -55,17 +54,16 @@ describe("promise/createExecutor", () => {
5554
expect(out).toBe("hi");
5655

5756
await executor.close();
58-
await db.close();
5957
});
6058

6159
it("promisifies plugin extension methods", async () => {
6260
const plugins = [echoPlugin()] as const;
63-
const db = await createSqliteTestFumaDb({
64-
tables: collectTables(plugins),
65-
namespace: "executor_promise_test",
66-
});
6761
const executor = await createExecutor({
68-
db: db.db,
62+
db: ({ tables }) =>
63+
createSqliteTestFumaDb({
64+
tables,
65+
namespace: "executor_promise_test",
66+
}),
6967
plugins,
7068
onElicitation: "accept-all",
7169
});
@@ -74,7 +72,6 @@ describe("promise/createExecutor", () => {
7472
expect(greeting).toBe("hello, world");
7573

7674
await executor.close();
77-
await db.close();
7875
});
7976

8077
it("per-invoke onElicitation override wins over the executor-level default", async () => {
@@ -107,12 +104,12 @@ describe("promise/createExecutor", () => {
107104
}));
108105

109106
const plugins = [approvedPlugin()] as const;
110-
const db = await createSqliteTestFumaDb({
111-
tables: collectTables(plugins),
112-
namespace: "executor_promise_test",
113-
});
114107
const executor = await createExecutor({
115-
db: db.db,
108+
db: ({ tables }) =>
109+
createSqliteTestFumaDb({
110+
tables,
111+
namespace: "executor_promise_test",
112+
}),
116113
plugins,
117114
onElicitation: "accept-all", // default → auto-approve
118115
});
@@ -137,6 +134,5 @@ describe("promise/createExecutor", () => {
137134
});
138135

139136
await executor.close();
140-
await db.close();
141137
});
142138
});

0 commit comments

Comments
 (0)