* 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.
Unified · Split
| @@ -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 | 185 | // Resolve secret-backed headers |
| 176 | 186 | const resolvedHeaders = yield* resolveHeaders(config.headers, opts.secrets, opts.scopeId); |
| @@ -6,7 +6,7 @@ import { Effect, Schema } from "effect"; · 5 unmodified lines | ||
| 6 | 6 | import { scopeKv, makeInMemoryScopedKv, type Kv, type ToolId, type ScopedKv } from "@executor/sdk"; |
| 7 | 7 | |
| 8 | 8 | import type { GraphqlOperationStore, StoredSource } from "./operation-store"; |
| 9 | −import { OperationBinding, InvocationConfig } from "./types"; | |
| 9 | +import { InvocationConfig, OperationBinding } from "./types"; | |
| 10 | 10 | import { StoredSourceSchema } from "./stored-source"; |
| 11 | 11 | |
| 12 | 12 | // --------------------------------------------------------------------------- |
| @@ -16,7 +16,6 @@ import { StoredSourceSchema } from "./stored-source"; · 3 unmodified lines | ||
| 16 | 16 | class StoredEntry extends Schema.Class<StoredEntry>("StoredEntry")({ |
| 17 | 17 | namespace: Schema.String, |
| 18 | 18 | binding: OperationBinding, |
| 19 | − config: InvocationConfig, | |
| 20 | 19 | }) {} |
| 21 | 20 | |
| 22 | 21 | const encodeEntry = Schema.encodeSync(Schema.parseJson(StoredEntry)); |
| @@ -29,18 +28,36 @@ const decodeSource = Schema.decodeUnknownSync(Schema.parseJson(StoredSourceSchem · 6 unmodified lines | ||
| 29 | 28 | // 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. | |
| 34 | +type DecodedSource = Omit<StoredSource, "invocationConfig"> & { | |
| 35 | + invocationConfig?: InvocationConfig; | |
| 36 | +}; | |
| 37 | + | |
| 38 | +const 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 | + | |
| 32 | 49 | const makeStore = (bindings: ScopedKv, sources: ScopedKv): GraphqlOperationStore => ({ |
| 33 | 50 | get: (toolId) => |
| 34 | 51 | Effect.gen(function* () { |
| 35 | 52 | const raw = yield* bindings.get(toolId); |
| 36 | 53 | if (!raw) return null; |
| 37 | 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 | 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 | 63 | remove: (toolId) => bindings.delete([toolId]).pipe(Effect.asVoid), |
| @@ -75,14 +92,20 @@ const makeStore = (bindings: ScopedKv, sources: ScopedKv): GraphqlOperationStore · 28 unmodified lines | ||
| 75 | 92 | listSources: () => |
| 76 | 93 | Effect.gen(function* () { |
| 77 | 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 | 99 | getSource: (namespace) => |
| 82 | 100 | Effect.gen(function* () { |
| 83 | 101 | const raw = yield* sources.get(namespace); |
| 84 | 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 | 111 | getSourceConfig: (namespace) => |
| @@ -18,18 +18,19 @@ export interface StoredSource { · 17 unmodified lines | ||
| 18 | 18 | readonly namespace: string; |
| 19 | 19 | readonly name: string; |
| 20 | 20 | readonly config: SourceConfig; |
| 21 | + /** Pre-resolved runtime invocation config (endpoint, headers). */ | |
| 22 | + readonly invocationConfig: InvocationConfig; | |
| 21 | 23 | } |
| 22 | 24 | |
| 23 | 25 | export interface GraphqlOperationStore { |
| 24 | 26 | readonly get: ( |
| 25 | 27 | toolId: ToolId, |
| 26 | − ) => Effect.Effect<{ binding: OperationBinding; config: InvocationConfig } | null>; | |
| 28 | + ) => Effect.Effect<{ binding: OperationBinding; namespace: string } | null>; | |
| 27 | 29 | |
| 28 | 30 | readonly put: ( |
| 29 | 31 | toolId: ToolId, |
| 30 | 32 | namespace: string, |
| 31 | 33 | binding: OperationBinding, |
| 32 | − config: InvocationConfig, | |
| 33 | 34 | ) => Effect.Effect<void>; |
| 34 | 35 | |
| 35 | 36 | readonly remove: (toolId: ToolId) => Effect.Effect<void>; |
| @@ -370,7 +370,7 @@ export const graphqlPlugin = (options?: { · 369 unmodified lines | ||
| 370 | 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 | 375 | { discard: true }, |
| 376 | 376 | ); |
| @@ -386,6 +386,7 @@ export const graphqlPlugin = (options?: { · 9 unmodified lines | ||
| 386 | 386 | namespace: config.namespace, |
| 387 | 387 | headers: config.headers, |
| 388 | 388 | }, |
| 389 | + invocationConfig, | |
| 389 | 390 | }); |
| 390 | 391 | |
| 391 | 392 | return { sourceId: namespace, toolCount: registrations.length }; |
| @@ -438,11 +439,11 @@ export const graphqlPlugin = (options?: { · 46 unmodified lines | ||
| 438 | 439 | |
| 439 | 440 | updateSource: (namespace: string, input: GraphqlUpdateSourceInput) => |
| 440 | 441 | Effect.gen(function* () { |
| 441 | − const existingConfig = yield* operationStore.getSourceConfig(namespace); | |
| 442 | − if (!existingConfig) return; | |
| 442 | + const existing = yield* operationStore.getSource(namespace); | |
| 443 | + if (!existing) return; | |
| 443 | 444 | |
| 444 | 445 | const updatedConfig = { |
| 445 | − ...existingConfig, | |
| 446 | + ...existing.config, | |
| 446 | 447 | ...(input.endpoint !== undefined ? { endpoint: input.endpoint } : {}), |
| 447 | 448 | ...(input.headers !== undefined |
| 448 | 449 | ? { headers: input.headers as Record<string, HeaderValueValue> } |
| @@ -454,26 +455,11 @@ export const graphqlPlugin = (options?: { · 5 unmodified lines | ||
| 454 | 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 | 458 | yield* operationStore.putSource({ |
| 474 | 459 | namespace, |
| 475 | − name: existingMeta?.name ?? namespace, | |
| 460 | + name: existing.name, | |
| 476 | 461 | config: updatedConfig, |
| 462 | + invocationConfig: newInvocationConfig, | |
| 477 | 463 | }); |
| 478 | 464 | }), |
| 479 | 465 | }, |
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | import { Schema } from "effect"; |
| 2 | 2 | |
| 3 | −import { HeaderValue } from "./types"; | |
| 3 | +import { HeaderValue, InvocationConfig } from "./types"; | |
| 4 | 4 | |
| 5 | 5 | // --------------------------------------------------------------------------- |
| 6 | 6 | // 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 | 16 | namespace: Schema.optional(Schema.String), |
| 17 | 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 | |
| 21 | 25 | export type StoredSourceSchemaType = typeof StoredSourceSchema.Type; |
| @@ -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 | 306 | const baseUrl = config.baseUrl; |
| 297 | 307 | |
| 298 | 308 | // Resolve secret-backed headers |
| @@ -8,7 +8,7 @@ import { Effect, Schema } from "effect"; · 7 unmodified lines | ||
| 8 | 8 | import { scopeKv, makeInMemoryScopedKv, type Kv, type ToolId, type ScopedKv } from "@executor/sdk"; |
| 9 | 9 | |
| 10 | 10 | import type { OpenApiOperationStore, StoredOperation, StoredSource } from "./operation-store"; |
| 11 | −import { OperationBinding, InvocationConfig } from "./types"; | |
| 11 | +import { InvocationConfig, OperationBinding } from "./types"; | |
| 12 | 12 | import { StoredSourceSchema } from "./stored-source"; |
| 13 | 13 | |
| 14 | 14 | // --------------------------------------------------------------------------- |
| @@ -18,12 +18,23 @@ import { StoredSourceSchema } from "./stored-source"; · 3 unmodified lines | ||
| 18 | 18 | class StoredEntry extends Schema.Class<StoredEntry>("StoredEntry")({ |
| 19 | 19 | namespace: Schema.String, |
| 20 | 20 | binding: OperationBinding, |
| 21 | − config: InvocationConfig, | |
| 22 | 21 | }) {} |
| 23 | 22 | |
| 24 | 23 | const encodeEntry = Schema.encodeSync(Schema.parseJson(StoredEntry)); |
| 25 | 24 | const 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. | |
| 30 | +class LegacyStoredEntry extends Schema.Class<LegacyStoredEntry>("LegacyStoredEntry")({ | |
| 31 | + namespace: Schema.String, | |
| 32 | + binding: OperationBinding, | |
| 33 | + config: Schema.optional(InvocationConfig), | |
| 34 | +}) {} | |
| 35 | + | |
| 36 | +const decodeLegacyEntry = Schema.decodeUnknownSync(Schema.parseJson(LegacyStoredEntry)); | |
| 37 | + | |
| 27 | 38 | const encodeSource = Schema.encodeSync(Schema.parseJson(StoredSourceSchema)); |
| 28 | 39 | const decodeSource = Schema.decodeUnknownSync(Schema.parseJson(StoredSourceSchema)); |
| 29 | 40 | |
| @@ -37,22 +48,53 @@ const makeStore = (bindings: ScopedKv, sources: ScopedKv): OpenApiOperationStore · 7 unmodified lines | ||
| 37 | 48 | effect: Effect.Effect<A, E, never>, |
| 38 | 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 | 82 | return { |
| 41 | 83 | get: (toolId) => |
| 42 | 84 | Effect.gen(function* () { |
| 43 | 85 | const raw = yield* bindings.get(toolId); |
| 44 | 86 | if (!raw) return null; |
| 45 | 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 | 91 | put: (entries: readonly StoredOperation[]) => |
| 50 | 92 | withKvTransaction( |
| 51 | 93 | bindings, |
| 52 | 94 | bindings.set( |
| 53 | − entries.map(({ toolId, namespace, binding, config }) => ({ | |
| 95 | + entries.map(({ toolId, namespace, binding }) => ({ | |
| 54 | 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 | 131 | listSources: () => |
| 90 | 132 | Effect.gen(function* () { |
| 91 | 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 | 147 | getSource: (namespace) => |
| 96 | 148 | Effect.gen(function* () { |
| 97 | 149 | const raw = yield* sources.get(namespace); |
| 98 | 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 | 159 | getSourceConfig: (namespace) => |
| @@ -18,19 +18,20 @@ export interface StoredSource { · 17 unmodified lines | ||
| 18 | 18 | readonly namespace: string; |
| 19 | 19 | readonly name: string; |
| 20 | 20 | readonly config: SourceConfig; |
| 21 | + /** Pre-resolved runtime invocation config (baseUrl resolved from servers, headers). */ | |
| 22 | + readonly invocationConfig: InvocationConfig; | |
| 21 | 23 | } |
| 22 | 24 | |
| 23 | 25 | export interface StoredOperation { |
| 24 | 26 | readonly toolId: ToolId; |
| 25 | 27 | readonly namespace: string; |
| 26 | 28 | readonly binding: OperationBinding; |
| 27 | − readonly config: InvocationConfig; | |
| 28 | 29 | } |
| 29 | 30 | |
| 30 | 31 | export interface OpenApiOperationStore { |
| 31 | 32 | readonly get: ( |
| 32 | 33 | toolId: ToolId, |
| 33 | − ) => Effect.Effect<{ binding: OperationBinding; config: InvocationConfig } | null>; | |
| 34 | + ) => Effect.Effect<{ binding: OperationBinding; namespace: string } | null>; | |
| 34 | 35 | |
| 35 | 36 | readonly put: (entries: readonly StoredOperation[]) => Effect.Effect<void>; |
| 36 | 37 | |
| @@ -275,7 +275,6 @@ export const openApiPlugin = (options?: { · 274 unmodified lines | ||
| 275 | 275 | toolId: ToolId.make(`${namespace}.${def.toolPath}`), |
| 276 | 276 | namespace, |
| 277 | 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 | 290 | namespace: config.namespace, |
| 292 | 291 | headers: config.headers, |
| 293 | 292 | }, |
| 293 | + invocationConfig, | |
| 294 | 294 | }); |
| 295 | 295 | |
| 296 | 296 | return { sourceId: namespace, toolCount: registrations.length }; |
| @@ -345,11 +345,11 @@ export const openApiPlugin = (options?: { · 48 unmodified lines | ||
| 345 | 345 | |
| 346 | 346 | updateSource: (namespace: string, input: OpenApiUpdateSourceInput) => |
| 347 | 347 | Effect.gen(function* () { |
| 348 | − const existingSource = yield* operationStore.getSourceConfig(namespace); | |
| 349 | − if (!existingSource) return; | |
| 348 | + const existing = yield* operationStore.getSource(namespace); | |
| 349 | + if (!existing) return; | |
| 350 | 350 | |
| 351 | 351 | const updatedConfig = { |
| 352 | − ...existingSource, | |
| 352 | + ...existing.config, | |
| 353 | 353 | ...(input.baseUrl !== undefined ? { baseUrl: input.baseUrl } : {}), |
| 354 | 354 | ...(input.headers !== undefined |
| 355 | 355 | ? { headers: input.headers as Record<string, HeaderValueValue> } |
| @@ -357,32 +357,15 @@ export const openApiPlugin = (options?: { · 1 unmodified line | ||
| 357 | 357 | }; |
| 358 | 358 | |
| 359 | 359 | const newInvocationConfig = new InvocationConfig({ |
| 360 | − baseUrl: updatedConfig.baseUrl ?? resolveBaseUrl([]), | |
| 360 | + baseUrl: updatedConfig.baseUrl ?? existing.invocationConfig.baseUrl, | |
| 361 | 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 | 364 | yield* operationStore.putSource({ |
| 383 | 365 | namespace, |
| 384 | − name: existingMeta?.name ?? namespace, | |
| 366 | + name: existing.name, | |
| 385 | 367 | config: updatedConfig, |
| 368 | + invocationConfig: newInvocationConfig, | |
| 386 | 369 | }); |
| 387 | 370 | }), |
| 388 | 371 | }, |
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | import { Schema } from "effect"; |
| 2 | 2 | |
| 3 | −import { HeaderValue } from "./types"; | |
| 3 | +import { HeaderValue, InvocationConfig } from "./types"; | |
| 4 | 4 | |
| 5 | 5 | // --------------------------------------------------------------------------- |
| 6 | 6 | // 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 | 16 | namespace: Schema.optional(Schema.String), |
| 17 | 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 | |
| 21 | 25 | export type StoredSourceSchemaType = typeof StoredSourceSchema.Type; |