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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion modules/server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ npm run prepare

## Federated Search

[Placeholder]

### Config

[Placeholder]
[Placeholder]
9 changes: 9 additions & 0 deletions modules/server/configTemplates/configs.json.schema
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,13 @@
"maxResultsWindow": 10000,
"rowIdFieldName": "analysis.analysis_id"
}
"network": {
"servers": [
{
"displayName": "Toronto",
"graphqlUrl": "http://<URL>/graphql",
"documentType": "file"
}
]
}
}
8 changes: 1 addition & 7 deletions modules/server/configTemplates/network.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
{
"network": {
"servers": [
{
"displayName": "Toronto",
"graphqlUrl": "http://.../graphql",
"documentType": "file"
}
]
"servers": []
}
}
2 changes: 0 additions & 2 deletions modules/server/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,9 @@ export default async function (rootPath = '') {

/**
* @param {boolean} enableAdmin
* @param {boolean} enableDocumentHits - enables including "hits" property in the GQL response
*/
return arranger({
enableAdmin: ENV_CONFIG.ENABLE_ADMIN,
enableDocumentHits: ENV_CONFIG.ENABLE_DOCUMENT_HITS,
}).then((router) => {
app.use(router);

Expand Down
4 changes: 2 additions & 2 deletions modules/server/src/config/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ export const ALLOW_CUSTOM_MAX_DOWNLOAD_ROWS = stringToBool(
);
export const CONFIG_FILES_PATH = process.env.CONFIG_PATH || './configs';
export const DATA_MASK_MIN_THRESHOLD =
process.env.DATA_MASK_MIN_THRESHOLD || Number.MAX_SAFE_INTEGER;
stringToNumber(process.env.DATA_MASK_MIN_THRESHOLD) || Number.MAX_SAFE_INTEGER;
export const DEBUG_MODE = stringToBool(process.env.DEBUG);
export const DOCUMENT_TYPE = process.env.DOCUMENT_TYPE || '';
export const DOWNLOAD_STREAM_BUFFER_SIZE =
stringToNumber(process.env.DOWNLOAD_STREAM_BUFFER_SIZE) || 2000;
export const ENABLE_ADMIN = stringToBool(process.env.ENABLE_ADMIN);
export const ENABLE_DOCUMENT_HITS = stringToBool(process.env.ENABLE_DOCUMENT_HITS);
export const ENABLE_LOGS = stringToBool(process.env.ENABLE_LOGS);
export const ENABLE_NETWORK_AGGREGATION = stringToBool(process.env.ENABLE_NETWORK_AGGREGATION);
export const ES_ARRANGER_SET_INDEX = process.env.ES_ARRANGER_SET_INDEX || 'arranger-sets';
export const ES_ARRANGER_SET_TYPE = process.env.ES_ARRANGER_SET_TYPE || 'arranger-sets';
export const ES_HOST = process.env.ES_HOST || 'http://127.0.0.1:9200';
Expand All @@ -27,4 +28,3 @@ export const PING_MS = stringToNumber(process.env.PING_MS) || 2200;
export const PING_PATH = process.env.PING_PATH || '/ping';
export const PORT = stringToNumber(process.env.PORT) || 5050;
export const ROW_ID_FIELD_NAME = process.env.ROW_ID_FIELD_NAME || 'id';
export const ENABLE_NETWORK_AGGREGATION = stringToBool(process.env.ENABLE_NETWORK_AGGREGATION);
15 changes: 8 additions & 7 deletions modules/server/src/config/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// TODO: will gradually tighten these as we migrate to TS

import { ES_TYPES } from '@/mapping/esToAggTypeMap';
import { DOCUMENT_TYPE } from './constants';

export const ConfigOptionalProperties = {
DOWNLOADS: 'downloads',
Expand Down Expand Up @@ -55,7 +54,7 @@ export const TableProperties = {
ROW_ID_FIELD_NAME: 'rowIdFieldName',
} as const;

export const NetworkAggregationProperties = {
const NetworkAggregationProperties = {
GRAPHQL_URL: 'graphqlUrl',
DOCUMENT_TYPE: 'documentType',
DISPLAY_NAME: 'displayName',
Expand Down Expand Up @@ -151,10 +150,12 @@ export interface TableConfigsInterface {
[ConfigProperties.ROW_ID_FIELD_NAME]?: string;
}

export interface NetworkAggregationInterface {
[NetworkAggregationProperties.GRAPHQL_URL]: string;
[NetworkAggregationProperties.DOCUMENT_TYPE]: string;
[NetworkAggregationProperties.DISPLAY_NAME]: string;
interface NetworkAggregationInterface {
servers: {
[NetworkAggregationProperties.GRAPHQL_URL]: string;
[NetworkAggregationProperties.DOCUMENT_TYPE]: string;
[NetworkAggregationProperties.DISPLAY_NAME]: string;
}[];
}

export interface ConfigObject {
Expand All @@ -165,7 +166,7 @@ export interface ConfigObject {
[ConfigProperties.INDEX]: string;
[ConfigProperties.MATCHBOX]: any[];
[ConfigProperties.TABLE]: TableConfigsInterface;
[ConfigProperties.NETWORK_AGGREGATION]: NetworkAggregationInterface[];
[ConfigProperties.NETWORK_AGGREGATION]: NetworkAggregationInterface;
}

export interface FieldFromMapping {
Expand Down
24 changes: 24 additions & 0 deletions modules/server/src/gqlServer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Client } from '@elastic/elasticsearch';
import { GraphQLResolveInfo } from 'graphql';

export type Context = {
esClient: Client;
Copy link
Member

Choose a reason for hiding this comment

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

TODO: we will need to abstract this into an ESClient provider, so users can switch seamlessly between ES v7 and v8

};

export type ResolverOutput<T> = T | Promise<T>;

/**
* GQL resolver
*
* @param root - Parent object of a query.
* @param args - Query arguments.
* @param context - Context passed to apollo-server for queries.
* @param info - GraphQL info object.
* @return Returns resolved value;
*/
export type Resolver<Root = {}, QueryArgs = Object, ReturnValue = undefined> = (
root: Root,
args: QueryArgs,
context: Context,
info: GraphQLResolveInfo,
) => ResolverOutput<ReturnValue> | ResolverOutput<ReturnValue>;
6 changes: 3 additions & 3 deletions modules/server/src/graphqlRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import expressPlayground from 'graphql-playground-middleware-express';

import { mergeSchemas } from '@graphql-tools/schema';
import getConfigObject, { initializeSets } from './config';
import { DEBUG_MODE, ENABLE_NETWORK_AGGREGATION, ES_PASS, ES_USER } from './config/constants';
import { DEBUG_MODE, ES_PASS, ES_USER } from './config/constants';
import { ConfigProperties } from './config/types';
import { addMappingsToTypes, extendFields, fetchMapping } from './mapping';
import { extendColumns, extendFacets, flattenMappingToFields } from './mapping/extendMapping';
Expand Down Expand Up @@ -243,7 +243,7 @@ const createEndpoint = async ({ esClient, graphqlOptions = {}, mockSchema, schem
return router;
};

const createSchemasFromConfigs = async ({
export const createSchemasFromConfigs = async ({
Copy link
Member

@justincorrigible justincorrigible Mar 14, 2025

Choose a reason for hiding this comment

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

🙏 I knew we'd need this export at some point, just couldn't remember which was the one function needed after a few months

configsSource = '',
enableAdmin,
enableDocumentHits,
Expand Down Expand Up @@ -272,7 +272,7 @@ const createSchemasFromConfigs = async ({

const schemasToMerge = [schema];

/*
/**
* Federated Network Search
*/
if (enableNetworkAggregation) {
Expand Down
21 changes: 14 additions & 7 deletions modules/server/src/mapping/createConnectionTypeDefs.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import mappingToAggsType from './mappingToAggsType';

export default ({ type, fields = '', createStateTypeDefs = true, showRecords }) => {
const dataMaskingType = !showRecords ? 'type DataMasking { thresholdValue: Int }' : '';
const createConnectionType = (enableDocumentHits) => {
return `type ${type.name}Connection {
total: Int!
${enableDocumentHits ? `edges: [${type.name}Edge]` : ''}
}`;
};

const createDataMaskingType = (enableDocumentHits) => {
return !enableDocumentHits ? `type DataMasking { thresholdValue: Int }` : '';
};

export default ({ type, fields = '', createStateTypeDefs = true, enableDocumentHits }) => {
return `
type ${type.name} {
aggregations(
Expand Down Expand Up @@ -34,12 +43,10 @@ export default ({ type, fields = '', createStateTypeDefs = true, showRecords })
${mappingToAggsType(type.mapping)}
}

${dataMaskingType}
${createDataMaskingType(enableDocumentHits)}

type ${type.name}Connection {
total: Int!
${showRecords ? `edges: [${type.name}Edge]` : ''}
}
${createConnectionType(enableDocumentHits)}


type ${type.name}Edge {
searchAfter: JSON
Expand Down
3 changes: 1 addition & 2 deletions modules/server/src/mapping/mappingToFields.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import mappingToObjectTypes from './mappingToObjectTypes';
import mappingToScalarFields from './mappingToScalarFields';

const mappingToFields = ({ enableDocumentHits, type, parent }) => {
const showRecords = enableDocumentHits;
return [
mappingToObjectTypes(type.name, type.mapping, parent, type.extendedFields),
Object.entries(type.mapping)
Expand All @@ -29,7 +28,7 @@ const mappingToFields = ({ enableDocumentHits, type, parent }) => {
type.customFields,
],
createStateTypeDefs: 'createState' in type ? type.createState : true,
showRecords,
enableDocumentHits,
}),
].join();
};
Expand Down
2 changes: 1 addition & 1 deletion modules/server/src/mapping/masking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const Relation = {
export type Relation = keyof typeof Relation;

/**
* This returns a total count that is less than or equal to the actual total hits in the query.
* Returns a total count that is less than or equal to the actual total hits in the query
* It is calculated by adding +1 for values under threshold or adding bucket.doc_count amount
* for values greater than or equal to
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,48 @@ import getFields from 'graphql-fields';

import { buildAggregations, buildQuery, flattenAggregations } from '../middleware';

import { Resolver } from '@/gqlServer';
import { GetServerSideFilterFn } from '@/utils/getDefaultServerSideFilter';
import { resolveSetsInSqon } from './hackyTemporaryEsSetResolution';
import { Relation } from './masking';
import { AggregationQuery, Root } from './types';
import compileFilter from './utils/compileFilter';
import esSearch from './utils/esSearch';

export default ({ type, getServerSideFilter }) => {
return async (
const toGraphqlField = (acc: Aggregations, [a, b]: [string, Aggregation]) => ({
...acc,
[a.replace(/\./g, '__')]: b,
});
export const aggregationsToGraphql = (aggregations: Aggregations) => {
return Object.entries(aggregations).reduce<Aggregations>(toGraphqlField, {});
};

/*
* Types
*/
export type Bucket = {
doc_count: number;
key: string;
relation: Relation;
};

export type Aggregation = {
bucket_count: number;
buckets: Bucket[];
};

export type Aggregations = Record<string, Aggregation>;

export type AggregationsResolver = Resolver<Root, AggregationQuery, Promise<Aggregations>>;

const getAggregationsResolver = ({
type,
getServerSideFilter,
}: {
type: Record<string, any>;
getServerSideFilter: GetServerSideFilterFn | undefined;
}) => {
const resolver: Resolver<unknown, AggregationQuery, Promise<Aggregations>> = async (
obj,
{ filters, aggregations_filter_themselves, include_missing = true },
context,
Expand All @@ -26,7 +62,7 @@ export default ({ type, getServerSideFilter }) => {
nestedFieldNames,
filters: compileFilter({
clientSideFilter: resolvedFilter,
serverSideFilter: getServerSideFilter(context),
serverSideFilter: getServerSideFilter && getServerSideFilter(),
}),
});

Expand All @@ -48,6 +84,7 @@ export default ({ type, getServerSideFilter }) => {
const response = await esSearch(esClient)({
index: type.index,
size: 0,
// @ts-expect-error - valid search query parameter in ES 7.17, not in types
Copy link
Member

Choose a reason for hiding this comment

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

hmmm!

_source: false,
body,
});
Expand All @@ -58,9 +95,8 @@ export default ({ type, getServerSideFilter }) => {

return aggregations;
};
};

const toGraphqlField = (acc, [a, b]) => ({ ...acc, [a.replace(/\./g, '__')]: b });
export const aggregationsToGraphql = (aggregations) => {
return Object.entries(aggregations).reduce(toGraphqlField, {});
return resolver;
};

export default getAggregationsResolver;
51 changes: 51 additions & 0 deletions modules/server/src/mapping/resolveHitsFromAggs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Resolver } from '@/gqlServer';
import { get } from 'lodash';
import { applyAggregationMasking } from './masking';
import { AggregationsResolver } from './resolveAggregations';
import { HitsQuery, Root } from './types';

type HitsResolver = Resolver<Root, HitsQuery, Promise<{ total: number }>>;

/**
* Resolver for "aggregation only mode" of Arranger where "hits" is based on "aggregations"
* Calculate hits from aggregation data, instead of using "hits" ES response field
*
* If "aggregations" field is not in query, return 0
*
* @param aggregationsResolver - resolver ES query code for aggregations
* @returns Returns a total count that is less than or equal to the actual total hits in the query.
*/
export const getHitsFromAggsResolver = (aggregationsResolver: AggregationsResolver) => {
const resolver: HitsResolver = async (obj, args, context, info) => {
/*
* Get "aggregations" field from full query if found
* Popular gql parsing libs parse the "info" property which may not include full query based on schema
*/
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',
);

if (aggregationsSelectionSet) {
const modifiedInfo = { ...info, fieldNodes: [aggregationsSelectionSet] };

const aggregations = await aggregationsResolver(
obj,
// @ts-ignore
// modifying the query info field inline so it can query aggregations correctly
// not idiomatic so doesn't line up with typings from graphql
info.variableValues,
context,
modifiedInfo,
);
const { hitsTotal: total } = applyAggregationMasking({
aggregations,
});
return { total };
} else {
return { total: 0 };
}
};
return resolver;
};
Loading