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/#873 retrieve remote schemas #877

Merged

Conversation

ciaranschutte
Copy link
Contributor

@ciaranschutte ciaranschutte commented Jun 5, 2024

  • retrieval of remote schemas
  • responses build to full schema using gql tools (means we can use the well tested inbuilt methods without manually parsing a gql request as intermediary objects)
  • output is array of full schemas as GraphQlObjects

Please note this is part of the bigger schema federation work merging into feat/featured_search
The broken build issues are for some missing TS types, implicit anys
These will be addressed before merging full feature into develop branch

Ciaran Schutte and others added 30 commits June 4, 2024 22:33
Co-authored-by: Anders Richardsson <[email protected]>
if (response.status === 200 && response.statusText === 'OK') {
// axios response "data" field, graphql response "data" field
const responseData = response.data?.data;
if (isGqlIntrospectionQuery(responseData)) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if (isGqlIntrospectionQuery(responseData)) {
if (isGqlIntrospectionQuery(responseData)) {

modules/server/src/network/index.ts Outdated Show resolved Hide resolved
modules/server/src/network/index.ts Outdated Show resolved Hide resolved
// type cast because rejected errors are handled
const networkQueries = (await Promise.allSettled(
networkQueryPromises,
)) as PromiseFulfilledResult<FetchRemoteSchemaResult>[];
Copy link
Contributor

Choose a reason for hiding this comment

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

Should not use this type assertion. Promise.allSettled returns a PromiseSettledResult<T> where T is your type.

This is important to use as any rejected response will NOT have a .value property, we need to always check for the .status in each element of network queries array.

See type information for PromiseSettledResult<T>: https://github.com/microsoft/TypeScript/blob/main/src/lib/es2020.promise.d.ts#L11


// build schema
const schemaResults = networkQueries.map((networkResult) => {
const { config, introspectionResult } = networkResult.value;
Copy link
Contributor

Choose a reason for hiding this comment

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

.value won't exist on rejected promises. We can filter those out if we only care about succeses:

const schemaResults = networkQueries
		.filter(
			(result): result is PromiseFulfilledResult<FetchRemoteSchemaResult> =>
				result.status === 'fulfilled',
		)
		.map((networkResult) => {
			const { config, introspectionResult } = networkResult.value;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

that's how the code is currently handling

modules/server/src/network/index.ts Outdated Show resolved Hide resolved
modules/server/src/network/index.ts Outdated Show resolved Hide resolved
@ciaranschutte
Copy link
Contributor Author

@joneubank addressed feedback. TS building. please take another look when you have some time.

Copy link
Contributor

@joneubank joneubank left a comment

Choose a reason for hiding this comment

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

Looking strong throughout.

The axios request failure handling I think is not what you would expect to see so consider that comment and if there are changes needed.

Comment on lines +4 to +7
/**
* Creates a graphql query string with variables for use in a POST request
* @returns string
*/
Copy link
Contributor

Choose a reason for hiding this comment

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

🙏

Comment on lines 13 to 16
/**
* TODO: Add typing for variables. Type will be dependant on gql endpoint being queried.
*/
variables: unknown;
Copy link
Contributor

Choose a reason for hiding this comment

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

Looking at how this is used, we don't really care about the specific contents of the variables. This function is doing no work to ensure that the variables expected in the query are included in variables (or vice versa). The only thing we know is we want to be able to run JSON.stringify(variables) which takes any. In practice, we expect this parameter to be an object with some variable values which themselves could be anything, and we probably don't want it to be string or number etc. So in this case I might reccomend:

variables: Record<string, any>

* Creates a graphql query string with variables for use in a POST request
* @returns string
*/
export const createGqlQuery = ({
Copy link
Contributor

Choose a reason for hiding this comment

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

Noticed this function isn't actually used in your code here, is that expected?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

expected. #877 (comment)

// queries need to be strings when using regular http
const query = print(queryAST);
return JSON.stringify({ query, variables });
};

export const fetchGql = ({ url, gqlRequest }) => {
export const fetchGql = ({ url, gqlRequest }: { url: string; gqlRequest: { query: string } }) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

TSDoc will be wanted before feature branch is complete.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think your TSDoc is incorrect, instead of returning an axios instance that you can use to send future requests, this sends your defined request with axios and returns the AxiosResponse (or throws error, stupid axios).

Copy link
Member

Choose a reason for hiding this comment

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

Great feedback, useful insight... but let's try keeping unhelpful personal opinions to a minimum.

@@ -18,7 +19,7 @@ const isGqlIntrospectionQuery = (input: unknown): input is IntrospectionQuery =>

type FetchRemoteSchemaResult = {
config: NetworkAggregationConfigInput;
introspectionResult: IntrospectionQuery | null;
introspectionResult: IntrospectionQuery | undefined;
Copy link
Contributor

Choose a reason for hiding this comment

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

why not just:

introspectionResult?: IntrospectionQuery

Comment on lines 57 to 58
throw new NetworkAggregationError(
`Unexpected data in response object. Please verify the endpoint at ${graphqlUrl} is returning a valid GQL Schema.`,
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

Comment on lines 67 to 71
} catch (error) {
console.log(`failed to retrieve schema from url: ${config.graphqlUrl}`);
console.error(error);
return { config, introspectionResult: undefined };
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Axios throws errors on status 4xx and 5xx. That means that the else block you have above is behaviour that you are likely expecting to be handled here as well. You should try checking if the error here is an instance of AxiosError, and if so you can handle these error statuses in the catch block. I imagine you will want to throw your NetworkAggregationError.

Comment on lines 86 to 89
.filter(
(result): result is PromiseFulfilledResult<FetchRemoteSchemaResult> =>
result.status === 'fulfilled',
)
Copy link
Contributor

Choose a reason for hiding this comment

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

With TS 5.5 we no longer require the result is return type :D

} catch (error) {
console.log(`failed to retrieve schema from url: ${config.graphqlUrl}`);
console.error(error);
return { config, introspectionResult: undefined };
Copy link
Contributor

Choose a reason for hiding this comment

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

PS. don't need to do introsepectionResult: undefined if you have the type defined as introspectionResult?: IntrospectionQuery

// queries need to be strings when using regular http
const query = print(queryAST);
return JSON.stringify({ query, variables });
};

export const fetchGql = ({ url, gqlRequest }) => {
export const fetchGql = ({ url, gqlRequest }: { url: string; gqlRequest: { query: string } }) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think your TSDoc is incorrect, instead of returning an axios instance that you can use to send future requests, this sends your defined request with axios and returns the AxiosResponse (or throws error, stupid axios).

Comment on lines +3 to +8
// environment config
export type NetworkAggregationConfigInput = {
graphqlUrl: string;
documentType: string;
displayName: string;
};
Copy link
Contributor

Choose a reason for hiding this comment

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

The comment here doesn't tell me anything new. You can use TSDoc on types as well to leave a description of the type in intellisense for future devs that use it.

Comment on lines +10 to +13
// additional properties after initialising network
export type NetworkAggregationConfig = NetworkAggregationConfigInput & {
schema?: GraphQLSchema;
};
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a helpful comment but it wasn't easily understandable. I had to read the type information to understand what you mean. Maybe something like:

Suggested change
// additional properties after initialising network
export type NetworkAggregationConfig = NetworkAggregationConfigInput & {
schema?: GraphQLSchema;
};
/**
* Complete aggregation config that defines the network search setup. This includes the original
* config information plus computed fields that are generated in the network search initialization process.
*/
export type NetworkAggregationConfig = NetworkAggregationConfigInput & {
schema?: GraphQLSchema;
};

Copy link
Member

Choose a reason for hiding this comment

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

That's a nice improvement, for sure!
Naive question: am I the only one who gets bugged by the line length inconsistency?
personally, I'd chop it like this

* Complete aggregation config that defines the network search setup.
* This includes the original config information plus computed fields
* that are generated in the network search initialization process.

* @returns
*/
const isGqlIntrospectionQuery = (input: unknown): input is IntrospectionQuery =>
!!input && typeof input === 'object' && '__schema' in input && typeof input.__schema === 'object';
Copy link
Member

Choose a reason for hiding this comment

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

wouldn't typeof input === 'object' inherently cover the !!?
(an object cannot be falsy)

Suggested change
!!input && typeof input === 'object' && '__schema' in input && typeof input.__schema === 'object';
typeof input === 'object' && '__schema' in input && typeof input.__schema === 'object';

Copy link
Member

Choose a reason for hiding this comment

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

does it matter that this function would allow input to be an array customised to include __schema?

Copy link
Contributor Author

@ciaranschutte ciaranschutte Jun 26, 2024

Choose a reason for hiding this comment

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

1st point:
typeof null is object - <3 js (so '__schema' in input would break)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

2nd point:
I don't think that's a concern. I'm using the getIntrospectionQuery which is part of the graphql library (conforming to spec) and this check is only happening to check response data from a GQL server.

It queries an automatically generated field in a graphql server, not a user created field.
For an array to occur the graphql spec would need to be changed.

Copy link
Member

@justincorrigible justincorrigible Jun 26, 2024

Choose a reason for hiding this comment

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

is there a null.__schema? otherwise, !! is still redundant

introspectionSchema !== undefined ? buildClientSchema(introspectionSchema) : undefined;
return { ...config, schema };
} catch (error) {
console.error('build schema error', error);
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be nice if these messages were more descriptive. The best error messages let the reader know what was being attempted, why it failed, and (if possible) what action should be taken.

It is likely that this is about on par with other error messaging in this repo, but working to improve these as we go would be a huge help.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

for sure - this is just the "retrieve schema" step
buildClientSchema is not implemented
will be keeping this in mind working on the "build schema" ticket

@ciaranschutte ciaranschutte merged commit 4f62745 into feature/federated_search Jun 26, 2024
0 of 2 checks passed
@ciaranschutte ciaranschutte deleted the feat/#873_retrieve_remote_schemas branch June 26, 2024 15:14
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.

3 participants