History

refactor(openapi,graphql): store invocation config on source, not per-tool (#222)

* refactor(openapi,graphql): store invocation config on source, not per-tool Per-tool binding rows were duplicating the full InvocationConfig (baseUrl/endpoint + headers) on every entry. A source with N operations stored N copies of the same config in KV, and every updateSource call read + rewrote every binding row. Move invocationConfig to the source row (one per namespace) alongside the user-facing SourceConfig. Per-tool rows now store only { namespace, binding }. The invoker resolves config via getSource on the hot path; updateSource is a single putSource (O(1) writes). * feat(openapi,graphql): self-heal legacy source rows on read Pre-refactor source rows don't carry invocationConfig. Mark the field decode-optional on StoredSourceSchema, and in the KV stores rehydrate missing invocationConfig on read: - GraphQL: synthesize from source.config.endpoint + headers (lossless, endpoint is always user-provided). - OpenAPI: scan for a legacy binding row whose inlined config carries the resolved baseUrl. Fall back to source.config.baseUrl. getSource writes the healed row back; listSources rehydrates in memory only to avoid N writes per list. All migration paths are tagged with TODO(migration) for a later cleanup pass.

Rhys Sullivan committed 7 months ago

Commit b28b4641 parent 3efffbb10 files · +146 −67Browse files

Unified · Split

10 files
  1. packages/plugins/graphql/src/sdk/invoke.ts +11 −1
  2. packages/plugins/graphql/src/sdk/kv-operation-store.ts +30 −7
  3. packages/plugins/graphql/src/sdk/operation-store.ts +3 −2
  4. packages/plugins/graphql/src/sdk/plugin.ts +7 −21
  5. packages/plugins/graphql/src/sdk/stored-source.ts +5 −1
  6. packages/plugins/openapi/src/sdk/invoke.ts +11 −1
  7. packages/plugins/openapi/src/sdk/kv-operation-store.ts +64 −7
  8. packages/plugins/openapi/src/sdk/operation-store.ts +3 −2
  9. packages/plugins/openapi/src/sdk/plugin.ts +7 −24
  10. packages/plugins/openapi/src/sdk/stored-source.ts +5 −1
packages/plugins/graphql/src/sdk/invoke.ts+11−1
@@ -170,7 +170,17 @@ export const makeGraphqlInvoker = (opts: { · 169 unmodified lines
170 });170 });
171 }171 }
172 172
173 const { binding, config } = entry;173 const source = yield* opts.operationStore.getSource(entry.namespace);
174 if (!source) {
175 return yield* new ToolInvocationError({
176 toolId,
177 message: `No source found for namespace "${entry.namespace}"`,
178 cause: undefined,
179 });
180 }
181
182 const { binding } = entry;
183 const { invocationConfig: config } = source;
174 184
175 // Resolve secret-backed headers185 // Resolve secret-backed headers
176 const resolvedHeaders = yield* resolveHeaders(config.headers, opts.secrets, opts.scopeId);186 const resolvedHeaders = yield* resolveHeaders(config.headers, opts.secrets, opts.scopeId);
packages/plugins/graphql/src/sdk/kv-operation-store.ts+30−7
@@ -6,7 +6,7 @@ import { Effect, Schema } from "effect"; · 5 unmodified lines
6import { scopeKv, makeInMemoryScopedKv, type Kv, type ToolId, type ScopedKv } from "@executor/sdk";6import { scopeKv, makeInMemoryScopedKv, type Kv, type ToolId, type ScopedKv } from "@executor/sdk";
7 7
8import type { GraphqlOperationStore, StoredSource } from "./operation-store";8import type { GraphqlOperationStore, StoredSource } from "./operation-store";
9import { OperationBinding, InvocationConfig } from "./types";9import { InvocationConfig, OperationBinding } from "./types";
10import { StoredSourceSchema } from "./stored-source";10import { StoredSourceSchema } from "./stored-source";
11 11
12// ---------------------------------------------------------------------------12// ---------------------------------------------------------------------------
@@ -16,7 +16,6 @@ import { StoredSourceSchema } from "./stored-source"; · 3 unmodified lines
16class StoredEntry extends Schema.Class<StoredEntry>("StoredEntry")({16class StoredEntry extends Schema.Class<StoredEntry>("StoredEntry")({
17 namespace: Schema.String,17 namespace: Schema.String,
18 binding: OperationBinding,18 binding: OperationBinding,
19 config: InvocationConfig,
20}) {}19}) {}
21 20
22const encodeEntry = Schema.encodeSync(Schema.parseJson(StoredEntry));21const encodeEntry = Schema.encodeSync(Schema.parseJson(StoredEntry));
@@ -29,18 +28,36 @@ const decodeSource = Schema.decodeUnknownSync(Schema.parseJson(StoredSourceSchem · 6 unmodified lines
29// Implementation28// Implementation
30// ---------------------------------------------------------------------------29// ---------------------------------------------------------------------------
31 30
31// TODO(migration): remove DecodedSource + rehydrate once all source rows
32// have been migrated to carry invocationConfig. For GraphQL the endpoint
33// is always user-provided in SourceConfig, so rehydration is lossless.
34type DecodedSource = Omit<StoredSource, "invocationConfig"> & {
35 invocationConfig?: InvocationConfig;
36};
37
38const rehydrate = (source: DecodedSource): StoredSource =>
39 source.invocationConfig
40 ? (source as StoredSource)
41 : {
42 ...source,
43 invocationConfig: new InvocationConfig({
44 endpoint: source.config.endpoint,
45 headers: source.config.headers ?? {},
46 }),
47 };
48
32const makeStore = (bindings: ScopedKv, sources: ScopedKv): GraphqlOperationStore => ({49const makeStore = (bindings: ScopedKv, sources: ScopedKv): GraphqlOperationStore => ({
33 get: (toolId) =>50 get: (toolId) =>
34 Effect.gen(function* () {51 Effect.gen(function* () {
35 const raw = yield* bindings.get(toolId);52 const raw = yield* bindings.get(toolId);
36 if (!raw) return null;53 if (!raw) return null;
37 const entry = decodeEntry(raw);54 const entry = decodeEntry(raw);
38 return { binding: entry.binding, config: entry.config };55 return { binding: entry.binding, namespace: entry.namespace };
39 }),56 }),
40 57
41 put: (toolId, namespace, binding, config) =>58 put: (toolId, namespace, binding) =>
42 bindings.set([59 bindings.set([
43 { key: toolId, value: encodeEntry(new StoredEntry({ namespace, binding, config })) },60 { key: toolId, value: encodeEntry(new StoredEntry({ namespace, binding })) },
44 ]),61 ]),
45 62
46 remove: (toolId) => bindings.delete([toolId]).pipe(Effect.asVoid),63 remove: (toolId) => bindings.delete([toolId]).pipe(Effect.asVoid),
@@ -75,14 +92,20 @@ const makeStore = (bindings: ScopedKv, sources: ScopedKv): GraphqlOperationStore · 28 unmodified lines
75 listSources: () =>92 listSources: () =>
76 Effect.gen(function* () {93 Effect.gen(function* () {
77 const entries = yield* sources.list();94 const entries = yield* sources.list();
78 return entries.map((e) => decodeSource(e.value) as StoredSource);95 // TODO(migration): rehydrate in memory only — avoid N writes per list.
96 return entries.map((e) => rehydrate(decodeSource(e.value) as DecodedSource));
79 }),97 }),
80 98
81 getSource: (namespace) =>99 getSource: (namespace) =>
82 Effect.gen(function* () {100 Effect.gen(function* () {
83 const raw = yield* sources.get(namespace);101 const raw = yield* sources.get(namespace);
84 if (!raw) return null;102 if (!raw) return null;
85 return decodeSource(raw) as StoredSource;103 const source = decodeSource(raw) as DecodedSource;
104 if (source.invocationConfig) return source as StoredSource;
105 // TODO(migration): self-heal — rehydrate and write back once.
106 const healed = rehydrate(source);
107 yield* sources.set([{ key: namespace, value: encodeSource(healed) }]);
108 return healed;
86 }),109 }),
87 110
88 getSourceConfig: (namespace) =>111 getSourceConfig: (namespace) =>
packages/plugins/graphql/src/sdk/operation-store.ts+3−2
@@ -18,18 +18,19 @@ export interface StoredSource { · 17 unmodified lines
18 readonly namespace: string;18 readonly namespace: string;
19 readonly name: string;19 readonly name: string;
20 readonly config: SourceConfig;20 readonly config: SourceConfig;
21 /** Pre-resolved runtime invocation config (endpoint, headers). */
22 readonly invocationConfig: InvocationConfig;
21}23}
22 24
23export interface GraphqlOperationStore {25export interface GraphqlOperationStore {
24 readonly get: (26 readonly get: (
25 toolId: ToolId,27 toolId: ToolId,
26 ) => Effect.Effect<{ binding: OperationBinding; config: InvocationConfig } | null>;28 ) => Effect.Effect<{ binding: OperationBinding; namespace: string } | null>;
27 29
28 readonly put: (30 readonly put: (
29 toolId: ToolId,31 toolId: ToolId,
30 namespace: string,32 namespace: string,
31 binding: OperationBinding,33 binding: OperationBinding,
32 config: InvocationConfig,
33 ) => Effect.Effect<void>;34 ) => Effect.Effect<void>;
34 35
35 readonly remove: (toolId: ToolId) => Effect.Effect<void>;36 readonly remove: (toolId: ToolId) => Effect.Effect<void>;
packages/plugins/graphql/src/sdk/plugin.ts+7−21
@@ -370,7 +370,7 @@ export const graphqlPlugin = (options?: { · 369 unmodified lines
370 variableNames: extractedField.arguments.map((a) => a.name),370 variableNames: extractedField.arguments.map((a) => a.name),
371 });371 });
372 372
373 return operationStore.put(reg.id, namespace, binding, invocationConfig);373 return operationStore.put(reg.id, namespace, binding);
374 },374 },
375 { discard: true },375 { discard: true },
376 );376 );
@@ -386,6 +386,7 @@ export const graphqlPlugin = (options?: { · 9 unmodified lines
386 namespace: config.namespace,386 namespace: config.namespace,
387 headers: config.headers,387 headers: config.headers,
388 },388 },
389 invocationConfig,
389 });390 });
390 391
391 return { sourceId: namespace, toolCount: registrations.length };392 return { sourceId: namespace, toolCount: registrations.length };
@@ -438,11 +439,11 @@ export const graphqlPlugin = (options?: { · 46 unmodified lines
438 439
439 updateSource: (namespace: string, input: GraphqlUpdateSourceInput) =>440 updateSource: (namespace: string, input: GraphqlUpdateSourceInput) =>
440 Effect.gen(function* () {441 Effect.gen(function* () {
441 const existingConfig = yield* operationStore.getSourceConfig(namespace);442 const existing = yield* operationStore.getSource(namespace);
442 if (!existingConfig) return;443 if (!existing) return;
443 444
444 const updatedConfig = {445 const updatedConfig = {
445 ...existingConfig,446 ...existing.config,
446 ...(input.endpoint !== undefined ? { endpoint: input.endpoint } : {}),447 ...(input.endpoint !== undefined ? { endpoint: input.endpoint } : {}),
447 ...(input.headers !== undefined448 ...(input.headers !== undefined
448 ? { headers: input.headers as Record<string, HeaderValueValue> }449 ? { headers: input.headers as Record<string, HeaderValueValue> }
@@ -454,26 +455,11 @@ export const graphqlPlugin = (options?: { · 5 unmodified lines
454 headers: (updatedConfig.headers ?? {}) as Record<string, HeaderValueValue>,455 headers: (updatedConfig.headers ?? {}) as Record<string, HeaderValueValue>,
455 });456 });
456 457
457 const toolIds = yield* operationStore.listByNamespace(namespace);
458 for (const toolId of toolIds) {
459 const entry = yield* operationStore.get(toolId);
460 if (entry) {
461 yield* operationStore.put(
462 toolId,
463 namespace,
464 entry.binding,
465 newInvocationConfig,
466 );
467 }
468 }
469
470 const sources = yield* operationStore.listSources();
471 const existingMeta = sources.find((s) => s.namespace === namespace);
472
473 yield* operationStore.putSource({458 yield* operationStore.putSource({
474 namespace,459 namespace,
475 name: existingMeta?.name ?? namespace,460 name: existing.name,
476 config: updatedConfig,461 config: updatedConfig,
462 invocationConfig: newInvocationConfig,
477 });463 });
478 }),464 }),
479 },465 },
packages/plugins/graphql/src/sdk/stored-source.ts+5−1
@@ -1,6 +1,6 @@
1import { Schema } from "effect";1import { Schema } from "effect";
2 2
3import { HeaderValue } from "./types";3import { HeaderValue, InvocationConfig } from "./types";
4 4
5// ---------------------------------------------------------------------------5// ---------------------------------------------------------------------------
6// Stored source — the shape persisted by the operation store and exposed6// Stored source — the shape persisted by the operation store and exposed
@@ -16,6 +16,10 @@ export class StoredSourceSchema extends Schema.Class<StoredSourceSchema>("Graphq · 9 unmodified lines
16 namespace: Schema.optional(Schema.String),16 namespace: Schema.optional(Schema.String),
17 headers: Schema.optional(Schema.Record({ key: Schema.String, value: HeaderValue })),17 headers: Schema.optional(Schema.Record({ key: Schema.String, value: HeaderValue })),
18 }),18 }),
19 // TODO(migration): make required once all rows have been migrated to
20 // carry invocationConfig. Left optional for decode compat with rows
21 // written before the source-level invocationConfig refactor.
22 invocationConfig: Schema.optional(InvocationConfig),
19}) {}23}) {}
20 24
21export type StoredSourceSchemaType = typeof StoredSourceSchema.Type;25export type StoredSourceSchemaType = typeof StoredSourceSchema.Type;
packages/plugins/openapi/src/sdk/invoke.ts+11−1
@@ -292,7 +292,17 @@ export const makeOpenApiInvoker = (opts: { · 291 unmodified lines
292 });292 });
293 }293 }
294 294
295 const { binding, config } = entry;295 const source = yield* opts.operationStore.getSource(entry.namespace);
296 if (!source) {
297 return yield* new ToolInvocationError({
298 toolId,
299 message: `No source found for namespace "${entry.namespace}"`,
300 cause: undefined,
301 });
302 }
303
304 const { binding } = entry;
305 const { invocationConfig: config } = source;
296 const baseUrl = config.baseUrl;306 const baseUrl = config.baseUrl;
297 307
298 // Resolve secret-backed headers308 // Resolve secret-backed headers
packages/plugins/openapi/src/sdk/kv-operation-store.ts+64−7
@@ -8,7 +8,7 @@ import { Effect, Schema } from "effect"; · 7 unmodified lines
8import { scopeKv, makeInMemoryScopedKv, type Kv, type ToolId, type ScopedKv } from "@executor/sdk";8import { scopeKv, makeInMemoryScopedKv, type Kv, type ToolId, type ScopedKv } from "@executor/sdk";
9 9
10import type { OpenApiOperationStore, StoredOperation, StoredSource } from "./operation-store";10import type { OpenApiOperationStore, StoredOperation, StoredSource } from "./operation-store";
11import { OperationBinding, InvocationConfig } from "./types";11import { InvocationConfig, OperationBinding } from "./types";
12import { StoredSourceSchema } from "./stored-source";12import { StoredSourceSchema } from "./stored-source";
13 13
14// ---------------------------------------------------------------------------14// ---------------------------------------------------------------------------
@@ -18,12 +18,23 @@ import { StoredSourceSchema } from "./stored-source"; · 3 unmodified lines
18class StoredEntry extends Schema.Class<StoredEntry>("StoredEntry")({18class StoredEntry extends Schema.Class<StoredEntry>("StoredEntry")({
19 namespace: Schema.String,19 namespace: Schema.String,
20 binding: OperationBinding,20 binding: OperationBinding,
21 config: InvocationConfig,
22}) {}21}) {}
23 22
24const encodeEntry = Schema.encodeSync(Schema.parseJson(StoredEntry));23const encodeEntry = Schema.encodeSync(Schema.parseJson(StoredEntry));
25const decodeEntry = Schema.decodeUnknownSync(Schema.parseJson(StoredEntry));24const decodeEntry = Schema.decodeUnknownSync(Schema.parseJson(StoredEntry));
26 25
26// TODO(migration): remove LegacyStoredEntry + rehydrateInvocationConfig
27// once all source rows have been migrated off the pre-refactor schema.
28// Old binding rows inlined the resolved InvocationConfig, which is the
29// only place the server-derived baseUrl was persisted.
30class LegacyStoredEntry extends Schema.Class<LegacyStoredEntry>("LegacyStoredEntry")({
31 namespace: Schema.String,
32 binding: OperationBinding,
33 config: Schema.optional(InvocationConfig),
34}) {}
35
36const decodeLegacyEntry = Schema.decodeUnknownSync(Schema.parseJson(LegacyStoredEntry));
37
27const encodeSource = Schema.encodeSync(Schema.parseJson(StoredSourceSchema));38const encodeSource = Schema.encodeSync(Schema.parseJson(StoredSourceSchema));
28const decodeSource = Schema.decodeUnknownSync(Schema.parseJson(StoredSourceSchema));39const decodeSource = Schema.decodeUnknownSync(Schema.parseJson(StoredSourceSchema));
29 40
@@ -37,22 +48,53 @@ const makeStore = (bindings: ScopedKv, sources: ScopedKv): OpenApiOperationStore · 7 unmodified lines
37 effect: Effect.Effect<A, E, never>,48 effect: Effect.Effect<A, E, never>,
38 ): Effect.Effect<A, E, never> => kv.withTransaction?.(effect) ?? effect;49 ): Effect.Effect<A, E, never> => kv.withTransaction?.(effect) ?? effect;
39 50
51 // TODO(migration): remove along with LegacyStoredEntry.
52 // Rebuild invocationConfig for a source row that predates the refactor.
53 // We try to recover the resolved baseUrl from any legacy binding row
54 // (old rows inlined the full InvocationConfig); if none is found, fall
55 // back to the user-provided baseUrl from the source config.
56 type DecodedSource = Omit<StoredSource, "invocationConfig"> & {
57 invocationConfig?: InvocationConfig;
58 };
59 const rehydrateInvocationConfig = (
60 source: DecodedSource,
61 ): Effect.Effect<StoredSource> =>
62 Effect.gen(function* () {
63 if (source.invocationConfig) return source as StoredSource;
64 let recovered: InvocationConfig | null = null;
65 const entries = yield* bindings.list();
66 for (const e of entries) {
67 const legacy = decodeLegacyEntry(e.value);
68 if (legacy.namespace === source.namespace && legacy.config) {
69 recovered = legacy.config;
70 break;
71 }
72 }
73 const invocationConfig =
74 recovered ??
75 new InvocationConfig({
76 baseUrl: source.config.baseUrl ?? "",
77 headers: source.config.headers ?? {},
78 });
79 return { ...source, invocationConfig };
80 });
81
40 return {82 return {
41 get: (toolId) =>83 get: (toolId) =>
42 Effect.gen(function* () {84 Effect.gen(function* () {
43 const raw = yield* bindings.get(toolId);85 const raw = yield* bindings.get(toolId);
44 if (!raw) return null;86 if (!raw) return null;
45 const entry = decodeEntry(raw);87 const entry = decodeEntry(raw);
46 return { binding: entry.binding, config: entry.config };88 return { binding: entry.binding, namespace: entry.namespace };
47 }),89 }),
48 90
49 put: (entries: readonly StoredOperation[]) =>91 put: (entries: readonly StoredOperation[]) =>
50 withKvTransaction(92 withKvTransaction(
51 bindings,93 bindings,
52 bindings.set(94 bindings.set(
53 entries.map(({ toolId, namespace, binding, config }) => ({95 entries.map(({ toolId, namespace, binding }) => ({
54 key: toolId,96 key: toolId,
55 value: encodeEntry(new StoredEntry({ namespace, binding, config })),97 value: encodeEntry(new StoredEntry({ namespace, binding })),
56 })),98 })),
57 ),99 ),
58 ),100 ),
@@ -89,14 +131,29 @@ const makeStore = (bindings: ScopedKv, sources: ScopedKv): OpenApiOperationStore · 30 unmodified lines
89 listSources: () =>131 listSources: () =>
90 Effect.gen(function* () {132 Effect.gen(function* () {
91 const entries = yield* sources.list();133 const entries = yield* sources.list();
92 return entries.map((e) => decodeSource(e.value) as StoredSource);134 const out: StoredSource[] = [];
135 for (const e of entries) {
136 const raw = decodeSource(e.value) as DecodedSource;
137 // TODO(migration): rehydrate in memory only — avoid N writes per list.
138 out.push(
139 raw.invocationConfig
140 ? (raw as StoredSource)
141 : yield* rehydrateInvocationConfig(raw),
142 );
143 }
144 return out;
93 }),145 }),
94 146
95 getSource: (namespace) =>147 getSource: (namespace) =>
96 Effect.gen(function* () {148 Effect.gen(function* () {
97 const raw = yield* sources.get(namespace);149 const raw = yield* sources.get(namespace);
98 if (!raw) return null;150 if (!raw) return null;
99 return decodeSource(raw) as StoredSource;151 const source = decodeSource(raw) as DecodedSource;
152 if (source.invocationConfig) return source as StoredSource;
153 // TODO(migration): self-heal — rehydrate and write back once.
154 const healed = yield* rehydrateInvocationConfig(source);
155 yield* sources.set([{ key: namespace, value: encodeSource(healed) }]);
156 return healed;
100 }),157 }),
101 158
102 getSourceConfig: (namespace) =>159 getSourceConfig: (namespace) =>
packages/plugins/openapi/src/sdk/operation-store.ts+3−2
@@ -18,19 +18,20 @@ export interface StoredSource { · 17 unmodified lines
18 readonly namespace: string;18 readonly namespace: string;
19 readonly name: string;19 readonly name: string;
20 readonly config: SourceConfig;20 readonly config: SourceConfig;
21 /** Pre-resolved runtime invocation config (baseUrl resolved from servers, headers). */
22 readonly invocationConfig: InvocationConfig;
21}23}
22 24
23export interface StoredOperation {25export interface StoredOperation {
24 readonly toolId: ToolId;26 readonly toolId: ToolId;
25 readonly namespace: string;27 readonly namespace: string;
26 readonly binding: OperationBinding;28 readonly binding: OperationBinding;
27 readonly config: InvocationConfig;
28}29}
29 30
30export interface OpenApiOperationStore {31export interface OpenApiOperationStore {
31 readonly get: (32 readonly get: (
32 toolId: ToolId,33 toolId: ToolId,
33 ) => Effect.Effect<{ binding: OperationBinding; config: InvocationConfig } | null>;34 ) => Effect.Effect<{ binding: OperationBinding; namespace: string } | null>;
34 35
35 readonly put: (entries: readonly StoredOperation[]) => Effect.Effect<void>;36 readonly put: (entries: readonly StoredOperation[]) => Effect.Effect<void>;
36 37
packages/plugins/openapi/src/sdk/plugin.ts+7−24
@@ -275,7 +275,6 @@ export const openApiPlugin = (options?: { · 274 unmodified lines
275 toolId: ToolId.make(`${namespace}.${def.toolPath}`),275 toolId: ToolId.make(`${namespace}.${def.toolPath}`),
276 namespace,276 namespace,
277 binding: toBinding(def),277 binding: toBinding(def),
278 config: invocationConfig,
279 })),278 })),
280 );279 );
281 280
@@ -291,6 +290,7 @@ export const openApiPlugin = (options?: { · 9 unmodified lines
291 namespace: config.namespace,290 namespace: config.namespace,
292 headers: config.headers,291 headers: config.headers,
293 },292 },
293 invocationConfig,
294 });294 });
295 295
296 return { sourceId: namespace, toolCount: registrations.length };296 return { sourceId: namespace, toolCount: registrations.length };
@@ -345,11 +345,11 @@ export const openApiPlugin = (options?: { · 48 unmodified lines
345 345
346 updateSource: (namespace: string, input: OpenApiUpdateSourceInput) =>346 updateSource: (namespace: string, input: OpenApiUpdateSourceInput) =>
347 Effect.gen(function* () {347 Effect.gen(function* () {
348 const existingSource = yield* operationStore.getSourceConfig(namespace);348 const existing = yield* operationStore.getSource(namespace);
349 if (!existingSource) return;349 if (!existing) return;
350 350
351 const updatedConfig = {351 const updatedConfig = {
352 ...existingSource,352 ...existing.config,
353 ...(input.baseUrl !== undefined ? { baseUrl: input.baseUrl } : {}),353 ...(input.baseUrl !== undefined ? { baseUrl: input.baseUrl } : {}),
354 ...(input.headers !== undefined354 ...(input.headers !== undefined
355 ? { headers: input.headers as Record<string, HeaderValueValue> }355 ? { headers: input.headers as Record<string, HeaderValueValue> }
@@ -357,32 +357,15 @@ export const openApiPlugin = (options?: { · 1 unmodified line
357 };357 };
358 358
359 const newInvocationConfig = new InvocationConfig({359 const newInvocationConfig = new InvocationConfig({
360 baseUrl: updatedConfig.baseUrl ?? resolveBaseUrl([]),360 baseUrl: updatedConfig.baseUrl ?? existing.invocationConfig.baseUrl,
361 headers: (updatedConfig.headers ?? {}) as Record<string, HeaderValueValue>,361 headers: (updatedConfig.headers ?? {}) as Record<string, HeaderValueValue>,
362 });362 });
363 363
364 const toolIds = yield* operationStore.listByNamespace(namespace);
365 for (const toolId of toolIds) {
366 const entry = yield* operationStore.get(toolId);
367 if (entry) {
368 yield* operationStore.put([
369 {
370 toolId,
371 namespace,
372 binding: entry.binding,
373 config: newInvocationConfig,
374 },
375 ]);
376 }
377 }
378
379 const sources = yield* operationStore.listSources();
380 const existingMeta = sources.find((s) => s.namespace === namespace);
381
382 yield* operationStore.putSource({364 yield* operationStore.putSource({
383 namespace,365 namespace,
384 name: existingMeta?.name ?? namespace,366 name: existing.name,
385 config: updatedConfig,367 config: updatedConfig,
368 invocationConfig: newInvocationConfig,
386 });369 });
387 }),370 }),
388 },371 },
packages/plugins/openapi/src/sdk/stored-source.ts+5−1
@@ -1,6 +1,6 @@
1import { Schema } from "effect";1import { Schema } from "effect";
2 2
3import { HeaderValue } from "./types";3import { HeaderValue, InvocationConfig } from "./types";
4 4
5// ---------------------------------------------------------------------------5// ---------------------------------------------------------------------------
6// Stored source — the shape persisted by the operation store and exposed6// Stored source — the shape persisted by the operation store and exposed
@@ -16,6 +16,10 @@ export class StoredSourceSchema extends Schema.Class<StoredSourceSchema>("OpenAp · 9 unmodified lines
16 namespace: Schema.optional(Schema.String),16 namespace: Schema.optional(Schema.String),
17 headers: Schema.optional(Schema.Record({ key: Schema.String, value: HeaderValue })),17 headers: Schema.optional(Schema.Record({ key: Schema.String, value: HeaderValue })),
18 }),18 }),
19 // TODO(migration): make required once all rows have been migrated to
20 // carry invocationConfig. Left optional for decode compat with rows
21 // written before the source-level invocationConfig refactor.
22 invocationConfig: Schema.optional(InvocationConfig),
19}) {}23}) {}
20 24
21export type StoredSourceSchemaType = typeof StoredSourceSchema.Type;25export type StoredSourceSchemaType = typeof StoredSourceSchema.Type;