Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api): add custom endpoint support to API #14086

Merged
merged 17 commits into from
Dec 23, 2024

Conversation

svidgen
Copy link
Member

@svidgen svidgen commented Dec 17, 2024

Description of changes

Adds optional endpoint and apiKey properties to generateClient() options, allowing customers to use Amplify to connect to any accessible GraphQL API from within an app.

Also adds support for subscribing to multiple endpoints simultaneously.

image

When an alternative endpoint is provided:

  1. authMode must be explicitly set. Omitting it is both a type error and runtime error.

image

  1. authMode: "apiKey" requires that apiKey also be explicitly set; whether this mode is set in generateClient() or .graphql() downstream. Omitting it is both a type error and runtime error.

image

Issue #, if available

Description of how you validated changes

  1. Tests added.
  2. Manual testing
  3. e2e's currently being authored

Checklist

  • PR description included
  • yarn test passes
  • Unit Tests are changed or added
  • Relevant documentation is changed or added (and PR referenced)

Checklist for repo maintainers

  • Verify E2E tests for existing workflows are working as expected or add E2E tests for newly added workflows
  • New source file paths included in this PR have been added to CODEOWNERS, if appropriate

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@svidgen svidgen changed the title multi-endpoint base implementation feat(api): add custom endpoint support to API Dec 18, 2024
@svidgen svidgen marked this pull request as ready for review December 18, 2024 18:08
const memoKey = appSyncGraphqlEndpoint ?? 'none';
const realtimeProvider =
this.appSyncRealTime.get(memoKey) ?? new AWSAppSyncRealTimeProvider();
this.appSyncRealTime.set(memoKey, realtimeProvider);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This overwrites the memoKey, even when we just retrieved the provider from the set doesn't it? It's probably not doing harm, but seems like an unnecessary operation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. I weighed it against a conditional check to do the same. This seemed simpler to read. But, I don't mind changing it if you disagree.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The conditional check seems like fewer moving parts or things to go wrong, but I also don't think I can come up with an example of when this would fail us, so this is a nit at best.

Comment on lines +74 to +77
if (!client[__endpoint]) {
if (isApiGraphQLConfig(apiGraphqlConfig)) {
addSchemaToClient(client, apiGraphqlConfig, getInternals);
} else {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The old code would always either run addSchemaToClient or generateModelsPropertyOnAmplifyConfigure where the new code could run neither when there is an endpoint in the params and we don't raise the auth errors above, right?

Is it a problem to return a client without running one of these two functions?

Thinking over the change, probably not since we don't have a schema or models to bind in these additional endpoint use-cases. Interesting.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's very deliberately skipping both to avoid attaching .models etc. and potentially causing confusion there — if someone were logging out the client and poking around for example, they'd see methods from their MIS that have nothing to do with the endpoint.

Comment on lines +55 to +57
if (client[__endpoint]) {
if (!client[__authMode]) {
throw new Error(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This error logic is repeated from above, is other similar code shared in some way?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably more LOC to do so to accommodate the slightly different error messaging. Is it worth it to you?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit. Probably not worth it.

packages/api-graphql/src/internals/v6.ts Outdated Show resolved Hide resolved
packages/api-graphql/src/internals/v6.ts Outdated Show resolved Hide resolved
packages/api-graphql/src/server/generateClient.ts Outdated Show resolved Hide resolved
Comment on lines +222 to +266
? {
query: TYPED_GQL_STRING | DocumentNode;
variables?: GraphQLVariablesV6<FALLBACK_TYPES, TYPED_GQL_STRING>;
authMode?: GraphQLAuthMode;
apiKey?: string;
authToken?: string;
/**
* @deprecated This property should not be used
*/
userAgentSuffix?: string;
}
:
| {
query: TYPED_GQL_STRING | DocumentNode;
variables?: GraphQLVariablesV6<FALLBACK_TYPES, TYPED_GQL_STRING>;
authMode?: never;
apiKey?: never;
authToken?: string;
/**
* @deprecated This property should not be used
*/
userAgentSuffix?: string;
}
| {
query: TYPED_GQL_STRING | DocumentNode;
variables?: GraphQLVariablesV6<FALLBACK_TYPES, TYPED_GQL_STRING>;
authMode: 'apiKey';
apiKey: string;
authToken?: string;
/**
* @deprecated This property should not be used
*/
userAgentSuffix?: string;
}
| {
query: TYPED_GQL_STRING | DocumentNode;
variables?: GraphQLVariablesV6<FALLBACK_TYPES, TYPED_GQL_STRING>;
authMode: Exclude<GraphQLAuthMode, 'apiKey'>;
apiKey?: never;
authToken?: string;
/**
* @deprecated This property should not be used
*/
userAgentSuffix?: string;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this be better expressed as a generic where we pass in override types to we can repeat it with different overrides to create this union? This seems like an easy point to accidentally inject errors trying to make simple modifications all spelled out like this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, I did go down the generic route and found that TS was too confused to offer suggestions while typing (pressing ctrl+space in particular). I'm not sure if I made a mistake, but I then had to go on a small crusade to simplify the types down into the "dumbest" possible versions of themselves and it seemed to contribute to resolving the issue.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. Don't think we have the prettify wrapper here, but I think that might improve the CX on a more concise solution. Not sure where I land on this, this seems pretty long form.

// attempting to connect to nowhere. So, I'm treating as the opposite of
// a Chesterton's fence for now. (A fence I shouldn't build, because I don't
// know why somethings depends on its absence!)
const memoKey = appSyncGraphqlEndpoint ?? 'none';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this memoKey sufficient? Wondering if a websocket might ever be accessed with different authtypes to achieve different permissions where both would have the same endpoint (or something similar)

I think in all case, overriding a graphql calls authtype works, but maybe a client with authtype A will be recreated with authtype B and the dev will be surprised that they get the original instance with type A...

Any other config that might be needed in the memoKey to avoid surprises?

Copy link
Member Author

@svidgen svidgen Dec 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be OK. We have normally used a single socket for all connections regardless of the authmode needed by the subscription itself. In chatting with Ivan about how to do this, I think he leaned towards keeping connections pooled by client — in most cases, I think that's slightly better for customers. Fewer local resources consumed at the very least, etc.. It's marginal. I wouldn't die on that hill.

If it causes issues, I think this behavior is pretty OK for us to change later.

iartemiev
iartemiev previously approved these changes Dec 19, 2024
stocaaro
stocaaro previously approved these changes Dec 19, 2024
const memoKey = appSyncGraphqlEndpoint ?? 'none';
const realtimeProvider =
this.appSyncRealTime.get(memoKey) ?? new AWSAppSyncRealTimeProvider();
this.appSyncRealTime.set(memoKey, realtimeProvider);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The conditional check seems like fewer moving parts or things to go wrong, but I also don't think I can come up with an example of when this would fail us, so this is a nit at best.

Comment on lines +55 to +57
if (client[__endpoint]) {
if (!client[__authMode]) {
throw new Error(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit. Probably not worth it.

Comment on lines +222 to +266
? {
query: TYPED_GQL_STRING | DocumentNode;
variables?: GraphQLVariablesV6<FALLBACK_TYPES, TYPED_GQL_STRING>;
authMode?: GraphQLAuthMode;
apiKey?: string;
authToken?: string;
/**
* @deprecated This property should not be used
*/
userAgentSuffix?: string;
}
:
| {
query: TYPED_GQL_STRING | DocumentNode;
variables?: GraphQLVariablesV6<FALLBACK_TYPES, TYPED_GQL_STRING>;
authMode?: never;
apiKey?: never;
authToken?: string;
/**
* @deprecated This property should not be used
*/
userAgentSuffix?: string;
}
| {
query: TYPED_GQL_STRING | DocumentNode;
variables?: GraphQLVariablesV6<FALLBACK_TYPES, TYPED_GQL_STRING>;
authMode: 'apiKey';
apiKey: string;
authToken?: string;
/**
* @deprecated This property should not be used
*/
userAgentSuffix?: string;
}
| {
query: TYPED_GQL_STRING | DocumentNode;
variables?: GraphQLVariablesV6<FALLBACK_TYPES, TYPED_GQL_STRING>;
authMode: Exclude<GraphQLAuthMode, 'apiKey'>;
apiKey?: never;
authToken?: string;
/**
* @deprecated This property should not be used
*/
userAgentSuffix?: string;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. Don't think we have the prettify wrapper here, but I think that might improve the CX on a more concise solution. Not sure where I land on this, this seems pretty long form.

iartemiev
iartemiev previously approved these changes Dec 20, 2024
@svidgen svidgen requested review from HuiSF and cshfang December 23, 2024 16:52
@iartemiev iartemiev merged commit ca2e4b8 into main Dec 23, 2024
30 checks passed
@iartemiev iartemiev deleted the feat/graphql-multi-client/main branch December 23, 2024 17:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants