Skip to content

Commit ad8a632

Browse files
authored
🪟🐛 [Connector Builder] Fix manifest conversion bugs (#21828)
* add check for undefined auth type * fix authenticator equality check and add test * don't allow nested objects/arrays in refresh_request_body and add test * explicitly do not use refs in json -> yaml dump * ensure the CheckStream configuration always has valid stream names * throw on any non-string value
1 parent 3981c57 commit ad8a632

File tree

4 files changed

+110
-8
lines changed

4 files changed

+110
-8
lines changed

airbyte-webapp/src/components/connectorBuilder/manifestToBuilderForm.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,39 @@ describe("Conversion throws error when", () => {
196196
};
197197
expect(convert).toThrow("api_token value must be of the form {{ config[");
198198
});
199+
200+
it("manifest has an OAuthAuthenticator with a refresh_request_body containing non-string values", () => {
201+
const convert = () => {
202+
const manifest: ConnectorManifest = {
203+
...baseManifest,
204+
streams: [
205+
merge({}, stream1, {
206+
retriever: {
207+
requester: {
208+
authenticator: {
209+
type: "OAuthAuthenticator",
210+
client_id: "{{ config['client_id'] }}",
211+
client_secret: "{{ config['client_secret'] }}",
212+
refresh_token: "{{ config['client_refresh_token'] }}",
213+
refresh_request_body: {
214+
key1: "val1",
215+
key2: {
216+
a: 1,
217+
b: 2,
218+
},
219+
},
220+
token_refresh_endpoint: "https://api.com/refresh_token",
221+
grant_type: "client_credentials",
222+
},
223+
},
224+
},
225+
}),
226+
],
227+
};
228+
convertToBuilderFormValues(manifest, DEFAULT_BUILDER_FORM_VALUES);
229+
};
230+
expect(convert).toThrow("OAuthAuthenticator contains a refresh_request_body with non-string values");
231+
});
199232
});
200233

201234
describe("Conversion successfully results in", () => {
@@ -454,4 +487,43 @@ describe("Conversion successfully results in", () => {
454487
},
455488
});
456489
});
490+
491+
it("OAuth authenticator refresh_request_body converted to array", () => {
492+
const manifest: ConnectorManifest = {
493+
...baseManifest,
494+
streams: [
495+
merge({}, stream1, {
496+
retriever: {
497+
requester: {
498+
authenticator: {
499+
type: "OAuthAuthenticator",
500+
client_id: "{{ config['client_id'] }}",
501+
client_secret: "{{ config['client_secret'] }}",
502+
refresh_token: "{{ config['client_refresh_token'] }}",
503+
refresh_request_body: {
504+
key1: "val1",
505+
key2: "val2",
506+
},
507+
token_refresh_endpoint: "https://api.com/refresh_token",
508+
grant_type: "client_credentials",
509+
},
510+
},
511+
},
512+
}),
513+
],
514+
};
515+
const formValues = convertToBuilderFormValues(manifest, DEFAULT_BUILDER_FORM_VALUES);
516+
expect(formValues.global.authenticator).toEqual({
517+
type: "OAuthAuthenticator",
518+
client_id: "{{ config['client_id'] }}",
519+
client_secret: "{{ config['client_secret'] }}",
520+
refresh_token: "{{ config['client_refresh_token'] }}",
521+
refresh_request_body: [
522+
["key1", "val1"],
523+
["key2", "val2"],
524+
],
525+
token_refresh_endpoint: "https://api.com/refresh_token",
526+
grant_type: "client_credentials",
527+
});
528+
});
457529
});

airbyte-webapp/src/components/connectorBuilder/manifestToBuilderForm.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,13 @@ export const convertToBuilderFormValues = (
8181

8282
const serializedStreamToIndex = Object.fromEntries(streams.map((stream, index) => [JSON.stringify(stream), index]));
8383
builderFormValues.streams = streams.map((stream, index) =>
84-
manifestStreamToBuilder(stream, index, serializedStreamToIndex, builderFormValues.global)
84+
manifestStreamToBuilder(
85+
stream,
86+
index,
87+
serializedStreamToIndex,
88+
streams[0].retriever.requester.url_base,
89+
streams[0].retriever.requester.authenticator
90+
)
8591
);
8692

8793
return builderFormValues;
@@ -91,7 +97,8 @@ const manifestStreamToBuilder = (
9197
stream: DeclarativeStream,
9298
index: number,
9399
serializedStreamToIndex: Record<string, number>,
94-
builderFormGlobal: BuilderFormValues["global"]
100+
firstStreamUrlBase: string,
101+
firstStreamAuthenticator?: HttpRequesterAuthenticator
95102
): BuilderStream => {
96103
assertType<SimpleRetriever>(stream.retriever, "SimpleRetriever", stream.name);
97104
const retriever = stream.retriever;
@@ -100,14 +107,14 @@ const manifestStreamToBuilder = (
100107
const requester = retriever.requester;
101108

102109
if (
103-
builderFormGlobal.authenticator.type === "NoAuth"
110+
!firstStreamAuthenticator || firstStreamAuthenticator.type === "NoAuth"
104111
? requester.authenticator && requester.authenticator.type !== "NoAuth"
105-
: !isEqual(retriever.requester.authenticator, builderFormGlobal.authenticator)
112+
: !isEqual(retriever.requester.authenticator, firstStreamAuthenticator)
106113
) {
107114
throw new ManifestCompatibilityError(stream.name, "authenticator does not match the first stream's");
108115
}
109116

110-
if (retriever.requester.url_base !== builderFormGlobal.urlBase) {
117+
if (retriever.requester.url_base !== firstStreamUrlBase) {
111118
throw new ManifestCompatibilityError(stream.name, "url_base does not match the first stream's");
112119
}
113120

@@ -150,7 +157,7 @@ const manifestStreamToBuilder = (
150157
),
151158
},
152159
primaryKey: manifestPrimaryKeyToBuilder(stream),
153-
paginator: manifestPaginatorToBuilder(retriever.paginator, stream.name, builderFormGlobal.urlBase),
160+
paginator: manifestPaginatorToBuilder(retriever.paginator, stream.name, firstStreamUrlBase),
154161
streamSlicer: manifestStreamSlicerToBuilder(retriever.stream_slicer, serializedStreamToIndex, stream.name),
155162
schema: manifestSchemaLoaderToBuilderSchema(stream.schema_loader),
156163
unsupportedFields: {
@@ -316,9 +323,21 @@ function manifestAuthenticatorToBuilder(
316323
builderAuthenticator = {
317324
type: "NoAuth",
318325
};
326+
} else if (manifestAuthenticator.type === undefined) {
327+
throw new ManifestCompatibilityError(streamName, "authenticator has no type");
319328
} else if (manifestAuthenticator.type === "CustomAuthenticator") {
320329
throw new ManifestCompatibilityError(streamName, "uses a CustomAuthenticator");
321330
} else if (manifestAuthenticator.type === "OAuthAuthenticator") {
331+
if (
332+
Object.values(manifestAuthenticator.refresh_request_body ?? {}).filter((value) => typeof value !== "string")
333+
.length > 0
334+
) {
335+
throw new ManifestCompatibilityError(
336+
streamName,
337+
"OAuthAuthenticator contains a refresh_request_body with non-string values"
338+
);
339+
}
340+
322341
builderAuthenticator = {
323342
...manifestAuthenticator,
324343
refresh_request_body: Object.entries(manifestAuthenticator.refresh_request_body ?? {}),

airbyte-webapp/src/components/connectorBuilder/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -640,12 +640,17 @@ export const convertToManifest = (values: BuilderFormValues): ConnectorManifest
640640
type: "Spec",
641641
};
642642

643+
const streamNames = values.streams.map((s) => s.name);
644+
const validCheckStreamNames = values.checkStreams.filter((checkStream) => streamNames.includes(checkStream));
645+
const correctedCheckStreams =
646+
validCheckStreamNames.length > 0 ? validCheckStreamNames : streamNames.length > 0 ? [streamNames[0]] : [];
647+
643648
return merge({
644649
version: values.version,
645650
type: "DeclarativeSource",
646651
check: {
647652
type: "CheckStream",
648-
stream_names: values.checkStreams,
653+
stream_names: correctedCheckStreams,
649654
},
650655
streams: manifestStreams,
651656
spec,

airbyte-webapp/src/services/connectorBuilder/ConnectorBuilderStateService.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,13 @@ export const ConnectorBuilderFormStateProvider: React.FC<React.PropsWithChildren
114114
const [yamlIsValid, setYamlIsValid] = useState(true);
115115
const [yamlEditorIsMounted, setYamlEditorIsMounted] = useState(true);
116116

117-
const yamlManifest = useMemo(() => dump(derivedJsonManifest), [derivedJsonManifest]);
117+
const yamlManifest = useMemo(
118+
() =>
119+
dump(derivedJsonManifest, {
120+
noRefs: true,
121+
}),
122+
[derivedJsonManifest]
123+
);
118124

119125
const lastValidBuilderFormValues = lastValidBuilderFormValuesRef.current;
120126
/**

0 commit comments

Comments
 (0)