From fe10dc26342c2e4c1f16768b1d5ad11e66989dfc Mon Sep 17 00:00:00 2001 From: Ciaran Schutte Date: Sat, 15 Mar 2025 17:28:58 -0400 Subject: [PATCH 1/8] cleanup utils --- modules/server/src/network/{util.ts => utils/gql.ts} | 12 ------------ modules/server/src/network/utils/promise.ts | 11 +++++++++++ 2 files changed, 11 insertions(+), 12 deletions(-) rename modules/server/src/network/{util.ts => utils/gql.ts} (79%) create mode 100644 modules/server/src/network/utils/promise.ts diff --git a/modules/server/src/network/util.ts b/modules/server/src/network/utils/gql.ts similarity index 79% rename from modules/server/src/network/util.ts rename to modules/server/src/network/utils/gql.ts index 6a76e5647..9413d06ed 100644 --- a/modules/server/src/network/util.ts +++ b/modules/server/src/network/utils/gql.ts @@ -8,18 +8,6 @@ export const ASTtoString = (ast: DocumentNode) => { return print(ast); }; -/** - * Type guard to filter fulfilled Promises - */ -export const fulfilledPromiseFilter = (result: unknown): result is Result => { - return ( - typeof result === 'object' && - result !== null && - 'status' in result && - result.status === 'fulfilled' - ); -}; - /** * Turns GraphQLResolveInfo into a map of the requested fields * diff --git a/modules/server/src/network/utils/promise.ts b/modules/server/src/network/utils/promise.ts new file mode 100644 index 000000000..1d442ecb0 --- /dev/null +++ b/modules/server/src/network/utils/promise.ts @@ -0,0 +1,11 @@ +/** + * Type guard to filter fulfilled Promises + */ +export const fulfilledPromiseFilter = (result: unknown): result is Result => { + return ( + typeof result === 'object' && + result !== null && + 'status' in result && + result.status === 'fulfilled' + ); +}; From 0b110e861b03bb01fac3bc35431301ec436c67e8 Mon Sep 17 00:00:00 2001 From: Ciaran Schutte Date: Sat, 15 Mar 2025 18:14:10 -0400 Subject: [PATCH 2/8] reorg types, decompose file, improve comments --- .../aggregations/AggregationAccumulator.ts | 7 +- modules/server/src/network/common.ts | 45 --- modules/server/src/network/gql.ts | 6 - modules/server/src/network/index.ts | 40 ++- modules/server/src/network/queries/index.ts | 47 --- .../src/network/resolvers/aggregations.ts | 270 ++++-------------- modules/server/src/network/resolvers/fetch.ts | 71 +++++ modules/server/src/network/resolvers/index.ts | 85 +++--- modules/server/src/network/resolvers/query.ts | 105 +++++++ .../server/src/network/resolvers/response.ts | 6 +- modules/server/src/network/setup/constants.ts | 16 ++ modules/server/src/network/setup/fields.ts | 26 +- modules/server/src/network/setup/query.ts | 70 ++++- ...regations.ts => ___networkAggregations.ts} | 0 .../src/network/typeDefs/aggregations.ts | 24 +- modules/server/src/network/typeDefs/index.ts | 2 +- .../network/types/{types.ts => __types.ts} | 0 .../server/src/network/types/aggregations.ts | 1 - 18 files changed, 414 insertions(+), 407 deletions(-) delete mode 100644 modules/server/src/network/common.ts delete mode 100644 modules/server/src/network/queries/index.ts create mode 100644 modules/server/src/network/resolvers/fetch.ts create mode 100644 modules/server/src/network/resolvers/query.ts create mode 100644 modules/server/src/network/setup/constants.ts rename modules/server/src/network/typeDefs/{networkAggregations.ts => ___networkAggregations.ts} (100%) rename modules/server/src/network/types/{types.ts => __types.ts} (100%) diff --git a/modules/server/src/network/aggregations/AggregationAccumulator.ts b/modules/server/src/network/aggregations/AggregationAccumulator.ts index cc69079bd..1b2019d79 100644 --- a/modules/server/src/network/aggregations/AggregationAccumulator.ts +++ b/modules/server/src/network/aggregations/AggregationAccumulator.ts @@ -1,10 +1,10 @@ import { Relation } from '@/mapping/masking'; import { ALL_NETWORK_AGGREGATION_TYPES_MAP } from '..'; -import { SupportedAggregation, SUPPORTED_AGGREGATIONS } from '../common'; +import { SUPPORTED_AGGREGATIONS } from '../setup/constants'; import { Aggregations, Bucket, NumericAggregations } from '../types/aggregations'; import { Hits } from '../types/hits'; import { AllAggregations } from '../types/types'; -import { RequestedFieldsMap } from '../util'; +import { RequestedFieldsMap } from '../utils/gql'; type ResolveAggregationInput = { data: { aggregations: AllAggregations; hits: Hits }; @@ -16,9 +16,8 @@ type AggregationsTuple = [Aggregations, Aggregations]; type NumericAggregationsTuple = [NumericAggregations, NumericAggregations]; const emptyAggregation = (hits: number): Aggregations => ({ - __typename: 'Aggregations', bucket_count: 1, - buckets: [{ key: '___aggregation_not_available___', doc_count: hits, relation: "eq" }], + buckets: [{ key: '___aggregation_not_available___', doc_count: hits, relation: 'eq' }], }); // mutation - update a single aggregations field in the accumulator diff --git a/modules/server/src/network/common.ts b/modules/server/src/network/common.ts deleted file mode 100644 index e79c43955..000000000 --- a/modules/server/src/network/common.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { ObjectValues } from '@/utils/types'; - -/* - * Network aggregations - */ -export const NETWORK_AGGREGATIONS = { - NetworkAggregations: 'NetworkAggregations', - NetworkNumericAggregations: 'NetworkNumericAggregations', -} as const; -export type NetworkAggregation = ObjectValues; - -/* - * Supported single aggregations that can be used by network aggregation - */ -export const SUPPORTED_AGGREGATIONS = { - Aggregations: 'Aggregations', - NumericAggregations: 'NumericAggregations', -} as const; - -export type SupportedAggregation = ObjectValues; - -export const SUPPORTED_AGGREGATIONS_LIST: SupportedAggregation[] = [ - SUPPORTED_AGGREGATIONS.Aggregations, - SUPPORTED_AGGREGATIONS.NumericAggregations, -]; - -/** - * Multiple cases when we need to map from the "single" aggregation types to the "network" - * - * @example - * "Aggregations" => "NetworkAggregations" - */ -export const singleToNetworkNameMap = new Map() - .set(SUPPORTED_AGGREGATIONS.Aggregations, NETWORK_AGGREGATIONS.NetworkAggregations) - .set(SUPPORTED_AGGREGATIONS.NumericAggregations, NETWORK_AGGREGATIONS.NetworkNumericAggregations); - -/** - * Multiple cases when we need to map from the "network" aggregation types to the "single" - * - * @example - * "NetworkAggregations" => "Aggregations" - */ -export const networkToSingleNameMap = new Map() - .set(NETWORK_AGGREGATIONS.NetworkAggregations, SUPPORTED_AGGREGATIONS.Aggregations) - .set(NETWORK_AGGREGATIONS.NetworkNumericAggregations, SUPPORTED_AGGREGATIONS.NumericAggregations); diff --git a/modules/server/src/network/gql.ts b/modules/server/src/network/gql.ts index f551c3ad1..e2c6329ab 100644 --- a/modules/server/src/network/gql.ts +++ b/modules/server/src/network/gql.ts @@ -1,5 +1,4 @@ import axios, { AxiosRequestConfig } from 'axios'; -import { GQLFieldType } from './queries'; /** * GQL fetch request with request options configured for graphql @@ -30,8 +29,3 @@ 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 }; -}; diff --git a/modules/server/src/network/index.ts b/modules/server/src/network/index.ts index 949210ff4..e627bff13 100644 --- a/modules/server/src/network/index.ts +++ b/modules/server/src/network/index.ts @@ -1,45 +1,59 @@ import { makeExecutableSchema } from '@graphql-tools/schema'; -import { SupportedAggregation, SUPPORTED_AGGREGATIONS_LIST } from './common'; import { createResolvers } from './resolvers'; -import { getAllFieldTypes } from './setup/fields'; +import { normalizeFieldTypes } 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(); +/** + * Map of all available fields with associated aggregation type + */ +export let ALL_NETWORK_AGGREGATION_TYPES_MAP: Map = + new Map(); /** * GQL Federated Search schema setup - * Connects to remote network connections, looks up field types, add field/type pairs to configs + * 1) Connects to remote Arranger instances as defined in Arranger config + * 2) Looks up available field types + * 3) Adds field/type pairs to config map + * + * ! Important ! - This functionality assumes Arranger instances are running identical versions * * @param { networkConfigs } - * @returns graphql schema for the network - types and resolvers combined + * @returns Graphql schema for the network - types and resolvers combined */ export const createSchemaFromNetworkConfig = async ({ networkConfigs, }: { networkConfigs: NetworkConfig[]; }) => { + /** + * Fetches meta data from remote Arranger instances + * Adds aggregation fied/type information to networked node configuration + */ const nodeConfig = await fetchAllNodeAggregations({ networkConfigs, }); - const networkFieldTypes = getAllFieldTypes(nodeConfig, SUPPORTED_AGGREGATIONS_LIST); + /** + * An array of unique supported aggregation types + */ + const aggregationTypes = normalizeFieldTypes(nodeConfig); /* - * make schema type available for resolvers at query time + * Runs on schema setup, once at bootstrap. + * Make schema type available for resolvers at query time * { name: "donor_age", type: "NumericAggregations" } - * donor_age => NumericAggregations - * runs on schema setup once at bootstrap */ - networkFieldTypes.forEach((field) => + aggregationTypes.forEach((field) => ALL_NETWORK_AGGREGATION_TYPES_MAP.set(field.name, field.type), ); - const typeDefs = createTypeDefs(networkFieldTypes); - + /* + * GQL typedef, resolver and schema creation + */ + const typeDefs = createTypeDefs(aggregationTypes); const resolvers = createResolvers(nodeConfig); - const networkSchema = makeExecutableSchema({ typeDefs, resolvers }); return { networkSchema }; diff --git a/modules/server/src/network/queries/index.ts b/modules/server/src/network/queries/index.ts deleted file mode 100644 index 554aaaeac..000000000 --- a/modules/server/src/network/queries/index.ts +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Part of the federated search is requesting aggregate data from - * individual remote connections which expose an Arranger GQL endpoint - * - * These are the GQL queries for the supported GQL types - */ - -import { Aggregations } from '../types/aggregations'; - -export type GQLFieldType = { - name: string; - type: { - name: string; - }; -}; - -export type GQLTypeQueryResponse = { - __type: { - name: string; - fields: GQLFieldType[]; - }; -}; - -// Data query response -export type AggregationGQLResponse = { - [documentName: string]: { - [fieldName: string]: Aggregations; - }; -}; - -/** - * Query to get field types - * eg. __type('aggregations') - */ -export const gqlAggregationTypeQuery = `#graphql - query getAggregationTypes($documentTypeName: String!) { - __type(name: $documentTypeName) { - name - fields { - name - type { - name - } - } - } - } -`; diff --git a/modules/server/src/network/resolvers/aggregations.ts b/modules/server/src/network/resolvers/aggregations.ts index 157e8f1da..e994ec533 100644 --- a/modules/server/src/network/resolvers/aggregations.ts +++ b/modules/server/src/network/resolvers/aggregations.ts @@ -1,20 +1,16 @@ -import { gql } from 'apollo-server-core'; -import axios, { AxiosError } from 'axios'; -import { DocumentNode } from 'graphql'; -import { isEmpty } from 'lodash'; +import { Aggregations } from '@/mapping/resolveAggregations'; import { AggregationAccumulator } from '../aggregations/AggregationAccumulator'; -import { fetchGql } from '../gql'; -import { failure, isSuccess, Result, success } from '../result'; +import { failure, isSuccess } from '../result'; +import { NodeConfig } from '../setup/query'; import { Hits } from '../types/hits'; -import { AllAggregations, NodeConfig } from '../types/types'; -import { ASTtoString, RequestedFieldsMap } from '../util'; -import { CONNECTION_STATUS, NetworkNode } from './networkNode'; +import { RequestedFieldsMap } from '../utils/gql'; +import { fetchData } from './fetch'; +import { createNetworkQuery } from './query'; -type NetworkQuery = { - url: string; - gqlQuery: DocumentNode; - queryVariables: QueryVariables; -}; +export const CONNECTION_STATUS = { + OK: 'OK', + ERROR: 'ERROR', +} as const; type QueryVariables = { filters?: object; @@ -22,169 +18,22 @@ type QueryVariables = { include_missing?: boolean; }; -/** - * Query remote connections and handle network responses - * - * @param query - * @returns - */ - -// narrows type -const isAxiosError = (error: unknown): error is AxiosError => axios.isAxiosError(error); - -const fetchData = async ( - query: NetworkQuery, -): Promise> => { - const { url, gqlQuery, queryVariables } = query; - - console.log(`Fetch data starting for ${url}`); - - try { - const response = await fetchGql({ - url, - gqlQuery: ASTtoString(gqlQuery), - variables: queryVariables, - }); - - // 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}`); - } - - if (axios.isAxiosError(error)) { - const errorResponse = error as AxiosError<{ errors: { message: string }[] }>; - - if (errorResponse.code === 'ECONNREFUSED') { - console.error(`Network failure: ${url}`); - return failure(CONNECTION_STATUS.ERROR, `Network failure: ${url}`); - } - - if (error.response) { - const errors = - errorResponse.response && - errorResponse.response.data.errors.map((gqlError) => gqlError.message).join('\n'); - console.error(errors); - return failure(CONNECTION_STATUS.ERROR, 'errors'); - } - } - return failure(CONNECTION_STATUS.ERROR, `Unknown error`); - } - // TS would like a return value outside of try/catch handling - return failure(CONNECTION_STATUS.ERROR, `Unknown error`); -}; - -/** - * Converts info field object into string - * @param requestedFields - * - * @example - * ### Input - * ``` - * { donors: { - * buckets: { - * bucket_count: {}, - * } - * }} - * ``` - * - * ### Output - * ``` - * ` - * { donors { - * buckets { - * bucket_count - * } - * }} - * ` - * ``` - */ -const convertFieldsToString = (requestedFields: RequestedFieldsMap) => { - const gqlFieldsString = JSON.stringify(requestedFields) - .replaceAll('"', '') - .replaceAll(':', '') - .replaceAll('{}', '') - .replaceAll(',', ' ') - .replaceAll('\\', ' '); - - return gqlFieldsString; -}; - -/** - * Creates individual GQL query string for a node. - * Includes aggregation GQL arguments (actual data is provided alongside query, not here) - * Requested fields are converted to GQL style strings - * - * @param documentName - * @param requestedFields - * @returns constructed query string - */ -export const createNodeQueryString = ( - documentName: string, - requestedFields: RequestedFieldsMap, -) => { - const fields = convertFieldsToString(requestedFields); - const aggregationsString = !isEmpty(fields) ? `aggregations ${fields}` : ''; - const gqlString = `query nodeQuery {${documentName} { hits { total } ${aggregationsString} }}`; - return gqlString; -}; - -/** - * Creates a GQL query for fields with query arguments. - * Only adds requested fields that are available on a node. - * - * @param config - * @param requestedFields - * @returns a GQL document node or undefined if a valid GQL document node cannot be created - */ -export const createNetworkQuery = ( - config: NodeConfig, - requestedFields: RequestedFieldsMap, -): DocumentNode | undefined => { - 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; - } - }, {}); +type SuccessResponse = Record; - const gqlString = createNodeQueryString(documentName, fieldsToRequest); - - /* - * convert string to AST object - * not needed if gqlString is formatted correctly but this acts as a validity check - */ - try { - const gqlQuery = gql` - ${gqlString} - `; - return gqlQuery; - } catch (err) { - console.error('invalid gql', err); - return undefined; - } +export type NetworkNode = { + name: string; + hits: number; + status: keyof typeof CONNECTION_STATUS; + errors: string; + aggregations: { name: string; type: string }[]; }; -type SuccessResponse = { [k: string]: { hits: Hits; aggregations: AllAggregations } }; - /** * Query each remote connection * * @param queries - Query for each remote connection - * @param requestedAggregationFields - * @returns + * @param requestedAggregationFields - Fields requested + * @returns Resolved aggregation and node info */ export const aggregationPipeline = async ( configs: NodeConfig[], @@ -195,45 +44,48 @@ export const aggregationPipeline = async ( const totalAgg = new AggregationAccumulator(requestedAggregationFields); - const aggregationResultPromises = configs.map(async (config) => { - const gqlQuery = createNetworkQuery(config, requestedAggregationFields); - const response = gqlQuery - ? await fetchData({ - url: config.graphqlUrl, - gqlQuery, - queryVariables, - }) - : failure(CONNECTION_STATUS.ERROR, 'Invalid GQL query'); - - const nodeName = config.displayName; - - if (isSuccess(response)) { - const documentName = config.documentName; - const responseData = response.data[documentName]; - const aggregations = responseData?.aggregations || {}; - const hits = responseData?.hits || { total: 0 }; - - totalAgg.resolve({ aggregations, hits }); - - nodeInfo.push({ - name: nodeName, - hits: hits.total, - status: CONNECTION_STATUS.OK, - errors: '', - aggregations: config.aggregations, - }); - } else { - nodeInfo.push({ - name: nodeName, - hits: 0, - status: CONNECTION_STATUS.ERROR, - errors: response?.message || 'Error', - aggregations: config.aggregations, - }); - } - }); + Promise.allSettled( + configs.map(async (config) => { + // create node query + const gqlQuery = createNetworkQuery(config, requestedAggregationFields); + + // query node + const response = gqlQuery + ? await fetchData({ + url: config.graphqlUrl, + gqlQuery, + queryVariables, + }) + : failure(CONNECTION_STATUS.ERROR, 'Invalid GQL query'); + + const nodeName = config.displayName; + + if (isSuccess(response)) { + const documentName = config.documentName; + const responseData = response.data[documentName]; + const aggregations = responseData?.aggregations || {}; + const hits = responseData?.hits || { total: 0 }; + + totalAgg.resolve({ aggregations, hits }); + + nodeInfo.push({ + name: nodeName, + hits: hits.total, + status: CONNECTION_STATUS.OK, + errors: '', + aggregations: config.aggregations, + }); + } else { + nodeInfo.push({ + name: nodeName, + hits: 0, + status: CONNECTION_STATUS.ERROR, + errors: response?.message || 'Error', + aggregations: config.aggregations, + }); + } + }), + ); - // return accumulated results - await Promise.allSettled(aggregationResultPromises); return { aggregationResults: totalAgg.result(), nodeInfo }; }; diff --git a/modules/server/src/network/resolvers/fetch.ts b/modules/server/src/network/resolvers/fetch.ts new file mode 100644 index 000000000..8c2f4f128 --- /dev/null +++ b/modules/server/src/network/resolvers/fetch.ts @@ -0,0 +1,71 @@ +import axios, { AxiosError } from 'axios'; +import { DocumentNode } from 'graphql'; + +import { fetchGql } from '../gql'; +import { failure, Result, success } from '../result'; +import { ASTtoString } from '../utils/gql'; +import { CONNECTION_STATUS } from './aggregations'; + +type NetworkQuery = { + url: string; + gqlQuery: DocumentNode; + queryVariables: { + filters?: object; + aggregations_filter_themselves?: boolean; + include_missing?: boolean; + }; +}; + +/** + * Query remote connections and handle network responses + * + * @param query + * @returns + */ +export const fetchData = async ( + query: NetworkQuery, +): Promise> => { + const { url, gqlQuery, queryVariables } = query; + + console.log(`Fetch data starting for ${url}..`); + + try { + const response = await fetchGql({ + url, + gqlQuery: ASTtoString(gqlQuery), + variables: queryVariables, + }); + + // 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}`); + } + + if (axios.isAxiosError(error)) { + const errorResponse = error as AxiosError<{ errors: { message: string }[] }>; + + if (errorResponse.code === 'ECONNREFUSED') { + console.error(`Network failure: ${url}`); + return failure(CONNECTION_STATUS.ERROR, `Network failure: ${url}`); + } + + if (error.response) { + const errors = + errorResponse.response && + errorResponse.response.data.errors.map((gqlError) => gqlError.message).join('\n'); + console.error(errors); + return failure(CONNECTION_STATUS.ERROR, 'errors'); + } + } + return failure(CONNECTION_STATUS.ERROR, `Unknown error`); + } + // TS would like a return value outside of try/catch handling + return failure(CONNECTION_STATUS.ERROR, `Unknown error`); +}; diff --git a/modules/server/src/network/resolvers/index.ts b/modules/server/src/network/resolvers/index.ts index f03a3a68d..e11b1370e 100644 --- a/modules/server/src/network/resolvers/index.ts +++ b/modules/server/src/network/resolvers/index.ts @@ -1,13 +1,12 @@ -import { type GraphQLResolveInfo } from 'graphql'; +import { Resolver } from '@/gqlServer'; import { isSuccess } from '../result'; -import { NodeConfig } from '../types/types'; -import { resolveInfoToMap } from '../util'; +import { NodeConfig } from '../setup/query'; +import { resolveInfoToMap } from '../utils/gql'; import { convertToSqon } from '../utils/sqon'; -import { aggregationPipeline } from './aggregations'; -import { NetworkNode } from './networkNode'; +import { aggregationPipeline, NetworkNode } from './aggregations'; import { createResponse } from './response'; -export type NetworkSearchRoot = { +type NetworkSearchRoot = { nodes: NetworkNode[]; aggregations: Record; }; @@ -15,55 +14,59 @@ export type NetworkSearchRoot = { /* * Type should match the "Network" GQL type definition arg types */ -export type NetworkAggregationArgs = { +// TODO: shared? +type NetworkAggregationArgs = { filters?: object; aggregations_filter_themselves?: boolean; include_missing?: boolean; }; /** - * Create GQL resolvers. - * - * It's important to have both remote connection data and aggregations under a single field - * as remote connection data is dependant on aggregations query + * Resolvers for network search. * * @param networkConfigsWithSchemas * @param networkFieldTypes * @returns */ export const createResolvers = (configs: NodeConfig[]) => { - return { - Root: { - network: async ( - parent: NetworkSearchRoot, - // as mentioned above, type should match gql typedefs - args: NetworkAggregationArgs, - context: unknown, - info: GraphQLResolveInfo, - ) => { - const requestedFieldsMap = resolveInfoToMap(info, 'aggregations'); + const network: Resolver = async ( + _unusedParentObj, + args, + _unusedContext, + info, + ) => { + const requestedFieldsMap = resolveInfoToMap(info, 'aggregations'); + + /* + * Checks validity of SQON + * For now we will pass through the non SQON object to the pipeline + * + * TODO: resolve Arranger / SQONBuilder SQON outer wrapper conflict + * {"content": [{...}], "op": "and"} + */ + if ('filters' in args) { + const result = convertToSqon(args.filters); + if (!isSuccess(result)) { + throw new Error(`${result.status} : ${result.message}`); + } + } + const queryVariables = { ...args }; - /* - * checks validity of SQON - * for now we will pass through the non SQON object to the pipeline - * TODO: resolve Arranger / SQONBuilder SQON outer wrapper conflict - * ie. {"content": [{...}], "op": "and"} - */ - if ('filters' in args) { - const result = convertToSqon(args.filters); - if (!isSuccess(result)) { - throw new Error(`${result.status} : ${result.message}`); - } - } - const queryVariables = { ...args }; + /* + * Aggregation pipeline entrypoint + */ + const { aggregationResults, nodeInfo } = await aggregationPipeline( + configs, + requestedFieldsMap, + queryVariables, + ); - const { aggregationResults, nodeInfo } = await aggregationPipeline( - configs, - requestedFieldsMap, - queryVariables, - ); - return createResponse({ aggregationResults, nodeInfo }); - }, + return createResponse({ aggregationResults, nodeInfo }); + }; + + return { + Root: { + network, }, }; }; diff --git a/modules/server/src/network/resolvers/query.ts b/modules/server/src/network/resolvers/query.ts new file mode 100644 index 000000000..ba115d256 --- /dev/null +++ b/modules/server/src/network/resolvers/query.ts @@ -0,0 +1,105 @@ +import { gql } from 'apollo-server-core'; +import { DocumentNode } from 'graphql'; +import { isEmpty } from 'lodash'; + +import { NodeConfig } from '../setup/query'; +import { RequestedFieldsMap } from '../utils/gql'; + +/** + * Converts info field object into string + * + * @param requestedFields - Query fields object + * @returns GQL string + * + * @example + * ### Input + * ``` + * { donors: { + * buckets: { + * bucket_count: {}, + * } + * }} + * ``` + * + * ### Output + * ``` + * ` + * { donors { + * buckets { + * bucket_count + * } + * }} + * ` + * ``` + */ +const convertFieldsToString = (requestedFields: RequestedFieldsMap) => { + const gqlFieldsString = JSON.stringify(requestedFields) + .replaceAll('"', '') + .replaceAll(':', '') + .replaceAll('{}', '') + .replaceAll(',', ' ') + .replaceAll('\\', ' '); + + return gqlFieldsString; +}; + +/** + * Creates individual GQL query string for a node. + * Includes aggregation GQL arguments (actual data is sent with query in additional param) + * Requested fields are converted to GQL style strings + * + * This hardcodes an aggregation query for network search + * + * @param documentName - File type document name configured on node + * @param requestedFields - Query fields object + * @returns Fully constructed query string + */ +const createFileGQLQuery = (documentName: string, requestedFields: RequestedFieldsMap) => { + const fields = convertFieldsToString(requestedFields); + const queryArgs = `(filters: $filters, aggregations_filter_themselves: $aggregations_filter_themselves, include_missing: $include_missing)`; + const aggregationsString = !isEmpty(fields) ? `aggregations${queryArgs} ${fields}` : ''; + const gqlString = `query nodeQuery${queryArgs} {${documentName} { hits { total } ${aggregationsString} }}`; + return gqlString; +}; + +/** + * Creates a GQL query for fields with query arguments. + * Only adds requested fields that are available on a node. + * + * @param config - Node config + * @param requestedFields - Fields from query + * @returns a GQL document node or undefined if a valid GQL document node cannot be created + */ +export const createNetworkQuery = ( + config: NodeConfig, + requestedFields: RequestedFieldsMap, +): DocumentNode | undefined => { + const availableFields = config.aggregations; + const documentName = config.documentName; + + // ensure requested field is available to query on node + const fieldsToRequest = Object.keys(requestedFields).reduce((fields, requestedFieldKey) => { + const field = requestedFields[requestedFieldKey]; + if (availableFields.some((field) => field.name === requestedFieldKey)) { + return { ...fields, [requestedFieldKey]: field }; + } else { + return fields; + } + }, {}); + + const gqlString = createFileGQLQuery(documentName, fieldsToRequest); + + /* + * convert string to AST object + * not needed if gqlString is formatted correctly but this acts as a validity check + */ + try { + const gqlQuery = gql` + ${gqlString} + `; + return gqlQuery; + } catch (err) { + console.error('invalid gql', err); + return undefined; + } +}; diff --git a/modules/server/src/network/resolvers/response.ts b/modules/server/src/network/resolvers/response.ts index b4e6cede7..cb95e103e 100644 --- a/modules/server/src/network/resolvers/response.ts +++ b/modules/server/src/network/resolvers/response.ts @@ -1,5 +1,5 @@ -import { AllAggregations } from '../types/types'; -import { NetworkNode } from './networkNode'; +import { Aggregations } from '@/mapping/resolveAggregations'; +import { NetworkNode } from './aggregations'; /** * Format response object to match GQL type defs @@ -8,7 +8,7 @@ export const createResponse = ({ aggregationResults, nodeInfo, }: { - aggregationResults: AllAggregations; + aggregationResults: Aggregations; nodeInfo: NetworkNode[]; }) => { return { nodes: nodeInfo, aggregations: aggregationResults }; diff --git a/modules/server/src/network/setup/constants.ts b/modules/server/src/network/setup/constants.ts new file mode 100644 index 000000000..4cc64b712 --- /dev/null +++ b/modules/server/src/network/setup/constants.ts @@ -0,0 +1,16 @@ +import { ObjectValues } from '@/utils/types'; + +/* + * Supported single aggregations that can be used by network aggregation + */ +export const SUPPORTED_AGGREGATIONS = { + Aggregations: 'Aggregations', + NumericAggregations: 'NumericAggregations', +} as const; + +export type SupportedAggregation = ObjectValues; + +export const SUPPORTED_AGGREGATIONS_LIST: SupportedAggregation[] = [ + SUPPORTED_AGGREGATIONS.Aggregations, + SUPPORTED_AGGREGATIONS.NumericAggregations, +]; diff --git a/modules/server/src/network/setup/fields.ts b/modules/server/src/network/setup/fields.ts index a62396195..54f31d3c9 100644 --- a/modules/server/src/network/setup/fields.ts +++ b/modules/server/src/network/setup/fields.ts @@ -1,15 +1,18 @@ import { SupportedAggregation } from '../common'; import { GQLFieldType } from '../queries'; -import { - NetworkFieldType, - NodeConfig, - SupportedAggregations, - SupportedNetworkFieldType, - UnsupportedAggregations, -} from '../types/types'; + +import { SUPPORTED_AGGREGATIONS_LIST } from './constants'; export type NetworkFields = { name: string; fields: GQLFieldType[] }; +type NetworkFieldType = { + name: string; + type: T; +}; +type SupportedNetworkFieldType = NetworkFieldType; +type SupportedAggregations = SupportedNetworkFieldType[]; +type UnsupportedAggregations = NetworkFieldType[]; + const isSupportedType = ( fieldObject: NetworkFieldType, supportedList: string[], @@ -23,10 +26,7 @@ const isSupportedType = ( * @param fields * @returns { supportedAggregations: [], unsupportedAggregations: [] } */ -export const getFieldTypes = ( - fields: NodeConfig['aggregations'], - supportedAggregationsList: SupportedAggregation[], -) => { +export const getFieldTypes = (fields: NodeConfig['aggregations']) => { const fieldTypes = fields.reduce( ( aggregations: { @@ -35,7 +35,7 @@ export const getFieldTypes = ( }, field, ) => { - if (isSupportedType(field, supportedAggregationsList)) { + if (isSupportedType(field, SUPPORTED_AGGREGATIONS_LIST)) { return { ...aggregations, supportedAggregations: aggregations.supportedAggregations.concat(field), @@ -62,7 +62,7 @@ export const getFieldTypes = ( * @param supportedTypes * @returns unique fields */ -export const getAllFieldTypes = ( +const getAllFieldTypes = ( nodeConfigs: NodeConfig[], supportedTypes: SupportedAggregation[], ): SupportedNetworkFieldType[] => { diff --git a/modules/server/src/network/setup/query.ts b/modules/server/src/network/setup/query.ts index fc9472e0c..1c9416cbd 100644 --- a/modules/server/src/network/setup/query.ts +++ b/modules/server/src/network/setup/query.ts @@ -1,20 +1,66 @@ import { NetworkAggregationError } from '../errors'; -import { fetchGql, normalizeGqlField } from '../gql'; -import { gqlAggregationTypeQuery, GQLTypeQueryResponse } from '../queries'; +import { fetchGql } from '../gql'; import { NetworkConfig } from '../types/setup'; -import { NodeConfig } from '../types/types'; -import { fulfilledPromiseFilter } from '../util'; +import { fulfilledPromiseFilter } from '../utils/promise'; + +export type NodeConfig = NetworkConfig & { aggregations: { name: string; type: string }[] }; type NetworkQueryResult = PromiseFulfilledResult<{ config: NetworkConfig; gqlResponse: GQLTypeQueryResponse; }>; +type GQLFieldType = { + name: string; + type: { + name: string; + }; +}; + +type GQLTypeQueryResponse = { + __type: { + name: string; + fields: GQLFieldType[]; + }; +}; + +/** + * Query to get field types + * eg. __type('aggregations') + */ +const gqlAggregationTypeQuery = `#graphql + query getAggregationTypes($documentTypeName: String!) { + __type(name: $documentTypeName) { + name + fields { + name + type { + name + } + } + } + } +`; + +/** + * Unnests graphql field + * { name: 'donor_gender', type: { name: 'Aggregation' }} + * => + * { name: 'donor_gender', type: 'Aggregation'} + * + * @param gqlField - Nested field object + * @returns An unnested object containing field name and type + */ +const normalizeGqlField = (gqlField: GQLFieldType): { name: string; type: string } => { + const fieldType = gqlField.type.name; + return { name: gqlField.name, type: fieldType }; +}; + /** * GQL query remote connection with __type query to retrieve list of types * * @param config - network config from env - * @returns a promise containing network config and the gql query result + * @returns A promise containing network config and the gql query result * * @throws Fetch failed error * @@ -26,10 +72,10 @@ const fetchNodeAggregations = async ( config: NetworkConfig, ): Promise => { const { graphqlUrl, documentType } = config; - /* + /** * documentType is an entire field name / type name in the case of a root field * eg. `file:file`, `torontoFile:torontoFile` - * it also prefixes dynamic type names like *Connection and *Aggregations + * Arranger also constructs dynamic type names like *Connection and *Aggregations * eg. `aggregations: fileAggregations`, `hits: fileConnection` */ try { @@ -41,13 +87,13 @@ const fetchNodeAggregations = async ( variables: { documentTypeName: typename }, }); - // axios response "data" field, graphql response "data" field + // axios response "data" field containing graphql response "data" field const responseData = response.data?.data; if (response.status === 200 && response.statusText === 'OK') { return responseData; } - console.error('unexpected response data in fetchRemoteSchema'); + console.error('Unexpected response data in fetchRemoteSchema'); throw new NetworkAggregationError( `Unexpected data in response object. Please verify the endpoint at ${graphqlUrl} is returning a valid GQL Schema.`, ); @@ -69,14 +115,14 @@ export const fetchAllNodeAggregations = async ({ networkConfigs: NetworkConfig[]; }): Promise => { // query remote connection types - const networkQueryPromises = networkConfigs.map(async (config) => { + const queryRequestPromises = networkConfigs.map(async (config) => { const gqlResponse = await fetchNodeAggregations(config); return { config, gqlResponse }; }); - const networkQueries = await Promise.allSettled(networkQueryPromises); + const queryResults = await Promise.allSettled(queryRequestPromises); - const nodeConfigs = networkQueries + const nodeConfigs = queryResults .filter(fulfilledPromiseFilter) .map((networkResult) => { const { config, gqlResponse } = networkResult.value; diff --git a/modules/server/src/network/typeDefs/networkAggregations.ts b/modules/server/src/network/typeDefs/___networkAggregations.ts similarity index 100% rename from modules/server/src/network/typeDefs/networkAggregations.ts rename to modules/server/src/network/typeDefs/___networkAggregations.ts diff --git a/modules/server/src/network/typeDefs/aggregations.ts b/modules/server/src/network/typeDefs/aggregations.ts index 901f45d7d..a29e639d7 100644 --- a/modules/server/src/network/typeDefs/aggregations.ts +++ b/modules/server/src/network/typeDefs/aggregations.ts @@ -11,10 +11,13 @@ import { SupportedNetworkFieldType } from '../types/types'; import { singleToNetworkAggregationMap } from './networkAggregations'; /** - * Converts field/types to GQLObjectType definition shape + * Creates nested GQL structured object from field type + * { name: 'donor_gender', type: 'Aggregation'} + * => + * { 'donor_gender', type: { name: 'Aggregation' }} * - * @example - * { name: "donor_age", type: "NumericAggregations" } => { donor_age: { type: "NetworkNumericAggregations" } } + * @param fieldTypes - An array of fields with types + * @returns Structured GQL fields object */ const convertToGQLObjectType = (networkFieldTypes: SupportedNetworkFieldType[]) => { return networkFieldTypes.reduce((allFields, currentField) => { @@ -26,16 +29,13 @@ const convertToGQLObjectType = (networkFieldTypes: SupportedNetworkFieldType[]) }; /** - * Returns available aggregations by filtering duplicates, and mapping from - * singular remote aggregation type to network aggregation types + * Typedefs for network search. + * Typenames need to be unique globally or gql will merge / throw errors. * - * eg. NumericAggregations to NetworkNumericAggregations - * - * There is no distinction on which types come from which remote connections - * This is the resolvers responsibility - * - * @param configs - * @returns + * TODO: Use a single convention of creating typedefs consistently + * Some redundancy here by creating typedefs in object centric way. + * There are typesdefs already declared as gql tagged template strings elsewhere. + * Objects are easier to compose and are typesafe. */ export const createNetworkAggregationTypeDefs = ( networkFieldTypes: SupportedNetworkFieldType[], diff --git a/modules/server/src/network/typeDefs/index.ts b/modules/server/src/network/typeDefs/index.ts index 1527d41c1..afe487439 100644 --- a/modules/server/src/network/typeDefs/index.ts +++ b/modules/server/src/network/typeDefs/index.ts @@ -1,4 +1,4 @@ -import { SupportedNetworkFieldType } from '../types/types'; +import { SupportedNetworkFieldType } from '../setup/fields'; import { createNetworkAggregationTypeDefs } from './aggregations'; export const createTypeDefs = (networkFieldTypes: SupportedNetworkFieldType[]) => { diff --git a/modules/server/src/network/types/types.ts b/modules/server/src/network/types/__types.ts similarity index 100% rename from modules/server/src/network/types/types.ts rename to modules/server/src/network/types/__types.ts diff --git a/modules/server/src/network/types/aggregations.ts b/modules/server/src/network/types/aggregations.ts index bd6c59deb..770b35491 100644 --- a/modules/server/src/network/types/aggregations.ts +++ b/modules/server/src/network/types/aggregations.ts @@ -11,7 +11,6 @@ export type Bucket = { }; export type Aggregations = { - __typename: 'Aggregations'; bucket_count: number; buckets: Bucket[]; }; From 38049063723bedf1922d198e1d0067734bcdcdb0 Mon Sep 17 00:00:00 2001 From: Ciaran Schutte Date: Mon, 17 Mar 2025 18:32:51 -0400 Subject: [PATCH 3/8] clean up types --- modules/server/src/gqlServer.ts | 6 +- modules/server/src/mapping/masking.ts | 23 ++----- .../server/src/mapping/resolveAggregations.ts | 63 +++++++++++++------ .../server/src/mapping/resolveHitsFromAggs.ts | 41 +++++++++++- modules/server/src/mapping/resolvers.ts | 9 ++- modules/server/src/mapping/types.ts | 57 ----------------- .../aggregations/AggregationAccumulator.ts | 24 ++++--- .../src/network/resolvers/aggregations.ts | 14 ++--- modules/server/src/network/resolvers/fetch.ts | 7 +-- modules/server/src/network/resolvers/index.ts | 14 ++--- .../server/src/network/resolvers/response.ts | 4 +- modules/server/src/network/setup/constants.ts | 2 - modules/server/src/network/setup/fields.ts | 5 +- .../src/network/typeDefs/aggregations.ts | 16 +++-- modules/server/src/network/types/__types.ts | 27 +------- .../server/src/network/types/aggregations.ts | 26 -------- 16 files changed, 138 insertions(+), 200 deletions(-) delete mode 100644 modules/server/src/mapping/types.ts delete mode 100644 modules/server/src/network/types/aggregations.ts diff --git a/modules/server/src/gqlServer.ts b/modules/server/src/gqlServer.ts index 83f93cc23..1eb8ebb87 100644 --- a/modules/server/src/gqlServer.ts +++ b/modules/server/src/gqlServer.ts @@ -5,6 +5,8 @@ export type Context = { esClient: Client; }; +export type Root = Record; + export type ResolverOutput = T | Promise; /** @@ -16,9 +18,9 @@ export type ResolverOutput = T | Promise; * @param info - GraphQL info object. * @return Returns resolved value; */ -export type Resolver = ( +export type Resolver, QueryArgs = Object, ReturnValue = undefined> = ( root: Root, args: QueryArgs, context: Context, info: GraphQLResolveInfo, -) => ResolverOutput | ResolverOutput; +) => ResolverOutput; diff --git a/modules/server/src/mapping/masking.ts b/modules/server/src/mapping/masking.ts index d1b8be41e..a9ac787e2 100644 --- a/modules/server/src/mapping/masking.ts +++ b/modules/server/src/mapping/masking.ts @@ -1,10 +1,11 @@ import { ENV_CONFIG } from '@/config'; -import { Aggregation } from './types'; +import { Aggregations, AllAggregationsMap } from './resolveAggregations'; export const Relation = { eq: 'eq', gte: 'gte', } as const; + export type Relation = keyof typeof Relation; /** @@ -18,7 +19,7 @@ export type Relation = keyof typeof Relation; const calculateHitsFromAggregation = ({ aggregation, }: { - aggregation: Aggregation | undefined; + aggregation: Aggregations | undefined; }) => { if (!aggregation) { console.error('No aggregation found for calculating hits.'); @@ -39,21 +40,7 @@ const calculateHitsFromAggregation = ({ * @param aggregations - aggregations from query * @returns aggregations with data masking applied and hits total */ -export const applyAggregationMasking = ({ - aggregations, -}: { - aggregations: Record< - string, - { - bucket_count: number; - buckets: Array<{ - doc_count: number; - key: string; - relation: Relation; - }>; - } - >; -}) => { +export const applyAggregationMasking = ({ aggregations }: { aggregations: AllAggregationsMap }) => { const thresholdMin = ENV_CONFIG.DATA_MASK_MIN_THRESHOLD; if (thresholdMin < 1) { throw Error('DATA_MASK_MIN_THRESHOLD environment variable has to be a positive integer.'); @@ -61,7 +48,7 @@ export const applyAggregationMasking = ({ const THRESHOLD_REPLACEMENT_VALUE = 1; const { aggsTotal: dataMaskedAggregations, totalHitsAgg } = Object.entries(aggregations).reduce<{ - aggsTotal: Record; + aggsTotal: AllAggregationsMap; totalHitsAgg: { key: string; bucketCount: number }; }>( ({ aggsTotal, totalHitsAgg }, [type, aggregation]) => { diff --git a/modules/server/src/mapping/resolveAggregations.ts b/modules/server/src/mapping/resolveAggregations.ts index 6fe5194e6..cad54515b 100644 --- a/modules/server/src/mapping/resolveAggregations.ts +++ b/modules/server/src/mapping/resolveAggregations.ts @@ -1,40 +1,65 @@ import getFields from 'graphql-fields'; -import { buildAggregations, buildQuery, flattenAggregations } from '../middleware'; - -import { Resolver } from '@/gqlServer'; +import { Resolver, Root } from '@/gqlServer'; import { GetServerSideFilterFn } from '@/utils/getDefaultServerSideFilter'; +import { buildAggregations, buildQuery, flattenAggregations } from '../middleware'; import { resolveSetsInSqon } from './hackyTemporaryEsSetResolution'; import { Relation } from './masking'; -import { AggregationQuery, Root } from './types'; import compileFilter from './utils/compileFilter'; import esSearch from './utils/esSearch'; -const toGraphqlField = (acc: Aggregations, [a, b]: [string, Aggregation]) => ({ - ...acc, - [a.replace(/\./g, '__')]: b, -}); -export const aggregationsToGraphql = (aggregations: Aggregations) => { - return Object.entries(aggregations).reduce(toGraphqlField, {}); -}; - -/* - * Types - */ export type Bucket = { doc_count: number; key: string; relation: Relation; }; -export type Aggregation = { +export type CommonAggregationProperties = { bucket_count: number; buckets: Bucket[]; }; -export type Aggregations = Record; +// the GQL Aggregations type +export type Aggregations = CommonAggregationProperties; + +type Stats = { + max: number; + min: number; + count: number; + avg: number; + sum: number; +}; + +// the GQL NumericAggregations type +export type NumericAggregations = { stats: Stats } & CommonAggregationProperties; + +// "Aggregations" plural is already a name for a field type that has aggregations +export type AllAggregations = Aggregations | NumericAggregations; +export type AllAggregationsMap = Record; + +export type AggregationsResolver = Resolver< + Root, + AggregationsQueryVariables, + Promise +>; + +export type AggregationsQueryVariables = { + filters: any; + aggregations_filter_themselves: boolean; + include_missing: boolean; +}; + +const toGraphqlField = ( + acc: AllAggregationsMap, + [a, b]: [string, CommonAggregationProperties], +) => ({ + ...acc, + [a.replace(/\./g, '__')]: b, +}); -export type AggregationsResolver = Resolver>; +export const aggregationsToGraphql = (aggregations: AllAggregationsMap) => { + return Object.entries(aggregations).reduce(toGraphqlField, {}); +}; const getAggregationsResolver = ({ type, @@ -43,7 +68,7 @@ const getAggregationsResolver = ({ type: Record; getServerSideFilter: GetServerSideFilterFn | undefined; }) => { - const resolver: Resolver> = async ( + const resolver: Resolver> = async ( obj, { filters, aggregations_filter_themselves, include_missing = true }, context, diff --git a/modules/server/src/mapping/resolveHitsFromAggs.ts b/modules/server/src/mapping/resolveHitsFromAggs.ts index b12a0d9d6..f8bf82a32 100644 --- a/modules/server/src/mapping/resolveHitsFromAggs.ts +++ b/modules/server/src/mapping/resolveHitsFromAggs.ts @@ -1,8 +1,45 @@ -import { Resolver } from '@/gqlServer'; import { get } from 'lodash'; + +import { Resolver, Root } from '@/gqlServer'; import { applyAggregationMasking } from './masking'; import { AggregationsResolver } from './resolveAggregations'; -import { HitsQuery, Root } from './types'; + +export enum Missing { + first, + last, +} + +export enum Mode { + avg, + max, + min, + sum, +} + +export enum Order { + asc, + desc, +} + +type Sort = { + fieldName: string; + order: Order; + mode: Mode; + missing: Missing; +}; + +type HitsQuery = { + score: string; + offset: number; + sort: [Sort]; + filters: JSON; + before: string; + after: string; + first: number; + last: number; + searchAfter: JSON; + trackTotalHits: boolean; +}; type HitsResolver = Resolver>; diff --git a/modules/server/src/mapping/resolvers.ts b/modules/server/src/mapping/resolvers.ts index dc78f0f42..43c32959b 100644 --- a/modules/server/src/mapping/resolvers.ts +++ b/modules/server/src/mapping/resolvers.ts @@ -1,4 +1,5 @@ import { ConfigProperties, ExtendedConfigsInterface } from '@/config/types'; +import { Resolver, Root } from '@/gqlServer'; import { CreateConnectionResolversArgs } from './createConnectionResolvers'; import { applyAggregationMasking } from './masking'; import getAggregationsResolver, { @@ -7,7 +8,6 @@ import getAggregationsResolver, { } from './resolveAggregations'; import resolveHits from './resolveHits'; import { getHitsFromAggsResolver } from './resolveHitsFromAggs'; -import { Root } from './types'; export const createResolvers = ({ createStateResolvers, @@ -16,8 +16,11 @@ export const createResolvers = ({ getServerSideFilter, enableDocumentHits, }: Omit) => { - // configs - const configs = async (_unusedParentObj: Root, { fieldNames }: { fieldNames: string[] }) => { + const configs: Resolver< + Root, + { fieldNames: string[] }, + Promise<{ facets?: any; matchbox?: any; table?: any; downloads: any; extended: any }> + > = async (_unusedParentObj, { fieldNames }) => { return { downloads: type.config?.[ConfigProperties.DOWNLOADS], extended: fieldNames diff --git a/modules/server/src/mapping/types.ts b/modules/server/src/mapping/types.ts deleted file mode 100644 index 0972f5e90..000000000 --- a/modules/server/src/mapping/types.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Relation } from './masking'; - -export type Bucket = { - doc_count: number; - key: string; - relation: Relation; -}; - -export type Aggregation = { - bucket_count: number; - buckets: Bucket[]; -}; - -export type Root = Record; - -enum Missing { - first, - last, -} - -enum Mode { - avg, - max, - min, - sum, -} - -enum Order { - asc, - desc, -} - -export type Sort = { - fieldName: string; - order: Order; - mode: Mode; - missing: Missing; -}; - -export type HitsQuery = { - score: string; - offset: number; - sort: [Sort]; - filters: JSON; - before: string; - after: string; - first: number; - last: number; - searchAfter: JSON; - trackTotalHits: boolean; -}; - -export type AggregationQuery = { - filters: any; - aggregations_filter_themselves: boolean; - include_missing: boolean; -}; diff --git a/modules/server/src/network/aggregations/AggregationAccumulator.ts b/modules/server/src/network/aggregations/AggregationAccumulator.ts index 1b2019d79..118dcf8c8 100644 --- a/modules/server/src/network/aggregations/AggregationAccumulator.ts +++ b/modules/server/src/network/aggregations/AggregationAccumulator.ts @@ -1,18 +1,23 @@ import { Relation } from '@/mapping/masking'; +import { + Aggregations, + AllAggregations, + AllAggregationsMap, + Bucket, + NumericAggregations, +} from '@/mapping/resolveAggregations'; import { ALL_NETWORK_AGGREGATION_TYPES_MAP } from '..'; -import { SUPPORTED_AGGREGATIONS } from '../setup/constants'; -import { Aggregations, Bucket, NumericAggregations } from '../types/aggregations'; +import { SupportedAggregation, SUPPORTED_AGGREGATIONS } from '../setup/constants'; import { Hits } from '../types/hits'; -import { AllAggregations } from '../types/types'; import { RequestedFieldsMap } from '../utils/gql'; type ResolveAggregationInput = { - data: { aggregations: AllAggregations; hits: Hits }; - accumulator: AllAggregations; + data: { aggregations: AllAggregationsMap; hits: Hits }; + accumulator: AllAggregationsMap; requestedFields: string[]; }; -type AggregationsTuple = [Aggregations, Aggregations]; +type AggregationsTuple = [AllAggregations, AllAggregations]; type NumericAggregationsTuple = [NumericAggregations, NumericAggregations]; const emptyAggregation = (hits: number): Aggregations => ({ @@ -21,7 +26,7 @@ const emptyAggregation = (hits: number): Aggregations => ({ }); // mutation - update a single aggregations field in the accumulator -const addToAccumulator = ({ +const addToAccumulator = ({ existingAggregation, aggregation, type, @@ -94,6 +99,7 @@ const resolveToNetworkAggregation = ( if (type === SUPPORTED_AGGREGATIONS.Aggregations) { return resolveAggregation(aggregations as AggregationsTuple); } else if (type === SUPPORTED_AGGREGATIONS.NumericAggregations) { + // TODO: REMOVE and other code return resolveNumericAggregation(aggregations as NumericAggregationsTuple); } else { // no types match @@ -240,7 +246,7 @@ const resolveNumericAggregation = (aggregations: NumericAggregationsTuple): Nume }; export class AggregationAccumulator { - totalAgg: AllAggregations = {}; + totalAgg: AllAggregationsMap = {}; requestedFields: string[]; constructor(requestedFieldsMap: RequestedFieldsMap) { @@ -248,7 +254,7 @@ export class AggregationAccumulator { this.requestedFields = requestedFields; } - resolve(data: { aggregations: AllAggregations; hits: Hits }) { + resolve(data: { aggregations: AllAggregationsMap; hits: Hits }) { resolveAggregations({ accumulator: this.totalAgg, data: structuredClone(data), diff --git a/modules/server/src/network/resolvers/aggregations.ts b/modules/server/src/network/resolvers/aggregations.ts index e994ec533..a2da8be79 100644 --- a/modules/server/src/network/resolvers/aggregations.ts +++ b/modules/server/src/network/resolvers/aggregations.ts @@ -1,4 +1,4 @@ -import { Aggregations } from '@/mapping/resolveAggregations'; +import { AggregationsQueryVariables, AllAggregationsMap } from '@/mapping/resolveAggregations'; import { AggregationAccumulator } from '../aggregations/AggregationAccumulator'; import { failure, isSuccess } from '../result'; import { NodeConfig } from '../setup/query'; @@ -12,14 +12,6 @@ export const CONNECTION_STATUS = { ERROR: 'ERROR', } as const; -type QueryVariables = { - filters?: object; - aggregations_filter_themselves?: boolean; - include_missing?: boolean; -}; - -type SuccessResponse = Record; - export type NetworkNode = { name: string; hits: number; @@ -28,6 +20,8 @@ export type NetworkNode = { aggregations: { name: string; type: string }[]; }; +type SuccessResponse = Record; + /** * Query each remote connection * @@ -38,7 +32,7 @@ export type NetworkNode = { export const aggregationPipeline = async ( configs: NodeConfig[], requestedAggregationFields: RequestedFieldsMap, - queryVariables: QueryVariables, + queryVariables: AggregationsQueryVariables, ) => { const nodeInfo: NetworkNode[] = []; diff --git a/modules/server/src/network/resolvers/fetch.ts b/modules/server/src/network/resolvers/fetch.ts index 8c2f4f128..b53a0ecbc 100644 --- a/modules/server/src/network/resolvers/fetch.ts +++ b/modules/server/src/network/resolvers/fetch.ts @@ -1,6 +1,7 @@ import axios, { AxiosError } from 'axios'; import { DocumentNode } from 'graphql'; +import { NetworkQueryVariables } from '.'; import { fetchGql } from '../gql'; import { failure, Result, success } from '../result'; import { ASTtoString } from '../utils/gql'; @@ -9,11 +10,7 @@ import { CONNECTION_STATUS } from './aggregations'; type NetworkQuery = { url: string; gqlQuery: DocumentNode; - queryVariables: { - filters?: object; - aggregations_filter_themselves?: boolean; - include_missing?: boolean; - }; + queryVariables: NetworkQueryVariables; }; /** diff --git a/modules/server/src/network/resolvers/index.ts b/modules/server/src/network/resolvers/index.ts index e11b1370e..b718bd048 100644 --- a/modules/server/src/network/resolvers/index.ts +++ b/modules/server/src/network/resolvers/index.ts @@ -1,4 +1,5 @@ import { Resolver } from '@/gqlServer'; +import { AggregationsQueryVariables } from '@/mapping/resolveAggregations'; import { isSuccess } from '../result'; import { NodeConfig } from '../setup/query'; import { resolveInfoToMap } from '../utils/gql'; @@ -11,15 +12,8 @@ type NetworkSearchRoot = { aggregations: Record; }; -/* - * Type should match the "Network" GQL type definition arg types - */ -// TODO: shared? -type NetworkAggregationArgs = { - filters?: object; - aggregations_filter_themselves?: boolean; - include_missing?: boolean; -}; +// top level query to pass variables down +export type NetworkQueryVariables = AggregationsQueryVariables; /** * Resolvers for network search. @@ -29,7 +23,7 @@ type NetworkAggregationArgs = { * @returns */ export const createResolvers = (configs: NodeConfig[]) => { - const network: Resolver = async ( + const network: Resolver = async ( _unusedParentObj, args, _unusedContext, diff --git a/modules/server/src/network/resolvers/response.ts b/modules/server/src/network/resolvers/response.ts index cb95e103e..155e4d79b 100644 --- a/modules/server/src/network/resolvers/response.ts +++ b/modules/server/src/network/resolvers/response.ts @@ -1,4 +1,4 @@ -import { Aggregations } from '@/mapping/resolveAggregations'; +import { AllAggregationsMap } from '@/mapping/resolveAggregations'; import { NetworkNode } from './aggregations'; /** @@ -8,7 +8,7 @@ export const createResponse = ({ aggregationResults, nodeInfo, }: { - aggregationResults: Aggregations; + aggregationResults: AllAggregationsMap; nodeInfo: NetworkNode[]; }) => { return { nodes: nodeInfo, aggregations: aggregationResults }; diff --git a/modules/server/src/network/setup/constants.ts b/modules/server/src/network/setup/constants.ts index 4cc64b712..f89637c5f 100644 --- a/modules/server/src/network/setup/constants.ts +++ b/modules/server/src/network/setup/constants.ts @@ -5,12 +5,10 @@ import { ObjectValues } from '@/utils/types'; */ export const SUPPORTED_AGGREGATIONS = { Aggregations: 'Aggregations', - NumericAggregations: 'NumericAggregations', } as const; export type SupportedAggregation = ObjectValues; export const SUPPORTED_AGGREGATIONS_LIST: SupportedAggregation[] = [ SUPPORTED_AGGREGATIONS.Aggregations, - SUPPORTED_AGGREGATIONS.NumericAggregations, ]; diff --git a/modules/server/src/network/setup/fields.ts b/modules/server/src/network/setup/fields.ts index 54f31d3c9..8f2a2a789 100644 --- a/modules/server/src/network/setup/fields.ts +++ b/modules/server/src/network/setup/fields.ts @@ -1,9 +1,6 @@ import { SupportedAggregation } from '../common'; -import { GQLFieldType } from '../queries'; - import { SUPPORTED_AGGREGATIONS_LIST } from './constants'; - -export type NetworkFields = { name: string; fields: GQLFieldType[] }; +import { NodeConfig } from './query'; type NetworkFieldType = { name: string; diff --git a/modules/server/src/network/typeDefs/aggregations.ts b/modules/server/src/network/typeDefs/aggregations.ts index a29e639d7..12434232e 100644 --- a/modules/server/src/network/typeDefs/aggregations.ts +++ b/modules/server/src/network/typeDefs/aggregations.ts @@ -7,6 +7,7 @@ import { GraphQLString, } from 'graphql'; import GraphQLJSON from 'graphql-type-json'; + import { SupportedNetworkFieldType } from '../types/types'; import { singleToNetworkAggregationMap } from './networkAggregations'; @@ -29,13 +30,16 @@ const convertToGQLObjectType = (networkFieldTypes: SupportedNetworkFieldType[]) }; /** - * Typedefs for network search. - * Typenames need to be unique globally or gql will merge / throw errors. + * Returns available aggregations by filtering duplicates, and mapping from + * singular remote aggregation type to network aggregation types + * + * eg. NumericAggregations to NetworkNumericAggregations + * + * There is no distinction on which types come from which remote connections + * This is the resolvers responsibility * - * TODO: Use a single convention of creating typedefs consistently - * Some redundancy here by creating typedefs in object centric way. - * There are typesdefs already declared as gql tagged template strings elsewhere. - * Objects are easier to compose and are typesafe. + * @param configs + * @returns */ export const createNetworkAggregationTypeDefs = ( networkFieldTypes: SupportedNetworkFieldType[], diff --git a/modules/server/src/network/types/__types.ts b/modules/server/src/network/types/__types.ts index a0a2571a9..5f0aa2da3 100644 --- a/modules/server/src/network/types/__types.ts +++ b/modules/server/src/network/types/__types.ts @@ -1,11 +1,8 @@ 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 = { +type NetworkAggregationConfigInput = { graphqlUrl: string; documentType: string; documentName: string; @@ -15,24 +12,4 @@ export type NetworkAggregationConfigInput = { }; }; -export type NetworkFieldType = { - name: string; - type: T; -}; - -export type SupportedNetworkFieldType = NetworkFieldType; - -export type SupportedAggregations = SupportedNetworkFieldType[]; -export type UnsupportedAggregations = NetworkFieldType[]; - -export type ConnectionStatus = ObjectValues; - -// the name "Aggregations" is already taken by a type -export type AllAggregations = Record; - -export type NetworkAggregation = { - bucket_count: number; - buckets: Bucket[]; -}; - -export type NodeConfig = NetworkConfig & { aggregations: { name: string; type: string }[] }; +type ConnectionStatus = ObjectValues; diff --git a/modules/server/src/network/types/aggregations.ts b/modules/server/src/network/types/aggregations.ts deleted file mode 100644 index 770b35491..000000000 --- a/modules/server/src/network/types/aggregations.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Typescript types for aggregations - */ - -import { Relation } from '@/mapping/masking'; - -export type Bucket = { - doc_count: number; - key: string; - relation: Relation; -}; - -export type Aggregations = { - bucket_count: number; - buckets: Bucket[]; -}; - -type Stats = { - max: number; - min: number; - count: number; - avg: number; - sum: number; -}; - -export type NumericAggregations = { __typename: 'NumericAggregations'; stats: Stats }; From a2f22dc132bfbef9b7bd23feb96b06fac2fe137f Mon Sep 17 00:00:00 2001 From: Ciaran Schutte Date: Wed, 19 Mar 2025 02:56:04 -0400 Subject: [PATCH 4/8] cleanup types, redundant code and fix a couple of bad merge files --- modules/server/src/graphqlRoutes.js | 22 ++++--- .../src/mapping/createConnectionTypeDefs.js | 8 +-- .../aggregations/AggregationAccumulator.ts | 28 +-------- modules/server/src/network/health.ts | 3 +- modules/server/src/network/index.ts | 5 +- .../src/network/resolvers/aggregations.ts | 2 +- modules/server/src/network/resolvers/query.ts | 7 ++- modules/server/src/network/setup/fields.ts | 26 ++++----- .../typeDefs/___networkAggregations.ts | 50 ---------------- .../src/network/typeDefs/aggregations.ts | 58 +++++++++++++++---- modules/server/src/network/types/__types.ts | 15 ----- 11 files changed, 88 insertions(+), 136 deletions(-) delete mode 100644 modules/server/src/network/typeDefs/___networkAggregations.ts delete mode 100644 modules/server/src/network/types/__types.ts diff --git a/modules/server/src/graphqlRoutes.js b/modules/server/src/graphqlRoutes.js index c737abcb6..22a7d0d27 100644 --- a/modules/server/src/graphqlRoutes.js +++ b/modules/server/src/graphqlRoutes.js @@ -276,15 +276,21 @@ export const createSchemasFromConfigs = async ({ * Federated Network Search */ if (enableNetworkAggregation) { + const networkConfig = configsFromFiles[ConfigProperties.NETWORK_AGGREGATION]; + if (!networkConfig || networkConfig?.servers.length === 0) { + throw Error('Network config not found. Please check file is valid.'); + } + + const remoteServerConfigs = networkConfig.servers.map((config) => ({ + ...config, + /* + * part of the gql schema is generated dynamically + * in the case of the "file" field, the field name and gql type name are the same + */ + documentName: config.documentType, + })), const networkSchema = await createSchemaFromNetworkConfig({ - networkConfigs: configsFromFiles[ConfigProperties.NETWORK_AGGREGATION].map((config) => ({ - ...config, - /* - * part of the gql schema is generated dynamically - * in the case of the "file" field, the field name and gql type name are the same - */ - documentName: config.documentType, - })), + networkConfigs: remoteServerConfigs }); schemasToMerge.push(networkSchema); } diff --git a/modules/server/src/mapping/createConnectionTypeDefs.js b/modules/server/src/mapping/createConnectionTypeDefs.js index 132694297..05132ac4c 100644 --- a/modules/server/src/mapping/createConnectionTypeDefs.js +++ b/modules/server/src/mapping/createConnectionTypeDefs.js @@ -1,13 +1,13 @@ import mappingToAggsType from './mappingToAggsType'; -const createConnectionType = (enableDocumentHits) => { +const createConnectionType = ({ type, enableDocumentHits }) => { return `type ${type.name}Connection { total: Int! ${enableDocumentHits ? `edges: [${type.name}Edge]` : ''} }`; }; -const createDataMaskingType = (enableDocumentHits) => { +const createDataMaskingType = ({ enableDocumentHits }) => { return !enableDocumentHits ? `type DataMasking { thresholdValue: Int }` : ''; }; @@ -43,9 +43,9 @@ export default ({ type, fields = '', createStateTypeDefs = true, enableDocumentH ${mappingToAggsType(type.mapping)} } - ${createDataMaskingType(enableDocumentHits)} + ${createDataMaskingType({ enableDocumentHits })} - ${createConnectionType(enableDocumentHits)} + ${createConnectionType({ type, enableDocumentHits })} type ${type.name}Edge { diff --git a/modules/server/src/network/aggregations/AggregationAccumulator.ts b/modules/server/src/network/aggregations/AggregationAccumulator.ts index 118dcf8c8..d39612e8c 100644 --- a/modules/server/src/network/aggregations/AggregationAccumulator.ts +++ b/modules/server/src/network/aggregations/AggregationAccumulator.ts @@ -18,7 +18,6 @@ type ResolveAggregationInput = { }; type AggregationsTuple = [AllAggregations, AllAggregations]; -type NumericAggregationsTuple = [NumericAggregations, NumericAggregations]; const emptyAggregation = (hits: number): Aggregations => ({ bucket_count: 1, @@ -38,7 +37,7 @@ const addToAccumulator = ({ // if first aggregation, nothing to resolve with yet return !existingAggregation ? aggregation - : resolveToNetworkAggregation(type, [aggregation, existingAggregation]); + : resolveAggregationByType(type, [aggregation, existingAggregation]); }; /** @@ -92,15 +91,12 @@ const resolveAggregations = ({ data, accumulator, requestedFields }: ResolveAggr * @param type * @param aggregations */ -const resolveToNetworkAggregation = ( +const resolveAggregationByType = ( type: string, aggregations: [T, T], ): Aggregations | NumericAggregations => { if (type === SUPPORTED_AGGREGATIONS.Aggregations) { return resolveAggregation(aggregations as AggregationsTuple); - } else if (type === SUPPORTED_AGGREGATIONS.NumericAggregations) { - // TODO: REMOVE and other code - return resolveNumericAggregation(aggregations as NumericAggregationsTuple); } else { // no types match throw Error('No matching aggregation type'); @@ -219,32 +215,12 @@ export const resolveAggregation = (aggregations: AggregationsTuple): Aggregation return { bucket_count: computedBuckets.length, buckets: computedBuckets, - __typename: resolvedAggregation.__typename, }; }); return resolvedAggregation; }; -const resolveNumericAggregation = (aggregations: NumericAggregationsTuple): NumericAggregations => { - return aggregations.reduce((resolvedAggregation, agg) => { - // max - resolvedAggregation.stats.max = Math.max(agg.stats.max, resolvedAggregation.stats.max); - - // min - resolvedAggregation.stats.min = Math.min(agg.stats.min, resolvedAggregation.stats.min); - - // count - resolvedAggregation.stats.count += agg.stats.count; - // sum - resolvedAggregation.stats.sum += agg.stats.sum; - // avg - resolvedAggregation.stats.avg = resolvedAggregation.stats.sum / resolvedAggregation.stats.count; - - return resolvedAggregation; - }); -}; - export class AggregationAccumulator { totalAgg: AllAggregationsMap = {}; requestedFields: string[]; diff --git a/modules/server/src/network/health.ts b/modules/server/src/network/health.ts index c558501b3..a5cc3f5e9 100644 --- a/modules/server/src/network/health.ts +++ b/modules/server/src/network/health.ts @@ -1,7 +1,6 @@ import axios from 'axios'; import urljoin from 'url-join'; import { CONNECTION_STATUS } from './resolvers/networkNode'; -import { ConnectionStatus } from './types/types'; /** * Check the status of remote connections @@ -9,7 +8,7 @@ import { ConnectionStatus } from './types/types'; * @param url - Remote connection url * @returns A connection status */ -export const gqlHealthCheck = async (url: string): Promise => { +export const gqlHealthCheck = async (url: string): Promise => { /* * recommended way to health check gql server is to run the `__typename` query that every server has. * very small query with no additional params, so using GET is not a concern for the GQL server. diff --git a/modules/server/src/network/index.ts b/modules/server/src/network/index.ts index e627bff13..82174c5e8 100644 --- a/modules/server/src/network/index.ts +++ b/modules/server/src/network/index.ts @@ -1,5 +1,6 @@ import { makeExecutableSchema } from '@graphql-tools/schema'; import { createResolvers } from './resolvers'; +import { SUPPORTED_AGGREGATIONS } from './setup/constants'; import { normalizeFieldTypes } from './setup/fields'; import { fetchAllNodeAggregations } from './setup/query'; import { createTypeDefs } from './typeDefs'; @@ -8,7 +9,7 @@ import { NetworkConfig } from './types/setup'; /** * Map of all available fields with associated aggregation type */ -export let ALL_NETWORK_AGGREGATION_TYPES_MAP: Map = +export let ALL_NETWORK_AGGREGATION_TYPES_MAP: Map = new Map(); /** @@ -56,5 +57,5 @@ export const createSchemaFromNetworkConfig = async ({ const resolvers = createResolvers(nodeConfig); const networkSchema = makeExecutableSchema({ typeDefs, resolvers }); - return { networkSchema }; + return networkSchema; }; diff --git a/modules/server/src/network/resolvers/aggregations.ts b/modules/server/src/network/resolvers/aggregations.ts index a2da8be79..ad5151050 100644 --- a/modules/server/src/network/resolvers/aggregations.ts +++ b/modules/server/src/network/resolvers/aggregations.ts @@ -38,7 +38,7 @@ export const aggregationPipeline = async ( const totalAgg = new AggregationAccumulator(requestedAggregationFields); - Promise.allSettled( + await Promise.allSettled( configs.map(async (config) => { // create node query const gqlQuery = createNetworkQuery(config, requestedAggregationFields); diff --git a/modules/server/src/network/resolvers/query.ts b/modules/server/src/network/resolvers/query.ts index ba115d256..e7f9f07e4 100644 --- a/modules/server/src/network/resolvers/query.ts +++ b/modules/server/src/network/resolvers/query.ts @@ -56,9 +56,10 @@ const convertFieldsToString = (requestedFields: RequestedFieldsMap) => { */ const createFileGQLQuery = (documentName: string, requestedFields: RequestedFieldsMap) => { const fields = convertFieldsToString(requestedFields); - const queryArgs = `(filters: $filters, aggregations_filter_themselves: $aggregations_filter_themselves, include_missing: $include_missing)`; - const aggregationsString = !isEmpty(fields) ? `aggregations${queryArgs} ${fields}` : ''; - const gqlString = `query nodeQuery${queryArgs} {${documentName} { hits { total } ${aggregationsString} }}`; + const queryArgsTypes = `($filters: JSON, $aggregations_filter_themselves: Boolean, $include_missing: Boolean)`; + const fieldQueryArgs = `(filters: $filters, aggregations_filter_themselves: $aggregations_filter_themselves, include_missing: $include_missing)`; + const aggregationsString = !isEmpty(fields) ? `aggregations${fieldQueryArgs} ${fields}` : ''; + const gqlString = `query nodeQuery${queryArgsTypes} {${documentName} { hits { total } ${aggregationsString} }}`; return gqlString; }; diff --git a/modules/server/src/network/setup/fields.ts b/modules/server/src/network/setup/fields.ts index 8f2a2a789..43083c112 100644 --- a/modules/server/src/network/setup/fields.ts +++ b/modules/server/src/network/setup/fields.ts @@ -1,12 +1,11 @@ -import { SupportedAggregation } from '../common'; -import { SUPPORTED_AGGREGATIONS_LIST } from './constants'; +import { SupportedAggregation, SUPPORTED_AGGREGATIONS_LIST } from './constants'; import { NodeConfig } from './query'; type NetworkFieldType = { name: string; type: T; }; -type SupportedNetworkFieldType = NetworkFieldType; +export type SupportedNetworkFieldType = NetworkFieldType; type SupportedAggregations = SupportedNetworkFieldType[]; type UnsupportedAggregations = NetworkFieldType[]; @@ -54,24 +53,22 @@ export const getFieldTypes = (fields: NodeConfig['aggregations']) => { }; /** + * Takes in all node aggregation field types and returns a single array. + * - dedupes + * - ensures types are supported * - * @param nodeConfigs - * @param supportedTypes - * @returns unique fields + * @param nodeConfigs - + * @returns Unique and supported aggregation field types. */ -const getAllFieldTypes = ( - nodeConfigs: NodeConfig[], - supportedTypes: SupportedAggregation[], -): SupportedNetworkFieldType[] => { +export const normalizeFieldTypes = (nodeConfigs: NodeConfig[]): SupportedNetworkFieldType[] => { + // split into supported and unsupported types const nodeFieldTypes = nodeConfigs.map((config) => { - const { supportedAggregations, unsupportedAggregations } = getFieldTypes( - config.aggregations, - supportedTypes, - ); + const { supportedAggregations, unsupportedAggregations } = getFieldTypes(config.aggregations); return { supportedAggregations, unsupportedAggregations }; }); + // flatten to single object const allSupportedAggregations = nodeFieldTypes.flatMap( (fieldType) => fieldType.supportedAggregations, ); @@ -85,5 +82,6 @@ const getAllFieldTypes = ( const uniqueSupportedAggregations = Array.from( new Set(allSupportedAggregations.map((nodeField) => JSON.stringify(nodeField))), ).map((nodeFieldString) => JSON.parse(nodeFieldString)); + return uniqueSupportedAggregations; }; diff --git a/modules/server/src/network/typeDefs/___networkAggregations.ts b/modules/server/src/network/typeDefs/___networkAggregations.ts deleted file mode 100644 index ef5007063..000000000 --- a/modules/server/src/network/typeDefs/___networkAggregations.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { GraphQLInt, GraphQLList, GraphQLObjectType, GraphQLString } from 'graphql'; -import { SupportedAggregation, SUPPORTED_AGGREGATIONS } from '../common'; - -const bucket = new GraphQLObjectType({ - name: 'bucket', - fields: { - doc_count: { - type: GraphQLInt, - }, - key: { - type: GraphQLString, - }, - relation: { - type: GraphQLString - } - }, -}); - -// TODO: Placeholder to expand type -const networkAggregations = new GraphQLObjectType({ - name: 'NetworkAggregations', - fields: { - bucket_count: { - type: GraphQLInt, - }, - buckets: { - type: new GraphQLList(bucket), - }, - }, -}); - -// TODO: Placeholder to expand type -const numericNetworkAggregations = new GraphQLObjectType({ - name: 'NumericNetworkAggregations', - fields: { - test: { - type: GraphQLString, - }, - }, -}); - -/** - * return network aggregation gql type - */ -export const singleToNetworkAggregationMap = new Map(); -singleToNetworkAggregationMap.set(SUPPORTED_AGGREGATIONS.Aggregations, networkAggregations); -singleToNetworkAggregationMap.set( - SUPPORTED_AGGREGATIONS.NumericAggregations, - numericNetworkAggregations, -); diff --git a/modules/server/src/network/typeDefs/aggregations.ts b/modules/server/src/network/typeDefs/aggregations.ts index 12434232e..4e4fb06b5 100644 --- a/modules/server/src/network/typeDefs/aggregations.ts +++ b/modules/server/src/network/typeDefs/aggregations.ts @@ -1,5 +1,6 @@ import { GraphQLBoolean, + GraphQLEnumType, GraphQLInt, GraphQLList, GraphQLObjectType, @@ -8,14 +9,53 @@ import { } from 'graphql'; import GraphQLJSON from 'graphql-type-json'; -import { SupportedNetworkFieldType } from '../types/types'; -import { singleToNetworkAggregationMap } from './networkAggregations'; +import { SupportedNetworkFieldType } from '../setup/fields'; + +const relation = new GraphQLEnumType({ + name: 'Relation', + values: { + eq: { value: 0 }, + gte: { value: 1 }, + }, +}); + +const bucket = new GraphQLObjectType({ + name: 'Bucket', + fields: { + doc_count: { + type: GraphQLInt, + }, + key: { + type: GraphQLString, + }, + relation: { + type: relation, + }, + }, +}); + +// GraphQL object style version of GQL type already defined, need object for this setup +const aggregationsTypeDef = new GraphQLObjectType({ + name: 'Aggregations', + fields: { + bucket_count: { + type: GraphQLInt, + }, + buckets: { + type: new GraphQLList(bucket), + }, + }, +}); + +const typeDefMap = { + Aggregations: aggregationsTypeDef, +}; /** * Creates nested GQL structured object from field type * { name: 'donor_gender', type: 'Aggregation'} * => - * { 'donor_gender', type: { name: 'Aggregation' }} + * { 'donor_gender', type: 'Aggregation' } * * @param fieldTypes - An array of fields with types * @returns Structured GQL fields object @@ -23,20 +63,16 @@ import { singleToNetworkAggregationMap } from './networkAggregations'; const convertToGQLObjectType = (networkFieldTypes: SupportedNetworkFieldType[]) => { return networkFieldTypes.reduce((allFields, currentField) => { const field = { - [currentField.name]: { type: singleToNetworkAggregationMap.get(currentField.type) }, + [currentField.name]: { + type: typeDefMap[currentField.type], + }, }; return { ...allFields, ...field }; }, {}); }; /** - * Returns available aggregations by filtering duplicates, and mapping from - * singular remote aggregation type to network aggregation types - * - * eg. NumericAggregations to NetworkNumericAggregations - * - * There is no distinction on which types come from which remote connections - * This is the resolvers responsibility + * Returns aggregations typedef * * @param configs * @returns diff --git a/modules/server/src/network/types/__types.ts b/modules/server/src/network/types/__types.ts deleted file mode 100644 index 5f0aa2da3..000000000 --- a/modules/server/src/network/types/__types.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ObjectValues } from '@/utils/types'; -import { CONNECTION_STATUS } from '../resolvers/networkNode'; - -// environment config -type NetworkAggregationConfigInput = { - graphqlUrl: string; - documentType: string; - documentName: string; - displayName: string; - options: { - timeout: number; - }; -}; - -type ConnectionStatus = ObjectValues; From 46d4c38b2fdbd2b83d8659b090adcea7469ac2f3 Mon Sep 17 00:00:00 2001 From: Ciaran Schutte Date: Wed, 19 Mar 2025 03:39:13 -0400 Subject: [PATCH 5/8] fix enum --- modules/server/src/network/typeDefs/aggregations.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/server/src/network/typeDefs/aggregations.ts b/modules/server/src/network/typeDefs/aggregations.ts index 4e4fb06b5..4e1954b20 100644 --- a/modules/server/src/network/typeDefs/aggregations.ts +++ b/modules/server/src/network/typeDefs/aggregations.ts @@ -14,8 +14,8 @@ import { SupportedNetworkFieldType } from '../setup/fields'; const relation = new GraphQLEnumType({ name: 'Relation', values: { - eq: { value: 0 }, - gte: { value: 1 }, + eq: { value: 'eq' }, + gte: { value: 'gte' }, }, }); From b9282f307e8e2dc57d425b5abf10a54669561e67 Mon Sep 17 00:00:00 2001 From: Ciaran Schutte Date: Wed, 19 Mar 2025 04:20:01 -0400 Subject: [PATCH 6/8] cleanup test types --- .../src/network/aggregations/tests/fixture.ts | 18 +----------------- modules/server/src/network/resolvers/query.ts | 2 +- .../server/src/network/tests/helpers.test.ts | 4 ++-- modules/server/src/network/tests/index.test.ts | 5 ++--- modules/server/src/network/tests/query.test.ts | 4 ++-- 5 files changed, 8 insertions(+), 25 deletions(-) diff --git a/modules/server/src/network/aggregations/tests/fixture.ts b/modules/server/src/network/aggregations/tests/fixture.ts index 21a2532db..aacac5923 100644 --- a/modules/server/src/network/aggregations/tests/fixture.ts +++ b/modules/server/src/network/aggregations/tests/fixture.ts @@ -1,8 +1,7 @@ import { Relation } from '@/mapping/masking'; -import { Aggregations, NumericAggregations } from '@/network/types/aggregations'; +import { Aggregations } from '@/mapping/resolveAggregations'; const inputA: Aggregations = { - __typename: 'Aggregations', bucket_count: 2, buckets: [ { @@ -19,7 +18,6 @@ const inputA: Aggregations = { }; const inputB: Aggregations = { - __typename: 'Aggregations', bucket_count: 2, buckets: [ { @@ -36,7 +34,6 @@ const inputB: Aggregations = { }; const inputC: Aggregations = { - __typename: 'Aggregations', bucket_count: 3, buckets: [ { @@ -57,21 +54,8 @@ const inputC: Aggregations = { ], }; -// NumericAggregations -const inputD: NumericAggregations = { - __typename: 'NumericAggregations', - stats: { max: 100, min: 12, count: 10, avg: 42, sum: 420 }, -}; - -const inputE: NumericAggregations = { - __typename: 'NumericAggregations', - stats: { max: 75, min: 1, count: 5, avg: 84, sum: 420 }, -}; - export const aggregation = { inputA, inputB, inputC, - inputD, - inputE, }; diff --git a/modules/server/src/network/resolvers/query.ts b/modules/server/src/network/resolvers/query.ts index e7f9f07e4..def5d72a3 100644 --- a/modules/server/src/network/resolvers/query.ts +++ b/modules/server/src/network/resolvers/query.ts @@ -54,7 +54,7 @@ const convertFieldsToString = (requestedFields: RequestedFieldsMap) => { * @param requestedFields - Query fields object * @returns Fully constructed query string */ -const createFileGQLQuery = (documentName: string, requestedFields: RequestedFieldsMap) => { +export const createFileGQLQuery = (documentName: string, requestedFields: RequestedFieldsMap) => { const fields = convertFieldsToString(requestedFields); const queryArgsTypes = `($filters: JSON, $aggregations_filter_themselves: Boolean, $include_missing: Boolean)`; const fieldQueryArgs = `(filters: $filters, aggregations_filter_themselves: $aggregations_filter_themselves, include_missing: $include_missing)`; diff --git a/modules/server/src/network/tests/helpers.test.ts b/modules/server/src/network/tests/helpers.test.ts index 004912779..8cd7fdbf5 100644 --- a/modules/server/src/network/tests/helpers.test.ts +++ b/modules/server/src/network/tests/helpers.test.ts @@ -1,4 +1,4 @@ -import { SupportedAggregation } from '../common'; +import { SupportedAggregation } from '../setup/constants'; import { getFieldTypes } from '../setup/fields'; describe('helpers', () => { @@ -19,7 +19,7 @@ describe('helpers', () => { }, ]; - const result = getFieldTypes(fields, supportedAggregations); + const result = getFieldTypes(fields); const expectedResult = { supportedAggregations: [ { name: 'analysis__analysis_id', type: 'Aggregations' }, diff --git a/modules/server/src/network/tests/index.test.ts b/modules/server/src/network/tests/index.test.ts index 18f59236d..4d2069c60 100644 --- a/modules/server/src/network/tests/index.test.ts +++ b/modules/server/src/network/tests/index.test.ts @@ -8,9 +8,8 @@ * Disabled using the "xdescribe" method */ -import { mergeTypeDefs, mergeTypeDefs } from '@graphql-tools/merge'; -import { makeExecutableSchema, mergeSchemas } from '@graphql-tools/schema'; -import { buildSchema, printType } from 'graphql'; +import { mergeTypeDefs } from '@graphql-tools/merge'; +import { buildSchema } from 'graphql'; import { createNetworkAggregationTypeDefs } from '../typeDefs/aggregations'; import { typeDefsA, diff --git a/modules/server/src/network/tests/query.test.ts b/modules/server/src/network/tests/query.test.ts index 6f3eb3e3c..d506d2fb1 100644 --- a/modules/server/src/network/tests/query.test.ts +++ b/modules/server/src/network/tests/query.test.ts @@ -1,8 +1,8 @@ -import { createNodeQueryString } from '../resolvers/aggregations'; +import { createFileGQLQuery } from '../resolvers/query'; describe('gql query creation', () => { test('it should create gql query string for hits and aggregations using document name', () => { - const result = createNodeQueryString('testFile', { + const result = createFileGQLQuery('testFile', { donors: { buckets: { bucket_count: {}, From 8bd95c6b9bffa393baf197102111900f705d18fa Mon Sep 17 00:00:00 2001 From: Ciaran Schutte Date: Wed, 19 Mar 2025 04:22:47 -0400 Subject: [PATCH 7/8] type fix --- modules/server/src/mapping/resolveAggregations.ts | 6 +++++- modules/server/src/mapping/resolveHitsFromAggs.ts | 3 +-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/modules/server/src/mapping/resolveAggregations.ts b/modules/server/src/mapping/resolveAggregations.ts index cad54515b..907e168ce 100644 --- a/modules/server/src/mapping/resolveAggregations.ts +++ b/modules/server/src/mapping/resolveAggregations.ts @@ -68,7 +68,11 @@ const getAggregationsResolver = ({ type: Record; getServerSideFilter: GetServerSideFilterFn | undefined; }) => { - const resolver: Resolver> = async ( + const resolver: Resolver< + unknown, + AggregationsQueryVariables, + Promise + > = async ( obj, { filters, aggregations_filter_themselves, include_missing = true }, context, diff --git a/modules/server/src/mapping/resolveHitsFromAggs.ts b/modules/server/src/mapping/resolveHitsFromAggs.ts index f8bf82a32..f4bcd8419 100644 --- a/modules/server/src/mapping/resolveHitsFromAggs.ts +++ b/modules/server/src/mapping/resolveHitsFromAggs.ts @@ -60,8 +60,7 @@ export const getHitsFromAggsResolver = (aggregationsResolver: AggregationsResolv */ const aggregationsPath = 'operation.selectionSet.selections[0].selectionSet.selections'; const aggregationsSelectionSet = get(info, aggregationsPath, []).find( - (selection: { kind: string; name: { value: string } }) => - selection.kind === 'Field' && selection.name.value === 'aggregations', + (selection) => selection.kind === 'Field' && selection.name.value === 'aggregations', ); if (aggregationsSelectionSet) { From 985d27245e22e1f3e5e9f4e738c5697316ccb483 Mon Sep 17 00:00:00 2001 From: Ciaran Schutte Date: Thu, 20 Mar 2025 10:54:30 -0400 Subject: [PATCH 8/8] explicit type import, use path alias --- modules/server/src/gqlServer.ts | 3 ++- modules/server/src/graphqlRoutes.js | 8 +++---- modules/server/src/mapping/masking.ts | 2 +- .../server/src/mapping/resolveAggregations.ts | 2 +- .../aggregations/AggregationAccumulator.ts | 22 ++++++++----------- modules/server/src/network/gql.ts | 2 +- modules/server/src/network/health.ts | 1 + modules/server/src/network/index.ts | 2 +- .../src/network/resolvers/aggregations.ts | 19 +++++++++------- modules/server/src/network/resolvers/fetch.ts | 10 ++++----- modules/server/src/network/resolvers/index.ts | 16 +++++++------- modules/server/src/network/resolvers/query.ts | 4 ++-- .../server/src/network/resolvers/response.ts | 4 ++-- modules/server/src/network/setup/fields.ts | 4 ++-- modules/server/src/network/setup/query.ts | 8 +++---- .../src/network/typeDefs/aggregations.ts | 2 +- modules/server/src/network/typeDefs/index.ts | 2 +- modules/server/src/network/utils/sqon.ts | 2 +- 18 files changed, 57 insertions(+), 56 deletions(-) diff --git a/modules/server/src/gqlServer.ts b/modules/server/src/gqlServer.ts index 1eb8ebb87..47730badf 100644 --- a/modules/server/src/gqlServer.ts +++ b/modules/server/src/gqlServer.ts @@ -18,7 +18,8 @@ export type ResolverOutput = T | Promise; * @param info - GraphQL info object. * @return Returns resolved value; */ -export type Resolver, QueryArgs = Object, ReturnValue = undefined> = ( +type DefaultRoot = Root; +export type Resolver = ( root: Root, args: QueryArgs, context: Context, diff --git a/modules/server/src/graphqlRoutes.js b/modules/server/src/graphqlRoutes.js index 22a7d0d27..c5f16c71a 100644 --- a/modules/server/src/graphqlRoutes.js +++ b/modules/server/src/graphqlRoutes.js @@ -2,8 +2,8 @@ import { ApolloServer } from 'apollo-server-express'; import { Router } from 'express'; import expressPlayground from 'graphql-playground-middleware-express'; - import { mergeSchemas } from '@graphql-tools/schema'; + import getConfigObject, { initializeSets } from './config'; import { DEBUG_MODE, ES_PASS, ES_USER } from './config/constants'; import { ConfigProperties } from './config/types'; @@ -276,12 +276,12 @@ export const createSchemasFromConfigs = async ({ * Federated Network Search */ if (enableNetworkAggregation) { - const networkConfig = configsFromFiles[ConfigProperties.NETWORK_AGGREGATION]; - if (!networkConfig || networkConfig?.servers.length === 0) { + const networkConfigsObj = configsFromFiles[ConfigProperties.NETWORK_AGGREGATION]; + if (!networkConfigsObj || networkConfigsObj?.servers.length === 0) { throw Error('Network config not found. Please check file is valid.'); } - const remoteServerConfigs = networkConfig.servers.map((config) => ({ + const remoteServerConfigs = networkConfigsObj.servers.map((config) => ({ ...config, /* * part of the gql schema is generated dynamically diff --git a/modules/server/src/mapping/masking.ts b/modules/server/src/mapping/masking.ts index a9ac787e2..deee6707c 100644 --- a/modules/server/src/mapping/masking.ts +++ b/modules/server/src/mapping/masking.ts @@ -1,5 +1,5 @@ import { ENV_CONFIG } from '@/config'; -import { Aggregations, AllAggregationsMap } from './resolveAggregations'; +import { type Aggregations, type AllAggregationsMap } from './resolveAggregations'; export const Relation = { eq: 'eq', diff --git a/modules/server/src/mapping/resolveAggregations.ts b/modules/server/src/mapping/resolveAggregations.ts index 907e168ce..d2a509ff8 100644 --- a/modules/server/src/mapping/resolveAggregations.ts +++ b/modules/server/src/mapping/resolveAggregations.ts @@ -1,6 +1,6 @@ import getFields from 'graphql-fields'; -import { Resolver, Root } from '@/gqlServer'; +import { type Resolver, type Root } from '@/gqlServer'; import { GetServerSideFilterFn } from '@/utils/getDefaultServerSideFilter'; import { buildAggregations, buildQuery, flattenAggregations } from '../middleware'; import { resolveSetsInSqon } from './hackyTemporaryEsSetResolution'; diff --git a/modules/server/src/network/aggregations/AggregationAccumulator.ts b/modules/server/src/network/aggregations/AggregationAccumulator.ts index d39612e8c..84277377d 100644 --- a/modules/server/src/network/aggregations/AggregationAccumulator.ts +++ b/modules/server/src/network/aggregations/AggregationAccumulator.ts @@ -1,15 +1,14 @@ import { Relation } from '@/mapping/masking'; import { - Aggregations, - AllAggregations, - AllAggregationsMap, - Bucket, - NumericAggregations, + type Aggregations, + type AllAggregations, + type AllAggregationsMap, + type Bucket, } from '@/mapping/resolveAggregations'; -import { ALL_NETWORK_AGGREGATION_TYPES_MAP } from '..'; -import { SupportedAggregation, SUPPORTED_AGGREGATIONS } from '../setup/constants'; -import { Hits } from '../types/hits'; -import { RequestedFieldsMap } from '../utils/gql'; +import { ALL_NETWORK_AGGREGATION_TYPES_MAP } from '@/network'; +import { SUPPORTED_AGGREGATIONS, type SupportedAggregation } from '@/network/setup/constants'; +import { type Hits } from '@/network/types/hits'; +import { RequestedFieldsMap } from '@/network/utils/gql'; type ResolveAggregationInput = { data: { aggregations: AllAggregationsMap; hits: Hits }; @@ -91,10 +90,7 @@ const resolveAggregations = ({ data, accumulator, requestedFields }: ResolveAggr * @param type * @param aggregations */ -const resolveAggregationByType = ( - type: string, - aggregations: [T, T], -): Aggregations | NumericAggregations => { +const resolveAggregationByType = (type: string, aggregations: [T, T]): Aggregations => { if (type === SUPPORTED_AGGREGATIONS.Aggregations) { return resolveAggregation(aggregations as AggregationsTuple); } else { diff --git a/modules/server/src/network/gql.ts b/modules/server/src/network/gql.ts index e2c6329ab..b31295de2 100644 --- a/modules/server/src/network/gql.ts +++ b/modules/server/src/network/gql.ts @@ -1,4 +1,4 @@ -import axios, { AxiosRequestConfig } from 'axios'; +import axios, { type AxiosRequestConfig } from 'axios'; /** * GQL fetch request with request options configured for graphql diff --git a/modules/server/src/network/health.ts b/modules/server/src/network/health.ts index a5cc3f5e9..a69bc38e9 100644 --- a/modules/server/src/network/health.ts +++ b/modules/server/src/network/health.ts @@ -1,5 +1,6 @@ import axios from 'axios'; import urljoin from 'url-join'; + import { CONNECTION_STATUS } from './resolvers/networkNode'; /** diff --git a/modules/server/src/network/index.ts b/modules/server/src/network/index.ts index 82174c5e8..89941b367 100644 --- a/modules/server/src/network/index.ts +++ b/modules/server/src/network/index.ts @@ -9,7 +9,7 @@ import { NetworkConfig } from './types/setup'; /** * Map of all available fields with associated aggregation type */ -export let ALL_NETWORK_AGGREGATION_TYPES_MAP: Map = +export const ALL_NETWORK_AGGREGATION_TYPES_MAP: Map = new Map(); /** diff --git a/modules/server/src/network/resolvers/aggregations.ts b/modules/server/src/network/resolvers/aggregations.ts index ad5151050..ed0512791 100644 --- a/modules/server/src/network/resolvers/aggregations.ts +++ b/modules/server/src/network/resolvers/aggregations.ts @@ -1,11 +1,14 @@ -import { AggregationsQueryVariables, AllAggregationsMap } from '@/mapping/resolveAggregations'; -import { AggregationAccumulator } from '../aggregations/AggregationAccumulator'; -import { failure, isSuccess } from '../result'; -import { NodeConfig } from '../setup/query'; -import { Hits } from '../types/hits'; -import { RequestedFieldsMap } from '../utils/gql'; -import { fetchData } from './fetch'; -import { createNetworkQuery } from './query'; +import { + type AggregationsQueryVariables, + type AllAggregationsMap, +} from '@/mapping/resolveAggregations'; +import { AggregationAccumulator } from '@/network/aggregations/AggregationAccumulator'; +import { fetchData } from '@/network/resolvers/fetch'; +import { createNetworkQuery } from '@/network/resolvers/query'; +import { failure, isSuccess } from '@/network/result'; +import { type NodeConfig } from '@/network/setup/query'; +import { type Hits } from '@/network/types/hits'; +import { RequestedFieldsMap } from '@/network/utils/gql'; export const CONNECTION_STATUS = { OK: 'OK', diff --git a/modules/server/src/network/resolvers/fetch.ts b/modules/server/src/network/resolvers/fetch.ts index b53a0ecbc..87d66f587 100644 --- a/modules/server/src/network/resolvers/fetch.ts +++ b/modules/server/src/network/resolvers/fetch.ts @@ -1,11 +1,11 @@ import axios, { AxiosError } from 'axios'; import { DocumentNode } from 'graphql'; -import { NetworkQueryVariables } from '.'; -import { fetchGql } from '../gql'; -import { failure, Result, success } from '../result'; -import { ASTtoString } from '../utils/gql'; -import { CONNECTION_STATUS } from './aggregations'; +import { fetchGql } from '@/network/gql'; +import { type NetworkQueryVariables } from '@/network/resolvers'; +import { CONNECTION_STATUS } from '@/network/resolvers/aggregations'; +import { failure, Result, success } from '@/network/result'; +import { ASTtoString } from '@/network/utils/gql'; type NetworkQuery = { url: string; diff --git a/modules/server/src/network/resolvers/index.ts b/modules/server/src/network/resolvers/index.ts index b718bd048..9d93347a5 100644 --- a/modules/server/src/network/resolvers/index.ts +++ b/modules/server/src/network/resolvers/index.ts @@ -1,11 +1,11 @@ -import { Resolver } from '@/gqlServer'; -import { AggregationsQueryVariables } from '@/mapping/resolveAggregations'; -import { isSuccess } from '../result'; -import { NodeConfig } from '../setup/query'; -import { resolveInfoToMap } from '../utils/gql'; -import { convertToSqon } from '../utils/sqon'; -import { aggregationPipeline, NetworkNode } from './aggregations'; -import { createResponse } from './response'; +import { type Resolver } from '@/gqlServer'; +import { type AggregationsQueryVariables } from '@/mapping/resolveAggregations'; +import { aggregationPipeline, type NetworkNode } from '@/network/resolvers/aggregations'; +import { createResponse } from '@/network/resolvers/response'; +import { isSuccess } from '@/network/result'; +import { type NodeConfig } from '@/network/setup/query'; +import { resolveInfoToMap } from '@/network/utils/gql'; +import { convertToSqon } from '@/network/utils/sqon'; type NetworkSearchRoot = { nodes: NetworkNode[]; diff --git a/modules/server/src/network/resolvers/query.ts b/modules/server/src/network/resolvers/query.ts index def5d72a3..ac5eb55e5 100644 --- a/modules/server/src/network/resolvers/query.ts +++ b/modules/server/src/network/resolvers/query.ts @@ -2,8 +2,8 @@ import { gql } from 'apollo-server-core'; import { DocumentNode } from 'graphql'; import { isEmpty } from 'lodash'; -import { NodeConfig } from '../setup/query'; -import { RequestedFieldsMap } from '../utils/gql'; +import { type NodeConfig } from '@/network/setup/query'; +import { type RequestedFieldsMap } from '@/network/utils/gql'; /** * Converts info field object into string diff --git a/modules/server/src/network/resolvers/response.ts b/modules/server/src/network/resolvers/response.ts index 155e4d79b..087bf61da 100644 --- a/modules/server/src/network/resolvers/response.ts +++ b/modules/server/src/network/resolvers/response.ts @@ -1,5 +1,5 @@ -import { AllAggregationsMap } from '@/mapping/resolveAggregations'; -import { NetworkNode } from './aggregations'; +import { type AllAggregationsMap } from '@/mapping/resolveAggregations'; +import { type NetworkNode } from './aggregations'; /** * Format response object to match GQL type defs diff --git a/modules/server/src/network/setup/fields.ts b/modules/server/src/network/setup/fields.ts index 43083c112..d5e25b5f9 100644 --- a/modules/server/src/network/setup/fields.ts +++ b/modules/server/src/network/setup/fields.ts @@ -1,5 +1,5 @@ -import { SupportedAggregation, SUPPORTED_AGGREGATIONS_LIST } from './constants'; -import { NodeConfig } from './query'; +import { SUPPORTED_AGGREGATIONS_LIST, type SupportedAggregation } from '@/network/setup/constants'; +import { type NodeConfig } from '@/network/setup/query'; type NetworkFieldType = { name: string; diff --git a/modules/server/src/network/setup/query.ts b/modules/server/src/network/setup/query.ts index 1c9416cbd..9aabab911 100644 --- a/modules/server/src/network/setup/query.ts +++ b/modules/server/src/network/setup/query.ts @@ -1,7 +1,7 @@ -import { NetworkAggregationError } from '../errors'; -import { fetchGql } from '../gql'; -import { NetworkConfig } from '../types/setup'; -import { fulfilledPromiseFilter } from '../utils/promise'; +import { NetworkAggregationError } from '@/network/errors'; +import { fetchGql } from '@/network/gql'; +import { type NetworkConfig } from '@/network/types/setup'; +import { fulfilledPromiseFilter } from '@/network/utils/promise'; export type NodeConfig = NetworkConfig & { aggregations: { name: string; type: string }[] }; diff --git a/modules/server/src/network/typeDefs/aggregations.ts b/modules/server/src/network/typeDefs/aggregations.ts index 4e1954b20..5268b7aaf 100644 --- a/modules/server/src/network/typeDefs/aggregations.ts +++ b/modules/server/src/network/typeDefs/aggregations.ts @@ -9,7 +9,7 @@ import { } from 'graphql'; import GraphQLJSON from 'graphql-type-json'; -import { SupportedNetworkFieldType } from '../setup/fields'; +import { type SupportedNetworkFieldType } from '@/network/setup/fields'; const relation = new GraphQLEnumType({ name: 'Relation', diff --git a/modules/server/src/network/typeDefs/index.ts b/modules/server/src/network/typeDefs/index.ts index afe487439..bfa99950f 100644 --- a/modules/server/src/network/typeDefs/index.ts +++ b/modules/server/src/network/typeDefs/index.ts @@ -1,4 +1,4 @@ -import { SupportedNetworkFieldType } from '../setup/fields'; +import { type SupportedNetworkFieldType } from '../setup/fields'; import { createNetworkAggregationTypeDefs } from './aggregations'; export const createTypeDefs = (networkFieldTypes: SupportedNetworkFieldType[]) => { diff --git a/modules/server/src/network/utils/sqon.ts b/modules/server/src/network/utils/sqon.ts index 8368eda70..dd174e4f2 100644 --- a/modules/server/src/network/utils/sqon.ts +++ b/modules/server/src/network/utils/sqon.ts @@ -1,5 +1,5 @@ +import { failure, Result, success } from '@/network/result'; import SQONBuilder, { SQON } from '@overture-stack/sqon-builder'; -import { failure, Result, success } from '../result'; /** * Attempts to convert a variable to a SQON. A Result is returned, where if successful