Skip to content
Closed
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 --noEmit"
},
"repository": {
"type": "git",
Expand Down
21 changes: 21 additions & 0 deletions modules/server/src/network/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,24 @@ export class NetworkAggregationError extends Error {
this.name = 'NetworkAggregationError';
}
}

type ErrorWithMessage = {
message: string;
};

const isErrorWithMessage = (error: unknown): error is ErrorWithMessage => {
return (
typeof error === 'object' &&
error !== null &&
'message' in error &&
typeof (error as Error).message === 'string'
);
};

const getErrorMessage = (error: unknown): string => {
if (isErrorWithMessage(error)) {
return error.message;
} else {
return 'Unknown error';
}
};
6 changes: 6 additions & 0 deletions modules/server/src/network/gql.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import axios, { AxiosRequestConfig } from 'axios';
import { ASTNode, print } from 'graphql';
import { GQLFieldType } from './queries';

/**
* Creates a graphql query string with variables for use in a POST request
Expand Down Expand Up @@ -46,3 +47,8 @@ export const fetchGql = ({

return axios(axiosOptions);
};

export const normalizeGqlField = (gqlField: GQLFieldType): { name: string; type: string } => {
const fieldType = gqlField.type.name;
return { name: gqlField.name, type: fieldType };
};
6 changes: 3 additions & 3 deletions modules/server/src/network/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ export const createSchemaFromNetworkConfig = async ({
}: {
networkConfigs: NetworkConfig[];
}) => {
const networkFields = await fetchAllNodeAggregations({
const nodeConfig = await fetchAllNodeAggregations({
networkConfigs,
});

const networkFieldTypes = getAllFieldTypes(networkFields, SUPPORTED_AGGREGATIONS_LIST);
const networkFieldTypes = getAllFieldTypes(nodeConfig, SUPPORTED_AGGREGATIONS_LIST);

const typeDefs = createTypeDefs(networkFieldTypes);

const resolvers = createResolvers(networkConfigs);
const resolvers = createResolvers(nodeConfig);

const networkSchema = makeExecutableSchema({ typeDefs, resolvers });

Expand Down
71 changes: 46 additions & 25 deletions modules/server/src/network/resolvers/aggregations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { fetchGql } from '../gql';
import { failure, isSuccess, Result, Success, success } from '../httpResponses';
import { Hits } from '../types/hits';
import { NetworkConfig } from '../types/setup';
import { AllAggregations } from '../types/types';
import { AllAggregations, NodeConfig } from '../types/types';
import { ASTtoString, RequestedFieldsMap } from '../util';
import { CONNECTION_STATUS, NetworkNode } from './networkNode';

Expand Down Expand Up @@ -58,7 +58,6 @@ type NetworkQuery = {

/**
* Converts info field object into string
*
* @param requestedFields
*
* @example
Expand Down Expand Up @@ -102,11 +101,24 @@ export const createNodeQueryString = (
return gqlString;
};

const createNetworkQuery = (
documentName: string,
export const createNetworkQuery = (
config: NodeConfig,
requestedFields: RequestedFieldsMap,
): DocumentNode => {
const gqlString = createNodeQueryString(documentName, requestedFields);
const availableFields = config.aggregations;
const documentName = config.documentName;

// ensure requested field is available to query on node
const fieldsToRequest = Object.keys(requestedFields).reduce((acc, requestedFieldKey) => {
const field = requestedFields[requestedFieldKey];
if (availableFields.some((field) => field.name === requestedFieldKey)) {
return { ...acc, [requestedFieldKey]: field };
} else {
return acc;
}
}, {});

const gqlString = createNodeQueryString(documentName, fieldsToRequest);

/*
* convert string to AST object to use as query
Expand All @@ -119,6 +131,7 @@ const createNetworkQuery = (
return gqlQuery;
} catch (err) {
console.error('invalid gql', err);
throw Error('Query creation failed');
}
};

Expand All @@ -132,37 +145,45 @@ type SuccessResponse = { [k: string]: { hits: Hits; aggregations: AllAggregation
* @returns
*/
export const aggregationPipeline = async (
configs: NetworkConfig[],
configs: NodeConfig[],
requestedAggregationFields: RequestedFieldsMap,
) => {
const nodeInfo: NetworkNode[] = [];

const totalAgg = new AggregationAccumulator(requestedAggregationFields);
const totalAgg = new AggregationAccumulator();

const aggregationResultPromises = configs.map(async (config) => {
const gqlQuery = createNetworkQuery(config.documentName, requestedAggregationFields);
const response = await fetchData<SuccessResponse>({ url: config.graphqlUrl, gqlQuery });

const nodeName = config.displayName;

if (isSuccess(response)) {
const documentName = config.documentName;
const aggregationData = response.data[documentName]?.aggregations || {};
const hitsData = response.data[documentName]?.hits || { total: 0 };

totalAgg.resolve(aggregationData);
nodeInfo.push({
name: nodeName,
hits: hitsData.total,
status: CONNECTION_STATUS.OK,
errors: '',
});
} else {
const nodeAvailableAggregations = config.aggregations;

try {
const gqlQuery = createNetworkQuery(config, requestedAggregationFields);
const response = await fetchData<SuccessResponse>({ url: config.graphqlUrl, gqlQuery });

if (isSuccess(response)) {
const documentName = config.documentName;
const aggregationData = response.data[documentName]?.aggregations || {};
const hitsData = response.data[documentName]?.hits || { total: 0 };

totalAgg.resolve(aggregationData);
nodeInfo.push({
name: nodeName,
hits: hitsData.total,
status: CONNECTION_STATUS.OK,
errors: '',
aggregations: nodeAvailableAggregations,
});
} else {
throw Error(`Request failed for node: ${nodeName}`);
}
} catch (error) {
const message = getErrorMessage(error);
nodeInfo.push({
name: nodeName,
hits: 0,
status: CONNECTION_STATUS.ERROR,
errors: response?.message || 'Error',
errors: message,
aggregations: nodeAvailableAggregations,
});
}
});
Expand Down
4 changes: 3 additions & 1 deletion modules/server/src/network/resolvers/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { type GraphQLResolveInfo } from 'graphql';
import { NetworkFields } from '../setup/fields';
import { NetworkConfig } from '../types/setup';
import { NodeConfig } from '../types/types';
import { resolveInfoToMap } from '../util';
import { aggregationPipeline } from './aggregations';
import { NetworkNode } from './networkNode';
Expand All @@ -20,7 +22,7 @@ export type NetworkSearchRoot = {
* @param networkFieldTypes
* @returns
*/
export const createResolvers = (configs: NetworkConfig[]) => {
export const createResolvers = (configs: NodeConfig[]) => {
return {
Query: {
network: async (
Expand Down
1 change: 1 addition & 0 deletions modules/server/src/network/resolvers/networkNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ export type NetworkNode = {
hits: number;
status: keyof typeof CONNECTION_STATUS;
errors: string;
aggregations: { name: string; type: string }[];
};
17 changes: 8 additions & 9 deletions modules/server/src/network/setup/fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { SupportedAggregation } from '../common';
import { GQLFieldType } from '../queries';
import {
NetworkFieldType,
NodeConfig,
SupportedAggregations,
SupportedNetworkFieldType,
UnsupportedAggregations,
Expand All @@ -23,7 +24,7 @@ const isSupportedType = (
* @returns { supportedAggregations: [], unsupportedAggregations: [] }
*/
export const getFieldTypes = (
fields: GQLFieldType[],
fields: NodeConfig['aggregations'],
supportedAggregationsList: SupportedAggregation[],
) => {
const fieldTypes = fields.reduce(
Expand All @@ -34,17 +35,15 @@ export const getFieldTypes = (
},
field,
) => {
const fieldType = field.type.name;
const fieldObject = { name: field.name, type: fieldType };
if (isSupportedType(fieldObject, supportedAggregationsList)) {
if (isSupportedType(field, supportedAggregationsList)) {
return {
...aggregations,
supportedAggregations: aggregations.supportedAggregations.concat(fieldObject),
supportedAggregations: aggregations.supportedAggregations.concat(field),
};
} else {
return {
...aggregations,
unsupportedAggregations: aggregations.unsupportedAggregations.concat(fieldObject),
unsupportedAggregations: aggregations.unsupportedAggregations.concat(field),
};
}
},
Expand All @@ -58,12 +57,12 @@ export const getFieldTypes = (
};

export const getAllFieldTypes = (
networkFields: NetworkFields[],
nodeConfigs: NodeConfig[],
supportedTypes: SupportedAggregation[],
) => {
const nodeFieldTypes = networkFields.map((networkField) => {
const nodeFieldTypes = nodeConfigs.map((config) => {
const { supportedAggregations, unsupportedAggregations } = getFieldTypes(
networkField.fields,
config.aggregations,
supportedTypes,
);

Expand Down
13 changes: 7 additions & 6 deletions modules/server/src/network/setup/query.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { NetworkAggregationError } from '../errors';
import { fetchGql } from '../gql';
import { fetchGql, normalizeGqlField } from '../gql';
import { gqlAggregationTypeQuery, GQLTypeQueryResponse } from '../queries';
import { NetworkConfig } from '../types/setup';
import { NodeConfig } from '../types/types';
import { fulfilledPromiseFilter } from '../util';
import { NetworkFields } from './fields';

type NetworkQueryResult = PromiseFulfilledResult<{
config: NetworkConfig;
Expand Down Expand Up @@ -67,7 +67,7 @@ export const fetchAllNodeAggregations = async ({
networkConfigs,
}: {
networkConfigs: NetworkConfig[];
}): Promise<NetworkFields[]> => {
}): Promise<NodeConfig[]> => {
// query remote connection types
const networkQueryPromises = networkConfigs.map(async (config) => {
const gqlResponse = await fetchNodeAggregations(config);
Expand All @@ -76,15 +76,16 @@ export const fetchAllNodeAggregations = async ({

const networkQueries = await Promise.allSettled(networkQueryPromises);

const nodeAggregations = networkQueries
const nodeConfigs = networkQueries
.filter(fulfilledPromiseFilter<NetworkQueryResult>)
.map((networkResult) => {
const { config, gqlResponse } = networkResult.value;
const fields = gqlResponse.__type.fields;
return { name: config.displayName, fields };
const aggregations = fields.map(normalizeGqlField);
return { ...config, aggregations };
});

console.log('\nSuccessfully fetched node schemas\n');

return nodeAggregations;
return nodeConfigs;
};
13 changes: 11 additions & 2 deletions modules/server/src/network/typeDefs/aggregations.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { GraphQLInt, GraphQLList, GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql';
import { SupportedNetworkFieldType } from '../types';
import { SupportedNetworkFieldType } from '../types/types';
import { singleToNetworkAggregationMap } from './networkAggregations';

/**
Expand All @@ -8,7 +8,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 Expand Up @@ -39,13 +39,22 @@ export const createNetworkAggregationTypeDefs = (
fields: allFields,
});

const aggregationList = new GraphQLObjectType({
name: 'aggregations',
fields: {
name: { type: GraphQLString },
type: { type: GraphQLString },
},
});

const remoteConnectionType = new GraphQLObjectType({
name: 'RemoteConnection',
fields: {
name: { type: GraphQLString },
hits: { type: GraphQLInt },
status: { type: GraphQLString },
errors: { type: GraphQLString },
aggregations: { type: new GraphQLList(aggregationList) },
},
});

Expand Down
3 changes: 3 additions & 0 deletions modules/server/src/network/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ObjectValues } from '@/utils/types';
import { SupportedAggregation } from '../common';
import { CONNECTION_STATUS } from '../resolvers/networkNode';
import { Aggregations, Bucket, NumericAggregations } from './aggregations';
import { NetworkConfig } from './setup';

// environment config
export type NetworkAggregationConfigInput = {
Expand Down Expand Up @@ -33,3 +34,5 @@ export type NetworkAggregation = {
bucket_count: number;
buckets: Bucket[];
};

export type NodeConfig = NetworkConfig & { aggregations: { name: string; type: string }[] };