History

Merge remote-tracking branch 'origin/codex/batch18-openapi-boundaries' into codex/effect-lint-integration

# Conflicts: # packages/plugins/openapi/src/react/AddOpenApiSource.tsx # packages/plugins/openapi/src/react/EditOpenApiSource.tsx

Rhys Sullivan committed 6 months ago

Commit 7257c892 parents 3f7841a 9494cb43 files · +407 −401Browse files

Unified · Split

3 files
  1. packages/plugins/openapi/src/react/AddOpenApiSource.tsx +82 −100
  2. packages/plugins/openapi/src/react/EditOpenApiSource.tsx +82 −91
  3. packages/plugins/openapi/src/sdk/store.ts +243 −210
packages/plugins/openapi/src/react/AddOpenApiSource.tsx+82−100
@@ -1,6 +1,8 @@
1import { useCallback, useEffect, useRef, useState } from "react";1import { useCallback, useEffect, useRef, useState } from "react";
2import { useAtomSet } from "@effect/atom-react";2import { useAtomSet } from "@effect/atom-react";
3import { Effect, Exit, Option, Schema } from "effect";3import * as Exit from "effect/Exit";
4import * as Option from "effect/Option";
5import * as Schema from "effect/Schema";
4 6
5import { ConnectionId, ScopeId, SecretId } from "@executor-js/sdk/core";7import { ConnectionId, ScopeId, SecretId } from "@executor-js/sdk/core";
6import { startOAuth } from "@executor-js/react/api/atoms";8import { startOAuth } from "@executor-js/react/api/atoms";
@@ -81,33 +83,19 @@ import { · 74 unmodified lines
81 type ServerVariable,83 type ServerVariable,
82} from "../sdk/types";84} from "../sdk/types";
83 85
84export const OPENAPI_OAUTH_POPUP_NAME = "openapi-oauth";86const ErrorMessage = Schema.Struct({ message: Schema.String });
85export const OPENAPI_OAUTH_CALLBACK_PATH = "/api/oauth/callback";
86const PublicErrorMessage = Schema.Struct({
87 _tag: Schema.Literals(["OpenApiParseError", "OpenApiExtractionError", "OpenApiOAuthError"]),
88 message: Schema.String,
89});
90
91const messageFromExit = (exit: Exit.Exit<unknown, unknown>, fallback: string): string => {
92 const error = Exit.findErrorOption(exit);
93 if (Option.isNone(error)) return fallback;
94 const errorMessage = Schema.decodeUnknownOption(PublicErrorMessage)(error.value);
95 return Option.match(errorMessage, {
96 onNone: () => fallback,
97 onSome: (value) => value.message,
98 });
99};
100 87
101const failPromise = <A,>(message: string): Promise<A> => Effect.runPromise(Effect.fail(message));88const errorMessageFromExit = (exit: Exit.Exit<unknown, unknown>, fallback: string): string =>
89 Option.match(
90 Option.flatMap(Exit.findErrorOption(exit), Schema.decodeUnknownOption(ErrorMessage)),
91 {
92 onNone: () => fallback,
93 onSome: ({ message }) => message,
94 },
95 );
102 96
103const parseUrlOption = (url: string, baseUrl?: string): URL | null => {97export const OPENAPI_OAUTH_POPUP_NAME = "openapi-oauth";
104 // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: URL constructor is the platform URL parser98export const OPENAPI_OAUTH_CALLBACK_PATH = "/api/oauth/callback";
105 try {
106 return baseUrl === undefined ? new URL(url) : new URL(url, baseUrl);
107 } catch {
108 return null;
109 }
110};
111 99
112const substituteUrlVariables = (url: string, values: Record<string, string>): string => {100const substituteUrlVariables = (url: string, values: Record<string, string>): string => {
113 let out = url;101 let out = url;
@@ -134,15 +122,15 @@ export const openApiOAuthConnectionId = ( · 20 unmodified lines
134 */122 */
135export function resolveOAuthUrl(url: string, baseUrl: string): string {123export function resolveOAuthUrl(url: string, baseUrl: string): string {
136 if (!url) return url;124 if (!url) return url;
137 if (parseUrlOption(url)) {125 if (URL.canParse(url)) {
138 return url;126 return url;
139 }127 }
140 if (!baseUrl) return url;128 if (!baseUrl || !URL.canParse(url, baseUrl)) return url;
141 return parseUrlOption(url, baseUrl)?.toString() ?? url;129 return new URL(url, baseUrl).toString();
142}130}
143 131
144export function inferOAuthIssuerUrl(authorizationUrl: string): string | null {132export function inferOAuthIssuerUrl(authorizationUrl: string): string | null {
145 return parseUrlOption(authorizationUrl)?.origin ?? null;133 return URL.canParse(authorizationUrl) ? new URL(authorizationUrl).origin : null;
146}134}
147 135
148type StrategySelection =136type StrategySelection =
@@ -293,7 +281,7 @@ export default function AddOpenApiSource(props: { · 144 unmodified lines
293 selectedServerIndex >= 0 ? (servers[selectedServerIndex] ?? null) : null;281 selectedServerIndex >= 0 ? (servers[selectedServerIndex] ?? null) : null;
294 282
295 const serverVariables: Record<string, ServerVariable> = selectedServer283 const serverVariables: Record<string, ServerVariable> = selectedServer
296 ? Option.getOrElse(selectedServer.variables, () => ({}) as Record<string, ServerVariable>)284 ? Option.getOrElse(selectedServer.variables, () => ({}))
297 : {};285 : {};
298 const serverVariableEntries: Array<[string, ServerVariable]> = Object.entries(serverVariables);286 const serverVariableEntries: Array<[string, ServerVariable]> = Object.entries(serverVariables);
299 287
@@ -305,10 +293,7 @@ export default function AddOpenApiSource(props: { · 5 unmodified lines
305 // Helper used by analyze + server selection: build a default selection map293 // Helper used by analyze + server selection: build a default selection map
306 // from a server's variable defaults.294 // from a server's variable defaults.
307 const defaultSelectionsFor = (server: ServerInfo): Record<string, string> => {295 const defaultSelectionsFor = (server: ServerInfo): Record<string, string> => {
308 const vars: Record<string, ServerVariable> = Option.getOrElse(296 const vars: Record<string, ServerVariable> = Option.getOrElse(server.variables, () => ({}));
309 server.variables,
310 () => ({}) as Record<string, ServerVariable>,
311 );
312 const out: Record<string, string> = {};297 const out: Record<string, string> = {};
313 for (const [name, v] of Object.entries(vars)) out[name] = v.default;298 for (const [name, v] of Object.entries(vars)) out[name] = v.default;
314 return out;299 return out;
@@ -387,19 +372,20 @@ export default function AddOpenApiSource(props: { · 72 unmodified lines
387 setAnalyzeError(null);372 setAnalyzeError(null);
388 setAddError(null);373 setAddError(null);
389 const credentials = serializeHttpCredentials(specFetchCredentials);374 const credentials = serializeHttpCredentials(specFetchCredentials);
390 const previewExit = await doPreview({375 const exit = await doPreview({
391 params: { scopeId },376 params: { scopeId },
392 payload: {377 payload: {
393 spec: specUrl,378 spec: specUrl,
394 specFetchCredentials: credentials,379 specFetchCredentials: credentials,
395 },380 },
396 });381 });
397 if (Exit.isFailure(previewExit)) {382 if (Exit.isFailure(exit)) {
398 setAnalyzeError(messageFromExit(previewExit, "Failed to parse spec"));383 setAnalyzeError(errorMessageFromExit(exit, "Failed to parse spec"));
399 setAnalyzing(false);384 setAnalyzing(false);
400 return;385 return;
401 }386 }
402 const result = previewExit.value;387
388 const result = exit.value;
403 setPreview(result);389 setPreview(result);
404 390
405 const firstServer = result.servers[0];391 const firstServer = result.servers[0];
@@ -500,7 +486,7 @@ export default function AddOpenApiSource(props: { · 94 unmodified lines
500 }486 }
501 setStartingOAuth(true);487 setStartingOAuth(true);
502 const connectionId = openApiOAuthConnectionId(resolvedSourceId, selectedOAuth2Preset.flow);488 const connectionId = openApiOAuthConnectionId(resolvedSourceId, selectedOAuth2Preset.flow);
503 const startExit = await doStartOAuth({489 const startOAuthExit = await doStartOAuth({
504 params: { scopeId },490 params: { scopeId },
505 payload: {491 payload: {
506 endpoint: tokenUrl,492 endpoint: tokenUrl,
@@ -519,11 +505,11 @@ export default function AddOpenApiSource(props: { · 12 unmodified lines
519 },505 },
520 });506 });
521 setStartingOAuth(false);507 setStartingOAuth(false);
522 if (Exit.isFailure(startExit)) {508 if (Exit.isFailure(startOAuthExit)) {
523 setOauth2Error(messageFromExit(startExit, "Failed to start OAuth"));509 setOauth2Error(errorMessageFromExit(startOAuthExit, "Failed to start OAuth"));
524 return;510 return;
525 }511 }
526 const response = startExit.value;512 const response = startOAuthExit.value;
527 if (!response.completedConnection) {513 if (!response.completedConnection) {
528 setOauth2Error("client_credentials flow did not mint a connection");514 setOauth2Error("client_credentials flow did not mint a connection");
529 return;515 return;
@@ -551,41 +537,41 @@ export default function AddOpenApiSource(props: { · 21 unmodified lines
551 resolvedBaseUrl,537 resolvedBaseUrl,
552 );538 );
553 const issuerUrl = inferOAuthIssuerUrl(authorizationUrl);539 const issuerUrl = inferOAuthIssuerUrl(authorizationUrl);
540 const startOAuthExit = await doStartOAuth({
541 params: { scopeId },
542 payload: {
543 endpoint: authorizationUrl,
544 connectionId: openApiOAuthConnectionId(resolvedSourceId, selectedOAuth2Preset.flow),
545 tokenScope: scopeId,
546 redirectUrl: oauth2RedirectUrl,
547 strategy: {
548 kind: "authorization-code",
549 authorizationEndpoint: authorizationUrl,
550 tokenEndpoint: tokenUrl,
551 issuerUrl,
552 clientIdSecretId: oauth2ClientIdSecretId,
553 clientSecretSecretId: oauth2ClientSecretSecretId ?? null,
554 scopes: [...oauth2SelectedScopes],
555 },
556 pluginId: "openapi",
557 identityLabel: `${displayName} OAuth`,
558 },
559 });
560 if (Exit.isFailure(startOAuthExit)) {
561 setOauth2Error(errorMessageFromExit(startOAuthExit, "Failed to start OAuth"));
562 return;
563 }
564 const response = startOAuthExit.value;
565 if (response.authorizationUrl === null) {
566 setOauth2Error("Unexpected response flow from server");
567 return;
568 }
554 569
555 await oauth.openAuthorization({570 await oauth.openAuthorization({
556 run: async () => {571 run: async () => ({
557 const startExit = await doStartOAuth({572 sessionId: response.sessionId,
558 params: { scopeId },573 authorizationUrl: response.authorizationUrl,
559 payload: {574 }),
560 endpoint: authorizationUrl,
561 connectionId: openApiOAuthConnectionId(resolvedSourceId, selectedOAuth2Preset.flow),
562 tokenScope: scopeId,
563 redirectUrl: oauth2RedirectUrl,
564 strategy: {
565 kind: "authorization-code",
566 authorizationEndpoint: authorizationUrl,
567 tokenEndpoint: tokenUrl,
568 issuerUrl,
569 clientIdSecretId: oauth2ClientIdSecretId,
570 clientSecretSecretId: oauth2ClientSecretSecretId ?? null,
571 scopes: [...oauth2SelectedScopes],
572 },
573 pluginId: "openapi",
574 identityLabel: `${displayName} OAuth`,
575 },
576 });
577 if (Exit.isFailure(startExit)) {
578 return failPromise(messageFromExit(startExit, "Failed to start OAuth"));
579 }
580 const response = startExit.value;
581 if (response.authorizationUrl === null) {
582 return failPromise("Unexpected response flow from server");
583 }
584 return {
585 sessionId: response.sessionId,
586 authorizationUrl: response.authorizationUrl,
587 };
588 },
589 onSuccess: (result) => {575 onSuccess: (result) => {
590 setOauth2AuthState({576 setOauth2AuthState({
591 fingerprint: selectedOAuth2Fingerprint,577 fingerprint: selectedOAuth2Fingerprint,
@@ -641,7 +627,12 @@ export default function AddOpenApiSource(props: { · 49 unmodified lines
641 kind: "openapi",627 kind: "openapi",
642 url: resolvedBaseUrl || undefined,628 url: resolvedBaseUrl || undefined,
643 });629 });
644 const addExit = await doAdd({630 const failAdd = (message: string) => {
631 setAddError(message);
632 setAdding(false);
633 placeholder.done();
634 };
635 const resultExit = await doAdd({
645 params: { scopeId },636 params: { scopeId },
646 payload: {637 payload: {
647 spec: specUrl,638 spec: specUrl,
@@ -657,14 +648,13 @@ export default function AddOpenApiSource(props: { · 9 unmodified lines
657 },648 },
658 reactivityKeys: addSpecWriteKeys,649 reactivityKeys: addSpecWriteKeys,
659 });650 });
660 if (Exit.isFailure(addExit)) {651
661 placeholder.done();652 if (Exit.isFailure(resultExit)) {
662 setAddError(messageFromExit(addExit, "Failed to add source"));653 failAdd(errorMessageFromExit(resultExit, "Failed to add source"));
663 setAdding(false);
664 return;654 return;
665 }655 }
666 656
667 const sourceId = addExit.value.namespace;657 const sourceId = resultExit.value.namespace;
668 const sourceScope = ScopeId.make(scopeId);658 const sourceScope = ScopeId.make(scopeId);
669 const bindingScope = ScopeId.make(userScope);659 const bindingScope = ScopeId.make(userScope);
670 660
@@ -684,15 +674,13 @@ export default function AddOpenApiSource(props: { · 13 unmodified lines
684 reactivityKeys: bindingWriteKeys,674 reactivityKeys: bindingWriteKeys,
685 });675 });
686 if (Exit.isFailure(bindingExit)) {676 if (Exit.isFailure(bindingExit)) {
687 placeholder.done();677 failAdd(errorMessageFromExit(bindingExit, "Failed to add source"));
688 setAddError(messageFromExit(bindingExit, "Failed to add source"));
689 setAdding(false);
690 return;678 return;
691 }679 }
692 }680 }
693 681
694 if (configuredOAuth2 && oauth2ClientIdSecretId) {682 if (configuredOAuth2 && oauth2ClientIdSecretId) {
695 const bindingExit = await doSetBinding({683 const clientIdBindingExit = await doSetBinding({
696 params: { scopeId },684 params: { scopeId },
697 payload: {685 payload: {
698 sourceId,686 sourceId,
@@ -706,16 +694,14 @@ export default function AddOpenApiSource(props: { · 7 unmodified lines
706 },694 },
707 reactivityKeys: bindingWriteKeys,695 reactivityKeys: bindingWriteKeys,
708 });696 });
709 if (Exit.isFailure(bindingExit)) {697 if (Exit.isFailure(clientIdBindingExit)) {
710 placeholder.done();698 failAdd(errorMessageFromExit(clientIdBindingExit, "Failed to add source"));
711 setAddError(messageFromExit(bindingExit, "Failed to add source"));
712 setAdding(false);
713 return;699 return;
714 }700 }
715 }701 }
716 702
717 if (configuredOAuth2?.clientSecretSlot && oauth2ClientSecretSecretId) {703 if (configuredOAuth2?.clientSecretSlot && oauth2ClientSecretSecretId) {
718 const bindingExit = await doSetBinding({704 const clientSecretBindingExit = await doSetBinding({
719 params: { scopeId },705 params: { scopeId },
720 payload: {706 payload: {
721 sourceId,707 sourceId,
@@ -729,16 +715,14 @@ export default function AddOpenApiSource(props: { · 7 unmodified lines
729 },715 },
730 reactivityKeys: bindingWriteKeys,716 reactivityKeys: bindingWriteKeys,
731 });717 });
732 if (Exit.isFailure(bindingExit)) {718 if (Exit.isFailure(clientSecretBindingExit)) {
733 placeholder.done();719 failAdd(errorMessageFromExit(clientSecretBindingExit, "Failed to add source"));
734 setAddError(messageFromExit(bindingExit, "Failed to add source"));
735 setAdding(false);
736 return;720 return;
737 }721 }
738 }722 }
739 723
740 if (configuredOAuth2 && oauth2Auth) {724 if (configuredOAuth2 && oauth2Auth) {
741 const bindingExit = await doSetBinding({725 const connectionBindingExit = await doSetBinding({
742 params: { scopeId },726 params: { scopeId },
743 payload: {727 payload: {
744 sourceId,728 sourceId,
@@ -752,10 +736,8 @@ export default function AddOpenApiSource(props: { · 7 unmodified lines
752 },736 },
753 reactivityKeys: bindingWriteKeys,737 reactivityKeys: bindingWriteKeys,
754 });738 });
755 if (Exit.isFailure(bindingExit)) {739 if (Exit.isFailure(connectionBindingExit)) {
756 placeholder.done();740 failAdd(errorMessageFromExit(connectionBindingExit, "Failed to add source"));
757 setAddError(messageFromExit(bindingExit, "Failed to add source"));
758 setAdding(false);
759 return;741 return;
760 }742 }
761 }743 }
packages/plugins/openapi/src/react/EditOpenApiSource.tsx+82−91
@@ -1,6 +1,8 @@
1import { useEffect, useMemo, useRef, useState } from "react";1import { useEffect, useMemo, useRef, useState } from "react";
2import { useAtomSet, useAtomValue } from "@effect/atom-react";2import { useAtomSet, useAtomValue } from "@effect/atom-react";
3import { Effect, Exit, Option, Schema } from "effect";3import * as Exit from "effect/Exit";
4import * as Option from "effect/Option";
5import * as Schema from "effect/Schema";
4import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";6import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
5 7
6import { connectionsAtom, sourceAtom, startOAuth } from "@executor-js/react/api/atoms";8import { connectionsAtom, sourceAtom, startOAuth } from "@executor-js/react/api/atoms";
@@ -43,7 +45,21 @@ import { · 36 unmodified lines
43 resolveOAuthUrl,45 resolveOAuthUrl,
44} from "./AddOpenApiSource";46} from "./AddOpenApiSource";
45import { oauth2ClientSecretSlot } from "../sdk/store";47import { oauth2ClientSecretSlot } from "../sdk/store";
46import type { OpenApiSourceBindingValue } from "../sdk/types";48import {
49 OpenApiSourceBindingValue,
50 type OpenApiSourceBindingValue as OpenApiSourceBindingValueType,
51} from "../sdk/types";
52
53const ErrorMessage = Schema.Struct({ message: Schema.String });
54
55const errorMessageFromExit = (exit: Exit.Exit<unknown, unknown>, fallback: string): string =>
56 Option.match(
57 Option.flatMap(Exit.findErrorOption(exit), Schema.decodeUnknownOption(ErrorMessage)),
58 {
59 onNone: () => fallback,
60 onSome: ({ message }) => message,
61 },
62 );
47 63
48type SlotDef =64type SlotDef =
49 | {65 | {
@@ -86,31 +102,6 @@ const openApiOAuthConnectionId = ( · 36 unmodified lines
86const bindingSecretId = (sourceId: string, slot: string, scopeId: string): string =>102const bindingSecretId = (sourceId: string, slot: string, scopeId: string): string =>
87 `source-binding-${slugify(sourceId)}-${slugify(slot)}-${slugify(scopeId)}`;103 `source-binding-${slugify(sourceId)}-${slugify(slot)}-${slugify(scopeId)}`;
88 104
89const PublicErrorMessage = Schema.Struct({
90 _tag: Schema.Literals(["OpenApiParseError", "OpenApiExtractionError", "OpenApiOAuthError"]),
91 message: Schema.String,
92});
93const SecretBindingValue = Schema.Struct({
94 kind: Schema.Literal("secret"),
95 secretId: Schema.String,
96});
97const ConnectionBindingValue = Schema.Struct({
98 kind: Schema.Literal("connection"),
99 connectionId: Schema.String,
100});
101
102const messageFromExit = (exit: Exit.Exit<unknown, unknown>, fallback: string): string => {
103 const error = Exit.findErrorOption(exit);
104 if (Option.isNone(error)) return fallback;
105 const errorMessage = Schema.decodeUnknownOption(PublicErrorMessage)(error.value);
106 return Option.match(errorMessage, {
107 onNone: () => fallback,
108 onSome: (value) => value.message,
109 });
110};
111
112const failPromise = <A,>(message: string): Promise<A> => Effect.runPromise(Effect.fail(message));
113
114const effectiveClientSecretSlot = (oauth2: {105const effectiveClientSecretSlot = (oauth2: {
115 readonly securitySchemeName: string;106 readonly securitySchemeName: string;
116 readonly clientSecretSlot: string | null;107 readonly clientSecretSlot: string | null;
@@ -145,13 +136,13 @@ const effectiveBindingForScope = ( · 28 unmodified lines
145 136
146const isSecretBindingValue = (137const isSecretBindingValue = (
147 value: unknown,138 value: unknown,
148): value is Extract<OpenApiSourceBindingValue, { readonly kind: "secret" }> =>139): value is Extract<OpenApiSourceBindingValueType, { readonly kind: "secret" }> =>
149 Option.isSome(Schema.decodeUnknownOption(SecretBindingValue)(value));140 Schema.is(OpenApiSourceBindingValue)(value) && value.kind === "secret";
150 141
151const isConnectionBindingValue = (142const isConnectionBindingValue = (
152 value: unknown,143 value: unknown,
153): value is Extract<OpenApiSourceBindingValue, { readonly kind: "connection" }> =>144): value is Extract<OpenApiSourceBindingValueType, { readonly kind: "connection" }> =>
154 Option.isSome(Schema.decodeUnknownOption(ConnectionBindingValue)(value));145 Schema.is(OpenApiSourceBindingValue)(value) && value.kind === "connection";
155 146
156export default function EditOpenApiSource(props: {147export default function EditOpenApiSource(props: {
157 readonly sourceId: string;148 readonly sourceId: string;
@@ -253,13 +244,12 @@ export default function EditOpenApiSource(props: { · 95 unmodified lines
253 },244 },
254 reactivityKeys: openApiWriteKeys,245 reactivityKeys: openApiWriteKeys,
255 });246 });
247 if (sourceSaveSeq.current !== seq) return;
256 if (Exit.isFailure(exit)) {248 if (Exit.isFailure(exit)) {
257 if (sourceSaveSeq.current !== seq) return;
258 setSourceSaveState("idle");249 setSourceSaveState("idle");
259 setError(messageFromExit(exit, "Failed to save source details"));250 setError(errorMessageFromExit(exit, "Failed to save source details"));
260 return;251 return;
261 }252 }
262 if (sourceSaveSeq.current !== seq) return;
263 setSourceSaveState("saved");253 setSourceSaveState("saved");
264 window.setTimeout(() => {254 window.setTimeout(() => {
265 if (sourceSaveSeq.current === seq) setSourceSaveState("idle");255 if (sourceSaveSeq.current === seq) setSourceSaveState("idle");
@@ -352,7 +342,7 @@ export default function EditOpenApiSource(props: { · 86 unmodified lines
352 reactivityKeys: sourceWriteKeys,342 reactivityKeys: sourceWriteKeys,
353 });343 });
354 if (Exit.isFailure(exit)) {344 if (Exit.isFailure(exit)) {
355 setError(messageFromExit(exit, "Failed to save credential binding"));345 setError(errorMessageFromExit(exit, "Failed to save credential binding"));
356 }346 }
357 setBusyKey(null);347 setBusyKey(null);
358 };348 };
@@ -371,7 +361,7 @@ export default function EditOpenApiSource(props: { · 12 unmodified lines
371 reactivityKeys: sourceWriteKeys,361 reactivityKeys: sourceWriteKeys,
372 });362 });
373 if (Exit.isFailure(exit)) {363 if (Exit.isFailure(exit)) {
374 setError(messageFromExit(exit, "Failed to clear credential binding"));364 setError(errorMessageFromExit(exit, "Failed to clear credential binding"));
375 }365 }
376 setBusyKey(null);366 setBusyKey(null);
377 };367 };
@@ -425,18 +415,23 @@ export default function EditOpenApiSource(props: { · 47 unmodified lines
425 setPendingOAuthConnection({415 setPendingOAuthConnection({
426 scopeId: targetScope,416 scopeId: targetScope,
427 slot: oauth2.connectionSlot,417 slot: oauth2.connectionSlot,
428 connectionId,418 connectionId: connectionId,
429 });419 });
430 setError(null);420 setError(null);
421 const failConnect = (message: string) => {
422 setError(message);
423 setPendingOAuthConnection(null);
424 setBusyKey(null);
425 };
431 const displayName = source.name;426 const displayName = source.name;
432 const tokenUrl = resolveOAuthUrl(oauth2.tokenUrl, source.config.baseUrl ?? "");427 const tokenUrl = resolveOAuthUrl(oauth2.tokenUrl, source.config.baseUrl ?? "");
433 if (oauth2.flow === "clientCredentials") {428 if (oauth2.flow === "clientCredentials") {
434 const startExit = await doStartOAuth({429 const startOAuthExit = await doStartOAuth({
435 params: { scopeId: displayScope },430 params: { scopeId: displayScope },
436 payload: {431 payload: {
437 endpoint: tokenUrl,432 endpoint: tokenUrl,
438 redirectUrl: tokenUrl,433 redirectUrl: tokenUrl,
439 connectionId,434 connectionId: connectionId,
440 tokenScope: targetScope,435 tokenScope: targetScope,
441 strategy: {436 strategy: {
442 kind: "client-credentials",437 kind: "client-credentials",
@@ -449,20 +444,16 @@ export default function EditOpenApiSource(props: { · 6 unmodified lines
449 identityLabel: `${displayName} OAuth`,444 identityLabel: `${displayName} OAuth`,
450 },445 },
451 });446 });
452 if (Exit.isFailure(startExit)) {447 if (Exit.isFailure(startOAuthExit)) {
453 setError(messageFromExit(startExit, "Failed to connect OAuth"));448 failConnect(errorMessageFromExit(startOAuthExit, "Failed to connect OAuth"));
454 setPendingOAuthConnection(null);
455 setBusyKey(null);
456 return;449 return;
457 }450 }
458 const response = startExit.value;451 const response = startOAuthExit.value;
459 if (!response.completedConnection) {452 if (!response.completedConnection) {
460 setError("Unexpected OAuth response");453 failConnect("Unexpected OAuth response");
461 setPendingOAuthConnection(null);
462 setBusyKey(null);
463 return;454 return;
464 }455 }
465 const bindingExit = await doSetBinding({456 const setBindingExit = await doSetBinding({
466 params: { scopeId: displayScope },457 params: { scopeId: displayScope },
467 payload: {458 payload: {
468 sourceId: props.sourceId,459 sourceId: props.sourceId,
@@ -476,8 +467,9 @@ export default function EditOpenApiSource(props: { · 7 unmodified lines
476 },467 },
477 reactivityKeys: [...sourceWriteKeys, ...connectionWriteKeys],468 reactivityKeys: [...sourceWriteKeys, ...connectionWriteKeys],
478 });469 });
479 if (Exit.isFailure(bindingExit)) {470 if (Exit.isFailure(setBindingExit)) {
480 setError(messageFromExit(bindingExit, "Failed to connect OAuth"));471 failConnect(errorMessageFromExit(setBindingExit, "Failed to connect OAuth"));
472 return;
481 }473 }
482 setPendingOAuthConnection(null);474 setPendingOAuthConnection(null);
483 setBusyKey(null);475 setBusyKey(null);
@@ -489,45 +481,46 @@ export default function EditOpenApiSource(props: { · 5 unmodified lines
489 source.config.baseUrl ?? "",481 source.config.baseUrl ?? "",
490 );482 );
491 const issuerUrl = oauth2.issuerUrl ?? inferOAuthIssuerUrl(authorizationUrl);483 const issuerUrl = oauth2.issuerUrl ?? inferOAuthIssuerUrl(authorizationUrl);
492 await oauth.openAuthorization({484 const startOAuthExit = await doStartOAuth({
493 run: async () => {485 params: { scopeId: displayScope },
494 const startExit = await doStartOAuth({486 payload: {
495 params: { scopeId: displayScope },487 endpoint: authorizationUrl,
496 payload: {488 connectionId,
497 endpoint: authorizationUrl,489 tokenScope: targetScope,
498 connectionId,490 redirectUrl: oauth2RedirectUrl,
499 tokenScope: targetScope,491 strategy: {
500 redirectUrl: oauth2RedirectUrl,492 kind: "authorization-code",
501 strategy: {493 authorizationEndpoint: authorizationUrl,
502 kind: "authorization-code",494 tokenEndpoint: tokenUrl,
503 authorizationEndpoint: authorizationUrl,495 issuerUrl,
504 tokenEndpoint: tokenUrl,496 clientIdSecretId,
505 issuerUrl,497 clientSecretSecretId:
506 clientIdSecretId,498 clientSecretBinding && isSecretBindingValue(clientSecretBinding.value)
507 clientSecretSecretId:499 ? clientSecretBinding.value.secretId
508 clientSecretBinding && isSecretBindingValue(clientSecretBinding.value)500 : null,
509 ? clientSecretBinding.value.secretId501 scopes: [...oauth2.scopes],
510 : null,502 },
511 scopes: [...oauth2.scopes],503 pluginId: "openapi",
512 },504 identityLabel: `${displayName} OAuth`,
513 pluginId: "openapi",
514 identityLabel: `${displayName} OAuth`,
515 },
516 });
517 if (Exit.isFailure(startExit)) {
518 return failPromise(messageFromExit(startExit, "Failed to connect OAuth"));
519 }
520 const response = startExit.value;
521 if (response.authorizationUrl === null) {
522 return failPromise("Unexpected OAuth response");
523 }
524 return {
525 sessionId: response.sessionId,
526 authorizationUrl: response.authorizationUrl,
527 };
528 },505 },
506 });
507 if (Exit.isFailure(startOAuthExit)) {
508 failConnect(errorMessageFromExit(startOAuthExit, "Failed to connect OAuth"));
509 return;
510 }
511 const response = startOAuthExit.value;
512 if (response.authorizationUrl === null) {
513 failConnect("Unexpected OAuth response");
514 return;
515 }
516
517 await oauth.openAuthorization({
518 run: async () => ({
519 sessionId: response.sessionId,
520 authorizationUrl: response.authorizationUrl,
521 }),
529 onSuccess: async (result) => {522 onSuccess: async (result) => {
530 const bindingExit = await doSetBinding({523 const setBindingExit = await doSetBinding({
531 params: { scopeId: displayScope },524 params: { scopeId: displayScope },
532 payload: {525 payload: {
533 sourceId: props.sourceId,526 sourceId: props.sourceId,
@@ -541,10 +534,8 @@ export default function EditOpenApiSource(props: { · 7 unmodified lines
541 },534 },
542 reactivityKeys: [...sourceWriteKeys, ...connectionWriteKeys],535 reactivityKeys: [...sourceWriteKeys, ...connectionWriteKeys],
543 });536 });
544 if (Exit.isFailure(bindingExit)) {537 if (Exit.isFailure(setBindingExit)) {
545 setError(messageFromExit(bindingExit, "Failed to connect OAuth"));538 failConnect(errorMessageFromExit(setBindingExit, "Failed to connect OAuth"));
546 setPendingOAuthConnection(null);
547 setBusyKey(null);
548 return;539 return;
549 }540 }
550 setPendingOAuthConnection(null);541 setPendingOAuthConnection(null);
packages/plugins/openapi/src/sdk/store.ts+243−210
@@ -1,4 +1,4 @@
1import { Effect, Schema } from "effect";1import { Effect, Option, Schema } from "effect";
2 2
3import {3import {
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
177export class StoredSourceSchema extends Schema.Class<StoredSourceSchema>(177export 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 credentials193 // Canonical source-owned OAuth config. Concrete client credentials
@@ -221,10 +211,85 @@ export interface StoredOperation { · 17 unmodified lines
221 211
222const encodeBinding = Schema.encodeSync(OperationBinding);212const encodeBinding = Schema.encodeSync(OperationBinding);
223const decodeBinding = Schema.decodeUnknownSync(OperationBinding);213const decodeBinding = Schema.decodeUnknownSync(OperationBinding);
214const decodeBindingJson = Schema.decodeUnknownSync(Schema.fromJsonString(OperationBinding));
224 215
225const decodeOAuth2 = Schema.decodeUnknownSync(OAuth2Auth);216const decodeOAuth2 = Schema.decodeUnknownSync(OAuth2Auth);
217const decodeOAuth2Option = Schema.decodeUnknownOption(OAuth2Auth);
218const decodeOAuth2JsonOption = Schema.decodeUnknownOption(Schema.fromJsonString(OAuth2Auth));
219const decodeOAuth2SourceConfigOption = Schema.decodeUnknownOption(OAuth2SourceConfig);
220const decodeOAuth2SourceConfigJsonOption = Schema.decodeUnknownOption(
221 Schema.fromJsonString(OAuth2SourceConfig),
222);
226const encodeOAuth2SourceConfig = Schema.encodeSync(OAuth2SourceConfig);223const encodeOAuth2SourceConfig = Schema.encodeSync(OAuth2SourceConfig);
227 224
225const decodeHeaderValueOption = Schema.decodeUnknownOption(HeaderValue);
226const UnknownRecord = Schema.Record(Schema.String, Schema.Unknown);
227const decodeUnknownRecord = Schema.decodeUnknownSync(UnknownRecord);
228const decodeUnknownRecordJson = Schema.decodeUnknownSync(Schema.fromJsonString(UnknownRecord));
229const decodeConfiguredHeaderBindingOption = Schema.decodeUnknownOption(ConfiguredHeaderBinding);
230
231const NullableString = Schema.NullOr(Schema.String);
232const OptionalNullableString = Schema.optional(NullableString);
233
234const 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});
241const decodeChildStorageRowOption = Schema.decodeUnknownOption(ChildStorageRow);
242
243const 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});
255const decodeSourceBindingStorageRow = Schema.decodeUnknownSync(SourceBindingStorageRow);
256
257const 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});
267const decodeSourceStorageRow = Schema.decodeUnknownSync(SourceStorageRow);
268
269const OperationStorageRow = Schema.Struct({
270 id: Schema.String,
271 source_id: Schema.String,
272 binding: Schema.Unknown,
273});
274const decodeOperationStorageRow = Schema.decodeUnknownSync(OperationStorageRow);
275
276const ChildUsageStorageRow = Schema.Struct({
277 source_id: Schema.String,
278 scope_id: Schema.String,
279 name: Schema.String,
280});
281const decodeChildUsageStorageRow = Schema.decodeUnknownSync(ChildUsageStorageRow);
282
283const SourceNameStorageRow = Schema.Struct({
284 id: Schema.String,
285 scope_id: Schema.String,
286 name: Schema.String,
287});
288const decodeSourceNameStorageRow = Schema.decodeUnknownSync(SourceNameStorageRow);
289
290const decodeStorageDate = (value: unknown): Date =>
291 value instanceof Date ? value : new Date(Schema.decodeUnknownSync(Schema.String)(value));
292
228interface ChildRow {293interface 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] = prefix348 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
293const 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>;362const toJsonRecord = (value: unknown): Record<string, unknown> => value as Record<string, unknown>;
295 363
296const toConfiguredHeaderBinding = (value: {364const 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
306const decodeHeaders = (value: unknown): Record<string, HeaderValue> => {374const 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
313const slugifySlotPart = (value: string): string =>380const 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 any621 // 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 Date636 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 ? undefined757 ? undefined
715 : {758 : {
716 ...(Object.keys(specFetchHeaders).length > 0759 ...(Object.keys(specFetchHeaders).length > 0 ? { headers: specFetchHeaders } : {}),
717 ? { headers: specFetchHeaders }
718 : {}),
719 ...(Object.keys(specFetchQueryParams).length > 0760 ...(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 > 0781 ...(Object.keys(normalizedHeaders.legacy).length > 0
742 ? { headers: normalizedHeaders.legacy }782 ? { headers: normalizedHeaders.legacy }
743 : {}),783 : {}),
744 ...(normalizedOAuth2.legacy784 ...(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 bulk803 // Replace the rows of one child table for a source: delete then bulk
761 // insert. Single helper so upsertSource and updateSourceMeta both804 // 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 ? value895 : 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.oauth2904 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 !== undefined957 patch.headers !== undefined ? patch.headers : (existing.config.headers ?? {});
918 ? patch.headers958 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: nextOAuth2981 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 rows1053 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 = rows1077 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 }),

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t