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