| @@ -1,4 +1,4 @@ |
| 1 | | −import { Effect, Schema } from "effect"; |
| 1 | +import { Effect, Option, Schema } from "effect"; |
| 2 | 2 | |
| 3 | 3 | import { |
| 4 | 4 | ConnectionId, |
| @@ -174,9 +174,7 @@ export interface StoredSource { · 169 unmodified lines |
| 174 | 174 | // an encodable/decodable shape for HTTP responses. |
| 175 | 175 | // --------------------------------------------------------------------------- |
| 176 | 176 | |
| 177 | | −export class StoredSourceSchema extends Schema.Class<StoredSourceSchema>( |
| 178 | | − "OpenApiStoredSource", |
| 179 | | −)({ |
| 177 | +export class StoredSourceSchema extends Schema.Class<StoredSourceSchema>("OpenApiStoredSource")({ |
| 180 | 178 | namespace: Schema.String, |
| 181 | 179 | name: Schema.String, |
| 182 | 180 | config: Schema.Struct({ |
| @@ -184,20 +182,12 @@ export class StoredSourceSchema extends Schema.Class<StoredSourceSchema>( · 1 unmodified line |
| 184 | 182 | sourceUrl: Schema.optional(Schema.String), |
| 185 | 183 | baseUrl: Schema.optional(Schema.String), |
| 186 | 184 | namespace: Schema.optional(Schema.String), |
| 187 | | − headers: Schema.optional( |
| 188 | | − Schema.Record(Schema.String, ConfiguredHeaderValue), |
| 189 | | − ), |
| 190 | | − queryParams: Schema.optional( |
| 191 | | − Schema.Record(Schema.String, HeaderValue), |
| 192 | | − ), |
| 185 | + headers: Schema.optional(Schema.Record(Schema.String, ConfiguredHeaderValue)), |
| 186 | + queryParams: Schema.optional(Schema.Record(Schema.String, HeaderValue)), |
| 193 | 187 | specFetchCredentials: Schema.optional( |
| 194 | 188 | Schema.Struct({ |
| 195 | | − headers: Schema.optional( |
| 196 | | − Schema.Record(Schema.String, HeaderValue), |
| 197 | | − ), |
| 198 | | − queryParams: Schema.optional( |
| 199 | | − Schema.Record(Schema.String, HeaderValue), |
| 200 | | − ), |
| 189 | + headers: Schema.optional(Schema.Record(Schema.String, HeaderValue)), |
| 190 | + queryParams: Schema.optional(Schema.Record(Schema.String, HeaderValue)), |
| 201 | 191 | }), |
| 202 | 192 | ), |
| 203 | 193 | // Canonical source-owned OAuth config. Concrete client credentials |
| @@ -221,10 +211,85 @@ export interface StoredOperation { · 17 unmodified lines |
| 221 | 211 | |
| 222 | 212 | const encodeBinding = Schema.encodeSync(OperationBinding); |
| 223 | 213 | const decodeBinding = Schema.decodeUnknownSync(OperationBinding); |
| 214 | +const decodeBindingJson = Schema.decodeUnknownSync(Schema.fromJsonString(OperationBinding)); |
| 224 | 215 | |
| 225 | 216 | const decodeOAuth2 = Schema.decodeUnknownSync(OAuth2Auth); |
| 217 | +const decodeOAuth2Option = Schema.decodeUnknownOption(OAuth2Auth); |
| 218 | +const decodeOAuth2JsonOption = Schema.decodeUnknownOption(Schema.fromJsonString(OAuth2Auth)); |
| 219 | +const decodeOAuth2SourceConfigOption = Schema.decodeUnknownOption(OAuth2SourceConfig); |
| 220 | +const decodeOAuth2SourceConfigJsonOption = Schema.decodeUnknownOption( |
| 221 | + Schema.fromJsonString(OAuth2SourceConfig), |
| 222 | +); |
| 226 | 223 | const encodeOAuth2SourceConfig = Schema.encodeSync(OAuth2SourceConfig); |
| 227 | 224 | |
| 225 | +const decodeHeaderValueOption = Schema.decodeUnknownOption(HeaderValue); |
| 226 | +const UnknownRecord = Schema.Record(Schema.String, Schema.Unknown); |
| 227 | +const decodeUnknownRecord = Schema.decodeUnknownSync(UnknownRecord); |
| 228 | +const decodeUnknownRecordJson = Schema.decodeUnknownSync(Schema.fromJsonString(UnknownRecord)); |
| 229 | +const decodeConfiguredHeaderBindingOption = Schema.decodeUnknownOption(ConfiguredHeaderBinding); |
| 230 | + |
| 231 | +const NullableString = Schema.NullOr(Schema.String); |
| 232 | +const OptionalNullableString = Schema.optional(NullableString); |
| 233 | + |
| 234 | +const ChildStorageRow = Schema.Struct({ |
| 235 | + name: Schema.String, |
| 236 | + kind: Schema.Literals(["text", "secret"]), |
| 237 | + text_value: OptionalNullableString, |
| 238 | + secret_id: OptionalNullableString, |
| 239 | + secret_prefix: OptionalNullableString, |
| 240 | +}); |
| 241 | +const decodeChildStorageRowOption = Schema.decodeUnknownOption(ChildStorageRow); |
| 242 | + |
| 243 | +const SourceBindingStorageRow = Schema.Struct({ |
| 244 | + source_id: Schema.String, |
| 245 | + source_scope_id: Schema.String, |
| 246 | + target_scope_id: Schema.String, |
| 247 | + slot: Schema.String, |
| 248 | + kind: Schema.Literals(["secret", "connection", "text"]), |
| 249 | + secret_id: OptionalNullableString, |
| 250 | + connection_id: OptionalNullableString, |
| 251 | + text_value: OptionalNullableString, |
| 252 | + created_at: Schema.Unknown, |
| 253 | + updated_at: Schema.Unknown, |
| 254 | +}); |
| 255 | +const decodeSourceBindingStorageRow = Schema.decodeUnknownSync(SourceBindingStorageRow); |
| 256 | + |
| 257 | +const SourceStorageRow = Schema.Struct({ |
| 258 | + id: Schema.String, |
| 259 | + scope_id: Schema.String, |
| 260 | + name: Schema.String, |
| 261 | + spec: Schema.String, |
| 262 | + source_url: OptionalNullableString, |
| 263 | + base_url: OptionalNullableString, |
| 264 | + headers: Schema.optional(Schema.Unknown), |
| 265 | + oauth2: Schema.optional(Schema.Unknown), |
| 266 | +}); |
| 267 | +const decodeSourceStorageRow = Schema.decodeUnknownSync(SourceStorageRow); |
| 268 | + |
| 269 | +const OperationStorageRow = Schema.Struct({ |
| 270 | + id: Schema.String, |
| 271 | + source_id: Schema.String, |
| 272 | + binding: Schema.Unknown, |
| 273 | +}); |
| 274 | +const decodeOperationStorageRow = Schema.decodeUnknownSync(OperationStorageRow); |
| 275 | + |
| 276 | +const ChildUsageStorageRow = Schema.Struct({ |
| 277 | + source_id: Schema.String, |
| 278 | + scope_id: Schema.String, |
| 279 | + name: Schema.String, |
| 280 | +}); |
| 281 | +const decodeChildUsageStorageRow = Schema.decodeUnknownSync(ChildUsageStorageRow); |
| 282 | + |
| 283 | +const SourceNameStorageRow = Schema.Struct({ |
| 284 | + id: Schema.String, |
| 285 | + scope_id: Schema.String, |
| 286 | + name: Schema.String, |
| 287 | +}); |
| 288 | +const decodeSourceNameStorageRow = Schema.decodeUnknownSync(SourceNameStorageRow); |
| 289 | + |
| 290 | +const decodeStorageDate = (value: unknown): Date => |
| 291 | + value instanceof Date ? value : new Date(Schema.decodeUnknownSync(Schema.String)(value)); |
| 292 | + |
| 228 | 293 | interface ChildRow { |
| 229 | 294 | readonly id: string; |
| 230 | 295 | readonly scope_id: string; |
| @@ -277,21 +342,24 @@ const childRowsToValueMap = ( · 46 unmodified lines |
| 277 | 342 | ): Record<string, HeaderValue> => { |
| 278 | 343 | const out: Record<string, HeaderValue> = {}; |
| 279 | 344 | for (const row of rows) { |
| 280 | | − const name = row.name as string; |
| 281 | | − if (row.kind === "secret" && typeof row.secret_id === "string") { |
| 282 | | − const prefix = row.secret_prefix as string | undefined | null; |
| 283 | | − out[name] = prefix |
| 284 | | − ? { secretId: row.secret_id, prefix } |
| 285 | | − : { secretId: row.secret_id }; |
| 286 | | − } else if (row.kind === "text" && typeof row.text_value === "string") { |
| 287 | | − out[name] = row.text_value; |
| 345 | + const decoded = decodeChildStorageRowOption(row); |
| 346 | + if (Option.isSome(decoded)) { |
| 347 | + const child = decoded.value; |
| 348 | + if (child.kind === "secret" && child.secret_id != null) { |
| 349 | + out[child.name] = |
| 350 | + child.secret_prefix != null |
| 351 | + ? { secretId: child.secret_id, prefix: child.secret_prefix } |
| 352 | + : { secretId: child.secret_id }; |
| 353 | + } else if (child.kind === "text" && child.text_value != null) { |
| 354 | + out[child.name] = child.text_value; |
| 355 | + } |
| 288 | 356 | } |
| 289 | 357 | } |
| 290 | 358 | return out; |
| 291 | 359 | }; |
| 292 | 360 | |
| 293 | | −const toJsonRecord = (value: unknown): Record<string, unknown> => |
| 294 | | − value as Record<string, unknown>; |
| 361 | +// oxlint-disable-next-line executor/no-explicit-unknown-record -- boundary: storage adapter accepts JSON object columns |
| 362 | +const toJsonRecord = (value: unknown): Record<string, unknown> => value as Record<string, unknown>; |
| 295 | 363 | |
| 296 | 364 | const toConfiguredHeaderBinding = (value: { |
| 297 | 365 | readonly slot?: unknown; |
| @@ -303,11 +371,10 @@ const toConfiguredHeaderBinding = (value: { · 5 unmodified lines |
| 303 | 371 | ...(typeof value.prefix === "string" ? { prefix: value.prefix } : {}), |
| 304 | 372 | }); |
| 305 | 373 | |
| 306 | | −const decodeHeaders = (value: unknown): Record<string, HeaderValue> => { |
| 374 | +const decodeHeaders = (value: unknown): Record<string, unknown> => { |
| 307 | 375 | if (value == null) return {}; |
| 308 | | − if (typeof value === "string") |
| 309 | | − return JSON.parse(value) as Record<string, HeaderValue>; |
| 310 | | − return value as Record<string, HeaderValue>; |
| 376 | + if (typeof value === "string") return decodeUnknownRecordJson(value); |
| 377 | + return decodeUnknownRecord(value); |
| 311 | 378 | }; |
| 312 | 379 | |
| 313 | 380 | const slugifySlotPart = (value: string): string => |
| @@ -344,20 +411,18 @@ const normalizeStoredHeaders = ( · 30 unmodified lines |
| 344 | 411 | legacy[name] = header; |
| 345 | 412 | continue; |
| 346 | 413 | } |
| 347 | | − if ( |
| 348 | | − header && |
| 349 | | − typeof header === "object" && |
| 350 | | − "kind" in header && |
| 351 | | − (header as { kind?: unknown }).kind === "binding" |
| 352 | | − ) { |
| 353 | | − headers[name] = toConfiguredHeaderBinding(header); |
| 414 | + const binding = decodeConfiguredHeaderBindingOption(header); |
| 415 | + if (Option.isSome(binding)) { |
| 416 | + headers[name] = toConfiguredHeaderBinding(binding.value); |
| 354 | 417 | continue; |
| 355 | 418 | } |
| 356 | | − legacy[name] = header; |
| 419 | + const legacyHeader = decodeHeaderValueOption(header); |
| 420 | + if (Option.isNone(legacyHeader)) continue; |
| 421 | + legacy[name] = legacyHeader.value; |
| 357 | 422 | headers[name] = new ConfiguredHeaderBinding({ |
| 358 | 423 | kind: "binding", |
| 359 | 424 | slot: headerBindingSlot(name), |
| 360 | | − prefix: header.prefix, |
| 425 | + prefix: typeof legacyHeader.value === "string" ? undefined : legacyHeader.value.prefix, |
| 361 | 426 | }); |
| 362 | 427 | } |
| 363 | 428 | return { headers, legacy }; |
| @@ -370,13 +435,16 @@ const normalizeStoredOAuth2 = ( · 6 unmodified lines |
| 370 | 435 | readonly legacy?: OAuth2Auth; |
| 371 | 436 | } => { |
| 372 | 437 | if (value == null) return {}; |
| 373 | | − const parsed = typeof value === "string" ? JSON.parse(value) : value; |
| 374 | | − if (parsed && typeof parsed === "object" && "connectionSlot" in parsed) { |
| 375 | | − return { |
| 376 | | − oauth2: Schema.decodeUnknownSync(OAuth2SourceConfig)(parsed), |
| 377 | | − }; |
| 438 | + const sourceConfig = |
| 439 | + typeof value === "string" |
| 440 | + ? decodeOAuth2SourceConfigJsonOption(value) |
| 441 | + : decodeOAuth2SourceConfigOption(value); |
| 442 | + if (Option.isSome(sourceConfig)) { |
| 443 | + return { oauth2: sourceConfig.value }; |
| 378 | 444 | } |
| 379 | | − const legacy = decodeOAuth2(parsed); |
| 445 | + const legacyOption = |
| 446 | + typeof value === "string" ? decodeOAuth2JsonOption(value) : decodeOAuth2Option(value); |
| 447 | + const legacy = Option.isSome(legacyOption) ? legacyOption.value : decodeOAuth2(value); |
| 380 | 448 | return { |
| 381 | 449 | legacy, |
| 382 | 450 | oauth2: new OAuth2SourceConfig({ |
| @@ -438,10 +506,7 @@ export interface OpenapiStore { · 55 unmodified lines |
| 438 | 506 | scope: string, |
| 439 | 507 | ) => Effect.Effect<StoredSource | null, StorageFailure>; |
| 440 | 508 | |
| 441 | | − readonly listSources: () => Effect.Effect< |
| 442 | | − readonly StoredSource[], |
| 443 | | − StorageFailure |
| 444 | | − >; |
| 509 | + readonly listSources: () => Effect.Effect<readonly StoredSource[], StorageFailure>; |
| 445 | 510 | |
| 446 | 511 | readonly getOperationByToolId: ( |
| 447 | 512 | toolId: string, |
| @@ -453,10 +518,7 @@ export interface OpenapiStore { · 5 unmodified lines |
| 453 | 518 | scope: string, |
| 454 | 519 | ) => Effect.Effect<readonly StoredOperation[], StorageFailure>; |
| 455 | 520 | |
| 456 | | − readonly removeSource: ( |
| 457 | | − namespace: string, |
| 458 | | − scope: string, |
| 459 | | − ) => Effect.Effect<void, StorageFailure>; |
| 521 | + readonly removeSource: (namespace: string, scope: string) => Effect.Effect<void, StorageFailure>; |
| 460 | 522 | |
| 461 | 523 | readonly listSourceBindings: ( |
| 462 | 524 | sourceId: string, |
| @@ -501,10 +563,7 @@ export interface OpenapiStore { · 38 unmodified lines |
| 501 | 563 | * `query_param:foo` or `spec_fetch_header:Authorization`. */ |
| 502 | 564 | readonly findChildRowsBySecret: (secretId: string) => Effect.Effect< |
| 503 | 565 | readonly { |
| 504 | | − readonly kind: |
| 505 | | − | "query_param" |
| 506 | | − | "spec_fetch_header" |
| 507 | | − | "spec_fetch_query_param"; |
| 566 | + readonly kind: "query_param" | "spec_fetch_header" | "spec_fetch_query_param"; |
| 508 | 567 | readonly source_id: string; |
| 509 | 568 | readonly scope_id: string; |
| 510 | 569 | readonly name: string; |
| @@ -527,14 +586,12 @@ export const makeDefaultOpenapiStore = ({ · 16 unmodified lines |
| 527 | 586 | adapter, |
| 528 | 587 | scopes, |
| 529 | 588 | }: StorageDeps<OpenapiSchema>): OpenapiStore => { |
| 530 | | − const scopeIds = scopes.map((scope) => scope.id as string); |
| 589 | + const scopeIds = scopes.map((scope) => String(scope.id)); |
| 531 | 590 | const scopePrecedence = new Map<string, number>(); |
| 532 | 591 | scopeIds.forEach((scope, index) => scopePrecedence.set(scope, index)); |
| 533 | | − const scopeRank = (scopeId: string): number => |
| 534 | | − scopePrecedence.get(scopeId) ?? Infinity; |
| 592 | + const scopeRank = (scopeId: string): number => scopePrecedence.get(scopeId) ?? Infinity; |
| 535 | 593 | |
| 536 | | − const encodeSyntheticRowIdPart = (value: string): string => |
| 537 | | − encodeURIComponent(value); |
| 594 | + const encodeSyntheticRowIdPart = (value: string): string => encodeURIComponent(value); |
| 538 | 595 | |
| 539 | 596 | const sourceBindingRowId = ( |
| 540 | 597 | sourceId: string, |
| @@ -550,47 +607,47 @@ export const makeDefaultOpenapiStore = ({ · 9 unmodified lines |
| 550 | 607 | encodeSyntheticRowIdPart(scopeId), |
| 551 | 608 | ].join("::"); |
| 552 | 609 | |
| 553 | | − const rowToSourceBindingValue = ( |
| 554 | | − row: Record<string, unknown>, |
| 555 | | − ): OpenApiSourceBindingValue => { |
| 556 | | − const kind = row.kind as string; |
| 557 | | − if (kind === "secret" && typeof row.secret_id === "string") { |
| 558 | | − return { kind: "secret", secretId: SecretId.make(row.secret_id) }; |
| 610 | + const rowToSourceBindingValue = (row: Record<string, unknown>): OpenApiSourceBindingValue => { |
| 611 | + const decoded = decodeSourceBindingStorageRow(row); |
| 612 | + if (decoded.kind === "secret" && decoded.secret_id != null) { |
| 613 | + return { kind: "secret", secretId: SecretId.make(decoded.secret_id) }; |
| 559 | 614 | } |
| 560 | | − if (kind === "connection" && typeof row.connection_id === "string") { |
| 615 | + if (decoded.kind === "connection" && decoded.connection_id != null) { |
| 561 | 616 | return { |
| 562 | 617 | kind: "connection", |
| 563 | | − connectionId: ConnectionId.make(row.connection_id), |
| 618 | + connectionId: ConnectionId.make(decoded.connection_id), |
| 564 | 619 | }; |
| 565 | 620 | } |
| 566 | 621 | // text fallback covers both well-formed text rows and any |
| 567 | 622 | // partial/null row that survived a malformed write — `text_value` |
| 568 | 623 | // defaults to "" so the type stays satisfied without a throw. |
| 569 | | − return { kind: "text", text: (row.text_value as string | null) ?? "" }; |
| 624 | + return { kind: "text", text: decoded.text_value ?? "" }; |
| 570 | 625 | }; |
| 571 | 626 | |
| 572 | | − const rowToSourceBinding = ( |
| 573 | | − row: Record<string, unknown>, |
| 574 | | − ): OpenApiSourceBindingRef => |
| 575 | | − new OpenApiSourceBindingRef({ |
| 576 | | − sourceId: row.source_id as string, |
| 577 | | − sourceScopeId: ScopeId.make(row.source_scope_id as string), |
| 578 | | − scopeId: ScopeId.make(row.target_scope_id as string), |
| 579 | | − slot: row.slot as string, |
| 627 | + const rowToSourceBinding = (row: Record<string, unknown>): OpenApiSourceBindingRef => { |
| 628 | + const decoded = decodeSourceBindingStorageRow(row); |
| 629 | + return new OpenApiSourceBindingRef({ |
| 630 | + sourceId: decoded.source_id, |
| 631 | + sourceScopeId: ScopeId.make(decoded.source_scope_id), |
| 632 | + scopeId: ScopeId.make(decoded.target_scope_id), |
| 633 | + slot: decoded.slot, |
| 580 | 634 | value: rowToSourceBindingValue(row), |
| 581 | | − createdAt: |
| 582 | | − row.created_at instanceof Date |
| 583 | | − ? row.created_at |
| 584 | | − : new Date(row.created_at as string), |
| 585 | | − updatedAt: |
| 586 | | − row.updated_at instanceof Date |
| 587 | | − ? row.updated_at |
| 588 | | − : new Date(row.updated_at as string), |
| 635 | + createdAt: decodeStorageDate(decoded.created_at), |
| 636 | + updatedAt: decodeStorageDate(decoded.updated_at), |
| 589 | 637 | }); |
| 638 | + }; |
| 639 | + |
| 640 | + const sourceBindingTargetScope = (row: Record<string, unknown>): string => |
| 641 | + decodeSourceBindingStorageRow(row).target_scope_id; |
| 590 | 642 | |
| 591 | 643 | const sourceBindingValueColumns = ( |
| 592 | 644 | value: OpenApiSourceBindingValue, |
| 593 | | − ): { kind: string; secret_id?: string; connection_id?: string; text_value?: string } => { |
| 645 | + ): { |
| 646 | + kind: string; |
| 647 | + secret_id?: string; |
| 648 | + connection_id?: string; |
| 649 | + text_value?: string; |
| 650 | + } => { |
| 594 | 651 | if (value.kind === "secret") { |
| 595 | 652 | return { kind: "secret", secret_id: value.secretId }; |
| 596 | 653 | } |
| @@ -606,24 +663,20 @@ export const makeDefaultOpenapiStore = ({ · 9 unmodified lines |
| 606 | 663 | }) => |
| 607 | 664 | Effect.gen(function* () { |
| 608 | 665 | if (!scopeIds.includes(params.sourceScope)) { |
| 609 | | − return yield* Effect.fail( |
| 610 | | − new StorageError({ |
| 611 | | − message: |
| 612 | | − `OpenAPI source binding references source scope "${params.sourceScope}" ` + |
| 613 | | − `which is not in the executor's scope stack [${scopeIds.join(", ")}].`, |
| 614 | | − cause: undefined, |
| 615 | | − }), |
| 616 | | − ); |
| 666 | + return yield* new StorageError({ |
| 667 | + message: |
| 668 | + `OpenAPI source binding references source scope "${params.sourceScope}" ` + |
| 669 | + `which is not in the executor's scope stack [${scopeIds.join(", ")}].`, |
| 670 | + cause: undefined, |
| 671 | + }); |
| 617 | 672 | } |
| 618 | 673 | if (!scopeIds.includes(params.targetScope)) { |
| 619 | | − return yield* Effect.fail( |
| 620 | | − new StorageError({ |
| 621 | | − message: |
| 622 | | − `OpenAPI source binding targets scope "${params.targetScope}" which is not ` + |
| 623 | | − `in the executor's scope stack [${scopeIds.join(", ")}].`, |
| 624 | | − cause: undefined, |
| 625 | | − }), |
| 626 | | − ); |
| 674 | + return yield* new StorageError({ |
| 675 | + message: |
| 676 | + `OpenAPI source binding targets scope "${params.targetScope}" which is not ` + |
| 677 | + `in the executor's scope stack [${scopeIds.join(", ")}].`, |
| 678 | + cause: undefined, |
| 679 | + }); |
| 627 | 680 | } |
| 628 | 681 | }); |
| 629 | 682 | |
| @@ -645,23 +698,19 @@ export const makeDefaultOpenapiStore = ({ · 15 unmodified lines |
| 645 | 698 | ], |
| 646 | 699 | }); |
| 647 | 700 | if (!source) { |
| 648 | | − return yield* Effect.fail( |
| 649 | | − new StorageError({ |
| 650 | | − message: `OpenAPI source "${params.sourceId}" does not exist at scope "${params.sourceScope}"`, |
| 651 | | − cause: undefined, |
| 652 | | − }), |
| 653 | | − ); |
| 701 | + return yield* new StorageError({ |
| 702 | + message: `OpenAPI source "${params.sourceId}" does not exist at scope "${params.sourceScope}"`, |
| 703 | + cause: undefined, |
| 704 | + }); |
| 654 | 705 | } |
| 655 | 706 | if (scopeRank(params.targetScope) > scopeRank(params.sourceScope)) { |
| 656 | | − return yield* Effect.fail( |
| 657 | | − new StorageError({ |
| 658 | | − message: |
| 659 | | − `OpenAPI source bindings for "${params.sourceId}" cannot be written at ` + |
| 660 | | − `outer scope "${params.targetScope}" because the base source lives at ` + |
| 661 | | − `"${params.sourceScope}"`, |
| 662 | | − cause: undefined, |
| 663 | | − }), |
| 664 | | − ); |
| 707 | + return yield* new StorageError({ |
| 708 | + message: |
| 709 | + `OpenAPI source bindings for "${params.sourceId}" cannot be written at ` + |
| 710 | + `outer scope "${params.targetScope}" because the base source lives at ` + |
| 711 | + `"${params.sourceScope}"`, |
| 712 | + cause: undefined, |
| 713 | + }); |
| 665 | 714 | } |
| 666 | 715 | return source; |
| 667 | 716 | }); |
| @@ -684,20 +733,15 @@ export const makeDefaultOpenapiStore = ({ · 16 unmodified lines |
| 684 | 733 | }) |
| 685 | 734 | .pipe(Effect.map(childRowsToValueMap)); |
| 686 | 735 | |
| 687 | | − const rowToSource = ( |
| 688 | | − row: Record<string, unknown>, |
| 689 | | − ): Effect.Effect<StoredSource, StorageFailure> => |
| 736 | + const rowToSource = (row: Record<string, unknown>): Effect.Effect<StoredSource, StorageFailure> => |
| 690 | 737 | Effect.gen(function* () { |
| 691 | | − const sourceId = row.id as string; |
| 692 | | − const scope = row.scope_id as string; |
| 693 | | − const normalizedHeaders = normalizeStoredHeaders(row.headers); |
| 694 | | − const normalizedOAuth2 = normalizeStoredOAuth2(row.oauth2); |
| 738 | + const sourceRow = decodeSourceStorageRow(row); |
| 739 | + const sourceId = sourceRow.id; |
| 740 | + const scope = sourceRow.scope_id; |
| 741 | + const normalizedHeaders = normalizeStoredHeaders(sourceRow.headers); |
| 742 | + const normalizedOAuth2 = normalizeStoredOAuth2(sourceRow.oauth2); |
| 695 | 743 | |
| 696 | | − const queryParams = yield* loadChildValueMap( |
| 697 | | − "openapi_source_query_param", |
| 698 | | − sourceId, |
| 699 | | − scope, |
| 700 | | − ); |
| 744 | + const queryParams = yield* loadChildValueMap("openapi_source_query_param", sourceId, scope); |
| 701 | 745 | const specFetchHeaders = yield* loadChildValueMap( |
| 702 | 746 | "openapi_source_spec_fetch_header", |
| 703 | 747 | sourceId, |
| @@ -709,13 +753,10 @@ export const makeDefaultOpenapiStore = ({ · 5 unmodified lines |
| 709 | 753 | scope, |
| 710 | 754 | ); |
| 711 | 755 | const specFetchCredentials: OpenApiSpecFetchCredentials | undefined = |
| 712 | | − Object.keys(specFetchHeaders).length === 0 && |
| 713 | | − Object.keys(specFetchQueryParams).length === 0 |
| 756 | + Object.keys(specFetchHeaders).length === 0 && Object.keys(specFetchQueryParams).length === 0 |
| 714 | 757 | ? undefined |
| 715 | 758 | : { |
| 716 | | − ...(Object.keys(specFetchHeaders).length > 0 |
| 717 | | − ? { headers: specFetchHeaders } |
| 718 | | − : {}), |
| 759 | + ...(Object.keys(specFetchHeaders).length > 0 ? { headers: specFetchHeaders } : {}), |
| 719 | 760 | ...(Object.keys(specFetchQueryParams).length > 0 |
| 720 | 761 | ? { queryParams: specFetchQueryParams } |
| 721 | 762 | : {}), |
| @@ -724,38 +765,40 @@ export const makeDefaultOpenapiStore = ({ · 2 unmodified lines |
| 724 | 765 | return { |
| 725 | 766 | namespace: sourceId, |
| 726 | 767 | scope, |
| 727 | | − name: row.name as string, |
| 768 | + name: sourceRow.name, |
| 728 | 769 | config: { |
| 729 | | − spec: row.spec as string, |
| 730 | | − sourceUrl: (row.source_url as string | null | undefined) ?? undefined, |
| 731 | | − baseUrl: (row.base_url as string | null | undefined) ?? undefined, |
| 770 | + spec: sourceRow.spec, |
| 771 | + sourceUrl: sourceRow.source_url ?? undefined, |
| 772 | + baseUrl: sourceRow.base_url ?? undefined, |
| 732 | 773 | headers: normalizedHeaders.headers, |
| 733 | 774 | queryParams, |
| 734 | 775 | specFetchCredentials, |
| 735 | 776 | oauth2: normalizedOAuth2.oauth2, |
| 736 | 777 | }, |
| 737 | 778 | legacy: |
| 738 | | − Object.keys(normalizedHeaders.legacy).length > 0 || |
| 739 | | − normalizedOAuth2.legacy |
| 779 | + Object.keys(normalizedHeaders.legacy).length > 0 || normalizedOAuth2.legacy |
| 740 | 780 | ? { |
| 741 | 781 | ...(Object.keys(normalizedHeaders.legacy).length > 0 |
| 742 | 782 | ? { headers: normalizedHeaders.legacy } |
| 743 | 783 | : {}), |
| 744 | | − ...(normalizedOAuth2.legacy |
| 745 | | − ? { oauth2: normalizedOAuth2.legacy } |
| 746 | | − : {}), |
| 784 | + ...(normalizedOAuth2.legacy ? { oauth2: normalizedOAuth2.legacy } : {}), |
| 747 | 785 | } |
| 748 | 786 | : undefined, |
| 749 | 787 | }; |
| 750 | 788 | }); |
| 751 | 789 | |
| 752 | | − const rowToOperation = (row: Record<string, unknown>): StoredOperation => ({ |
| 753 | | − toolId: row.id as string, |
| 754 | | − sourceId: row.source_id as string, |
| 755 | | − binding: decodeBinding( |
| 756 | | − typeof row.binding === "string" ? JSON.parse(row.binding) : row.binding, |
| 757 | | − ), |
| 758 | | − }); |
| 790 | + const rowToOperation = (row: Record<string, unknown>): StoredOperation => { |
| 791 | + const operationRow = decodeOperationStorageRow(row); |
| 792 | + return { |
| 793 | + toolId: operationRow.id, |
| 794 | + sourceId: operationRow.source_id, |
| 795 | + binding: decodeBinding( |
| 796 | + typeof operationRow.binding === "string" |
| 797 | + ? decodeBindingJson(operationRow.binding) |
| 798 | + : operationRow.binding, |
| 799 | + ), |
| 800 | + }; |
| 801 | + }; |
| 759 | 802 | |
| 760 | 803 | // Replace the rows of one child table for a source: delete then bulk |
| 761 | 804 | // insert. Single helper so upsertSource and updateSourceMeta both |
| @@ -845,20 +888,18 @@ export const makeDefaultOpenapiStore = ({ · 83 unmodified lines |
| 845 | 888 | source_url: input.config.sourceUrl ?? undefined, |
| 846 | 889 | base_url: input.config.baseUrl ?? undefined, |
| 847 | 890 | headers: Object.fromEntries( |
| 848 | | − Object.entries(input.config.headers ?? {}).map( |
| 849 | | − ([name, value]) => [ |
| 850 | | − name, |
| 851 | | − typeof value === "string" |
| 852 | | − ? value |
| 853 | | − : value.kind === "binding" |
| 854 | | − ? { |
| 855 | | − kind: value.kind, |
| 856 | | − slot: value.slot, |
| 857 | | − ...(value.prefix ? { prefix: value.prefix } : {}), |
| 858 | | − } |
| 859 | | − : value, |
| 860 | | − ], |
| 861 | | − ), |
| 891 | + Object.entries(input.config.headers ?? {}).map(([name, value]) => [ |
| 892 | + name, |
| 893 | + typeof value === "string" |
| 894 | + ? value |
| 895 | + : value.kind === "binding" |
| 896 | + ? { |
| 897 | + kind: value.kind, |
| 898 | + slot: value.slot, |
| 899 | + ...(value.prefix ? { prefix: value.prefix } : {}), |
| 900 | + } |
| 901 | + : value, |
| 902 | + ]), |
| 862 | 903 | ) as Record<string, unknown>, |
| 863 | 904 | oauth2: input.config.oauth2 |
| 864 | 905 | ? toJsonRecord(encodeOAuth2SourceConfig(input.config.oauth2)) |
| @@ -911,14 +952,10 @@ export const makeDefaultOpenapiStore = ({ · 46 unmodified lines |
| 911 | 952 | const existing = yield* rowToSource(existingRow); |
| 912 | 953 | |
| 913 | 954 | const nextName = patch.name?.trim() || existing.name; |
| 914 | | − const nextBaseUrl = |
| 915 | | − patch.baseUrl !== undefined ? patch.baseUrl : existing.config.baseUrl; |
| 955 | + const nextBaseUrl = patch.baseUrl !== undefined ? patch.baseUrl : existing.config.baseUrl; |
| 916 | 956 | const nextHeaders = |
| 917 | | − patch.headers !== undefined |
| 918 | | − ? patch.headers |
| 919 | | − : (existing.config.headers ?? {}); |
| 920 | | − const nextOAuth2 = |
| 921 | | − patch.oauth2 !== undefined ? patch.oauth2 : existing.config.oauth2; |
| 957 | + patch.headers !== undefined ? patch.headers : (existing.config.headers ?? {}); |
| 958 | + const nextOAuth2 = patch.oauth2 !== undefined ? patch.oauth2 : existing.config.oauth2; |
| 922 | 959 | |
| 923 | 960 | yield* adapter.update({ |
| 924 | 961 | model: "openapi_source", |
| @@ -941,9 +978,7 @@ export const makeDefaultOpenapiStore = ({ · 16 unmodified lines |
| 941 | 978 | }, |
| 942 | 979 | ]), |
| 943 | 980 | ) as Record<string, unknown>, |
| 944 | | − oauth2: nextOAuth2 |
| 945 | | − ? toJsonRecord(encodeOAuth2SourceConfig(nextOAuth2)) |
| 946 | | − : undefined, |
| 981 | + oauth2: nextOAuth2 ? toJsonRecord(encodeOAuth2SourceConfig(nextOAuth2)) : undefined, |
| 947 | 982 | }, |
| 948 | 983 | }); |
| 949 | 984 | if (patch.queryParams !== undefined) { |
| @@ -999,8 +1034,7 @@ export const makeDefaultOpenapiStore = ({ · 49 unmodified lines |
| 999 | 1034 | }) |
| 1000 | 1035 | .pipe(Effect.map((rows) => rows.map(rowToOperation))), |
| 1001 | 1036 | |
| 1002 | | − removeSource: (namespace, scope) => |
| 1003 | | − deleteSource(namespace, scope, { includeBindings: true }), |
| 1037 | + removeSource: (namespace, scope) => deleteSource(namespace, scope, { includeBindings: true }), |
| 1004 | 1038 | |
| 1005 | 1039 | listSourceBindings: (sourceId, sourceScope) => |
| 1006 | 1040 | Effect.gen(function* () { |
| @@ -1017,14 +1051,10 @@ export const makeDefaultOpenapiStore = ({ · 10 unmodified lines |
| 1017 | 1051 | ], |
| 1018 | 1052 | }); |
| 1019 | 1053 | return rows |
| 1020 | | − .filter( |
| 1021 | | − (row) => |
| 1022 | | − scopeRank(row.target_scope_id as string) <= sourceScopeRank, |
| 1023 | | − ) |
| 1054 | + .filter((row) => scopeRank(sourceBindingTargetScope(row)) <= sourceScopeRank) |
| 1024 | 1055 | .sort( |
| 1025 | 1056 | (a, b) => |
| 1026 | | − scopeRank(a.target_scope_id as string) - |
| 1027 | | − scopeRank(b.target_scope_id as string), |
| 1057 | + scopeRank(sourceBindingTargetScope(a)) - scopeRank(sourceBindingTargetScope(b)), |
| 1028 | 1058 | ) |
| 1029 | 1059 | .map(rowToSourceBinding); |
| 1030 | 1060 | }), |
| @@ -1045,31 +1075,24 @@ export const makeDefaultOpenapiStore = ({ · 14 unmodified lines |
| 1045 | 1075 | }); |
| 1046 | 1076 | const sourceScopeRank = scopeRank(sourceScope); |
| 1047 | 1077 | const row = rows |
| 1048 | | − .filter( |
| 1049 | | − (candidate) => |
| 1050 | | − scopeRank(candidate.target_scope_id as string) <= sourceScopeRank, |
| 1051 | | − ) |
| 1078 | + .filter((candidate) => scopeRank(sourceBindingTargetScope(candidate)) <= sourceScopeRank) |
| 1052 | 1079 | .sort( |
| 1053 | 1080 | (a, b) => |
| 1054 | | − scopeRank(a.target_scope_id as string) - |
| 1055 | | − scopeRank(b.target_scope_id as string), |
| 1081 | + scopeRank(sourceBindingTargetScope(a)) - scopeRank(sourceBindingTargetScope(b)), |
| 1056 | 1082 | )[0]; |
| 1057 | 1083 | return row ? rowToSourceBinding(row) : null; |
| 1058 | 1084 | }), |
| 1059 | 1085 | |
| 1060 | 1086 | setSourceBinding: (input) => |
| 1061 | 1087 | Effect.gen(function* () { |
| 1088 | + const sourceScope = String(input.sourceScope); |
| 1089 | + const targetScope = String(input.scope); |
| 1062 | 1090 | yield* validateBindingTarget({ |
| 1063 | 1091 | sourceId: input.sourceId, |
| 1064 | | − sourceScope: input.sourceScope as string, |
| 1065 | | − targetScope: input.scope as string, |
| 1092 | + sourceScope, |
| 1093 | + targetScope, |
| 1066 | 1094 | }); |
| 1067 | | − const id = sourceBindingRowId( |
| 1068 | | − input.sourceId, |
| 1069 | | − input.sourceScope as string, |
| 1070 | | − input.slot, |
| 1071 | | − input.scope as string, |
| 1072 | | − ); |
| 1095 | + const id = sourceBindingRowId(input.sourceId, sourceScope, input.slot, targetScope); |
| 1073 | 1096 | const now = new Date(); |
| 1074 | 1097 | const valueColumns = sourceBindingValueColumns(input.value); |
| 1075 | 1098 | yield* adapter.delete({ |
| @@ -1081,8 +1104,8 @@ export const makeDefaultOpenapiStore = ({ · 5 unmodified lines |
| 1081 | 1104 | data: { |
| 1082 | 1105 | id, |
| 1083 | 1106 | source_id: input.sourceId, |
| 1084 | | − source_scope_id: input.sourceScope as string, |
| 1085 | | − target_scope_id: input.scope as string, |
| 1107 | + source_scope_id: sourceScope, |
| 1108 | + target_scope_id: targetScope, |
| 1086 | 1109 | slot: input.slot, |
| 1087 | 1110 | ...valueColumns, |
| 1088 | 1111 | created_at: now, |
| @@ -1139,8 +1162,14 @@ export const makeDefaultOpenapiStore = ({ · 50 unmodified lines |
| 1139 | 1162 | Effect.gen(function* () { |
| 1140 | 1163 | const tables = [ |
| 1141 | 1164 | { model: "openapi_source_query_param" as const, kind: "query_param" as const }, |
| 1142 | | − { model: "openapi_source_spec_fetch_header" as const, kind: "spec_fetch_header" as const }, |
| 1143 | | − { model: "openapi_source_spec_fetch_query_param" as const, kind: "spec_fetch_query_param" as const }, |
| 1165 | + { |
| 1166 | + model: "openapi_source_spec_fetch_header" as const, |
| 1167 | + kind: "spec_fetch_header" as const, |
| 1168 | + }, |
| 1169 | + { |
| 1170 | + model: "openapi_source_spec_fetch_query_param" as const, |
| 1171 | + kind: "spec_fetch_query_param" as const, |
| 1172 | + }, |
| 1144 | 1173 | ]; |
| 1145 | 1174 | const perTable = yield* Effect.forEach( |
| 1146 | 1175 | tables, |
| @@ -1152,12 +1181,15 @@ export const makeDefaultOpenapiStore = ({ · 5 unmodified lines |
| 1152 | 1181 | }) |
| 1153 | 1182 | .pipe( |
| 1154 | 1183 | Effect.map((rows) => |
| 1155 | | − rows.map((r) => ({ |
| 1156 | | − kind: t.kind, |
| 1157 | | − source_id: r.source_id as string, |
| 1158 | | − scope_id: r.scope_id as string, |
| 1159 | | − name: r.name as string, |
| 1160 | | − })), |
| 1184 | + rows.map((r) => { |
| 1185 | + const row = decodeChildUsageStorageRow(r); |
| 1186 | + return { |
| 1187 | + kind: t.kind, |
| 1188 | + source_id: row.source_id, |
| 1189 | + scope_id: row.scope_id, |
| 1190 | + name: row.name, |
| 1191 | + }; |
| 1192 | + }), |
| 1161 | 1193 | ), |
| 1162 | 1194 | ), |
| 1163 | 1195 | { concurrency: "unbounded" }, |
| @@ -1172,8 +1204,9 @@ export const makeDefaultOpenapiStore = ({ · 8 unmodified lines |
| 1172 | 1204 | const requested = new Set(keys); |
| 1173 | 1205 | const out = new Map<string, string>(); |
| 1174 | 1206 | for (const r of rows) { |
| 1175 | | − const key = `${r.scope_id as string}:${r.id as string}`; |
| 1176 | | − if (requested.has(key)) out.set(key, r.name as string); |
| 1207 | + const row = decodeSourceNameStorageRow(r); |
| 1208 | + const key = `${row.scope_id}:${row.id}`; |
| 1209 | + if (requested.has(key)) out.set(key, row.name); |
| 1177 | 1210 | } |
| 1178 | 1211 | return out; |
| 1179 | 1212 | }), |