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/filters #904

Merged
merged 23 commits into from
Oct 21, 2024
Merged

Feat/filters #904

merged 23 commits into from
Oct 21, 2024

Conversation

ciaranschutte
Copy link
Contributor

@ciaranschutte ciaranschutte commented Oct 11, 2024

  • add graphql args to network field, this is a Query top level field and the args can be passed on through to node query creation
  • uses already established gql query functions to pass gql args as seperate variable (JSON type appears to only work in this manner anyway)
  • API always has to query aggregations as a subfield of network

@ciaranschutte ciaranschutte changed the base branch from feat/rearrange_gql_endpoint to feat/aggregate_not_available October 11, 2024 01:59
Base automatically changed from feat/aggregate_not_available to feature/federated_search October 12, 2024 05:38
@ciaranschutte ciaranschutte marked this pull request as ready for review October 13, 2024 00:51
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.

I think this looks fine but have a question about making sure our variables in the fetch request match the variables we list in the gql query. Perhaps important thing to test.

@@ -33,7 +33,7 @@ export const fetchGql = ({
}: {
url: string;
gqlQuery: string;
variables?: Record<string, string>;
variables?: Record<string, unknown>;
Copy link
Contributor

Choose a reason for hiding this comment

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

🙌

const aggregationsString = !isEmpty(fields)
? `aggregations(filters: $filters, aggregations_filter_themselves: $aggregations_filter_themselves, include_missing: $include_missing) ${fields}`
: '';
const gqlString = `query nodeQuery($filters: JSON, $aggregations_filter_themselves: Boolean, $include_missing: Boolean) {${documentName} { hits { total } ${aggregationsString} }}`;
Copy link
Contributor

Choose a reason for hiding this comment

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

we are declaring 3 variables here... if they are not provided in the args object will GQL throw an error?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it's ok for these. default gql args are nullable. there would be an error if args were required using '!' eg. (filter: JSON!)

Comment on lines 57 to 61
type NetworkQuery = {
url: string;
gqlQuery: DocumentNode;
args: Record<string, unknown>;
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Type declared after it is used.

@@ -55,6 +57,7 @@ const fetchData = async <SuccessType>(
type NetworkQuery = {
url: string;
gqlQuery: DocumentNode;
args: Record<string, unknown>;
Copy link
Contributor

Choose a reason for hiding this comment

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

We declare some specific variables in the gql query, should we be requiring these in the args?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We could. I'd love your typescript insight here.

Queries passing args that aren't defined will get an error straight from the gql server. Something like:

 "message": "Unknown argument \"random_arg\" on field \"Query.network\".",
      "extensions": {
        "code": "GRAPHQL_VALIDATION_FAILED",

So once we trust gql (and we should, strongly typed queries is a benefit it provides), args: Record<string, unknown> could always be args: Record<string, { ...known gql args types... }>

I think it's safe to pass on through the args "as is" because:

  • We don't do anything with the args right now, they're just passed through as variables.
    The query creation is only string manipulation so TS wouldn't provide any benefit there by typing the args.
  • GQL server will error itself because a type not matching the schema will throw an error

Bigger question is something that impacts a lot of the project - we have an intersection of GQL types and TS types. There's no quick and easy way to do this. The most straightforward we've had so far requires a build step (that's the thing Sam added for platform - I can't recall the library name - some kind of gql-codegen)

Copy link
Contributor

Choose a reason for hiding this comment

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

Luckily for us, this module only exposes a single function whose interface needs to be typed. The function aggregationPipeline is exported and shouldn't allow any args, it should be specifying what we expect to be set. To that effect, consider introducing the type NetworkAggregationArgs and then update the function declaration to:

type NetworkAggregationArgs = {
	filters: object;
	aggregations_filter_themselves: boolean;
	include_missing: boolean;
};

/**
 * Query each remote connection
 *
 * @param queries - Query for each remote connection
 * @param requestedAggregationFields
 * @returns
 */
export const aggregationPipeline = async (
	configs: NodeConfig[],
	requestedAggregationFields: RequestedFieldsMap,
	args: NetworkAggregationArgs,
) => {

You'll then need to put corresponding updates into the GQL network resolvers to make sure the args provided from the client are properly validated and typed to be passed into the aggregation pipeline.

Comment on lines 3 to 6
export const isSQONFilter = (filter: unknown) => {
// An error will be thrown if the provided input is invalid.
SQONBuilder.from(filter);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't this return boolean to check the data type? If you want an error thrown at a specific location then throw from there, but this won't be very reusable if it either does nothing or throws.

Also, lets add a TSDocs comment for this exported utility.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the return value is a SQON object, which we don't want, we want to keep it as a string.
So the only purpose this has is to throw an error if it's not a SQON

try {
isSQONFilter(args.filters);
} catch (err) {
throw `SQON filters are not valid. ${JSON.stringify(err)}`;
Copy link
Contributor

@joneubank joneubank Oct 16, 2024

Choose a reason for hiding this comment

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

Does GQL want us to throw strings instead of an error like new Error(message)?

It seems weird that you created isSQONFilter to throw an error and then immediately caught and threw a different error. Would be more reusable to have the sqon type check function return a Result with success containing the SQON and the failure containing the error message you want to include in your new thrown error.

import SQONBuilder, {SQON} from '@overture-stack/sqon-builder';

/**
 * Attempts to convert a variable to a SQON. A Result is returned, where if successful
 * the type checked SQON object is returned, and if a failure occurs then the error data
 * from the SQONBuilder is returned as the failure message.
 * 
 * @param filter 
 * @returns 
 */
export const convertToSqon = (filter: unknown): Result<SQON, 'INVALID_SQON'> => {
	try {
		const output = SQONBuilder.from(filter);
		return success(output.toValue());
	} catch (error: unknown) { 
		let message = `${error}`; // Will convert the error to a string, works fine with the expected ZodError without reuiring importing Zod to arranger
		return failure('INVALID_SQON', message)
	}
};

With this, you can also update the NetworkAggregationArgs to have filters as a SQON instead of object.

Copy link
Contributor

Choose a reason for hiding this comment

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

Bonus, I'd rename your file httpResponse.ts to be result.ts.

@@ -20,12 +31,15 @@ import { CONNECTION_STATUS, NetworkNode } from './networkNode';
const fetchData = async <SuccessType>(
query: NetworkQuery,
): Promise<Result<SuccessType, typeof CONNECTION_STATUS.error>> => {
const { url, gqlQuery } = query;
const { url, gqlQuery, queryVariables } = query;

console.log(`Fetch data starting for ${url}`);
Copy link
Member

Choose a reason for hiding this comment

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

ToDo: consider logger instance/level, so this doesn't come up in regular Arranger instances (unless that may actually be desirable?)

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.

Looks functionally good. Some comments here for small updates, mostly regarding commenting the code.

@@ -93,18 +102,28 @@ const convertFieldsToString = (requestedFields: RequestedFieldsMap) => {
return gqlFieldsString;
};

/**
* Query string creation
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 not a useful description. Consider expanding to describe what the content of the query string will be and how that string can be used. Also useful, consider: what are the arguments and how are they used?

Comment on lines +15 to +19
export type NetworkAggregationArgs = {
filters?: object;
aggregations_filter_themselves?: boolean;
include_missing?: boolean;
};
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 a comment block for this would be good. You have a comment below that says "type should match gql typedefs", but it would be good to directly mention which gql typedef, maybe including the path to that file or at least its name. As a first time reader of this file, I should know that this type is describing the inputs to a specific gql query.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea! I'll include some kind of name link. I will avoid using path because if files are moved there's a string in a comment that needs to be updated.

Comment on lines +12 to +20
export const convertToSqon = (filter: unknown): Result<SQON, 'INVALID_SQON', void> => {
try {
const output = SQONBuilder.from(filter);
return success(output);
} catch (error: unknown) {
const message = `${error}`; // Will convert the error to a string, works fine with the expected ZodError without requiring importing Zod to Arranger
return failure('INVALID_SQON', message);
}
};
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 working fine, consider using the SQON schema exported with SQONBuilder:

import SQONBuilder, {SQON} from '@overture-stack/sqon-builder';

const isSqon = (input: unknown): input is SQON => SQON.safeParse(input).success;

@ciaranschutte ciaranschutte merged commit 0e03dee into feature/federated_search Oct 21, 2024
0 of 2 checks passed
@ciaranschutte ciaranschutte deleted the feat/filters branch October 21, 2024 17:30
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