Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion modules/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
45 changes: 35 additions & 10 deletions modules/server/src/network/aggregations/AggregationAccumulator.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -14,36 +14,57 @@ 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 = <T extends Aggregations | NumericAggregations>({
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<T>(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
*/
const resolveAggregations = ({ data, accumulator, requestedFields }: ResolveAggregationInput) => {
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 {
Expand All @@ -66,9 +87,9 @@ const resolveAggregations = ({ data, accumulator, requestedFields }: ResolveAggr
* @param type
* @param aggregations
*/
const resolveToNetworkAggregation = (
const resolveToNetworkAggregation = <T>(
type: string,
aggregations: AggregationsTuple | NumericAggregationsTuple,
aggregations: [T, T],
): Aggregations | NumericAggregations => {
if (type === SUPPORTED_AGGREGATIONS.Aggregations) {
return resolveAggregation(aggregations as AggregationsTuple);
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -13,12 +22,6 @@ jest.mock('../../index', () => ({
ALL_NETWORK_AGGREGATION_TYPES_MAP: new Map<string, string>([['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(() => {
Expand Down
4 changes: 2 additions & 2 deletions modules/server/src/network/index.ts
Original file line number Diff line number Diff line change
@@ -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<string, string> = new Map();
export let ALL_NETWORK_AGGREGATION_TYPES_MAP: Map<string, SupportedAggregation> = new Map();

/**
* GQL Federated Search schema setup
Expand Down
25 changes: 15 additions & 10 deletions modules/server/src/network/resolvers/aggregations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ type QueryVariables = {
*/
const fetchData = async <SuccessType>(
query: NetworkQuery,
): Promise<Result<SuccessType, typeof CONNECTION_STATUS.error>> => {
): Promise<Result<SuccessType, typeof CONNECTION_STATUS.ERROR>> => {
const { url, gqlQuery, queryVariables } = query;

console.log(`Fetch data starting for ${url}`);
Expand All @@ -45,10 +45,12 @@ const fetchData = async <SuccessType>(
// 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}`);
}

Expand All @@ -61,9 +63,9 @@ const fetchData = async <SuccessType>(
}
}
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`);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

even though results are handled in the try/catch block, TS doesn't like the implicit undefined from the function.
It is a tsconfig setting, but I think it's best to return something rather than tweak the project setting.

TS also doesn't like nothing returned in a finally block, and returning something here would override the returned values in the catch block

};

/**
Expand Down Expand Up @@ -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 => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Since this may return undefined, and this funciton is exported for use by other modules, it would be good to include a description in the TSDoc about when this will return undefined so other devs understand what this response is communicating.

const availableFields = config.aggregations;
const documentName = config.documentName;

Expand All @@ -161,6 +163,7 @@ export const createNetworkQuery = (
return gqlQuery;
} catch (err) {
console.error('invalid gql', err);
return undefined;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

TS doesn't like implicit undefined

}
};

Expand All @@ -184,11 +187,13 @@ export const aggregationPipeline = async (

const aggregationResultPromises = configs.map(async (config) => {
const gqlQuery = createNetworkQuery(config, requestedAggregationFields);
const response = await fetchData<SuccessResponse>({
url: config.graphqlUrl,
gqlQuery,
queryVariables,
});
const response = gqlQuery
? await fetchData<SuccessResponse>({
url: config.graphqlUrl,
gqlQuery,
queryVariables,
})
: failure(CONNECTION_STATUS.ERROR, 'Invalid GQL query');

const nodeName = config.displayName;

Expand Down
13 changes: 11 additions & 2 deletions modules/server/src/network/resolvers/response.ts
Original file line number Diff line number Diff line change
@@ -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 };
};
4 changes: 2 additions & 2 deletions modules/server/src/network/setup/fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand Down
17 changes: 6 additions & 11 deletions modules/server/src/network/tests/helpers.test.ts
Original file line number Diff line number Diff line change
@@ -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',
},
];

Expand Down
4 changes: 2 additions & 2 deletions modules/server/src/network/typeDefs/aggregations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand All @@ -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) },
Expand Down
5 changes: 1 addition & 4 deletions modules/server/src/network/typeDefs/index.ts
Original file line number Diff line number Diff line change
@@ -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;
};
12 changes: 0 additions & 12 deletions modules/server/src/network/typeDefs/remoteConnections.ts

This file was deleted.