diff --git a/modules/server/package.json b/modules/server/package.json index 0b9f72bcf..fbbd66164 100644 --- a/modules/server/package.json +++ b/modules/server/package.json @@ -19,7 +19,8 @@ "start:prod": "ts-node index.ts", "test": "jest", "test:watch": "jest --watch", - "watch": "npm run cleanDist && npm run build -- --watch" + "watch": "npm run cleanDist && npm run build -- --watch", + "typeCheck": "tsc -w --noEmit" }, "repository": { "type": "git", diff --git a/modules/server/src/network/aggregations/AggregationAccumulator.ts b/modules/server/src/network/aggregations/AggregationAccumulator.ts index 1e6cf259a..add3f33f6 100644 --- a/modules/server/src/network/aggregations/AggregationAccumulator.ts +++ b/modules/server/src/network/aggregations/AggregationAccumulator.ts @@ -1,5 +1,5 @@ import { ALL_NETWORK_AGGREGATION_TYPES_MAP } from '..'; -import { SUPPORTED_AGGREGATIONS } from '../common'; +import { SupportedAggregation, SUPPORTED_AGGREGATIONS } from '../common'; import { Aggregations, Bucket, NumericAggregations } from '../types/aggregations'; import { Hits } from '../types/hits'; import { AllAggregations } from '../types/types'; @@ -14,21 +14,31 @@ type ResolveAggregationInput = { type AggregationsTuple = [Aggregations, Aggregations]; type NumericAggregationsTuple = [NumericAggregations, NumericAggregations]; -const emptyAggregation = (hits: number) => ({ +const emptyAggregation = (hits: number): Aggregations => ({ + __typename: 'Aggregations', bucket_count: 1, buckets: [{ key: '___aggregation_not_available___', doc_count: hits }], }); // mutation - update a single aggregations field in the accumulator -const addToAccumulator = ({ existingAggregation, aggregation, type }) => { +const addToAccumulator = ({ + existingAggregation, + aggregation, + type, +}: { + existingAggregation: T | undefined; + aggregation: T; + type: SupportedAggregation; +}) => { // if first aggregation, nothing to resolve with yet return !existingAggregation ? aggregation - : resolveToNetworkAggregation(type, [aggregation, existingAggregation]); + : resolveToNetworkAggregation(type, [aggregation, existingAggregation]); }; /** * Resolves returned aggregations from network queries into single accumulated aggregation + * ALL_NETWORK_AGGREGATION_TYPES_MAP should be initialised before using this function * * @param */ @@ -36,14 +46,25 @@ const resolveAggregations = ({ data, accumulator, requestedFields }: ResolveAggr requestedFields.forEach((requestedField) => { const { aggregations, hits } = data; - const isFieldAvailable = !!aggregations[requestedField]; + /* + * requested field will always be in ALL_NETWORK_AGGREGATION_TYPES_MAP + * GQL schema validation will throw an error earlier if a requested field isn't in the schema + */ const type = ALL_NETWORK_AGGREGATION_TYPES_MAP.get(requestedField); + if (type === undefined) { + console.log( + 'Could not find aggregation type.\nPlease ensure ALL_NETWORK_AGGREGATION_TYPES_MAP is initialised.', + ); + return; + } + + const aggregation = aggregations[requestedField]; const existingAggregation = accumulator[requestedField]; - if (isFieldAvailable) { + if (aggregation !== undefined) { accumulator[requestedField] = addToAccumulator({ existingAggregation, - aggregation: aggregations[requestedField], + aggregation, type, }); } else { @@ -66,9 +87,9 @@ const resolveAggregations = ({ data, accumulator, requestedFields }: ResolveAggr * @param type * @param aggregations */ -const resolveToNetworkAggregation = ( +const resolveToNetworkAggregation = ( type: string, - aggregations: AggregationsTuple | NumericAggregationsTuple, + aggregations: [T, T], ): Aggregations | NumericAggregations => { if (type === SUPPORTED_AGGREGATIONS.Aggregations) { return resolveAggregation(aggregations as AggregationsTuple); @@ -179,7 +200,11 @@ export const resolveAggregation = (aggregations: AggregationsTuple): Aggregation const resolvedAggregation = aggregations.reduce((resolvedAggregation, agg) => { const computedBuckets = resolvedAggregation.buckets; agg.buckets.forEach((bucket) => updateComputedBuckets(bucket, computedBuckets)); - return { bucket_count: computedBuckets.length, buckets: computedBuckets }; + return { + bucket_count: computedBuckets.length, + buckets: computedBuckets, + __typename: resolvedAggregation.__typename, + }; }); return resolvedAggregation; diff --git a/modules/server/src/network/aggregations/tests/aggregation.test.ts b/modules/server/src/network/aggregations/tests/aggregation.test.ts index a09038106..e9833bc11 100644 --- a/modules/server/src/network/aggregations/tests/aggregation.test.ts +++ b/modules/server/src/network/aggregations/tests/aggregation.test.ts @@ -1,3 +1,12 @@ +/* + * TODO: needs work to resolve ALL_NETWORK_AGGREGATION_TYPES_MAP correctly mocked + * only works at this top level, once. Jest will hoist + * jest.doMock requires extra configuration with "require" instead of "import" + * doesn't mock correctly right now between tests + */ + +// @ts-nocheck + import { ALL_NETWORK_AGGREGATION_TYPES_MAP } from '@/network'; import { AggregationAccumulator } from '../AggregationAccumulator'; import { aggregation as fixture } from './fixture'; @@ -13,12 +22,6 @@ jest.mock('../../index', () => ({ ALL_NETWORK_AGGREGATION_TYPES_MAP: new Map([['donors_gender', 'Aggregations']]), })); -/* - * TODO: needs work to resolve ALL_NETWORK_AGGREGATION_TYPES_MAP correctly mocked - * only works at this top level, once. Jest will hoist - * jest.doMock requires extra configuration with "require" instead of "import" - * doesn't mock correctly right now between tests - */ xdescribe('Network aggregation resolution', () => { describe('resolves multiple aggregations into a single aggregation:', () => { beforeEach(() => { diff --git a/modules/server/src/network/index.ts b/modules/server/src/network/index.ts index e90e9afdd..dc1761e65 100644 --- a/modules/server/src/network/index.ts +++ b/modules/server/src/network/index.ts @@ -1,12 +1,12 @@ import { makeExecutableSchema } from '@graphql-tools/schema'; -import { SUPPORTED_AGGREGATIONS_LIST } from './common'; +import { SupportedAggregation, SUPPORTED_AGGREGATIONS_LIST } from './common'; import { createResolvers } from './resolvers'; import { getAllFieldTypes } from './setup/fields'; import { fetchAllNodeAggregations } from './setup/query'; import { createTypeDefs } from './typeDefs'; import { NetworkConfig } from './types/setup'; -export let ALL_NETWORK_AGGREGATION_TYPES_MAP: Map = new Map(); +export let ALL_NETWORK_AGGREGATION_TYPES_MAP: Map = new Map(); /** * GQL Federated Search schema setup diff --git a/modules/server/src/network/resolvers/aggregations.ts b/modules/server/src/network/resolvers/aggregations.ts index 0bb9d6dd8..deeae5fd1 100644 --- a/modules/server/src/network/resolvers/aggregations.ts +++ b/modules/server/src/network/resolvers/aggregations.ts @@ -30,7 +30,7 @@ type QueryVariables = { */ const fetchData = async ( query: NetworkQuery, -): Promise> => { +): Promise> => { const { url, gqlQuery, queryVariables } = query; console.log(`Fetch data starting for ${url}`); @@ -45,10 +45,12 @@ const fetchData = async ( // axios response "data" field, graphql response "data" field const responseData = response.data?.data; if (response.status === 200 && response.statusText === 'OK') { + console.log(`Fetch data completing for ${query.url}`); return success(responseData); } } catch (error) { if (axios.isCancel(error)) { + console.log(`Fetch data cancelled for ${query.url}`); return failure(CONNECTION_STATUS.ERROR, `Request cancelled: ${url}`); } @@ -61,9 +63,9 @@ const fetchData = async ( } } return failure(CONNECTION_STATUS.ERROR, `Unknown error`); - } finally { - console.log(`Fetch data completing for ${query.url}`); } + // TS would like a return value outside of try/catch handling + return failure(CONNECTION_STATUS.ERROR, `Unknown error`); }; /** @@ -129,12 +131,12 @@ export const createNodeQueryString = ( * * @param config * @param requestedFields - * @returns + * @returns a GQL document node or undefined if a valid GQL document node cannot be created */ export const createNetworkQuery = ( config: NodeConfig, requestedFields: RequestedFieldsMap, -): DocumentNode => { +): DocumentNode | undefined => { const availableFields = config.aggregations; const documentName = config.documentName; @@ -161,6 +163,7 @@ export const createNetworkQuery = ( return gqlQuery; } catch (err) { console.error('invalid gql', err); + return undefined; } }; @@ -184,11 +187,13 @@ export const aggregationPipeline = async ( const aggregationResultPromises = configs.map(async (config) => { const gqlQuery = createNetworkQuery(config, requestedAggregationFields); - const response = await fetchData({ - url: config.graphqlUrl, - gqlQuery, - queryVariables, - }); + const response = gqlQuery + ? await fetchData({ + url: config.graphqlUrl, + gqlQuery, + queryVariables, + }) + : failure(CONNECTION_STATUS.ERROR, 'Invalid GQL query'); const nodeName = config.displayName; diff --git a/modules/server/src/network/resolvers/response.ts b/modules/server/src/network/resolvers/response.ts index 3bdd9b28f..0a54bbfa5 100644 --- a/modules/server/src/network/resolvers/response.ts +++ b/modules/server/src/network/resolvers/response.ts @@ -1,6 +1,15 @@ +import { AllAggregations } from '../types/types'; +import { NetworkNode } from './networkNode'; + /** - * Format response object to match gql type defs + * Format response object to match GQL type defs */ -export const createResponse = ({ aggregationResults, nodeInfo }) => { +export const createResponse = ({ + aggregationResults, + nodeInfo, +}: { + aggregationResults: AllAggregations; + nodeInfo: NetworkNode[]; +}) => { return { remoteConnections: nodeInfo, aggregations: aggregationResults }; }; diff --git a/modules/server/src/network/setup/fields.ts b/modules/server/src/network/setup/fields.ts index 911245a95..181d8bf00 100644 --- a/modules/server/src/network/setup/fields.ts +++ b/modules/server/src/network/setup/fields.ts @@ -59,7 +59,7 @@ export const getFieldTypes = ( export const getAllFieldTypes = ( nodeConfigs: NodeConfig[], supportedTypes: SupportedAggregation[], -) => { +): SupportedNetworkFieldType[] => { const nodeFieldTypes = nodeConfigs.map((config) => { const { supportedAggregations, unsupportedAggregations } = getFieldTypes( config.aggregations, @@ -75,7 +75,7 @@ export const getAllFieldTypes = ( /* * Returns unique fields * eg. if NodeA and NodeB both have `analysis__analysis__id`, only include it once - * This during server startup for creating the Apollo server. + * This is during server startup for creating the Apollo server. * Please do not use expensive stringify and parsing for server queries. */ const uniqueSupportedAggregations = Array.from( diff --git a/modules/server/src/network/tests/helpers.test.ts b/modules/server/src/network/tests/helpers.test.ts index e36c7b91e..004912779 100644 --- a/modules/server/src/network/tests/helpers.test.ts +++ b/modules/server/src/network/tests/helpers.test.ts @@ -1,26 +1,21 @@ -import { getFieldTypes } from '..'; +import { SupportedAggregation } from '../common'; +import { getFieldTypes } from '../setup/fields'; describe('helpers', () => { test('getField returns both supported and unsupported types', () => { - const supportedAggregations = ['Aggregations']; + const supportedAggregations: SupportedAggregation[] = ['Aggregations']; const fields = [ { name: 'analysis__analysis_id', - type: { - name: 'Aggregations', - }, + type: 'Aggregations', }, { name: 'analysis__analysis_state', - type: { - name: 'Aggregations', - }, + type: 'Aggregations', }, { name: 'clinical__donor__number_of_children', - type: { - name: 'HumanAggregate', - }, + type: 'HumanAggregate', }, ]; diff --git a/modules/server/src/network/typeDefs/aggregations.ts b/modules/server/src/network/typeDefs/aggregations.ts index 3262c465b..28a510d75 100644 --- a/modules/server/src/network/typeDefs/aggregations.ts +++ b/modules/server/src/network/typeDefs/aggregations.ts @@ -7,7 +7,7 @@ import { GraphQLString, } from 'graphql'; import GraphQLJSON from 'graphql-type-json'; -import { SupportedNetworkFieldType } from '../types'; +import { SupportedNetworkFieldType } from '../types/types'; import { singleToNetworkAggregationMap } from './networkAggregations'; /** @@ -16,7 +16,7 @@ import { singleToNetworkAggregationMap } from './networkAggregations'; * @example * { name: "donor_age", type: "NumericAggregations" } => { donor_age: { type: "NetworkNumericAggregations" } } */ -const convertToGQLObjectType = (networkFieldTypes) => { +const convertToGQLObjectType = (networkFieldTypes: SupportedNetworkFieldType[]) => { return networkFieldTypes.reduce((allFields, currentField) => { const field = { [currentField.name]: { type: singleToNetworkAggregationMap.get(currentField.type) }, diff --git a/modules/server/src/network/typeDefs/index.ts b/modules/server/src/network/typeDefs/index.ts index a37d979a6..1527d41c1 100644 --- a/modules/server/src/network/typeDefs/index.ts +++ b/modules/server/src/network/typeDefs/index.ts @@ -1,10 +1,7 @@ -import { mergeTypeDefs } from '@graphql-tools/merge'; import { SupportedNetworkFieldType } from '../types/types'; import { createNetworkAggregationTypeDefs } from './aggregations'; -import { remoteConnectionTypes } from './remoteConnections'; export const createTypeDefs = (networkFieldTypes: SupportedNetworkFieldType[]) => { - const aggregationTypes = createNetworkAggregationTypeDefs(networkFieldTypes); - const typeDefs = mergeTypeDefs([remoteConnectionTypes, aggregationTypes]); + const typeDefs = createNetworkAggregationTypeDefs(networkFieldTypes); return typeDefs; }; diff --git a/modules/server/src/network/typeDefs/remoteConnections.ts b/modules/server/src/network/typeDefs/remoteConnections.ts deleted file mode 100644 index 946400872..000000000 --- a/modules/server/src/network/typeDefs/remoteConnections.ts +++ /dev/null @@ -1,12 +0,0 @@ -export const remoteConnectionTypes = `#graphql - type Query { - nodes: [RemoteConnectionNode] - } - - type RemoteConnectionNode { - name: String - count: Int - status: String - errors: [String] - } -`;