@gen3/core 0.12.69 → 0.12.70
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/index.js +2124 -2154
- package/dist/cjs/index.js.map +1 -1
- package/dist/dts/constants.d.ts.map +1 -1
- package/dist/dts/dataAccess.d.ts.map +1 -1
- package/dist/dts/features/aiSearch/aiSearchSlice.d.ts +174 -174
- package/dist/dts/features/app/store.d.ts.map +1 -1
- package/dist/dts/features/authz/authzMappingSlice.d.ts +403 -403
- package/dist/dts/features/cedar/cedarApi.d.ts +54 -54
- package/dist/dts/features/cedar/cedarApi.d.ts.map +1 -1
- package/dist/dts/features/cedar/createCedarInstance.d.ts.map +1 -1
- package/dist/dts/features/cohort/cohortManagerSelector.d.ts +5 -5
- package/dist/dts/features/cohort/cohortManagerSelector.d.ts.map +1 -1
- package/dist/dts/features/cohort/cohortManagerSlice.d.ts.map +1 -1
- package/dist/dts/features/cohort/filterCombineModeSlice.d.ts.map +1 -1
- package/dist/dts/features/cohort/filterExpandSlice.d.ts.map +1 -1
- package/dist/dts/features/cohort/sharedFiltersSlice.d.ts +1 -1
- package/dist/dts/features/cohort/sharedFiltersSlice.d.ts.map +1 -1
- package/dist/dts/features/cohort/storage/CohortStorage.d.ts.map +1 -1
- package/dist/dts/features/cohort/utils.d.ts.map +1 -1
- package/dist/dts/features/cohortComparison/cohortFacetSlice.d.ts +60 -60
- package/dist/dts/features/cohortComparison/pValueApi.d.ts +60 -60
- package/dist/dts/features/cohortComparison/vennDiagramSlice.d.ts +60 -60
- package/dist/dts/features/config/registry.d.ts.map +1 -1
- package/dist/dts/features/dataLibrary/storage/APIStorageService.d.ts.map +1 -1
- package/dist/dts/features/dataLibrary/storage/CachedAPIStorageService.d.ts.map +1 -1
- package/dist/dts/features/dataLibrary/storage/DataLibraryStorageService.d.ts.map +1 -1
- package/dist/dts/features/dataLibrary/storage/LocalStorageService.d.ts.map +1 -1
- package/dist/dts/features/dataLibrary/storage/tests/data.d.ts.map +1 -1
- package/dist/dts/features/dataLibrary/types.d.ts.map +1 -1
- package/dist/dts/features/dataLibrary/useDataLibrary.d.ts.map +1 -1
- package/dist/dts/features/dataLibrary/utils.d.ts.map +1 -1
- package/dist/dts/features/download/constants.d.ts.map +1 -1
- package/dist/dts/features/download/downloadStatusApi.d.ts +235 -235
- package/dist/dts/features/drsResolver/drsHostnameSlice.d.ts.map +1 -1
- package/dist/dts/features/drsResolver/resolvers/cachedDRSResolver.d.ts.map +1 -1
- package/dist/dts/features/drsResolver/resolvers/dataGUIDSDotOrg.d.ts.map +1 -1
- package/dist/dts/features/drsResolver/utils.d.ts.map +1 -1
- package/dist/dts/features/fence/credentialsApi.d.ts +222 -222
- package/dist/dts/features/fence/fenceApi.d.ts +300 -300
- package/dist/dts/features/fence/fenceApi.d.ts.map +1 -1
- package/dist/dts/features/fence/fetchFence.d.ts.map +1 -1
- package/dist/dts/features/fence/jwtApi.d.ts +120 -120
- package/dist/dts/features/fence/utils.d.ts.map +1 -1
- package/dist/dts/features/filters/filters.d.ts +1 -1
- package/dist/dts/features/filters/filters.d.ts.map +1 -1
- package/dist/dts/features/filters/nestedFilters.d.ts +1 -1
- package/dist/dts/features/filters/nestedFilters.d.ts.map +1 -1
- package/dist/dts/features/filters/types.d.ts.map +1 -1
- package/dist/dts/features/filters/utils.d.ts.map +1 -1
- package/dist/dts/features/gen3Apps/Gen3App.d.ts.map +1 -1
- package/dist/dts/features/gen3Apps/Gen3AppRTKQ.d.ts +1 -1
- package/dist/dts/features/gen3Apps/Gen3AppRTKQ.d.ts.map +1 -1
- package/dist/dts/features/gen3Apps/gen3AppRegistry.d.ts.map +1 -1
- package/dist/dts/features/gen3Apps/gen3AppsSlice.d.ts.map +1 -1
- package/dist/dts/features/graphQL/graphQLSlice.d.ts +61 -61
- package/dist/dts/features/guppy/download.d.ts +9 -0
- package/dist/dts/features/guppy/download.d.ts.map +1 -0
- package/dist/dts/features/guppy/guppyApi.d.ts +2 -2
- package/dist/dts/features/guppy/guppyDownloadSlice.d.ts +120 -120
- package/dist/dts/features/guppy/guppySlice.d.ts +2141 -2141
- package/dist/dts/features/guppy/guppySlice.d.ts.map +1 -1
- package/dist/dts/features/guppy/index.d.ts +2 -1
- package/dist/dts/features/guppy/index.d.ts.map +1 -1
- package/dist/dts/features/guppy/processing.d.ts.map +1 -1
- package/dist/dts/features/guppy/queryGenerators.d.ts.map +1 -1
- package/dist/dts/features/guppy/range.d.ts.map +1 -1
- package/dist/dts/features/guppy/utils.d.ts +1 -27
- package/dist/dts/features/guppy/utils.d.ts.map +1 -1
- package/dist/dts/features/indexd/indexdSlice.d.ts +240 -240
- package/dist/dts/features/manifest/manifestApi.d.ts +585 -585
- package/dist/dts/features/metadata/metadataSlice.d.ts +540 -540
- package/dist/dts/features/metadata/utils.d.ts.map +1 -1
- package/dist/dts/features/modals/modalsSlice.d.ts.map +1 -1
- package/dist/dts/features/notifications/notificationService.d.ts.map +1 -1
- package/dist/dts/features/requestor/remoteSupport/registeredRemoteSupportServices.d.ts.map +1 -1
- package/dist/dts/features/requestor/remoteSupport/types.d.ts.map +1 -1
- package/dist/dts/features/requestor/requestorSlice.d.ts +354 -354
- package/dist/dts/features/requestor/requestorSlice.d.ts.map +1 -1
- package/dist/dts/features/sower/sowerApi.d.ts +558 -558
- package/dist/dts/features/sower/sowerApi.d.ts.map +1 -1
- package/dist/dts/features/sower/sowerJobDatetime.d.ts.map +1 -1
- package/dist/dts/features/submission/authMappingUtils.d.ts.map +1 -1
- package/dist/dts/features/submission/submissionApi.d.ts +481 -481
- package/dist/dts/features/updateStudyInMDS/mdsApi.d.ts +54 -54
- package/dist/dts/features/updateStudyInMDS/updateStudyInMDS.d.ts.map +1 -1
- package/dist/dts/features/user/externalLoginsSlice.d.ts +241 -241
- package/dist/dts/features/user/externalLoginsSlice.d.ts.map +1 -1
- package/dist/dts/features/user/hooks.d.ts +1 -1
- package/dist/dts/features/user/hooks.d.ts.map +1 -1
- package/dist/dts/features/user/userSlice.d.ts +2 -2
- package/dist/dts/features/user/userSlice.d.ts.map +1 -1
- package/dist/dts/features/user/userSliceRTK.d.ts +672 -692
- package/dist/dts/features/user/userSliceRTK.d.ts.map +1 -1
- package/dist/dts/features/user/utils.d.ts.map +1 -1
- package/dist/dts/features/workspace/jegKernelSlice.d.ts +83 -83
- package/dist/dts/features/workspace/jegWorkspaceSlice.d.ts.map +1 -1
- package/dist/dts/features/workspace/tieredWorkspaceSlice.d.ts.map +1 -1
- package/dist/dts/features/workspace/utils.d.ts.map +1 -1
- package/dist/dts/features/workspace/workspaceApi.d.ts +939 -939
- package/dist/dts/features/workspace/workspaceSlice.d.ts.map +1 -1
- package/dist/dts/hooks.d.ts +1 -1
- package/dist/dts/reducers.d.ts +24 -24
- package/dist/dts/storage-persist.d.ts.map +1 -1
- package/dist/dts/store.d.ts +3 -3
- package/dist/dts/store.d.ts.map +1 -1
- package/dist/dts/types/types.d.ts.map +1 -1
- package/dist/dts/utils/caseConversion.d.ts.map +1 -1
- package/dist/dts/utils/conversions.d.ts.map +1 -1
- package/dist/dts/utils/extractvalues.d.ts.map +1 -1
- package/dist/dts/utils/fetch.d.ts +2 -2
- package/dist/dts/utils/fetch.d.ts.map +1 -1
- package/dist/dts/utils/time.d.ts.map +1 -1
- package/dist/dts/utils/ts-utils.d.ts.map +1 -1
- package/dist/dts/utils/url.d.ts.map +1 -1
- package/dist/esm/index.js +2124 -2154
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.ts +8363 -8394
- package/package.json +4 -4
package/dist/esm/index.js
CHANGED
|
@@ -6,19 +6,19 @@ import { QueryStatus, setupListeners } from '@reduxjs/toolkit/query';
|
|
|
6
6
|
import { createApi, fetchBaseQuery, buildCreateApi, coreModule, reactHooksModule } from '@reduxjs/toolkit/query/react';
|
|
7
7
|
import { getCookie } from 'cookies-next';
|
|
8
8
|
import { GraphQLError, parse } from 'graphql';
|
|
9
|
-
import { JSONPath } from 'jsonpath-plus';
|
|
10
9
|
import { isEqual, toString } from 'lodash';
|
|
11
10
|
import { customAlphabet } from 'nanoid';
|
|
12
11
|
import { persistReducer, FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER, persistStore } from 'redux-persist';
|
|
13
12
|
import createWebStorage from 'redux-persist/lib/storage/createWebStorage';
|
|
14
13
|
import useSWR from 'swr';
|
|
15
|
-
import {
|
|
16
|
-
import Papa from 'papaparse';
|
|
14
|
+
import { JSONPath } from 'jsonpath-plus';
|
|
17
15
|
import { PersistGate } from 'redux-persist/integration/react';
|
|
18
16
|
import { openDB } from 'idb';
|
|
19
17
|
import { useDeepCompareMemo } from 'use-deep-compare';
|
|
20
18
|
import { v5 } from 'uuid';
|
|
21
19
|
import { CookiesProvider } from 'react-cookie';
|
|
20
|
+
import { flatten } from 'flat';
|
|
21
|
+
import Papa from 'papaparse';
|
|
22
22
|
import Queue from 'queue';
|
|
23
23
|
|
|
24
24
|
const GEN3_COMMONS_NAME = process.env.NEXT_PUBLIC_GEN3_COMMONS_NAME || 'gen3';
|
|
@@ -658,1054 +658,931 @@ const { selectById: selectCartItem, selectIds: selectCartItems, selectAll: selec
|
|
|
658
658
|
const cartReducerPath = 'cart';
|
|
659
659
|
|
|
660
660
|
/**
|
|
661
|
-
*
|
|
662
|
-
*
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
if (!Array.isArray(guidsForHostnameResolution) || !guidsForHostnameResolution.every((guid)=>typeof guid === 'string')) {
|
|
699
|
-
throw new Error(INVALID_INPUT_ERROR);
|
|
700
|
-
}
|
|
701
|
-
try {
|
|
702
|
-
const responses = await Promise.allSettled(guidsForHostnameResolution.map(async (guid)=>{
|
|
703
|
-
try {
|
|
704
|
-
const response = await fetch(`https://dataguids.org/index/${guid}`);
|
|
705
|
-
if (response.status === 200) {
|
|
706
|
-
try {
|
|
707
|
-
return await response.json();
|
|
708
|
-
// oxlint-disable-next-line no-unused-vars
|
|
709
|
-
} catch (error) {
|
|
710
|
-
throw new ResolveDRSError('Failed to parse response as JSON');
|
|
661
|
+
* Creates a base class core API for guppy API calls.
|
|
662
|
+
* @returns: guppy core API with guppyAPIFetch base query
|
|
663
|
+
*/ const guppyApi = createApi({
|
|
664
|
+
reducerPath: 'guppy',
|
|
665
|
+
// TODO: refactor to use fetchBaseQuery
|
|
666
|
+
baseQuery: async (query, api)=>{
|
|
667
|
+
const csrfToken = selectCSRFToken(api.getState());
|
|
668
|
+
let accessToken = undefined;
|
|
669
|
+
if (process.env.NODE_ENV === 'development') {
|
|
670
|
+
// NOTE: This cookie can only be accessed from the client side
|
|
671
|
+
// in development mode. Otherwise, the cookie is set as httpOnly
|
|
672
|
+
accessToken = getCookie('credentials_token');
|
|
673
|
+
}
|
|
674
|
+
const headers = {
|
|
675
|
+
Accept: 'application/json',
|
|
676
|
+
'Content-Type': 'application/json',
|
|
677
|
+
...csrfToken && {
|
|
678
|
+
'X-CSRF-Token': csrfToken
|
|
679
|
+
},
|
|
680
|
+
...accessToken && {
|
|
681
|
+
Authorization: `Bearer ${accessToken}`
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
try {
|
|
685
|
+
const response = await fetch(`${GEN3_GUPPY_API}/graphql`, {
|
|
686
|
+
headers: headers,
|
|
687
|
+
method: 'POST',
|
|
688
|
+
body: JSON.stringify(query)
|
|
689
|
+
});
|
|
690
|
+
const data = await response.json();
|
|
691
|
+
// Check the response for GraphQL errors
|
|
692
|
+
if (data && 'errors' in data) {
|
|
693
|
+
// Return an object with an 'error' property if GraphQL errors are present
|
|
694
|
+
return {
|
|
695
|
+
error: {
|
|
696
|
+
message: data.errors[0].message,
|
|
697
|
+
locations: data.errors[0].locations
|
|
711
698
|
}
|
|
712
|
-
}
|
|
713
|
-
throw new ResolveDRSError(RESOLVE_DRS_ERROR);
|
|
714
|
-
}
|
|
715
|
-
// oxlint-disable-next-line no-unused-vars
|
|
716
|
-
} catch (error) {
|
|
717
|
-
throw new ResolveDRSError(RESOLVE_DRS_ERROR);
|
|
699
|
+
};
|
|
718
700
|
}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
701
|
+
return {
|
|
702
|
+
data
|
|
703
|
+
};
|
|
704
|
+
} catch (e) {
|
|
705
|
+
if (e instanceof GraphQLError) {
|
|
722
706
|
return {
|
|
723
|
-
|
|
724
|
-
|
|
707
|
+
error: {
|
|
708
|
+
message: e.message,
|
|
709
|
+
locations: e.locations,
|
|
710
|
+
path: e.path
|
|
711
|
+
}
|
|
725
712
|
};
|
|
726
|
-
} else {
|
|
727
|
-
throw new ResolveDRSError(RESOLVE_DRS_ERROR);
|
|
728
713
|
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
};
|
|
735
|
-
|
|
736
|
-
const resolveCachedDRS = async (guidsForHostnameResolution)=>{
|
|
737
|
-
const state = coreStore.getState();
|
|
738
|
-
const resolvedIds = {};
|
|
739
|
-
for (const guid of guidsForHostnameResolution){
|
|
740
|
-
const hostname = drsHostnamesSelector(guid, state);
|
|
741
|
-
if (hostname) {
|
|
742
|
-
resolvedIds[guid] = hostname;
|
|
714
|
+
if (e instanceof Error) return {
|
|
715
|
+
error: e.message
|
|
716
|
+
};
|
|
717
|
+
return {
|
|
718
|
+
error: e
|
|
719
|
+
};
|
|
743
720
|
}
|
|
721
|
+
},
|
|
722
|
+
endpoints: ()=>({})
|
|
723
|
+
});
|
|
724
|
+
const guppyAPISliceMiddleware = guppyApi.middleware;
|
|
725
|
+
const guppyApiSliceReducerPath = guppyApi.reducerPath;
|
|
726
|
+
const guppyApiReducer = guppyApi.reducer;
|
|
727
|
+
|
|
728
|
+
const isFilterSet = (input)=>{
|
|
729
|
+
if (typeof input !== 'object' || input === null) {
|
|
730
|
+
return false;
|
|
744
731
|
}
|
|
745
|
-
|
|
732
|
+
const { root, mode } = input;
|
|
733
|
+
if (typeof root !== 'object' || root === null) {
|
|
734
|
+
return false;
|
|
735
|
+
}
|
|
736
|
+
if (![
|
|
737
|
+
'and',
|
|
738
|
+
'or'
|
|
739
|
+
].includes(mode)) {
|
|
740
|
+
return false;
|
|
741
|
+
}
|
|
742
|
+
return true;
|
|
746
743
|
};
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
744
|
+
/**
|
|
745
|
+
* Type guard for Includes
|
|
746
|
+
* @param o - operator to check
|
|
747
|
+
* @category Filters
|
|
748
|
+
*/ const isIncludes = (o)=>o.operator === 'includes' || o.operator === 'in';
|
|
749
|
+
const isUnion = (value)=>{
|
|
750
|
+
return typeof value === 'object' && value !== null && value.operator === 'or' && Array.isArray(value.operands);
|
|
751
751
|
};
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
return DRS_Resolvers[resolver](guidsForHostnameResolution);
|
|
752
|
+
const isIntersection = (value)=>{
|
|
753
|
+
return typeof value === 'object' && value !== null && value.operator === 'and' && Array.isArray(value.operands);
|
|
755
754
|
};
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
755
|
+
/**
|
|
756
|
+
* Type guard for Union or Intersection
|
|
757
|
+
* @param o - operator to check
|
|
758
|
+
* @category Filters
|
|
759
|
+
*/ const isIntersectionOrUnion = (o)=>o.operator === 'and' || o.operator === 'or';
|
|
760
|
+
const isOperandsType = (operation)=>{
|
|
761
|
+
return operation?.operands !== undefined;
|
|
759
762
|
};
|
|
760
|
-
const
|
|
761
|
-
return
|
|
763
|
+
const ifOperationWithField = (operation)=>{
|
|
764
|
+
return operation.field !== undefined;
|
|
762
765
|
};
|
|
763
|
-
const
|
|
764
|
-
return
|
|
766
|
+
const isNestedFilter = (operation)=>{
|
|
767
|
+
return operation.operator === 'nested';
|
|
765
768
|
};
|
|
766
|
-
const
|
|
767
|
-
|
|
769
|
+
const isIndexedFilterSetEmpty = (filters)=>Object.values(filters).every((filterSet)=>Object.keys(filterSet).length === 0);
|
|
770
|
+
const EmptyFilterSet = {
|
|
771
|
+
mode: 'and',
|
|
772
|
+
root: {}
|
|
768
773
|
};
|
|
769
774
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
*
|
|
773
|
-
* @param {string} apiUrl - The base URL to be used for preparing the download URL.
|
|
774
|
-
* @returns {URL} - The prepared download URL as a URL object.
|
|
775
|
-
*/ const prepareUrl$1 = (apiUrl)=>new URL(apiUrl + '/download');
|
|
776
|
-
|
|
777
|
-
var HttpMethod = /*#__PURE__*/ function(HttpMethod) {
|
|
778
|
-
HttpMethod["GET"] = "GET";
|
|
779
|
-
HttpMethod["POST"] = "POST";
|
|
780
|
-
HttpMethod["PUT"] = "PUT";
|
|
781
|
-
HttpMethod["DELETE"] = "DELETE";
|
|
782
|
-
return HttpMethod;
|
|
783
|
-
}({});
|
|
784
|
-
const DEFAULT_METHOD = "GET";
|
|
785
|
-
const CONTENT_TYPE_HEADER = 'Content-Type';
|
|
786
|
-
const CONTENT_TYPE_JSON = 'application/json';
|
|
787
|
-
const HTTPErrorMessages = {
|
|
788
|
-
// 4xx Client Errors
|
|
789
|
-
400: 'Bad Request',
|
|
790
|
-
401: 'Unauthorized',
|
|
791
|
-
402: 'Payment Required',
|
|
792
|
-
403: 'Forbidden',
|
|
793
|
-
404: 'Not Found',
|
|
794
|
-
405: 'Method Not Allowed',
|
|
795
|
-
406: 'Not Acceptable',
|
|
796
|
-
407: 'Proxy Authentication Required',
|
|
797
|
-
408: 'Request Timeout',
|
|
798
|
-
409: 'Conflict',
|
|
799
|
-
410: 'Gone',
|
|
800
|
-
411: 'Length Required',
|
|
801
|
-
412: 'Precondition Failed',
|
|
802
|
-
413: 'Payload Too Large',
|
|
803
|
-
414: 'URI Too Long',
|
|
804
|
-
415: 'Unsupported Media Type',
|
|
805
|
-
416: 'Range Not Satisfiable',
|
|
806
|
-
417: 'Expectation Failed',
|
|
807
|
-
418: "I'm a teapot",
|
|
808
|
-
421: 'Misdirected Request',
|
|
809
|
-
422: 'Unprocessable Entity',
|
|
810
|
-
423: 'Locked',
|
|
811
|
-
424: 'Failed Dependency',
|
|
812
|
-
425: 'Too Early',
|
|
813
|
-
426: 'Upgrade Required',
|
|
814
|
-
428: 'Precondition Required',
|
|
815
|
-
429: 'Too Many Requests',
|
|
816
|
-
431: 'Request Header Fields Too Large',
|
|
817
|
-
451: 'Unavailable For Legal Reasons',
|
|
818
|
-
// 5xx Server Errors
|
|
819
|
-
500: 'Internal Server Error',
|
|
820
|
-
501: 'Not Implemented',
|
|
821
|
-
502: 'Bad Gateway',
|
|
822
|
-
503: 'Service Unavailable',
|
|
823
|
-
504: 'Gateway Timeout',
|
|
824
|
-
505: 'HTTP Version Not Supported',
|
|
825
|
-
506: 'Variant Also Negotiates',
|
|
826
|
-
507: 'Insufficient Storage',
|
|
827
|
-
508: 'Loop Detected',
|
|
828
|
-
510: 'Not Extended',
|
|
829
|
-
511: 'Network Authentication Required'
|
|
830
|
-
};
|
|
831
|
-
/**
|
|
832
|
-
* Represents an error that occurs during an HTTP request.
|
|
833
|
-
* Extends the built-in `Error` class to provide additional information
|
|
834
|
-
* about the HTTP status code and optional response data.
|
|
835
|
-
*/ class HTTPError extends Error {
|
|
836
|
-
constructor(status, message, responseData){
|
|
837
|
-
super(message), this.status = status, this.responseData = responseData;
|
|
838
|
-
this.name = 'HTTPError';
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
const fetchFencePresignedURL = async ({ guid, method = 'GET', onAbort = ()=>null, signal = undefined })=>{
|
|
842
|
-
const csrfToken = selectCSRFToken(coreStore.getState());
|
|
843
|
-
const headers = new Headers();
|
|
844
|
-
headers.set('Content-Type', 'application/json');
|
|
845
|
-
if (process.env.NODE_ENV === 'development') {
|
|
846
|
-
// NOTE: This cookie can only be accessed from the client side
|
|
847
|
-
// in development mode. Otherwise, the cookie is set as httpOnly
|
|
848
|
-
const accessToken = getCookie('credentials_token');
|
|
849
|
-
if (accessToken) {
|
|
850
|
-
headers.set('Authorization', `Bearer ${accessToken}`);
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
if (csrfToken) headers.set('X-CSRF-Token', csrfToken);
|
|
854
|
-
const url = `${GEN3_FENCE_API}/data/download/${guid}`;
|
|
855
|
-
try {
|
|
856
|
-
const response = await fetch(url, {
|
|
857
|
-
method: method,
|
|
858
|
-
headers: headers,
|
|
859
|
-
...signal ? {
|
|
860
|
-
signal: signal
|
|
861
|
-
} : {}
|
|
862
|
-
});
|
|
863
|
-
// Check if the response is ok before proceeding
|
|
864
|
-
if (!response.ok) {
|
|
865
|
-
let errorMessage;
|
|
866
|
-
let errorData;
|
|
867
|
-
try {
|
|
868
|
-
// Attempt to parse error response as JSON
|
|
869
|
-
errorData = await response.json();
|
|
870
|
-
errorMessage = errorData.message || response.statusText;
|
|
871
|
-
} catch {
|
|
872
|
-
// If JSON parsing fails, use status text
|
|
873
|
-
errorMessage = response.statusText;
|
|
874
|
-
}
|
|
875
|
-
throw new HTTPError(response.status, errorMessage, errorData);
|
|
876
|
-
}
|
|
877
|
-
return (await response.json())['url'];
|
|
878
|
-
} catch (error) {
|
|
879
|
-
if (error instanceof Error) {
|
|
880
|
-
if (error.name === 'AbortError') {
|
|
881
|
-
onAbort?.();
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
throw error;
|
|
885
|
-
}
|
|
886
|
-
};
|
|
887
|
-
/**
|
|
888
|
-
* Retrieves a CSRF token from the server.
|
|
889
|
-
*
|
|
890
|
-
* This asynchronous function sends a GET request to the server's status endpoint
|
|
891
|
-
* to fetch the CSRF token in the response. The token is expected to be included
|
|
892
|
-
* in the JSON response under the `csrf_token` field.
|
|
893
|
-
*
|
|
894
|
-
* @returns {Promise<string | null>} A promise that resolves to the CSRF token as a string if successfully retrieved,
|
|
895
|
-
* or null if the token is not present in the response.
|
|
896
|
-
* @throws {HTTPError} Throws an HTTPError if the server response is not successful.
|
|
897
|
-
*/ const getCSRFToken = async ()=>{
|
|
898
|
-
const requestHeaders = new Headers({
|
|
899
|
-
[CONTENT_TYPE_HEADER]: CONTENT_TYPE_JSON
|
|
900
|
-
});
|
|
901
|
-
const response = await fetch(`${GEN3_API}/_status`, {
|
|
902
|
-
headers: requestHeaders
|
|
903
|
-
});
|
|
904
|
-
if (!response.ok) {
|
|
905
|
-
throw new HTTPError(response.status, response.statusText);
|
|
906
|
-
}
|
|
907
|
-
const { csrf: csrfToken } = await response.json();
|
|
908
|
-
return csrfToken || null;
|
|
909
|
-
};
|
|
910
|
-
/**
|
|
911
|
-
* Fetches JSON data from a specified URL using the Fetch API.
|
|
912
|
-
*
|
|
913
|
-
* @param {string} url - The URL to fetch the JSON data from.
|
|
914
|
-
* @param {boolean} [requiresCSRF=false] - Indicates whether a CSRF token is required for the request.
|
|
915
|
-
* If true, the CSRF token will be added to the request headers.
|
|
916
|
-
* @param {string} [method=DEFAULT_METHOD] - The HTTP method to use for the request (e.g., 'GET', 'POST').
|
|
917
|
-
* @param {unknown} [body=undefined] - The request body to send, applicable when using methods like 'POST'.
|
|
918
|
-
*
|
|
919
|
-
* @param signal - Abort fetch
|
|
920
|
-
* @returns {Promise<T>} A promise that resolves to the parsed JSON data from the response.
|
|
921
|
-
*
|
|
922
|
-
* @throws {HTTPError} Throws an error if the HTTP response status indicates a failure.
|
|
923
|
-
*/ const fetchJSONDataFromURL = async (url, requiresCSRF = false, method = DEFAULT_METHOD, body = undefined, signal)=>{
|
|
924
|
-
const requestHeaders = new Headers({
|
|
925
|
-
[CONTENT_TYPE_HEADER]: CONTENT_TYPE_JSON
|
|
926
|
-
});
|
|
927
|
-
if (requiresCSRF) {
|
|
928
|
-
const csrfToken = await getCSRFToken();
|
|
929
|
-
if (csrfToken) {
|
|
930
|
-
requestHeaders.set('X-CSRF-Token', csrfToken);
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
if (process.env.NODE_ENV === 'development') {
|
|
934
|
-
const accessToken = getCookie('credentials_token');
|
|
935
|
-
if (accessToken) {
|
|
936
|
-
requestHeaders.set('Authorization', `Bearer ${accessToken}`);
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
const response = await fetch(url, {
|
|
940
|
-
method,
|
|
941
|
-
headers: requestHeaders,
|
|
942
|
-
body: [
|
|
943
|
-
'POST',
|
|
944
|
-
'PUT',
|
|
945
|
-
'PATCH'
|
|
946
|
-
].includes(method.toUpperCase()) ? body : undefined,
|
|
947
|
-
...signal ? {
|
|
948
|
-
signal: signal
|
|
949
|
-
} : {}
|
|
950
|
-
});
|
|
951
|
-
if (!response.ok) {
|
|
952
|
-
const errorBody = await response.json();
|
|
953
|
-
throw new HTTPError(response.status, response.statusText, errorBody);
|
|
954
|
-
}
|
|
955
|
-
if (response.status === 204) {
|
|
956
|
-
// no content so returns null
|
|
957
|
-
return null;
|
|
958
|
-
}
|
|
959
|
-
return await response.json();
|
|
775
|
+
const isOperationWithField = (operation)=>{
|
|
776
|
+
return operation?.field !== undefined;
|
|
960
777
|
};
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
path: jsonPath,
|
|
967
|
-
json: obj
|
|
968
|
-
});
|
|
969
|
-
return extractedValues.length > 0 ? extractedValues[0] : undefined;
|
|
970
|
-
};
|
|
971
|
-
for(const key in jsonPathMappings){
|
|
972
|
-
if (key in Object.keys(jsonPathMappings)) {
|
|
973
|
-
// Extract value from an object and store it in the result.
|
|
974
|
-
result[key] = extractObjectValue(jsonPathMappings[key], obj);
|
|
975
|
-
}
|
|
778
|
+
const isOperatorWithFieldAndArrayOfOperands = (operation)=>{
|
|
779
|
+
if (typeof operation === 'object' && operation !== null && 'operands' in operation && Array.isArray(operation.operands) && 'field' in operation && typeof operation.field === 'string' // Assuming `field` should be a string
|
|
780
|
+
) {
|
|
781
|
+
const { operator } = operation;
|
|
782
|
+
return operator === 'in' || operator === 'includes' || operator === 'excludes' || operator === 'excludeifany';
|
|
976
783
|
}
|
|
977
|
-
return
|
|
978
|
-
};
|
|
979
|
-
const ExtractValueFromObject = (obj, key, valueIfNotFound)=>{
|
|
980
|
-
return obj?.[key] ?? valueIfNotFound;
|
|
981
|
-
};
|
|
982
|
-
|
|
983
|
-
// type guard functions
|
|
984
|
-
const isHistogramRangeData = (key)=>{
|
|
985
|
-
return Array.isArray(key) && key.length === 2 && key.every((item)=>typeof item === 'number');
|
|
986
|
-
};
|
|
987
|
-
const isJSONObject = (data)=>{
|
|
988
|
-
return typeof data === 'object' && data !== null && !Array.isArray(data);
|
|
784
|
+
return false;
|
|
989
785
|
};
|
|
990
|
-
const
|
|
991
|
-
|
|
786
|
+
const extractFilterValue = (op)=>{
|
|
787
|
+
const valueExtractorHandler = new ValueExtractorHandler();
|
|
788
|
+
return handleOperation(valueExtractorHandler, op);
|
|
992
789
|
};
|
|
993
|
-
const
|
|
994
|
-
|
|
790
|
+
const extractEnumFilterValue = (op)=>{
|
|
791
|
+
const enumValueExtractorHandler = new EnumValueExtractorHandler();
|
|
792
|
+
const results = handleOperation(enumValueExtractorHandler, op);
|
|
793
|
+
return results ?? [];
|
|
995
794
|
};
|
|
996
|
-
const
|
|
997
|
-
|
|
998
|
-
return isValidObject(data) && 'key' in data && 'count' in data;
|
|
795
|
+
const assertNever = (x)=>{
|
|
796
|
+
throw Error(`Exhaustive comparison did not handle: ${x}`);
|
|
999
797
|
};
|
|
1000
|
-
const
|
|
1001
|
-
|
|
1002
|
-
|
|
798
|
+
const handleOperation = (handler, op)=>{
|
|
799
|
+
switch(op.operator){
|
|
800
|
+
case '=':
|
|
801
|
+
return handler.handleEquals(op);
|
|
802
|
+
case '!=':
|
|
803
|
+
return handler.handleNotEquals(op);
|
|
804
|
+
case '<':
|
|
805
|
+
return handler.handleLessThan(op);
|
|
806
|
+
case '<=':
|
|
807
|
+
return handler.handleLessThanOrEquals(op);
|
|
808
|
+
case '>':
|
|
809
|
+
return handler.handleGreaterThan(op);
|
|
810
|
+
case '>=':
|
|
811
|
+
return handler.handleGreaterThanOrEquals(op);
|
|
812
|
+
case 'and':
|
|
813
|
+
return handler.handleIntersection(op);
|
|
814
|
+
case 'or':
|
|
815
|
+
return handler.handleUnion(op);
|
|
816
|
+
case 'nested':
|
|
817
|
+
return handler.handleNestedFilter(op);
|
|
818
|
+
case 'in':
|
|
819
|
+
case 'includes':
|
|
820
|
+
return handler.handleIncludes(op);
|
|
821
|
+
case 'excludeifany':
|
|
822
|
+
return handler.handleExcludeIfAny(op);
|
|
823
|
+
case 'excludes':
|
|
824
|
+
return handler.handleExcludes(op);
|
|
825
|
+
case 'exists':
|
|
826
|
+
return handler.handleExists(op);
|
|
827
|
+
case 'missing':
|
|
828
|
+
return handler.handleMissing(op);
|
|
829
|
+
default:
|
|
830
|
+
return assertNever(op);
|
|
1003
831
|
}
|
|
1004
|
-
return input.histogram.every(isHistogramData);
|
|
1005
|
-
};
|
|
1006
|
-
const isHistogramDataCollection = (obj)=>{
|
|
1007
|
-
return isValidObject(obj) && 'histogram' in obj && isHistogramData(obj.histogram);
|
|
1008
832
|
};
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
833
|
+
/**
|
|
834
|
+
* Return true if a FilterSet's root value is an empty object
|
|
835
|
+
* @param fs - FilterSet to test
|
|
836
|
+
*/ const isFilterEmpty = (fs)=>isEqual({}, fs);
|
|
837
|
+
/**
|
|
838
|
+
* Type guard to check if an object is a GQLIntersection
|
|
839
|
+
* @param value - The value to check
|
|
840
|
+
* @returns True if the value is a GQLIntersection
|
|
841
|
+
*/ const isGQLIntersection = (value)=>{
|
|
842
|
+
return typeof value === 'object' && value !== null && 'and' in value && Array.isArray(value.and);
|
|
1018
843
|
};
|
|
1019
|
-
|
|
1020
|
-
|
|
844
|
+
/**
|
|
845
|
+
* Type guard to check if an object is a GQLIntersection
|
|
846
|
+
* @param value - The value to check
|
|
847
|
+
* @returns True if the value is a GQLIntersection
|
|
848
|
+
*/ const isGQLUnion = (value)=>{
|
|
849
|
+
return typeof value === 'object' && value !== null && 'or' in value && Array.isArray(value.or);
|
|
1021
850
|
};
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
851
|
+
class ToGqlHandler {
|
|
852
|
+
constructor(){
|
|
853
|
+
this.handleEquals = (op)=>({
|
|
854
|
+
'=': {
|
|
855
|
+
[op.field]: op.operand
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
this.handleNotEquals = (op)=>({
|
|
859
|
+
'!=': {
|
|
860
|
+
[op.field]: op.operand
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
this.handleLessThan = (op)=>({
|
|
864
|
+
'<': {
|
|
865
|
+
[op.field]: op.operand
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
this.handleLessThanOrEquals = (op)=>({
|
|
869
|
+
'<=': {
|
|
870
|
+
[op.field]: op.operand
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
this.handleGreaterThan = (op)=>({
|
|
874
|
+
'>': {
|
|
875
|
+
[op.field]: op.operand
|
|
876
|
+
}
|
|
877
|
+
});
|
|
878
|
+
this.handleGreaterThanOrEquals = (op)=>({
|
|
879
|
+
'>=': {
|
|
880
|
+
[op.field]: op.operand
|
|
881
|
+
}
|
|
882
|
+
});
|
|
883
|
+
this.handleIncludes = (op)=>({
|
|
884
|
+
in: {
|
|
885
|
+
[op.field]: op.operands
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
this.handleExcludes = (op)=>({
|
|
889
|
+
exclude: {
|
|
890
|
+
[op.field]: op.operands
|
|
891
|
+
}
|
|
892
|
+
});
|
|
893
|
+
this.handleExcludeIfAny = (op)=>({
|
|
894
|
+
excludeifany: {
|
|
895
|
+
[op.field]: op.operands
|
|
896
|
+
}
|
|
897
|
+
});
|
|
898
|
+
this.handleIntersection = (op)=>({
|
|
899
|
+
and: op.operands.map((x)=>convertFilterToGqlFilter(x))
|
|
900
|
+
});
|
|
901
|
+
this.handleUnion = (op)=>({
|
|
902
|
+
or: op.operands.map((x)=>convertFilterToGqlFilter(x))
|
|
903
|
+
});
|
|
904
|
+
this.handleMissing = (op)=>({
|
|
905
|
+
is: {
|
|
906
|
+
[op.field]: 'MISSING'
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
this.handleExists = (op)=>({
|
|
910
|
+
not: {
|
|
911
|
+
[op.field]: op?.operand ?? null
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
this.handleNestedFilter = (op)=>{
|
|
915
|
+
const child = convertFilterToGqlFilter(op.operand);
|
|
916
|
+
return {
|
|
917
|
+
nested: {
|
|
918
|
+
path: op.path,
|
|
919
|
+
...child
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
};
|
|
1046
923
|
}
|
|
1047
|
-
return true;
|
|
1048
|
-
};
|
|
1049
|
-
const isStatsValuesArray = (data)=>{
|
|
1050
|
-
return Array.isArray(data) && data.every(isStatsValue);
|
|
1051
|
-
};
|
|
1052
|
-
const isHistogramDataAArray = (data)=>{
|
|
1053
|
-
return Array.isArray(data) && data.every(isHistogramData);
|
|
1054
|
-
};
|
|
1055
|
-
const isHistogramDataArrayAnEnum = (data)=>{
|
|
1056
|
-
return Array.isArray(data) && data.every(isHistogramDataAnEnum);
|
|
1057
|
-
};
|
|
1058
|
-
const isHistogramDataArrayARange = (data)=>{
|
|
1059
|
-
return Array.isArray(data) && data.every((item)=>isHistogramRangeData(item.key));
|
|
1060
|
-
};
|
|
1061
|
-
/**
|
|
1062
|
-
* Type predicate to narrow an unknown error to `FetchBaseQueryError`
|
|
1063
|
-
*/ function isFetchBaseQueryError(error) {
|
|
1064
|
-
return typeof error === 'object' && error != null && 'status' in error;
|
|
1065
|
-
}
|
|
1066
|
-
/**
|
|
1067
|
-
* Type predicate to narrow an unknown error to an object with a string 'message' property
|
|
1068
|
-
*/ function isErrorWithMessage(error) {
|
|
1069
|
-
return typeof error === 'object' && error != null && 'message' in error && typeof error.message === 'string';
|
|
1070
|
-
}
|
|
1071
|
-
function isHttpStatusError(error) {
|
|
1072
|
-
return typeof error === 'object' && error != null && 'status' in error && typeof error.status === 'number';
|
|
1073
|
-
}
|
|
1074
|
-
/**
|
|
1075
|
-
* Type predicate to narrow an unknown error to an object with a string 'message' property
|
|
1076
|
-
*/ function isFetchParseError(error) {
|
|
1077
|
-
return typeof error === 'object' && error != null && 'originalStatus' in error && 'status' in error && error['status'] === 'PARSING_ERROR';
|
|
1078
|
-
}
|
|
1079
|
-
/**
|
|
1080
|
-
* Type predicate to narrow an unknown error to a `SerializedError`
|
|
1081
|
-
*/ function isSerializedError(error) {
|
|
1082
|
-
return typeof error === 'object' && error != null && !('status' in error) && ('message' in error || 'name' in error || 'code' in error || 'stack' in error);
|
|
1083
924
|
}
|
|
1084
|
-
|
|
1085
|
-
const
|
|
1086
|
-
|
|
1087
|
-
400: 'Sorry, there was a problem with your request. Please check the information you provided and try again.',
|
|
1088
|
-
401: 'Please sign in to access this content. If you need to create an account, please contact support.',
|
|
1089
|
-
402: 'This feature requires payment to continue. Please update your subscription.',
|
|
1090
|
-
403: "You don't have permission to access this. If you think this is a mistake, please contact support.",
|
|
1091
|
-
404: "We couldn't find what you're looking for. The link might be broken or the content may have been moved.",
|
|
1092
|
-
405: 'This action is not supported. Please try a different approach.',
|
|
1093
|
-
406: "Your browser sent a request that this server couldn't understand. Try updating your browser.",
|
|
1094
|
-
407: 'Please authenticate with the network proxy first.',
|
|
1095
|
-
408: 'The request took too long to complete. Please check your internet connection and try again.',
|
|
1096
|
-
409: 'There was a conflict with your request. Someone might have modified the same content.',
|
|
1097
|
-
410: 'This content has been permanently removed and is no longer available.',
|
|
1098
|
-
411: 'There was a technical problem with your request. Please try again.',
|
|
1099
|
-
412: "Some conditions for this request weren't met. Please try again.",
|
|
1100
|
-
413: "The file you're trying to upload is too large. Please choose a smaller file.",
|
|
1101
|
-
414: 'The web address is too long. Please try a shorter URL.',
|
|
1102
|
-
415: 'This type of file is not supported. Please try a different format.',
|
|
1103
|
-
416: 'The requested content range is not available.',
|
|
1104
|
-
417: "Your request couldn't be completed as expected. Please try again.",
|
|
1105
|
-
418: 'Unusual error occurred. Please try again or contact support if the problem persists.',
|
|
1106
|
-
421: 'Your request was sent to the wrong server. Please try again.',
|
|
1107
|
-
422: "We couldn't process your request. Please check the information and try again.",
|
|
1108
|
-
423: 'This resource is temporarily locked. Please try again in a few moments.',
|
|
1109
|
-
424: 'The request failed because it depends on another request that failed.',
|
|
1110
|
-
425: 'Your request was processed too early. Please try again in a moment.',
|
|
1111
|
-
426: 'You need to upgrade your browser or app to continue.',
|
|
1112
|
-
428: "Some required conditions weren't met. Please check all requirements and try again.",
|
|
1113
|
-
429: "You've made too many requests. Please wait a while before trying again.",
|
|
1114
|
-
431: 'Request failed because the headers are too large. Please simplify your request.',
|
|
1115
|
-
451: 'This content is not available for legal reasons.',
|
|
1116
|
-
// 5xx Server Errors
|
|
1117
|
-
500: 'Something went wrong with the service. Please try again later.',
|
|
1118
|
-
501: "This feature isn't available yet. Check back later.",
|
|
1119
|
-
502: "We're having trouble connecting to our servers. Please try again in a few minutes.",
|
|
1120
|
-
503: "Our service is temporarily unavailable. We're working to restore it as quickly as possible.",
|
|
1121
|
-
504: 'The server took too long to respond. Please try again later.',
|
|
1122
|
-
505: "Your browser's HTTP version isn't supported. Please try updating your browser.",
|
|
1123
|
-
506: 'We encountered a configuration error.',
|
|
1124
|
-
507: "We're out of storage space.",
|
|
1125
|
-
508: 'We detected an infinite loop in our servers.',
|
|
1126
|
-
510: 'This server requires additional extensions to process your request.',
|
|
1127
|
-
511: 'Please sign in to the network before continuing.'
|
|
925
|
+
const convertFilterToGqlFilter = (filter)=>{
|
|
926
|
+
const handler = new ToGqlHandler();
|
|
927
|
+
return handleOperation(handler, filter);
|
|
1128
928
|
};
|
|
1129
|
-
|
|
1130
|
-
const
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
if (
|
|
1144
|
-
return
|
|
929
|
+
const convertFilterSetToGqlFilter = (fs, toplevelOp = 'and')=>{
|
|
930
|
+
const fsKeys = Object.keys(fs.root);
|
|
931
|
+
// if no keys return undefined
|
|
932
|
+
if (fsKeys.length === 0) return {
|
|
933
|
+
and: []
|
|
934
|
+
};
|
|
935
|
+
return toplevelOp === 'and' ? {
|
|
936
|
+
and: fsKeys.map((key)=>convertFilterToGqlFilter(fs.root[key]))
|
|
937
|
+
} : {
|
|
938
|
+
or: fsKeys.map((key)=>convertFilterToGqlFilter(fs.root[key]))
|
|
939
|
+
};
|
|
940
|
+
};
|
|
941
|
+
const handleGqlOperation = (handler, op)=>{
|
|
942
|
+
const operationKeys = Object.keys(op);
|
|
943
|
+
if (operationKeys.includes('=')) {
|
|
944
|
+
return handler.handleEquals(op);
|
|
1145
945
|
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
/** Best-effort message extraction from an HTTP error body */ function extractMessage(data) {
|
|
1149
|
-
if (typeof data === 'string') return data;
|
|
1150
|
-
if (typeof data === 'object' && data != null) {
|
|
1151
|
-
const obj = data;
|
|
1152
|
-
// common API error shapes: { message }, { error }, { detail } (FastAPI)
|
|
1153
|
-
for (const key of [
|
|
1154
|
-
'message',
|
|
1155
|
-
'error',
|
|
1156
|
-
'detail'
|
|
1157
|
-
]){
|
|
1158
|
-
const val = obj[key];
|
|
1159
|
-
if (typeof val === 'string') return val;
|
|
1160
|
-
}
|
|
946
|
+
if (operationKeys.includes('!=')) {
|
|
947
|
+
return handler.handleNotEquals(op);
|
|
1161
948
|
}
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
function normalizeRtkError(error) {
|
|
1165
|
-
if (!error) {
|
|
1166
|
-
return {
|
|
1167
|
-
type: 'UNKNOWN_ERROR',
|
|
1168
|
-
message: 'Unknown error'
|
|
1169
|
-
};
|
|
949
|
+
if (operationKeys.includes('<')) {
|
|
950
|
+
return handler.handleLessThan(op);
|
|
1170
951
|
}
|
|
1171
|
-
if (
|
|
1172
|
-
|
|
1173
|
-
if (typeof error.status === 'number') {
|
|
1174
|
-
const extractedMessage = extractMessage(error.data);
|
|
1175
|
-
return {
|
|
1176
|
-
type: 'HTTP_ERROR',
|
|
1177
|
-
status: error.status,
|
|
1178
|
-
message: sanitizeErrorMessage(extractedMessage) !== DEFAULT_API_ERROR_MESSAGE ? sanitizeErrorMessage(extractedMessage) : HTTPUserFriendlyErrorMessages[error.status] ?? `Request failed with status ${error.status}`,
|
|
1179
|
-
data: error.data
|
|
1180
|
-
};
|
|
1181
|
-
}
|
|
1182
|
-
switch(error.status){
|
|
1183
|
-
case 'FETCH_ERROR':
|
|
1184
|
-
return {
|
|
1185
|
-
type: 'FETCH_ERROR',
|
|
1186
|
-
message: error.error
|
|
1187
|
-
};
|
|
1188
|
-
case 'PARSING_ERROR':
|
|
1189
|
-
return {
|
|
1190
|
-
type: 'PARSING_ERROR',
|
|
1191
|
-
status: error.originalStatus,
|
|
1192
|
-
message: HTTPUserFriendlyErrorMessages[error.originalStatus] ?? `Request failed with status ${error.originalStatus}`,
|
|
1193
|
-
data: error.data
|
|
1194
|
-
};
|
|
1195
|
-
case 'TIMEOUT_ERROR':
|
|
1196
|
-
return {
|
|
1197
|
-
type: 'TIMEOUT_ERROR',
|
|
1198
|
-
message: sanitizeErrorMessage(error.error)
|
|
1199
|
-
};
|
|
1200
|
-
case 'CUSTOM_ERROR':
|
|
1201
|
-
return {
|
|
1202
|
-
type: 'CUSTOM_ERROR',
|
|
1203
|
-
message: sanitizeErrorMessage(error.error),
|
|
1204
|
-
data: error.data
|
|
1205
|
-
};
|
|
1206
|
-
}
|
|
952
|
+
if (operationKeys.includes('<=')) {
|
|
953
|
+
return handler.handleLessThanOrEquals(op);
|
|
1207
954
|
}
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
return {
|
|
1211
|
-
type: 'SERIALIZED_ERROR',
|
|
1212
|
-
message: sanitizeErrorMessage(error.message) || 'An error occurred'
|
|
1213
|
-
};
|
|
955
|
+
if (operationKeys.includes('>')) {
|
|
956
|
+
return handler.handleGreaterThan(op);
|
|
1214
957
|
}
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
message: 'Unknown error'
|
|
1218
|
-
};
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
const DAYS_IN_YEAR = 365.25;
|
|
1222
|
-
/**
|
|
1223
|
-
* Converts HistogramData to HistogramDataAsStringKey by ensuring the key is a string.
|
|
1224
|
-
* If the key is already a string, it's used as is.
|
|
1225
|
-
* If the key is a number tuple [min, max], it's converted to a string in the format "min-max".
|
|
1226
|
-
*
|
|
1227
|
-
* @param data The HistogramData object to convert
|
|
1228
|
-
* @returns HistogramDataAsStringKey with the converted key
|
|
1229
|
-
*/ const convertToHistogramDataAsStringKey = (data)=>{
|
|
1230
|
-
return {
|
|
1231
|
-
key: typeof data.key === 'string' ? data.key : `${data.key[0]}-${data.key[1]}`,
|
|
1232
|
-
count: data.count
|
|
1233
|
-
};
|
|
1234
|
-
};
|
|
1235
|
-
const calculatePercentageAsNumber = (count, total)=>count ? count / total * 100 : 0;
|
|
1236
|
-
const calculatePercentageAsString = (count, total)=>`${(count / total * 100).toFixed(2)}%`;
|
|
1237
|
-
const capitalize$1 = (original)=>{
|
|
1238
|
-
const customCapitalizations = {
|
|
1239
|
-
id: 'ID',
|
|
1240
|
-
uuid: 'UUID',
|
|
1241
|
-
dna: 'DNA',
|
|
1242
|
-
dbsnp: 'dbSNP',
|
|
1243
|
-
cosmic: 'COSMIC',
|
|
1244
|
-
civic: 'CIViC',
|
|
1245
|
-
dbgap: 'dbGaP',
|
|
1246
|
-
ecog: 'ECOG',
|
|
1247
|
-
bmi: 'BMI',
|
|
1248
|
-
gdc: 'GDC',
|
|
1249
|
-
cnv: 'CNV',
|
|
1250
|
-
ssm: 'SSM',
|
|
1251
|
-
aa: 'AA'
|
|
1252
|
-
};
|
|
1253
|
-
return original.split(' ').map((word)=>customCapitalizations[word.toLowerCase()] || `${word.charAt(0).toUpperCase()}${word.slice(1)}`).join(' ');
|
|
1254
|
-
};
|
|
1255
|
-
const humanify = ({ term = '', capitalize: cap = true, facetTerm = false })=>{
|
|
1256
|
-
let original;
|
|
1257
|
-
let humanified;
|
|
1258
|
-
if (facetTerm) {
|
|
1259
|
-
// Splits on capital letters followed by lowercase letters to find
|
|
1260
|
-
// words squished together in a string.
|
|
1261
|
-
original = term?.split(/(?=[A-Z][a-z])/).join(' ');
|
|
1262
|
-
humanified = term?.replace(/\./g, ' ').replace(/_/g, ' ').trim();
|
|
1263
|
-
} else {
|
|
1264
|
-
const split = (original || term)?.split('.');
|
|
1265
|
-
humanified = split[split.length - 1]?.replace(/_/g, ' ').trim();
|
|
1266
|
-
// Special case 'name' to include any parent nested for sake of
|
|
1267
|
-
// specificity in the UI
|
|
1268
|
-
if (humanified === 'name' && split?.length > 1) {
|
|
1269
|
-
humanified = `${split[split?.length - 2]} ${humanified}`;
|
|
1270
|
-
}
|
|
958
|
+
if (operationKeys.includes('>=')) {
|
|
959
|
+
return handler.handleGreaterThanOrEquals(op);
|
|
1271
960
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
/*https://github.com/NCI-GDC/portal-ui/blob/develop/src/packages/%40ncigdc/utils/ageDisplay.js*/ /**
|
|
1275
|
-
* Converts age in days into a human-readable format.
|
|
1276
|
-
*
|
|
1277
|
-
* @param ageInDays - The age in days.
|
|
1278
|
-
* @param yearsOnly - If true, only display years.
|
|
1279
|
-
* @defaultValue false
|
|
1280
|
-
* @param defaultValue - The default value to return if ageInDays is falsy.
|
|
1281
|
-
* @defaultValue "--"
|
|
1282
|
-
* @returns The formatted age string.
|
|
1283
|
-
*/ const ageDisplay = (ageInDays, yearsOnly = false, defaultValue = '--')=>{
|
|
1284
|
-
if (ageInDays !== 0 && !ageInDays) {
|
|
1285
|
-
return defaultValue;
|
|
961
|
+
if (operationKeys.includes('in')) {
|
|
962
|
+
return handler.handleIncludes(op);
|
|
1286
963
|
}
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
0
|
|
1290
|
-
] : [
|
|
1291
|
-
years,
|
|
1292
|
-
days
|
|
1293
|
-
];
|
|
1294
|
-
const ABS_AGE_DAYS = Math.abs(ageInDays);
|
|
1295
|
-
const [years, remainingDays] = calculateYearsAndDays(Math.floor(ABS_AGE_DAYS / DAYS_IN_YEAR), Math.ceil(ABS_AGE_DAYS % DAYS_IN_YEAR));
|
|
1296
|
-
const formattedYears = years === 0 ? '' : `${years} ${years === 1 ? 'year' : 'years'}`;
|
|
1297
|
-
const formattedDays = !yearsOnly && remainingDays > 0 ? `${remainingDays} ${remainingDays === 1 ? 'day' : 'days'}` : years === 0 && remainingDays === 0 ? '0 days' : '';
|
|
1298
|
-
const ageString = [
|
|
1299
|
-
formattedYears,
|
|
1300
|
-
formattedDays
|
|
1301
|
-
].filter(Boolean).join(' ');
|
|
1302
|
-
return ageInDays >= 0 ? ageString : `-${ageString}`;
|
|
1303
|
-
};
|
|
1304
|
-
/**
|
|
1305
|
-
* Given an object of JSON, stringify it into a string.
|
|
1306
|
-
* @param obj - the object to stringify
|
|
1307
|
-
* @param defaults - the default value to return if the object is undefined
|
|
1308
|
-
* @category Utility
|
|
1309
|
-
*/ const stringifyJSONParam = (obj, defaults = '{}')=>obj ? JSON.stringify(obj) : defaults;
|
|
1310
|
-
|
|
1311
|
-
const queryWTSFederatedLoginStatus = async (signal)=>{
|
|
1312
|
-
try {
|
|
1313
|
-
const results = await fetchJSONDataFromURL(`${GEN3_WTS_API}/external_oidc/`, false, HttpMethod.GET, undefined, signal);
|
|
1314
|
-
// Handle both null response and response without providers
|
|
1315
|
-
if (!results || !results.providers) {
|
|
1316
|
-
return {
|
|
1317
|
-
providers: []
|
|
1318
|
-
};
|
|
1319
|
-
}
|
|
1320
|
-
return results;
|
|
1321
|
-
} catch (error) {
|
|
1322
|
-
return {
|
|
1323
|
-
providers: [],
|
|
1324
|
-
error: error instanceof Error ? error : new Error('Unknown error')
|
|
1325
|
-
};
|
|
964
|
+
if (operationKeys.includes('exclude')) {
|
|
965
|
+
return handler.handleExcludes(op);
|
|
1326
966
|
}
|
|
967
|
+
if (operationKeys.includes('excludeifany')) {
|
|
968
|
+
return handler.handleExcludeIfAny(op);
|
|
969
|
+
}
|
|
970
|
+
if (operationKeys.includes('and')) {
|
|
971
|
+
return handler.handleIntersection(op);
|
|
972
|
+
}
|
|
973
|
+
if (operationKeys.includes('or')) {
|
|
974
|
+
return handler.handleUnion(op);
|
|
975
|
+
}
|
|
976
|
+
if (operationKeys.includes('nested')) {
|
|
977
|
+
return handler.handleNestedFilter(op);
|
|
978
|
+
}
|
|
979
|
+
if (operationKeys.includes('is')) {
|
|
980
|
+
return handler.handleExists(op);
|
|
981
|
+
}
|
|
982
|
+
if (operationKeys.includes('not')) {
|
|
983
|
+
return handler.handleMissing(op);
|
|
984
|
+
}
|
|
985
|
+
return assertNever(op);
|
|
1327
986
|
};
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
* Output is a array of GUID prefixes
|
|
1332
|
-
* @param selectedFiles
|
|
1333
|
-
*/ const gatherGUIDPrefixes = (selectedFiles)=>{
|
|
1334
|
-
return selectedFiles.reduce((acc, selectedFile)=>{
|
|
1335
|
-
if (selectedFile.object_id) {
|
|
1336
|
-
const guidDomainPrefix = (selectedFile.object_id.match(GUID_PREFIX_PATTERN) || []).shift();
|
|
1337
|
-
if (guidDomainPrefix) {
|
|
1338
|
-
if (!acc.includes(guidDomainPrefix)) {
|
|
1339
|
-
acc.push(guidDomainPrefix);
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
|
-
return acc;
|
|
1344
|
-
}, []);
|
|
1345
|
-
};
|
|
1346
|
-
/**
|
|
1347
|
-
* Resolves DRS object ID from a list of guids using dataguids.org or other DRS resolver
|
|
1348
|
-
* @param guidPrefixes
|
|
1349
|
-
*/ const resolveDRSPrefixes = async (guidPrefixes)=>{
|
|
1350
|
-
return await resolveDRSObjectId(guidPrefixes);
|
|
987
|
+
const convertGqlFilterToFilter = (gqlFilter)=>{
|
|
988
|
+
const handler = new ToOperationHandler();
|
|
989
|
+
return handleGqlOperation(handler, gqlFilter);
|
|
1351
990
|
};
|
|
1352
991
|
/**
|
|
1353
|
-
*
|
|
1354
|
-
*
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
externalHosts: {},
|
|
1365
|
-
resolvedFiles: [],
|
|
1366
|
-
unresolvedFiles: []
|
|
1367
|
-
};
|
|
1368
|
-
}
|
|
1369
|
-
const guidPrefixes = gatherGUIDPrefixes(selectedFiles);
|
|
1370
|
-
const resolvedDRSPrefixes = await resolveDRSPrefixes(guidPrefixes);
|
|
1371
|
-
const resolvedIds = [];
|
|
1372
|
-
const unresolvedIds = [];
|
|
1373
|
-
selectedFiles.forEach((selectedFile)=>{
|
|
1374
|
-
if (selectedFile.object_id) {
|
|
1375
|
-
const guidDomainPrefix = (selectedFile.object_id.match(GUID_PREFIX_PATTERN) || []).shift();
|
|
1376
|
-
if (guidDomainPrefix && resolvedDRSPrefixes[guidDomainPrefix]) {
|
|
1377
|
-
resolvedIds.push({
|
|
1378
|
-
...selectedFile,
|
|
1379
|
-
drsPrefix: guidDomainPrefix,
|
|
1380
|
-
url: resolvedDRSPrefixes[guidDomainPrefix]
|
|
1381
|
-
});
|
|
1382
|
-
}
|
|
1383
|
-
} else {
|
|
1384
|
-
unresolvedIds.push(selectedFile);
|
|
1385
|
-
}
|
|
1386
|
-
});
|
|
1387
|
-
return {
|
|
1388
|
-
externalHosts: resolvedDRSPrefixes,
|
|
1389
|
-
resolvedFiles: resolvedIds,
|
|
1390
|
-
unresolvedFiles: unresolvedIds
|
|
1391
|
-
};
|
|
1392
|
-
};
|
|
1393
|
-
const fetchExternalLogins = async (externalProviders, selectedFiles)=>{
|
|
1394
|
-
const providers = externalProviders ?? [];
|
|
1395
|
-
// find all the providers that do not have tokens
|
|
1396
|
-
const unauthenticatedProviders = providers.filter((provider)=>!provider.refresh_token_expiration);
|
|
1397
|
-
try {
|
|
1398
|
-
const guidResolutions = await resolveGUIDsInSelectedFiles(selectedFiles);
|
|
1399
|
-
const providersToAuthenticate = unauthenticatedProviders.filter((unauthenticatedProvider)=>Object.values(guidResolutions.externalHosts).includes(new URL(unauthenticatedProvider.base_url).hostname));
|
|
1400
|
-
const missingProviders = providersToAuthenticate.filter((provider)=>!Object.values(guidResolutions.externalHosts).includes(new URL(provider.base_url).hostname));
|
|
1401
|
-
return {
|
|
1402
|
-
providersToAuthenticate: providersToAuthenticate,
|
|
1403
|
-
missingProviders: missingProviders
|
|
992
|
+
* Convert GQL to Filterset
|
|
993
|
+
* Note assumes all GqlOperators have one field: value
|
|
994
|
+
*/ class ToOperationHandler {
|
|
995
|
+
constructor(){
|
|
996
|
+
this.handleEquals = (op)=>{
|
|
997
|
+
const [field, value] = Object.entries(op['='])[0];
|
|
998
|
+
return {
|
|
999
|
+
operator: '=',
|
|
1000
|
+
field: field,
|
|
1001
|
+
operand: value
|
|
1002
|
+
};
|
|
1404
1003
|
};
|
|
1405
|
-
|
|
1406
|
-
|
|
1004
|
+
this.handleNotEquals = (op)=>{
|
|
1005
|
+
const [field, value] = Object.entries(op['!='])[0];
|
|
1407
1006
|
return {
|
|
1408
|
-
|
|
1007
|
+
operator: '!=',
|
|
1008
|
+
field: field,
|
|
1009
|
+
operand: value
|
|
1409
1010
|
};
|
|
1410
|
-
}
|
|
1411
|
-
return {
|
|
1412
|
-
error: new Error('Unknown error')
|
|
1413
1011
|
};
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
const fetchData = async ()=>{
|
|
1422
|
-
if (wtsError || !wtsResults) {
|
|
1423
|
-
if (wtsError instanceof Error) setResult({
|
|
1424
|
-
error: wtsError
|
|
1425
|
-
});
|
|
1426
|
-
else setResult({
|
|
1427
|
-
error: new Error('Unknown error')
|
|
1428
|
-
});
|
|
1429
|
-
return;
|
|
1430
|
-
}
|
|
1431
|
-
const results = await fetchExternalLogins(wtsResults.providers, selectedFiles);
|
|
1432
|
-
setResult(results);
|
|
1012
|
+
this.handleLessThan = (op)=>{
|
|
1013
|
+
const [field, value] = Object.entries(op['<'])[0];
|
|
1014
|
+
return {
|
|
1015
|
+
operator: '<',
|
|
1016
|
+
field: field,
|
|
1017
|
+
operand: value
|
|
1018
|
+
};
|
|
1433
1019
|
};
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
wtsError,
|
|
1442
|
-
wtsResults
|
|
1443
|
-
]);
|
|
1444
|
-
return {
|
|
1445
|
-
isLoading: wstIsLoading,
|
|
1446
|
-
data: result,
|
|
1447
|
-
error: result?.error || wtsError
|
|
1448
|
-
};
|
|
1449
|
-
};
|
|
1450
|
-
const getFederatedLoginStatus = async (selectedFiles)=>{
|
|
1451
|
-
const providers = await queryWTSFederatedLoginStatus();
|
|
1452
|
-
return await fetchExternalLogins(providers.providers, selectedFiles);
|
|
1453
|
-
};
|
|
1454
|
-
|
|
1455
|
-
/**
|
|
1456
|
-
* Creates a base class core API for guppy API calls.
|
|
1457
|
-
* @returns: guppy core API with guppyAPIFetch base query
|
|
1458
|
-
*/ const guppyApi = createApi({
|
|
1459
|
-
reducerPath: 'guppy',
|
|
1460
|
-
// TODO: refactor to use fetchBaseQuery
|
|
1461
|
-
baseQuery: async (query, api)=>{
|
|
1462
|
-
const csrfToken = selectCSRFToken(api.getState());
|
|
1463
|
-
let accessToken = undefined;
|
|
1464
|
-
if (process.env.NODE_ENV === 'development') {
|
|
1465
|
-
// NOTE: This cookie can only be accessed from the client side
|
|
1466
|
-
// in development mode. Otherwise, the cookie is set as httpOnly
|
|
1467
|
-
accessToken = getCookie('credentials_token');
|
|
1468
|
-
}
|
|
1469
|
-
const headers = {
|
|
1470
|
-
Accept: 'application/json',
|
|
1471
|
-
'Content-Type': 'application/json',
|
|
1472
|
-
...csrfToken && {
|
|
1473
|
-
'X-CSRF-Token': csrfToken
|
|
1474
|
-
},
|
|
1475
|
-
...accessToken && {
|
|
1476
|
-
Authorization: `Bearer ${accessToken}`
|
|
1477
|
-
}
|
|
1020
|
+
this.handleLessThanOrEquals = (op)=>{
|
|
1021
|
+
const [field, value] = Object.entries(op['<='])[0];
|
|
1022
|
+
return {
|
|
1023
|
+
operator: '<=',
|
|
1024
|
+
field: field,
|
|
1025
|
+
operand: value
|
|
1026
|
+
};
|
|
1478
1027
|
};
|
|
1479
|
-
|
|
1480
|
-
const
|
|
1481
|
-
headers: headers,
|
|
1482
|
-
method: 'POST',
|
|
1483
|
-
body: JSON.stringify(query)
|
|
1484
|
-
});
|
|
1485
|
-
const data = await response.json();
|
|
1486
|
-
// Check the response for GraphQL errors
|
|
1487
|
-
if (data && 'errors' in data) {
|
|
1488
|
-
// Return an object with an 'error' property if GraphQL errors are present
|
|
1489
|
-
return {
|
|
1490
|
-
error: {
|
|
1491
|
-
message: data.errors[0].message,
|
|
1492
|
-
locations: data.errors[0].locations
|
|
1493
|
-
}
|
|
1494
|
-
};
|
|
1495
|
-
}
|
|
1028
|
+
this.handleGreaterThan = (op)=>{
|
|
1029
|
+
const [field, value] = Object.entries(op['>'])[0];
|
|
1496
1030
|
return {
|
|
1497
|
-
|
|
1031
|
+
operator: '>',
|
|
1032
|
+
field: field,
|
|
1033
|
+
operand: value
|
|
1498
1034
|
};
|
|
1499
|
-
}
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
}
|
|
1507
|
-
};
|
|
1508
|
-
}
|
|
1509
|
-
if (e instanceof Error) return {
|
|
1510
|
-
error: e.message
|
|
1035
|
+
};
|
|
1036
|
+
this.handleGreaterThanOrEquals = (op)=>{
|
|
1037
|
+
const [field, value] = Object.entries(op['>='])[0];
|
|
1038
|
+
return {
|
|
1039
|
+
operator: '>=',
|
|
1040
|
+
field: field,
|
|
1041
|
+
operand: value
|
|
1511
1042
|
};
|
|
1043
|
+
};
|
|
1044
|
+
this.handleIncludes = (op)=>{
|
|
1045
|
+
const [field, value] = Object.entries(op.in)[0];
|
|
1512
1046
|
return {
|
|
1513
|
-
|
|
1047
|
+
operator: 'in',
|
|
1048
|
+
field: field,
|
|
1049
|
+
operands: value
|
|
1514
1050
|
};
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1051
|
+
};
|
|
1052
|
+
this.handleExcludes = (op)=>{
|
|
1053
|
+
const [field, value] = Object.entries(op.exclude)[0];
|
|
1054
|
+
return {
|
|
1055
|
+
operator: 'excludes',
|
|
1056
|
+
field: field,
|
|
1057
|
+
operands: value
|
|
1058
|
+
};
|
|
1059
|
+
};
|
|
1060
|
+
this.handleExcludeIfAny = (op)=>{
|
|
1061
|
+
const [field, value] = Object.entries(op.excludeifany)[0];
|
|
1062
|
+
return {
|
|
1063
|
+
operator: 'excludeifany',
|
|
1064
|
+
field: field,
|
|
1065
|
+
operands: value
|
|
1066
|
+
};
|
|
1067
|
+
};
|
|
1068
|
+
this.handleIntersection = (op)=>({
|
|
1069
|
+
operator: 'and',
|
|
1070
|
+
operands: op.and.map(convertGqlFilterToFilter)
|
|
1071
|
+
});
|
|
1072
|
+
this.handleUnion = (op)=>({
|
|
1073
|
+
operator: 'or',
|
|
1074
|
+
operands: op.or.map(convertGqlFilterToFilter)
|
|
1075
|
+
});
|
|
1076
|
+
this.handleExists = (op)=>{
|
|
1077
|
+
const [field, value] = Object.entries(op.not)[0];
|
|
1078
|
+
return {
|
|
1079
|
+
operator: 'exists',
|
|
1080
|
+
field: field,
|
|
1081
|
+
operand: value
|
|
1082
|
+
};
|
|
1083
|
+
};
|
|
1084
|
+
this.handleMissing = (op)=>{
|
|
1085
|
+
const field = Object.keys(op.is)[0];
|
|
1086
|
+
return {
|
|
1087
|
+
operator: 'missing',
|
|
1088
|
+
field: field
|
|
1089
|
+
};
|
|
1090
|
+
};
|
|
1091
|
+
this.handleNestedFilter = (op)=>({
|
|
1092
|
+
operator: 'nested',
|
|
1093
|
+
path: op.nested.path,
|
|
1094
|
+
operand: convertGqlFilterToFilter(op.nested)
|
|
1095
|
+
});
|
|
1530
1096
|
}
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1097
|
+
}
|
|
1098
|
+
/**
|
|
1099
|
+
* Extract the operand values, if operands themselves have values, otherwise undefined.
|
|
1100
|
+
*/ class ValueExtractorHandler {
|
|
1101
|
+
constructor(){
|
|
1102
|
+
this.handleEquals = (op)=>op.operand;
|
|
1103
|
+
this.handleNotEquals = (op)=>op.operand;
|
|
1104
|
+
this.handleIncludes = (op)=>op.operands;
|
|
1105
|
+
this.handleExcludes = (op)=>op.operands;
|
|
1106
|
+
this.handleExcludeIfAny = (op)=>op.operands;
|
|
1107
|
+
this.handleGreaterThanOrEquals = (op)=>op.operand;
|
|
1108
|
+
this.handleGreaterThan = (op)=>op.operand;
|
|
1109
|
+
this.handleLessThan = (op)=>op.operand;
|
|
1110
|
+
this.handleLessThanOrEquals = (op)=>op.operand;
|
|
1111
|
+
this.handleIntersection = (_arg)=>undefined;
|
|
1112
|
+
this.handleUnion = (_)=>undefined;
|
|
1113
|
+
this.handleNestedFilter = (_)=>undefined;
|
|
1114
|
+
this.handleExists = (_)=>undefined;
|
|
1115
|
+
this.handleMissing = (_)=>undefined;
|
|
1536
1116
|
}
|
|
1537
|
-
|
|
1538
|
-
};
|
|
1117
|
+
}
|
|
1539
1118
|
/**
|
|
1540
|
-
*
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1119
|
+
* Extract the operand values, if operands themselves have values, otherwise undefined.
|
|
1120
|
+
*/ class EnumValueExtractorHandler {
|
|
1121
|
+
constructor(){
|
|
1122
|
+
this.handleEquals = (_)=>undefined;
|
|
1123
|
+
this.handleNotEquals = (_)=>undefined;
|
|
1124
|
+
this.handleIncludes = (op)=>op.operands;
|
|
1125
|
+
this.handleExcludes = (op)=>op.operands;
|
|
1126
|
+
this.handleExcludeIfAny = (op)=>op.operands;
|
|
1127
|
+
this.handleGreaterThanOrEquals = (_)=>undefined;
|
|
1128
|
+
this.handleGreaterThan = (_)=>undefined;
|
|
1129
|
+
this.handleLessThan = (_)=>undefined;
|
|
1130
|
+
this.handleLessThanOrEquals = (_)=>undefined;
|
|
1131
|
+
this.handleIntersection = (_)=>undefined;
|
|
1132
|
+
this.handleUnion = (_)=>undefined;
|
|
1133
|
+
this.handleNestedFilter = (op)=>{
|
|
1134
|
+
return extractEnumFilterValue(op.operand);
|
|
1135
|
+
};
|
|
1136
|
+
this.handleExists = (_)=>undefined;
|
|
1137
|
+
this.handleMissing = (_)=>undefined;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
const appendFilterToOperation = (filter, addition)=>{
|
|
1141
|
+
if (filter === undefined && addition === undefined) return {
|
|
1142
|
+
operator: 'and',
|
|
1143
|
+
operands: []
|
|
1144
|
+
};
|
|
1145
|
+
if (addition === undefined && filter) return filter;
|
|
1146
|
+
if (filter === undefined && addition) return addition;
|
|
1147
|
+
return {
|
|
1148
|
+
...filter,
|
|
1149
|
+
operands: [
|
|
1150
|
+
...filter?.operands || [],
|
|
1151
|
+
addition
|
|
1152
|
+
]
|
|
1153
|
+
};
|
|
1549
1154
|
};
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1155
|
+
const filterSetToOperation = (fs)=>{
|
|
1156
|
+
if (!fs) return undefined;
|
|
1157
|
+
switch(fs.mode){
|
|
1158
|
+
case 'and':
|
|
1159
|
+
return Object.keys(fs.root).length == 0 ? undefined : {
|
|
1160
|
+
operator: fs.mode,
|
|
1161
|
+
operands: Object.keys(fs.root).map((k)=>{
|
|
1162
|
+
return fs.root[k];
|
|
1163
|
+
})
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
return undefined;
|
|
1560
1167
|
};
|
|
1561
|
-
|
|
1562
|
-
|
|
1168
|
+
/**
|
|
1169
|
+
* Constructs a nested operation object based on the provided field and leaf operand.
|
|
1170
|
+
* If the field does not contain a dot '.', it either assigns the field to the leaf operand (if applicable)
|
|
1171
|
+
* or returns the leaf operand as is. When the field contains dots, it splits the field into parts,
|
|
1172
|
+
* creates a "nested" operation for the root field, and recursively constructs the nested structure
|
|
1173
|
+
* for the remaining portion of the field.
|
|
1174
|
+
*
|
|
1175
|
+
* @param {string} field - The hierarchical field path, with segments separated by dots (e.g., "root.child").
|
|
1176
|
+
* @param {Operation} leafOperand - The operation to be nested within the specified path.
|
|
1177
|
+
* @param parentPath - The parent path of the current field. Guppy nested filters require a parent path.
|
|
1178
|
+
* @param depth
|
|
1179
|
+
* @returns {Operation} A nested operation object that represents the structured path and operand.
|
|
1180
|
+
*/ const buildNestedGQLFilter = (field, leafOperand, parentPath = undefined)=>{
|
|
1181
|
+
if (!field.includes('.')) {
|
|
1182
|
+
return leafOperand;
|
|
1183
|
+
}
|
|
1184
|
+
const splitFieldArray = field.split('.');
|
|
1185
|
+
const nextField = splitFieldArray.shift();
|
|
1186
|
+
if (!nextField) {
|
|
1187
|
+
console.warn('Invalid field path:', field);
|
|
1188
|
+
return leafOperand;
|
|
1189
|
+
}
|
|
1190
|
+
const currentPath = parentPath ? `${parentPath}.${nextField}` : nextField;
|
|
1191
|
+
return {
|
|
1192
|
+
nested: {
|
|
1193
|
+
path: currentPath,
|
|
1194
|
+
...buildNestedGQLFilter(splitFieldArray.join('.'), leafOperand, currentPath)
|
|
1195
|
+
}
|
|
1196
|
+
};
|
|
1563
1197
|
};
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1198
|
+
/**
|
|
1199
|
+
* Constructs a nested operation object based on the provided field and leaf operand.
|
|
1200
|
+
* If the field does not contain a dot '.', it either assigns the field to the leaf operand (if applicable)
|
|
1201
|
+
* or returns the leaf operand as is. When the field contains dots, it splits the field into parts,
|
|
1202
|
+
* creates a "nested" operation for the root field, and recursively constructs the nested structure
|
|
1203
|
+
* for the remaining portion of the field.
|
|
1204
|
+
*
|
|
1205
|
+
* @param {string} field - The hierarchical field path, with segments separated by dots (e.g., "root.child").
|
|
1206
|
+
* @param {Operation} leafOperand - The operation to be nested within the specified path.
|
|
1207
|
+
* @param parentPath - The parent path of the current field. Guppy nested filters require a parent path.
|
|
1208
|
+
* @returns {Operation} A nested operation object that represents the structured path and operand.
|
|
1209
|
+
*/ const buildNestedFilterForOperation = (field, leafOperand, parentPath = undefined)=>{
|
|
1210
|
+
if (!field.includes('.')) {
|
|
1211
|
+
// at the end of the path
|
|
1212
|
+
if (ifOperationWithField(leafOperand)) {
|
|
1213
|
+
// update the field for operations with a field
|
|
1214
|
+
return {
|
|
1215
|
+
...leafOperand,
|
|
1216
|
+
field
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
return leafOperand;
|
|
1220
|
+
}
|
|
1221
|
+
const splitFieldArray = field.split('.');
|
|
1222
|
+
const nextField = splitFieldArray.shift();
|
|
1223
|
+
if (!nextField) {
|
|
1224
|
+
console.warn('Invalid field path:', field);
|
|
1225
|
+
return leafOperand;
|
|
1226
|
+
}
|
|
1227
|
+
const currentPath = parentPath ? `${parentPath}.${nextField}` : nextField;
|
|
1228
|
+
return {
|
|
1229
|
+
operator: 'nested',
|
|
1230
|
+
path: currentPath,
|
|
1231
|
+
operand: buildNestedFilterForOperation(splitFieldArray.join('.'), leafOperand, currentPath)
|
|
1232
|
+
};
|
|
1568
1233
|
};
|
|
1569
1234
|
|
|
1570
|
-
const
|
|
1571
|
-
|
|
1235
|
+
const FieldNameOverrides = {
|
|
1236
|
+
gender: 'Sex',
|
|
1237
|
+
Gender: 'Sex'
|
|
1572
1238
|
};
|
|
1573
|
-
const
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1239
|
+
const COMMON_PREPOSITIONS = [
|
|
1240
|
+
'a',
|
|
1241
|
+
'an',
|
|
1242
|
+
'and',
|
|
1243
|
+
'at',
|
|
1244
|
+
'but',
|
|
1245
|
+
'by',
|
|
1246
|
+
'for',
|
|
1247
|
+
'in',
|
|
1248
|
+
'is',
|
|
1249
|
+
'nor',
|
|
1250
|
+
'of',
|
|
1251
|
+
'on',
|
|
1252
|
+
'or',
|
|
1253
|
+
'out',
|
|
1254
|
+
'so',
|
|
1255
|
+
'the',
|
|
1256
|
+
'to',
|
|
1257
|
+
'up',
|
|
1258
|
+
'yet'
|
|
1259
|
+
];
|
|
1260
|
+
const capitalize$1 = (s)=>s.length > 0 ? s[0].toUpperCase() + s.slice(1) : '';
|
|
1261
|
+
const trimFirstfieldNameToLabel = (fieldName, trim = false)=>{
|
|
1262
|
+
if (trim) {
|
|
1263
|
+
const source = fieldName.slice(fieldName.indexOf('.') + 1);
|
|
1264
|
+
return fieldNameToLabel(source ? source : fieldName, 0);
|
|
1578
1265
|
}
|
|
1579
|
-
return
|
|
1580
|
-
};
|
|
1581
|
-
const extractFilterValue = (op)=>{
|
|
1582
|
-
const valueExtractorHandler = new ValueExtractorHandler();
|
|
1583
|
-
return handleOperation(valueExtractorHandler, op);
|
|
1584
|
-
};
|
|
1585
|
-
const extractEnumFilterValue = (op)=>{
|
|
1586
|
-
const enumValueExtractorHandler = new EnumValueExtractorHandler();
|
|
1587
|
-
const results = handleOperation(enumValueExtractorHandler, op);
|
|
1588
|
-
return results ?? [];
|
|
1589
|
-
};
|
|
1590
|
-
const assertNever = (x)=>{
|
|
1591
|
-
throw Error(`Exhaustive comparison did not handle: ${x}`);
|
|
1266
|
+
return fieldNameToLabel(fieldName);
|
|
1592
1267
|
};
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
return handler.handleLessThanOrEquals(op);
|
|
1603
|
-
case '>':
|
|
1604
|
-
return handler.handleGreaterThan(op);
|
|
1605
|
-
case '>=':
|
|
1606
|
-
return handler.handleGreaterThanOrEquals(op);
|
|
1607
|
-
case 'and':
|
|
1608
|
-
return handler.handleIntersection(op);
|
|
1609
|
-
case 'or':
|
|
1610
|
-
return handler.handleUnion(op);
|
|
1611
|
-
case 'nested':
|
|
1612
|
-
return handler.handleNestedFilter(op);
|
|
1613
|
-
case 'in':
|
|
1614
|
-
case 'includes':
|
|
1615
|
-
return handler.handleIncludes(op);
|
|
1616
|
-
case 'excludeifany':
|
|
1617
|
-
return handler.handleExcludeIfAny(op);
|
|
1618
|
-
case 'excludes':
|
|
1619
|
-
return handler.handleExcludes(op);
|
|
1620
|
-
case 'exists':
|
|
1621
|
-
return handler.handleExists(op);
|
|
1622
|
-
case 'missing':
|
|
1623
|
-
return handler.handleMissing(op);
|
|
1624
|
-
default:
|
|
1625
|
-
return assertNever(op);
|
|
1626
|
-
}
|
|
1268
|
+
/**
|
|
1269
|
+
* Converts a filter name to a title,
|
|
1270
|
+
* For example files.input.experimental_strategy will get converted to Experimental Strategy
|
|
1271
|
+
* if sections == 2 then the output would be Input Experimental Strategy
|
|
1272
|
+
* @param fieldName input filter expected to be: string.firstpart_secondpart
|
|
1273
|
+
* @param sections number of "sections" string.string.string to got back from the end of the field
|
|
1274
|
+
*/ const fieldNameToLabel = (fieldName, sections = 1)=>{
|
|
1275
|
+
if (fieldName === undefined) return 'No Title';
|
|
1276
|
+
return fieldName.split('.').slice(-sections).map((s)=>s.split('_')).flat().map((word)=>COMMON_PREPOSITIONS.includes(word) ? word : capitalize$1(word)).map((word)=>word in FieldNameOverrides ? FieldNameOverrides[word] : word).join(' ');
|
|
1627
1277
|
};
|
|
1628
1278
|
/**
|
|
1629
|
-
*
|
|
1630
|
-
* @param
|
|
1631
|
-
*/ const
|
|
1279
|
+
* Extracts the index name from the field name
|
|
1280
|
+
* @param fieldName
|
|
1281
|
+
*/ const extractIndexFromFullFieldName = (fieldName)=>fieldName.split('.')[0];
|
|
1632
1282
|
/**
|
|
1633
|
-
*
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1283
|
+
* prepend the index name to the field name
|
|
1284
|
+
*/ const prependIndexToFieldName = (fieldName, index)=>`${index}.${fieldName}`;
|
|
1285
|
+
/**
|
|
1286
|
+
* extract the field name from the index.field name
|
|
1287
|
+
*/ const extractFieldNameFromFullFieldName = (fieldName)=>fieldName.split('.').slice(1).join('.');
|
|
1288
|
+
/**
|
|
1289
|
+
* extract the field name and the index from the index.field name returning as a tuple
|
|
1290
|
+
*/ const extractIndexAndFieldNameFromFullFieldName = (fieldName)=>{
|
|
1291
|
+
const [index, ...rest] = fieldName.split('.');
|
|
1292
|
+
return [
|
|
1293
|
+
index,
|
|
1294
|
+
rest.join('.')
|
|
1295
|
+
];
|
|
1638
1296
|
};
|
|
1297
|
+
|
|
1639
1298
|
/**
|
|
1640
|
-
*
|
|
1641
|
-
*
|
|
1642
|
-
*
|
|
1643
|
-
|
|
1644
|
-
|
|
1299
|
+
* Constructs a nested operation object based on the provided field and leaf operand.
|
|
1300
|
+
* If the field does not contain a dot '.', it either assigns the field to the leaf operand (if applicable)
|
|
1301
|
+
* or returns the leaf operand as is. When the field contains dots, it splits the field into parts,
|
|
1302
|
+
* creates a "nested" operation for the root field, and recursively constructs the nested structure
|
|
1303
|
+
* for the remaining portion of the field.
|
|
1304
|
+
*
|
|
1305
|
+
* @param {string} field - The hierarchical field path, with segments separated by dots (e.g., "root.child").
|
|
1306
|
+
* @param {Operation} leafOperand - The operation to be nested within the specified path.
|
|
1307
|
+
* @param parentPath - The parent path of the current field. Guppy nested filters require a parent path.
|
|
1308
|
+
* @param depth
|
|
1309
|
+
* @returns {Operation} A nested operation object that represents the structured path and operand.
|
|
1310
|
+
*/ const buildNestedWithParentPathGQLFilter = (field, leafOperand, parentPath = undefined)=>{
|
|
1311
|
+
if (!field.includes('.')) {
|
|
1312
|
+
return leafOperand;
|
|
1313
|
+
}
|
|
1314
|
+
const splitFieldArray = field.split('.');
|
|
1315
|
+
const nextField = splitFieldArray.shift();
|
|
1316
|
+
if (!nextField) {
|
|
1317
|
+
console.warn('Invalid field path:', field);
|
|
1318
|
+
return leafOperand;
|
|
1319
|
+
}
|
|
1320
|
+
const currentPath = parentPath ? `${parentPath}.${nextField}` : nextField;
|
|
1321
|
+
return {
|
|
1322
|
+
nested: {
|
|
1323
|
+
path: currentPath,
|
|
1324
|
+
...buildNestedGQLFilter(splitFieldArray.join('.'), leafOperand, currentPath)
|
|
1325
|
+
}
|
|
1326
|
+
};
|
|
1645
1327
|
};
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1328
|
+
const buildCohortGqlOperator = (fs)=>{
|
|
1329
|
+
if (!fs || !fs.root) return undefined;
|
|
1330
|
+
const fsKeys = Object.keys(fs.root);
|
|
1331
|
+
// if no keys return undefined
|
|
1332
|
+
if (fsKeys.length === 0) return undefined;
|
|
1333
|
+
// TODO consider changing FilterSet: mode to support joinOrToAll as FilterSet mode
|
|
1334
|
+
// find key using keyword "joinOrToAll"
|
|
1335
|
+
const joinOrToAllKey = fsKeys.filter((x)=>x.includes('joinOrToAll'));
|
|
1336
|
+
switch(fs.mode){
|
|
1337
|
+
case 'and':
|
|
1338
|
+
if (joinOrToAllKey.length === 1) {
|
|
1339
|
+
const firstJoinOrToAllKey = joinOrToAllKey[0];
|
|
1340
|
+
// Remove firstJoinOrToAllKey from Array
|
|
1341
|
+
fsKeys.splice(fsKeys.indexOf(firstJoinOrToAllKey), 1);
|
|
1342
|
+
const firstJoinOrToAllObj = fs.root[firstJoinOrToAllKey];
|
|
1343
|
+
// make sure type is or/ Union
|
|
1344
|
+
if (firstJoinOrToAllObj.operator === 'or') {
|
|
1345
|
+
return {
|
|
1346
|
+
or: firstJoinOrToAllObj?.operands.map((orObj)=>{
|
|
1347
|
+
// go through each or statement and add all other filters to it
|
|
1348
|
+
return {
|
|
1349
|
+
and: [
|
|
1350
|
+
convertFilterToNestedGqlFilter(orObj),
|
|
1351
|
+
...fsKeys.map((k)=>{
|
|
1352
|
+
return convertFilterToNestedGqlFilter(fs.root[k]);
|
|
1353
|
+
})
|
|
1354
|
+
]
|
|
1355
|
+
};
|
|
1356
|
+
})
|
|
1357
|
+
};
|
|
1358
|
+
} else {
|
|
1359
|
+
console.error(`function buildCohortGqlOperator expecting "or" received "${firstJoinOrToAllObj.operator}" on key "${firstJoinOrToAllKey}"`);
|
|
1360
|
+
}
|
|
1361
|
+
} else if (joinOrToAllKey.length > 1) {
|
|
1362
|
+
console.error(`function buildCohortGqlOperator expecting only one joinOrToAll received: ${joinOrToAllKey.length}`, fsKeys);
|
|
1363
|
+
}
|
|
1364
|
+
return {
|
|
1365
|
+
// TODO: Replace fixed AND with cohort top level operation like Union or Intersection
|
|
1366
|
+
[fs.mode]: fsKeys.map((k)=>{
|
|
1367
|
+
return convertFilterToGqlFilter(fs.root[k]);
|
|
1368
|
+
})
|
|
1369
|
+
};
|
|
1370
|
+
case 'or':
|
|
1371
|
+
return {
|
|
1372
|
+
[fs.mode]: fsKeys.map((k)=>{
|
|
1373
|
+
return convertFilterToGqlFilter(fs.root[k]);
|
|
1374
|
+
})
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
};
|
|
1378
|
+
/**
|
|
1379
|
+
* Merged two FilterSets returning the merged pair.
|
|
1380
|
+
* @param a - first FilterSet
|
|
1381
|
+
* @param b - other FilterSet
|
|
1382
|
+
*/ const joinFilters = (a, b)=>{
|
|
1383
|
+
return {
|
|
1384
|
+
mode: a.mode,
|
|
1385
|
+
root: {
|
|
1386
|
+
...a.root,
|
|
1387
|
+
...b.root
|
|
1388
|
+
}
|
|
1389
|
+
};
|
|
1390
|
+
};
|
|
1391
|
+
const convertFilterSetToOperation = (fs)=>{
|
|
1392
|
+
if (!fs) return undefined;
|
|
1393
|
+
switch(fs.mode){
|
|
1394
|
+
case 'and':
|
|
1395
|
+
return Object.keys(fs.root).length == 0 ? undefined : {
|
|
1396
|
+
operator: fs.mode,
|
|
1397
|
+
operands: Object.keys(fs.root).map((k)=>{
|
|
1398
|
+
return fs.root[k];
|
|
1399
|
+
})
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
return undefined;
|
|
1403
|
+
};
|
|
1404
|
+
const extractContents = (filter)=>{
|
|
1405
|
+
if (isGQLUnion(filter)) {
|
|
1406
|
+
return filter.or;
|
|
1407
|
+
}
|
|
1408
|
+
if (isGQLIntersection(filter)) {
|
|
1409
|
+
return filter.and;
|
|
1410
|
+
}
|
|
1411
|
+
return undefined;
|
|
1412
|
+
};
|
|
1413
|
+
class ToGqlAllNested {
|
|
1414
|
+
constructor(){
|
|
1415
|
+
this.handleEquals = (op)=>{
|
|
1416
|
+
if (op.field.includes('.')) {
|
|
1417
|
+
const leafField = op.field.split('.').at(-1) ?? 'unset';
|
|
1418
|
+
return buildNestedGQLFilter(op.field, {
|
|
1419
|
+
'=': {
|
|
1420
|
+
[leafField]: op.operand
|
|
1421
|
+
}
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
return {
|
|
1425
|
+
'=': {
|
|
1426
|
+
[op.field]: op.operand
|
|
1427
|
+
}
|
|
1428
|
+
};
|
|
1429
|
+
};
|
|
1430
|
+
this.handleNotEquals = (op)=>{
|
|
1431
|
+
if (op.field.includes('.')) {
|
|
1432
|
+
const leafField = op.field.split('.').at(-1) ?? 'unset';
|
|
1433
|
+
return buildNestedGQLFilter(op.field, {
|
|
1434
|
+
'!=': {
|
|
1435
|
+
[leafField]: op.operand
|
|
1436
|
+
}
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
return {
|
|
1440
|
+
'!=': {
|
|
1441
|
+
[op.field]: op.operand
|
|
1442
|
+
}
|
|
1443
|
+
};
|
|
1444
|
+
};
|
|
1445
|
+
this.handleLessThan = (op)=>{
|
|
1446
|
+
if (op.field.includes('.')) {
|
|
1447
|
+
const leafField = op.field.split('.').at(-1) ?? 'unset';
|
|
1448
|
+
return buildNestedGQLFilter(op.field, {
|
|
1449
|
+
'<': {
|
|
1450
|
+
[leafField]: op.operand
|
|
1451
|
+
}
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
return {
|
|
1455
|
+
'<': {
|
|
1456
|
+
[op.field]: op.operand
|
|
1457
|
+
}
|
|
1458
|
+
};
|
|
1459
|
+
};
|
|
1460
|
+
this.handleLessThanOrEquals = (op)=>{
|
|
1461
|
+
if (op.field.includes('.')) {
|
|
1462
|
+
const leafField = op.field.split('.').at(-1) ?? 'unset';
|
|
1463
|
+
return buildNestedGQLFilter(op.field, {
|
|
1464
|
+
'<=': {
|
|
1465
|
+
[leafField]: op.operand
|
|
1466
|
+
}
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
return {
|
|
1664
1470
|
'<=': {
|
|
1665
1471
|
[op.field]: op.operand
|
|
1666
1472
|
}
|
|
1667
|
-
}
|
|
1668
|
-
|
|
1473
|
+
};
|
|
1474
|
+
};
|
|
1475
|
+
this.handleGreaterThan = (op)=>{
|
|
1476
|
+
if (op.field.includes('.')) {
|
|
1477
|
+
const leafField = op.field.split('.').at(-1) ?? 'unset';
|
|
1478
|
+
return buildNestedGQLFilter(op.field, {
|
|
1479
|
+
'>': {
|
|
1480
|
+
[leafField]: op.operand
|
|
1481
|
+
}
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
return {
|
|
1669
1485
|
'>': {
|
|
1670
1486
|
[op.field]: op.operand
|
|
1671
1487
|
}
|
|
1672
|
-
}
|
|
1673
|
-
|
|
1488
|
+
};
|
|
1489
|
+
};
|
|
1490
|
+
this.handleGreaterThanOrEquals = (op)=>{
|
|
1491
|
+
if (op.field.includes('.')) {
|
|
1492
|
+
const leafField = op.field.split('.').at(-1) ?? 'unset';
|
|
1493
|
+
return buildNestedGQLFilter(op.field, {
|
|
1494
|
+
'>=': {
|
|
1495
|
+
[leafField]: op.operand
|
|
1496
|
+
}
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
return {
|
|
1674
1500
|
'>=': {
|
|
1675
1501
|
[op.field]: op.operand
|
|
1676
1502
|
}
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1503
|
+
};
|
|
1504
|
+
};
|
|
1505
|
+
this.handleIncludes = (op)=>{
|
|
1506
|
+
if (op.field.includes('.')) {
|
|
1507
|
+
const leafField = op.field.split('.').at(-1) ?? 'unset';
|
|
1508
|
+
return buildNestedGQLFilter(op.field, {
|
|
1509
|
+
in: {
|
|
1510
|
+
[leafField]: op.operands
|
|
1511
|
+
}
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
return {
|
|
1679
1515
|
in: {
|
|
1680
1516
|
[op.field]: op.operands
|
|
1681
1517
|
}
|
|
1682
|
-
}
|
|
1683
|
-
|
|
1518
|
+
};
|
|
1519
|
+
};
|
|
1520
|
+
this.handleExcludes = (op)=>{
|
|
1521
|
+
if (op.field.includes('.')) {
|
|
1522
|
+
const leafField = op.field.split('.').at(-1) ?? 'unset';
|
|
1523
|
+
return buildNestedGQLFilter(op.field, {
|
|
1524
|
+
exclude: {
|
|
1525
|
+
[leafField]: op.operands
|
|
1526
|
+
}
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
return {
|
|
1684
1530
|
exclude: {
|
|
1685
1531
|
[op.field]: op.operands
|
|
1686
1532
|
}
|
|
1687
|
-
}
|
|
1688
|
-
|
|
1533
|
+
};
|
|
1534
|
+
};
|
|
1535
|
+
this.handleExcludeIfAny = (op)=>{
|
|
1536
|
+
if (op.field.includes('.')) {
|
|
1537
|
+
const leafField = op.field.split('.').at(-1) ?? 'unset';
|
|
1538
|
+
return buildNestedGQLFilter(op.field, {
|
|
1539
|
+
excludeifany: {
|
|
1540
|
+
[leafField]: op.operands
|
|
1541
|
+
}
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
return {
|
|
1689
1545
|
excludeifany: {
|
|
1690
1546
|
[op.field]: op.operands
|
|
1691
1547
|
}
|
|
1692
|
-
}
|
|
1548
|
+
};
|
|
1549
|
+
};
|
|
1693
1550
|
this.handleIntersection = (op)=>({
|
|
1694
1551
|
and: op.operands.map((x)=>convertFilterToGqlFilter(x))
|
|
1695
1552
|
});
|
|
1696
1553
|
this.handleUnion = (op)=>({
|
|
1697
1554
|
or: op.operands.map((x)=>convertFilterToGqlFilter(x))
|
|
1698
1555
|
});
|
|
1699
|
-
this.handleMissing = (op)=>
|
|
1556
|
+
this.handleMissing = (op)=>{
|
|
1557
|
+
if (op.field.includes('.')) {
|
|
1558
|
+
const leafField = op.field.split('.').at(-1) ?? 'unset';
|
|
1559
|
+
return buildNestedGQLFilter(op.field, {
|
|
1560
|
+
is: {
|
|
1561
|
+
[leafField]: 'MISSING'
|
|
1562
|
+
}
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
return {
|
|
1700
1566
|
is: {
|
|
1701
1567
|
[op.field]: 'MISSING'
|
|
1702
1568
|
}
|
|
1703
|
-
}
|
|
1704
|
-
|
|
1569
|
+
};
|
|
1570
|
+
};
|
|
1571
|
+
this.handleExists = (op)=>{
|
|
1572
|
+
if (op.field.includes('.')) {
|
|
1573
|
+
const leafField = op.field.split('.').at(-1) ?? 'unset';
|
|
1574
|
+
return buildNestedGQLFilter(op.field, {
|
|
1575
|
+
not: {
|
|
1576
|
+
[leafField]: op?.operand ?? null
|
|
1577
|
+
}
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
return {
|
|
1705
1581
|
not: {
|
|
1706
1582
|
[op.field]: op?.operand ?? null
|
|
1707
1583
|
}
|
|
1708
|
-
}
|
|
1584
|
+
};
|
|
1585
|
+
};
|
|
1709
1586
|
this.handleNestedFilter = (op)=>{
|
|
1710
1587
|
const child = convertFilterToGqlFilter(op.operand);
|
|
1711
1588
|
return {
|
|
@@ -1717,964 +1594,292 @@ class ToGqlHandler {
|
|
|
1717
1594
|
};
|
|
1718
1595
|
}
|
|
1719
1596
|
}
|
|
1720
|
-
const
|
|
1721
|
-
const handler = new
|
|
1597
|
+
const convertFilterToNestedGqlFilter = (filter)=>{
|
|
1598
|
+
const handler = new ToGqlAllNested();
|
|
1722
1599
|
return handleOperation(handler, filter);
|
|
1723
1600
|
};
|
|
1724
|
-
const
|
|
1601
|
+
const convertFilterSetToNestedGqlFilter = (fs, toplevelOp = 'and')=>{
|
|
1725
1602
|
const fsKeys = Object.keys(fs.root);
|
|
1726
1603
|
// if no keys return undefined
|
|
1727
1604
|
if (fsKeys.length === 0) return {
|
|
1728
1605
|
and: []
|
|
1729
1606
|
};
|
|
1730
1607
|
return toplevelOp === 'and' ? {
|
|
1731
|
-
and: fsKeys.map((key)=>
|
|
1608
|
+
and: fsKeys.map((key)=>convertFilterToNestedGqlFilter(fs.root[key]))
|
|
1732
1609
|
} : {
|
|
1733
|
-
or: fsKeys.map((key)=>
|
|
1610
|
+
or: fsKeys.map((key)=>convertFilterToNestedGqlFilter(fs.root[key]))
|
|
1734
1611
|
};
|
|
1735
1612
|
};
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
return handler.handleLessThan(op);
|
|
1746
|
-
}
|
|
1747
|
-
if (operationKeys.includes('<=')) {
|
|
1748
|
-
return handler.handleLessThanOrEquals(op);
|
|
1749
|
-
}
|
|
1750
|
-
if (operationKeys.includes('>')) {
|
|
1751
|
-
return handler.handleGreaterThan(op);
|
|
1752
|
-
}
|
|
1753
|
-
if (operationKeys.includes('>=')) {
|
|
1754
|
-
return handler.handleGreaterThanOrEquals(op);
|
|
1755
|
-
}
|
|
1756
|
-
if (operationKeys.includes('in')) {
|
|
1757
|
-
return handler.handleIncludes(op);
|
|
1758
|
-
}
|
|
1759
|
-
if (operationKeys.includes('exclude')) {
|
|
1760
|
-
return handler.handleExcludes(op);
|
|
1761
|
-
}
|
|
1762
|
-
if (operationKeys.includes('excludeifany')) {
|
|
1763
|
-
return handler.handleExcludeIfAny(op);
|
|
1764
|
-
}
|
|
1765
|
-
if (operationKeys.includes('and')) {
|
|
1766
|
-
return handler.handleIntersection(op);
|
|
1767
|
-
}
|
|
1768
|
-
if (operationKeys.includes('or')) {
|
|
1769
|
-
return handler.handleUnion(op);
|
|
1770
|
-
}
|
|
1771
|
-
if (operationKeys.includes('nested')) {
|
|
1772
|
-
return handler.handleNestedFilter(op);
|
|
1773
|
-
}
|
|
1774
|
-
if (operationKeys.includes('is')) {
|
|
1775
|
-
return handler.handleExists(op);
|
|
1776
|
-
}
|
|
1777
|
-
if (operationKeys.includes('not')) {
|
|
1778
|
-
return handler.handleMissing(op);
|
|
1779
|
-
}
|
|
1780
|
-
return assertNever(op);
|
|
1613
|
+
|
|
1614
|
+
const defaultCohortNameGenerator = ()=>`Custom cohort ${new Date().toLocaleString('en-CA', {
|
|
1615
|
+
timeZone: 'America/Chicago',
|
|
1616
|
+
hour12: false
|
|
1617
|
+
}).replace(',', '')}`;
|
|
1618
|
+
const isNameUnique = (entities, name, excludeId)=>{
|
|
1619
|
+
const trimmedName = name.trim();
|
|
1620
|
+
if (!trimmedName) return false;
|
|
1621
|
+
return !entities.some((cohort)=>cohort && cohort.id !== excludeId && cohort.name.trim().toLowerCase() === trimmedName.toLowerCase());
|
|
1781
1622
|
};
|
|
1782
|
-
const
|
|
1783
|
-
const
|
|
1784
|
-
|
|
1623
|
+
const generateUniqueName = (entities, baseName)=>{
|
|
1624
|
+
const trimmedBaseName = baseName.trim();
|
|
1625
|
+
// If base name is unique, use it
|
|
1626
|
+
if (isNameUnique(entities, trimmedBaseName)) {
|
|
1627
|
+
return trimmedBaseName;
|
|
1628
|
+
}
|
|
1629
|
+
// Find a unique name by appending numbers
|
|
1630
|
+
let counter = 1;
|
|
1631
|
+
let uniqueName;
|
|
1632
|
+
do {
|
|
1633
|
+
uniqueName = `${trimmedBaseName} (${counter})`;
|
|
1634
|
+
counter++;
|
|
1635
|
+
}while (!isNameUnique(entities, uniqueName))
|
|
1636
|
+
return uniqueName;
|
|
1785
1637
|
};
|
|
1786
1638
|
/**
|
|
1787
|
-
*
|
|
1788
|
-
*
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
return {
|
|
1802
|
-
operator: '!=',
|
|
1803
|
-
field: field,
|
|
1804
|
-
operand: value
|
|
1805
|
-
};
|
|
1806
|
-
};
|
|
1807
|
-
this.handleLessThan = (op)=>{
|
|
1808
|
-
const [field, value] = Object.entries(op['<'])[0];
|
|
1809
|
-
return {
|
|
1810
|
-
operator: '<',
|
|
1811
|
-
field: field,
|
|
1812
|
-
operand: value
|
|
1813
|
-
};
|
|
1814
|
-
};
|
|
1815
|
-
this.handleLessThanOrEquals = (op)=>{
|
|
1816
|
-
const [field, value] = Object.entries(op['<='])[0];
|
|
1817
|
-
return {
|
|
1818
|
-
operator: '<=',
|
|
1819
|
-
field: field,
|
|
1820
|
-
operand: value
|
|
1821
|
-
};
|
|
1822
|
-
};
|
|
1823
|
-
this.handleGreaterThan = (op)=>{
|
|
1824
|
-
const [field, value] = Object.entries(op['>'])[0];
|
|
1825
|
-
return {
|
|
1826
|
-
operator: '>',
|
|
1827
|
-
field: field,
|
|
1828
|
-
operand: value
|
|
1829
|
-
};
|
|
1830
|
-
};
|
|
1831
|
-
this.handleGreaterThanOrEquals = (op)=>{
|
|
1832
|
-
const [field, value] = Object.entries(op['>='])[0];
|
|
1833
|
-
return {
|
|
1834
|
-
operator: '>=',
|
|
1835
|
-
field: field,
|
|
1836
|
-
operand: value
|
|
1837
|
-
};
|
|
1838
|
-
};
|
|
1839
|
-
this.handleIncludes = (op)=>{
|
|
1840
|
-
const [field, value] = Object.entries(op.in)[0];
|
|
1841
|
-
return {
|
|
1842
|
-
operator: 'in',
|
|
1843
|
-
field: field,
|
|
1844
|
-
operands: value
|
|
1845
|
-
};
|
|
1846
|
-
};
|
|
1847
|
-
this.handleExcludes = (op)=>{
|
|
1848
|
-
const [field, value] = Object.entries(op.exclude)[0];
|
|
1849
|
-
return {
|
|
1850
|
-
operator: 'excludes',
|
|
1851
|
-
field: field,
|
|
1852
|
-
operands: value
|
|
1853
|
-
};
|
|
1854
|
-
};
|
|
1855
|
-
this.handleExcludeIfAny = (op)=>{
|
|
1856
|
-
const [field, value] = Object.entries(op.excludeifany)[0];
|
|
1857
|
-
return {
|
|
1858
|
-
operator: 'excludeifany',
|
|
1859
|
-
field: field,
|
|
1860
|
-
operands: value
|
|
1861
|
-
};
|
|
1862
|
-
};
|
|
1863
|
-
this.handleIntersection = (op)=>({
|
|
1864
|
-
operator: 'and',
|
|
1865
|
-
operands: op.and.map(convertGqlFilterToFilter)
|
|
1866
|
-
});
|
|
1867
|
-
this.handleUnion = (op)=>({
|
|
1868
|
-
operator: 'or',
|
|
1869
|
-
operands: op.or.map(convertGqlFilterToFilter)
|
|
1870
|
-
});
|
|
1871
|
-
this.handleExists = (op)=>{
|
|
1872
|
-
const [field, value] = Object.entries(op.not)[0];
|
|
1873
|
-
return {
|
|
1874
|
-
operator: 'exists',
|
|
1875
|
-
field: field,
|
|
1876
|
-
operand: value
|
|
1877
|
-
};
|
|
1878
|
-
};
|
|
1879
|
-
this.handleMissing = (op)=>{
|
|
1880
|
-
const field = Object.keys(op.is)[0];
|
|
1881
|
-
return {
|
|
1882
|
-
operator: 'missing',
|
|
1883
|
-
field: field
|
|
1884
|
-
};
|
|
1639
|
+
* This function takes a FilterSet object and a prefix string as input.
|
|
1640
|
+
* It filters the root property of the FilterSet object and returns a
|
|
1641
|
+
* new FilterSet object that only contains filters with field names
|
|
1642
|
+
* that start with the specified prefix.
|
|
1643
|
+
*
|
|
1644
|
+
* @param fs - The FilterSet object to filter
|
|
1645
|
+
* @param prefix - The prefix to filter by
|
|
1646
|
+
* @returns - A new FilterSet object that only contains filters with field names that start with the specified prefix
|
|
1647
|
+
* @category Filters
|
|
1648
|
+
*/ const extractFiltersWithPrefixFromFilterSet = (fs, prefix)=>{
|
|
1649
|
+
if (fs === undefined || fs.root === undefined) {
|
|
1650
|
+
return {
|
|
1651
|
+
mode: 'and',
|
|
1652
|
+
root: {}
|
|
1885
1653
|
};
|
|
1886
|
-
this.handleNestedFilter = (op)=>({
|
|
1887
|
-
operator: 'nested',
|
|
1888
|
-
path: op.nested.path,
|
|
1889
|
-
operand: convertGqlFilterToFilter(op.nested)
|
|
1890
|
-
});
|
|
1891
|
-
}
|
|
1892
|
-
}
|
|
1893
|
-
/**
|
|
1894
|
-
* Extract the operand values, if operands themselves have values, otherwise undefined.
|
|
1895
|
-
*/ class ValueExtractorHandler {
|
|
1896
|
-
constructor(){
|
|
1897
|
-
this.handleEquals = (op)=>op.operand;
|
|
1898
|
-
this.handleNotEquals = (op)=>op.operand;
|
|
1899
|
-
this.handleIncludes = (op)=>op.operands;
|
|
1900
|
-
this.handleExcludes = (op)=>op.operands;
|
|
1901
|
-
this.handleExcludeIfAny = (op)=>op.operands;
|
|
1902
|
-
this.handleGreaterThanOrEquals = (op)=>op.operand;
|
|
1903
|
-
this.handleGreaterThan = (op)=>op.operand;
|
|
1904
|
-
this.handleLessThan = (op)=>op.operand;
|
|
1905
|
-
this.handleLessThanOrEquals = (op)=>op.operand;
|
|
1906
|
-
this.handleIntersection = (_arg)=>undefined;
|
|
1907
|
-
this.handleUnion = (_)=>undefined;
|
|
1908
|
-
this.handleNestedFilter = (_)=>undefined;
|
|
1909
|
-
this.handleExists = (_)=>undefined;
|
|
1910
|
-
this.handleMissing = (_)=>undefined;
|
|
1911
1654
|
}
|
|
1912
|
-
|
|
1655
|
+
return Object.values(fs.root).reduce((acc, filter)=>{
|
|
1656
|
+
if (isIntersectionOrUnion(filter) || isNestedFilter(filter)) return acc;
|
|
1657
|
+
if (filter.field.startsWith(prefix)) {
|
|
1658
|
+
acc.root[filter.field] = filter;
|
|
1659
|
+
}
|
|
1660
|
+
return acc;
|
|
1661
|
+
}, {
|
|
1662
|
+
mode: 'and',
|
|
1663
|
+
root: {}
|
|
1664
|
+
});
|
|
1665
|
+
};
|
|
1666
|
+
|
|
1913
1667
|
/**
|
|
1914
|
-
*
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
this.handleGreaterThanOrEquals = (_)=>undefined;
|
|
1923
|
-
this.handleGreaterThan = (_)=>undefined;
|
|
1924
|
-
this.handleLessThan = (_)=>undefined;
|
|
1925
|
-
this.handleLessThanOrEquals = (_)=>undefined;
|
|
1926
|
-
this.handleIntersection = (_)=>undefined;
|
|
1927
|
-
this.handleUnion = (_)=>undefined;
|
|
1928
|
-
this.handleNestedFilter = (op)=>{
|
|
1929
|
-
return extractEnumFilterValue(op.operand);
|
|
1930
|
-
};
|
|
1931
|
-
this.handleExists = (_)=>undefined;
|
|
1932
|
-
this.handleMissing = (_)=>undefined;
|
|
1933
|
-
}
|
|
1934
|
-
}
|
|
1935
|
-
const appendFilterToOperation = (filter, addition)=>{
|
|
1936
|
-
if (filter === undefined && addition === undefined) return {
|
|
1937
|
-
operator: 'and',
|
|
1938
|
-
operands: []
|
|
1939
|
-
};
|
|
1940
|
-
if (addition === undefined && filter) return filter;
|
|
1941
|
-
if (filter === undefined && addition) return addition;
|
|
1668
|
+
* Cohorts in Gen3 are defined as a set of filters for each index in the data.
|
|
1669
|
+
* This means one cohort id defined for all "tabs" in CohortBuilder (explorer)
|
|
1670
|
+
* Switching a cohort id means all the cohorts for the index are changed.
|
|
1671
|
+
*/ const DEFAULT_COHORT_NAME = 'Cohort';
|
|
1672
|
+
const newCohort = ({ filters = {}, customName })=>{
|
|
1673
|
+
const ts = new Date().toISOString();
|
|
1674
|
+
const newName = customName ?? defaultCohortNameGenerator();
|
|
1675
|
+
const newId = createCohortId();
|
|
1942
1676
|
return {
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
if (!fs) return undefined;
|
|
1952
|
-
switch(fs.mode){
|
|
1953
|
-
case 'and':
|
|
1954
|
-
return Object.keys(fs.root).length == 0 ? undefined : {
|
|
1955
|
-
operator: fs.mode,
|
|
1956
|
-
operands: Object.keys(fs.root).map((k)=>{
|
|
1957
|
-
return fs.root[k];
|
|
1958
|
-
})
|
|
1959
|
-
};
|
|
1960
|
-
}
|
|
1961
|
-
return undefined;
|
|
1962
|
-
};
|
|
1963
|
-
/**
|
|
1964
|
-
* Constructs a nested operation object based on the provided field and leaf operand.
|
|
1965
|
-
* If the field does not contain a dot '.', it either assigns the field to the leaf operand (if applicable)
|
|
1966
|
-
* or returns the leaf operand as is. When the field contains dots, it splits the field into parts,
|
|
1967
|
-
* creates a "nested" operation for the root field, and recursively constructs the nested structure
|
|
1968
|
-
* for the remaining portion of the field.
|
|
1969
|
-
*
|
|
1970
|
-
* @param {string} field - The hierarchical field path, with segments separated by dots (e.g., "root.child").
|
|
1971
|
-
* @param {Operation} leafOperand - The operation to be nested within the specified path.
|
|
1972
|
-
* @param parentPath - The parent path of the current field. Guppy nested filters require a parent path.
|
|
1973
|
-
* @param depth
|
|
1974
|
-
* @returns {Operation} A nested operation object that represents the structured path and operand.
|
|
1975
|
-
*/ const buildNestedGQLFilter = (field, leafOperand, parentPath = undefined)=>{
|
|
1976
|
-
if (!field.includes('.')) {
|
|
1977
|
-
return leafOperand;
|
|
1978
|
-
}
|
|
1979
|
-
const splitFieldArray = field.split('.');
|
|
1980
|
-
const nextField = splitFieldArray.shift();
|
|
1981
|
-
if (!nextField) {
|
|
1982
|
-
console.warn('Invalid field path:', field);
|
|
1983
|
-
return leafOperand;
|
|
1984
|
-
}
|
|
1985
|
-
const currentPath = parentPath ? `${parentPath}.${nextField}` : nextField;
|
|
1986
|
-
return {
|
|
1987
|
-
nested: {
|
|
1988
|
-
path: currentPath,
|
|
1989
|
-
...buildNestedGQLFilter(splitFieldArray.join('.'), leafOperand, currentPath)
|
|
1990
|
-
}
|
|
1991
|
-
};
|
|
1992
|
-
};
|
|
1993
|
-
/**
|
|
1994
|
-
* Constructs a nested operation object based on the provided field and leaf operand.
|
|
1995
|
-
* If the field does not contain a dot '.', it either assigns the field to the leaf operand (if applicable)
|
|
1996
|
-
* or returns the leaf operand as is. When the field contains dots, it splits the field into parts,
|
|
1997
|
-
* creates a "nested" operation for the root field, and recursively constructs the nested structure
|
|
1998
|
-
* for the remaining portion of the field.
|
|
1999
|
-
*
|
|
2000
|
-
* @param {string} field - The hierarchical field path, with segments separated by dots (e.g., "root.child").
|
|
2001
|
-
* @param {Operation} leafOperand - The operation to be nested within the specified path.
|
|
2002
|
-
* @param parentPath - The parent path of the current field. Guppy nested filters require a parent path.
|
|
2003
|
-
* @returns {Operation} A nested operation object that represents the structured path and operand.
|
|
2004
|
-
*/ const buildNestedFilterForOperation = (field, leafOperand, parentPath = undefined)=>{
|
|
2005
|
-
if (!field.includes('.')) {
|
|
2006
|
-
// at the end of the path
|
|
2007
|
-
if (ifOperationWithField(leafOperand)) {
|
|
2008
|
-
// update the field for operations with a field
|
|
2009
|
-
return {
|
|
2010
|
-
...leafOperand,
|
|
2011
|
-
field
|
|
2012
|
-
};
|
|
2013
|
-
}
|
|
2014
|
-
return leafOperand;
|
|
2015
|
-
}
|
|
2016
|
-
const splitFieldArray = field.split('.');
|
|
2017
|
-
const nextField = splitFieldArray.shift();
|
|
2018
|
-
if (!nextField) {
|
|
2019
|
-
console.warn('Invalid field path:', field);
|
|
2020
|
-
return leafOperand;
|
|
2021
|
-
}
|
|
2022
|
-
const currentPath = parentPath ? `${parentPath}.${nextField}` : nextField;
|
|
2023
|
-
return {
|
|
2024
|
-
operator: 'nested',
|
|
2025
|
-
path: currentPath,
|
|
2026
|
-
operand: buildNestedFilterForOperation(splitFieldArray.join('.'), leafOperand, currentPath)
|
|
1677
|
+
name: newName,
|
|
1678
|
+
id: newId,
|
|
1679
|
+
filters: filters ?? {},
|
|
1680
|
+
modified: false,
|
|
1681
|
+
saved: false,
|
|
1682
|
+
createdDatetime: ts,
|
|
1683
|
+
modifiedDatetime: ts,
|
|
1684
|
+
counts: {}
|
|
2027
1685
|
};
|
|
2028
1686
|
};
|
|
2029
|
-
|
|
2030
|
-
const
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
'so',
|
|
2050
|
-
'the',
|
|
2051
|
-
'to',
|
|
2052
|
-
'up',
|
|
2053
|
-
'yet'
|
|
2054
|
-
];
|
|
2055
|
-
const capitalize = (s)=>s.length > 0 ? s[0].toUpperCase() + s.slice(1) : '';
|
|
2056
|
-
const trimFirstfieldNameToLabel = (fieldName, trim = false)=>{
|
|
2057
|
-
if (trim) {
|
|
2058
|
-
const source = fieldName.slice(fieldName.indexOf('.') + 1);
|
|
2059
|
-
return fieldNameToLabel(source ? source : fieldName, 0);
|
|
2060
|
-
}
|
|
2061
|
-
return fieldNameToLabel(fieldName);
|
|
2062
|
-
};
|
|
2063
|
-
/**
|
|
2064
|
-
* Converts a filter name to a title,
|
|
2065
|
-
* For example files.input.experimental_strategy will get converted to Experimental Strategy
|
|
2066
|
-
* if sections == 2 then the output would be Input Experimental Strategy
|
|
2067
|
-
* @param fieldName input filter expected to be: string.firstpart_secondpart
|
|
2068
|
-
* @param sections number of "sections" string.string.string to got back from the end of the field
|
|
2069
|
-
*/ const fieldNameToLabel = (fieldName, sections = 1)=>{
|
|
2070
|
-
if (fieldName === undefined) return 'No Title';
|
|
2071
|
-
return fieldName.split('.').slice(-sections).map((s)=>s.split('_')).flat().map((word)=>COMMON_PREPOSITIONS.includes(word) ? word : capitalize(word)).map((word)=>word in FieldNameOverrides ? FieldNameOverrides[word] : word).join(' ');
|
|
2072
|
-
};
|
|
2073
|
-
/**
|
|
2074
|
-
* Extracts the index name from the field name
|
|
2075
|
-
* @param fieldName
|
|
2076
|
-
*/ const extractIndexFromFullFieldName = (fieldName)=>fieldName.split('.')[0];
|
|
2077
|
-
/**
|
|
2078
|
-
* prepend the index name to the field name
|
|
2079
|
-
*/ const prependIndexToFieldName = (fieldName, index)=>`${index}.${fieldName}`;
|
|
2080
|
-
/**
|
|
2081
|
-
* extract the field name from the index.field name
|
|
2082
|
-
*/ const extractFieldNameFromFullFieldName = (fieldName)=>fieldName.split('.').slice(1).join('.');
|
|
2083
|
-
/**
|
|
2084
|
-
* extract the field name and the index from the index.field name returning as a tuple
|
|
2085
|
-
*/ const extractIndexAndFieldNameFromFullFieldName = (fieldName)=>{
|
|
2086
|
-
const [index, ...rest] = fieldName.split('.');
|
|
2087
|
-
return [
|
|
2088
|
-
index,
|
|
2089
|
-
rest.join('.')
|
|
2090
|
-
];
|
|
2091
|
-
};
|
|
2092
|
-
|
|
1687
|
+
const nanoid = customAlphabet('1234567890abcdef', 16);
|
|
1688
|
+
const createCohortId = ()=>nanoid();
|
|
1689
|
+
const cohortsAdapter = createEntityAdapter({
|
|
1690
|
+
sortComparer: (a, b)=>{
|
|
1691
|
+
if (a.modifiedDatetime <= b.modifiedDatetime) return 1;
|
|
1692
|
+
else return -1;
|
|
1693
|
+
},
|
|
1694
|
+
selectId: (cohort)=>cohort.id
|
|
1695
|
+
});
|
|
1696
|
+
// Create an initial unsaved cohort
|
|
1697
|
+
const initialCohort = newCohort({
|
|
1698
|
+
customName: DEFAULT_COHORT_NAME
|
|
1699
|
+
});
|
|
1700
|
+
const emptyInitialState = cohortsAdapter.getInitialState({
|
|
1701
|
+
currentCohortId: initialCohort.id,
|
|
1702
|
+
message: undefined
|
|
1703
|
+
});
|
|
1704
|
+
// Set the initial cohort in the adapter state
|
|
1705
|
+
const initialState$7 = cohortsAdapter.setOne(emptyInitialState, initialCohort);
|
|
1706
|
+
const getCurrentCohortId = (state)=>state.currentCohortId;
|
|
2093
1707
|
/**
|
|
2094
|
-
*
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
const splitFieldArray = field.split('.');
|
|
2110
|
-
const nextField = splitFieldArray.shift();
|
|
2111
|
-
if (!nextField) {
|
|
2112
|
-
console.warn('Invalid field path:', field);
|
|
2113
|
-
return leafOperand;
|
|
2114
|
-
}
|
|
2115
|
-
const currentPath = parentPath ? `${parentPath}.${nextField}` : nextField;
|
|
2116
|
-
return {
|
|
2117
|
-
nested: {
|
|
2118
|
-
path: currentPath,
|
|
2119
|
-
...buildNestedGQLFilter(splitFieldArray.join('.'), leafOperand, currentPath)
|
|
2120
|
-
}
|
|
2121
|
-
};
|
|
2122
|
-
};
|
|
2123
|
-
const buildCohortGqlOperator = (fs)=>{
|
|
2124
|
-
if (!fs || !fs.root) return undefined;
|
|
2125
|
-
const fsKeys = Object.keys(fs.root);
|
|
2126
|
-
// if no keys return undefined
|
|
2127
|
-
if (fsKeys.length === 0) return undefined;
|
|
2128
|
-
// TODO consider changing FilterSet: mode to support joinOrToAll as FilterSet mode
|
|
2129
|
-
// find key using keyword "joinOrToAll"
|
|
2130
|
-
const joinOrToAllKey = fsKeys.filter((x)=>x.includes('joinOrToAll'));
|
|
2131
|
-
switch(fs.mode){
|
|
2132
|
-
case 'and':
|
|
2133
|
-
if (joinOrToAllKey.length === 1) {
|
|
2134
|
-
const firstJoinOrToAllKey = joinOrToAllKey[0];
|
|
2135
|
-
// Remove firstJoinOrToAllKey from Array
|
|
2136
|
-
fsKeys.splice(fsKeys.indexOf(firstJoinOrToAllKey), 1);
|
|
2137
|
-
const firstJoinOrToAllObj = fs.root[firstJoinOrToAllKey];
|
|
2138
|
-
// make sure type is or/ Union
|
|
2139
|
-
if (firstJoinOrToAllObj.operator === 'or') {
|
|
2140
|
-
return {
|
|
2141
|
-
or: firstJoinOrToAllObj?.operands.map((orObj)=>{
|
|
2142
|
-
// go through each or statement and add all other filters to it
|
|
2143
|
-
return {
|
|
2144
|
-
and: [
|
|
2145
|
-
convertFilterToNestedGqlFilter(orObj),
|
|
2146
|
-
...fsKeys.map((k)=>{
|
|
2147
|
-
return convertFilterToNestedGqlFilter(fs.root[k]);
|
|
2148
|
-
})
|
|
2149
|
-
]
|
|
2150
|
-
};
|
|
2151
|
-
})
|
|
2152
|
-
};
|
|
2153
|
-
} else {
|
|
2154
|
-
console.error(`function buildCohortGqlOperator expecting "or" received "${firstJoinOrToAllObj.operator}" on key "${firstJoinOrToAllKey}"`);
|
|
2155
|
-
}
|
|
2156
|
-
} else if (joinOrToAllKey.length > 1) {
|
|
2157
|
-
console.error(`function buildCohortGqlOperator expecting only one joinOrToAll received: ${joinOrToAllKey.length}`, fsKeys);
|
|
1708
|
+
* Redux slice for cohort filters
|
|
1709
|
+
*/ const cohortManagerSlice = createSlice({
|
|
1710
|
+
name: 'cohort',
|
|
1711
|
+
initialState: initialState$7,
|
|
1712
|
+
reducers: {
|
|
1713
|
+
createNewCohort: (state, action)=>{
|
|
1714
|
+
const baseName = action.payload.name || `Cohort`;
|
|
1715
|
+
const uniqueName = generateUniqueName(Object.values(state.entities), baseName);
|
|
1716
|
+
const cohort = newCohort({
|
|
1717
|
+
filters: action.payload.filters,
|
|
1718
|
+
customName: uniqueName
|
|
1719
|
+
});
|
|
1720
|
+
cohortsAdapter.addOne(state, cohort);
|
|
1721
|
+
if (action.payload?.setAsCurrent) {
|
|
1722
|
+
state.currentCohortId = cohort.id;
|
|
2158
1723
|
}
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
return convertFilterToGqlFilter(fs.root[k]);
|
|
2169
|
-
})
|
|
2170
|
-
};
|
|
2171
|
-
}
|
|
2172
|
-
};
|
|
2173
|
-
/**
|
|
2174
|
-
* Merged two FilterSets returning the merged pair.
|
|
2175
|
-
* @param a - first FilterSet
|
|
2176
|
-
* @param b - other FilterSet
|
|
2177
|
-
*/ const joinFilters = (a, b)=>{
|
|
2178
|
-
return {
|
|
2179
|
-
mode: a.mode,
|
|
2180
|
-
root: {
|
|
2181
|
-
...a.root,
|
|
2182
|
-
...b.root
|
|
2183
|
-
}
|
|
2184
|
-
};
|
|
2185
|
-
};
|
|
2186
|
-
const convertFilterSetToOperation = (fs)=>{
|
|
2187
|
-
if (!fs) return undefined;
|
|
2188
|
-
switch(fs.mode){
|
|
2189
|
-
case 'and':
|
|
2190
|
-
return Object.keys(fs.root).length == 0 ? undefined : {
|
|
2191
|
-
operator: fs.mode,
|
|
2192
|
-
operands: Object.keys(fs.root).map((k)=>{
|
|
2193
|
-
return fs.root[k];
|
|
2194
|
-
})
|
|
2195
|
-
};
|
|
2196
|
-
}
|
|
2197
|
-
return undefined;
|
|
2198
|
-
};
|
|
2199
|
-
const extractContents = (filter)=>{
|
|
2200
|
-
if (isGQLUnion(filter)) {
|
|
2201
|
-
return filter.or;
|
|
2202
|
-
}
|
|
2203
|
-
if (isGQLIntersection(filter)) {
|
|
2204
|
-
return filter.and;
|
|
2205
|
-
}
|
|
2206
|
-
return undefined;
|
|
2207
|
-
};
|
|
2208
|
-
class ToGqlAllNested {
|
|
2209
|
-
constructor(){
|
|
2210
|
-
this.handleEquals = (op)=>{
|
|
2211
|
-
if (op.field.includes('.')) {
|
|
2212
|
-
const leafField = op.field.split('.').at(-1) ?? 'unset';
|
|
2213
|
-
return buildNestedGQLFilter(op.field, {
|
|
2214
|
-
'=': {
|
|
2215
|
-
[leafField]: op.operand
|
|
2216
|
-
}
|
|
2217
|
-
});
|
|
2218
|
-
}
|
|
2219
|
-
return {
|
|
2220
|
-
'=': {
|
|
2221
|
-
[op.field]: op.operand
|
|
1724
|
+
},
|
|
1725
|
+
updateCohortName: (state, action)=>{
|
|
1726
|
+
const { id, name } = action.payload;
|
|
1727
|
+
cohortsAdapter.updateOne(state, {
|
|
1728
|
+
id: id,
|
|
1729
|
+
changes: {
|
|
1730
|
+
name: name,
|
|
1731
|
+
modified: true,
|
|
1732
|
+
modifiedDatetime: new Date().toISOString()
|
|
2222
1733
|
}
|
|
2223
|
-
};
|
|
2224
|
-
}
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
1734
|
+
});
|
|
1735
|
+
},
|
|
1736
|
+
removeCohort: (state, action)=>{
|
|
1737
|
+
const { id: cohortId } = action.payload;
|
|
1738
|
+
const removedCohortName = state.entities[cohortId].name;
|
|
1739
|
+
const totalCohorts = Object.keys(state.entities).length;
|
|
1740
|
+
if (totalCohorts <= 1) {
|
|
1741
|
+
cohortsAdapter.removeAll(state);
|
|
1742
|
+
const defaultCohort = newCohort({
|
|
1743
|
+
filters: {},
|
|
1744
|
+
customName: DEFAULT_COHORT_NAME
|
|
2232
1745
|
});
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
1746
|
+
cohortsAdapter.addOne(state, defaultCohort);
|
|
1747
|
+
state.currentCohortId = defaultCohort.id;
|
|
1748
|
+
if (action?.payload.shouldShowMessage) {
|
|
1749
|
+
state.message = [
|
|
1750
|
+
`deleteCohort|${removedCohortName}|${state.currentCohortId}`
|
|
1751
|
+
];
|
|
2237
1752
|
}
|
|
2238
|
-
|
|
2239
|
-
};
|
|
2240
|
-
this.handleLessThan = (op)=>{
|
|
2241
|
-
if (op.field.includes('.')) {
|
|
2242
|
-
const leafField = op.field.split('.').at(-1) ?? 'unset';
|
|
2243
|
-
return buildNestedGQLFilter(op.field, {
|
|
2244
|
-
'<': {
|
|
2245
|
-
[leafField]: op.operand
|
|
2246
|
-
}
|
|
2247
|
-
});
|
|
1753
|
+
return;
|
|
2248
1754
|
}
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
};
|
|
2255
|
-
this.handleLessThanOrEquals = (op)=>{
|
|
2256
|
-
if (op.field.includes('.')) {
|
|
2257
|
-
const leafField = op.field.split('.').at(-1) ?? 'unset';
|
|
2258
|
-
return buildNestedGQLFilter(op.field, {
|
|
2259
|
-
'<=': {
|
|
2260
|
-
[leafField]: op.operand
|
|
2261
|
-
}
|
|
2262
|
-
});
|
|
1755
|
+
cohortsAdapter.removeOne(state, cohortId);
|
|
1756
|
+
// deleted the current cohort so set to the most recent cohort
|
|
1757
|
+
if (state.currentCohortId === cohortId) {
|
|
1758
|
+
const remainingIds = Object.keys(state.entities);
|
|
1759
|
+
state.currentCohortId = remainingIds[0];
|
|
2263
1760
|
}
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
};
|
|
2269
|
-
};
|
|
2270
|
-
this.handleGreaterThan = (op)=>{
|
|
2271
|
-
if (op.field.includes('.')) {
|
|
2272
|
-
const leafField = op.field.split('.').at(-1) ?? 'unset';
|
|
2273
|
-
return buildNestedGQLFilter(op.field, {
|
|
2274
|
-
'>': {
|
|
2275
|
-
[leafField]: op.operand
|
|
2276
|
-
}
|
|
2277
|
-
});
|
|
1761
|
+
if (action?.payload.shouldShowMessage) {
|
|
1762
|
+
state.message = [
|
|
1763
|
+
`deleteCohort|${removedCohortName}|${state.currentCohortId}`
|
|
1764
|
+
];
|
|
2278
1765
|
}
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
if (op.field.includes('.')) {
|
|
2287
|
-
const leafField = op.field.split('.').at(-1) ?? 'unset';
|
|
2288
|
-
return buildNestedGQLFilter(op.field, {
|
|
2289
|
-
'>=': {
|
|
2290
|
-
[leafField]: op.operand
|
|
2291
|
-
}
|
|
2292
|
-
});
|
|
1766
|
+
},
|
|
1767
|
+
// adds a filter to the cohort filter set at the given index
|
|
1768
|
+
updateCohortFilter: (state, action)=>{
|
|
1769
|
+
const { index, field, filter } = action.payload;
|
|
1770
|
+
const currentCohortId = getCurrentCohortId(state);
|
|
1771
|
+
if (!state.entities[currentCohortId]) {
|
|
1772
|
+
return;
|
|
2293
1773
|
}
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
1774
|
+
cohortsAdapter.updateOne(state, {
|
|
1775
|
+
id: currentCohortId,
|
|
1776
|
+
changes: {
|
|
1777
|
+
filters: {
|
|
1778
|
+
...state.entities[currentCohortId].filters,
|
|
1779
|
+
[index]: {
|
|
1780
|
+
mode: state.entities[currentCohortId]?.filters[index]?.mode ?? 'and',
|
|
1781
|
+
root: {
|
|
1782
|
+
...state.entities[currentCohortId]?.filters[index]?.root,
|
|
1783
|
+
[field]: filter
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
},
|
|
1787
|
+
modified: true,
|
|
1788
|
+
modifiedDatetime: new Date().toISOString()
|
|
2297
1789
|
}
|
|
2298
|
-
};
|
|
2299
|
-
}
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
}
|
|
2307
|
-
});
|
|
1790
|
+
});
|
|
1791
|
+
},
|
|
1792
|
+
setCohortFilter: (state, action)=>{
|
|
1793
|
+
const { index, filters } = action.payload;
|
|
1794
|
+
const currentCohortId = getCurrentCohortId(state);
|
|
1795
|
+
if (!state.entities[currentCohortId]) {
|
|
1796
|
+
console.error(`no cohort with id=${currentCohortId} defined`);
|
|
1797
|
+
return;
|
|
2308
1798
|
}
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
1799
|
+
cohortsAdapter.updateOne(state, {
|
|
1800
|
+
id: currentCohortId,
|
|
1801
|
+
changes: {
|
|
1802
|
+
filters: {
|
|
1803
|
+
...state.entities[currentCohortId].filters,
|
|
1804
|
+
[index]: filters
|
|
1805
|
+
},
|
|
1806
|
+
modified: true,
|
|
1807
|
+
modifiedDatetime: new Date().toISOString()
|
|
2312
1808
|
}
|
|
2313
|
-
};
|
|
2314
|
-
}
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
[leafField]: op.operands
|
|
2321
|
-
}
|
|
2322
|
-
});
|
|
1809
|
+
});
|
|
1810
|
+
},
|
|
1811
|
+
setCohortIndexFilters: (state, action)=>{
|
|
1812
|
+
const currentCohortId = getCurrentCohortId(state);
|
|
1813
|
+
if (!state.entities[currentCohortId]) {
|
|
1814
|
+
console.error(`no cohort with id=${currentCohortId} defined`);
|
|
1815
|
+
return;
|
|
2323
1816
|
}
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
1817
|
+
cohortsAdapter.updateOne(state, {
|
|
1818
|
+
id: currentCohortId,
|
|
1819
|
+
changes: {
|
|
1820
|
+
filters: action.payload.filters,
|
|
1821
|
+
modified: true,
|
|
1822
|
+
modifiedDatetime: new Date().toISOString()
|
|
2327
1823
|
}
|
|
2328
|
-
};
|
|
2329
|
-
};
|
|
2330
|
-
this.handleExcludeIfAny = (op)=>{
|
|
2331
|
-
if (op.field.includes('.')) {
|
|
2332
|
-
const leafField = op.field.split('.').at(-1) ?? 'unset';
|
|
2333
|
-
return buildNestedGQLFilter(op.field, {
|
|
2334
|
-
excludeifany: {
|
|
2335
|
-
[leafField]: op.operands
|
|
2336
|
-
}
|
|
2337
|
-
});
|
|
2338
|
-
}
|
|
2339
|
-
return {
|
|
2340
|
-
excludeifany: {
|
|
2341
|
-
[op.field]: op.operands
|
|
2342
|
-
}
|
|
2343
|
-
};
|
|
2344
|
-
};
|
|
2345
|
-
this.handleIntersection = (op)=>({
|
|
2346
|
-
and: op.operands.map((x)=>convertFilterToGqlFilter(x))
|
|
2347
|
-
});
|
|
2348
|
-
this.handleUnion = (op)=>({
|
|
2349
|
-
or: op.operands.map((x)=>convertFilterToGqlFilter(x))
|
|
2350
1824
|
});
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
}
|
|
2360
|
-
return {
|
|
2361
|
-
is: {
|
|
2362
|
-
[op.field]: 'MISSING'
|
|
2363
|
-
}
|
|
2364
|
-
};
|
|
2365
|
-
};
|
|
2366
|
-
this.handleExists = (op)=>{
|
|
2367
|
-
if (op.field.includes('.')) {
|
|
2368
|
-
const leafField = op.field.split('.').at(-1) ?? 'unset';
|
|
2369
|
-
return buildNestedGQLFilter(op.field, {
|
|
2370
|
-
not: {
|
|
2371
|
-
[leafField]: op?.operand ?? null
|
|
2372
|
-
}
|
|
2373
|
-
});
|
|
1825
|
+
},
|
|
1826
|
+
// removes a filter to the cohort filter set at the given index
|
|
1827
|
+
removeCohortFilter: (state, action)=>{
|
|
1828
|
+
const { index, field } = action.payload;
|
|
1829
|
+
const currentCohortId = getCurrentCohortId(state);
|
|
1830
|
+
if (!state.entities[currentCohortId]) {
|
|
1831
|
+
console.error(`no cohort with id=${currentCohortId} defined`);
|
|
1832
|
+
return;
|
|
2374
1833
|
}
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
}
|
|
2379
|
-
};
|
|
2380
|
-
};
|
|
2381
|
-
this.handleNestedFilter = (op)=>{
|
|
2382
|
-
const child = convertFilterToGqlFilter(op.operand);
|
|
2383
|
-
return {
|
|
2384
|
-
nested: {
|
|
2385
|
-
path: op.path,
|
|
2386
|
-
...child
|
|
2387
|
-
}
|
|
2388
|
-
};
|
|
2389
|
-
};
|
|
2390
|
-
}
|
|
2391
|
-
}
|
|
2392
|
-
const convertFilterToNestedGqlFilter = (filter)=>{
|
|
2393
|
-
const handler = new ToGqlAllNested();
|
|
2394
|
-
return handleOperation(handler, filter);
|
|
2395
|
-
};
|
|
2396
|
-
const convertFilterSetToNestedGqlFilter = (fs, toplevelOp = 'and')=>{
|
|
2397
|
-
const fsKeys = Object.keys(fs.root);
|
|
2398
|
-
// if no keys return undefined
|
|
2399
|
-
if (fsKeys.length === 0) return {
|
|
2400
|
-
and: []
|
|
2401
|
-
};
|
|
2402
|
-
return toplevelOp === 'and' ? {
|
|
2403
|
-
and: fsKeys.map((key)=>convertFilterToNestedGqlFilter(fs.root[key]))
|
|
2404
|
-
} : {
|
|
2405
|
-
or: fsKeys.map((key)=>convertFilterToNestedGqlFilter(fs.root[key]))
|
|
2406
|
-
};
|
|
2407
|
-
};
|
|
2408
|
-
|
|
2409
|
-
const defaultCohortNameGenerator = ()=>`Custom cohort ${new Date().toLocaleString('en-CA', {
|
|
2410
|
-
timeZone: 'America/Chicago',
|
|
2411
|
-
hour12: false
|
|
2412
|
-
}).replace(',', '')}`;
|
|
2413
|
-
const isNameUnique = (entities, name, excludeId)=>{
|
|
2414
|
-
const trimmedName = name.trim();
|
|
2415
|
-
if (!trimmedName) return false;
|
|
2416
|
-
return !entities.some((cohort)=>cohort && cohort.id !== excludeId && cohort.name.trim().toLowerCase() === trimmedName.toLowerCase());
|
|
2417
|
-
};
|
|
2418
|
-
const generateUniqueName = (entities, baseName)=>{
|
|
2419
|
-
const trimmedBaseName = baseName.trim();
|
|
2420
|
-
// If base name is unique, use it
|
|
2421
|
-
if (isNameUnique(entities, trimmedBaseName)) {
|
|
2422
|
-
return trimmedBaseName;
|
|
2423
|
-
}
|
|
2424
|
-
// Find a unique name by appending numbers
|
|
2425
|
-
let counter = 1;
|
|
2426
|
-
let uniqueName;
|
|
2427
|
-
do {
|
|
2428
|
-
uniqueName = `${trimmedBaseName} (${counter})`;
|
|
2429
|
-
counter++;
|
|
2430
|
-
}while (!isNameUnique(entities, uniqueName))
|
|
2431
|
-
return uniqueName;
|
|
2432
|
-
};
|
|
2433
|
-
/**
|
|
2434
|
-
* This function takes a FilterSet object and a prefix string as input.
|
|
2435
|
-
* It filters the root property of the FilterSet object and returns a
|
|
2436
|
-
* new FilterSet object that only contains filters with field names
|
|
2437
|
-
* that start with the specified prefix.
|
|
2438
|
-
*
|
|
2439
|
-
* @param fs - The FilterSet object to filter
|
|
2440
|
-
* @param prefix - The prefix to filter by
|
|
2441
|
-
* @returns - A new FilterSet object that only contains filters with field names that start with the specified prefix
|
|
2442
|
-
* @category Filters
|
|
2443
|
-
*/ const extractFiltersWithPrefixFromFilterSet = (fs, prefix)=>{
|
|
2444
|
-
if (fs === undefined || fs.root === undefined) {
|
|
2445
|
-
return {
|
|
2446
|
-
mode: 'and',
|
|
2447
|
-
root: {}
|
|
2448
|
-
};
|
|
2449
|
-
}
|
|
2450
|
-
return Object.values(fs.root).reduce((acc, filter)=>{
|
|
2451
|
-
if (isIntersectionOrUnion(filter) || isNestedFilter(filter)) return acc;
|
|
2452
|
-
if (filter.field.startsWith(prefix)) {
|
|
2453
|
-
acc.root[filter.field] = filter;
|
|
2454
|
-
}
|
|
2455
|
-
return acc;
|
|
2456
|
-
}, {
|
|
2457
|
-
mode: 'and',
|
|
2458
|
-
root: {}
|
|
2459
|
-
});
|
|
2460
|
-
};
|
|
2461
|
-
|
|
2462
|
-
/**
|
|
2463
|
-
* Cohorts in Gen3 are defined as a set of filters for each index in the data.
|
|
2464
|
-
* This means one cohort id defined for all "tabs" in CohortBuilder (explorer)
|
|
2465
|
-
* Switching a cohort id means all the cohorts for the index are changed.
|
|
2466
|
-
*/ const DEFAULT_COHORT_NAME = 'Cohort';
|
|
2467
|
-
const newCohort = ({ filters = {}, customName })=>{
|
|
2468
|
-
const ts = new Date().toISOString();
|
|
2469
|
-
const newName = customName ?? defaultCohortNameGenerator();
|
|
2470
|
-
const newId = createCohortId();
|
|
2471
|
-
return {
|
|
2472
|
-
name: newName,
|
|
2473
|
-
id: newId,
|
|
2474
|
-
filters: filters ?? {},
|
|
2475
|
-
modified: false,
|
|
2476
|
-
saved: false,
|
|
2477
|
-
createdDatetime: ts,
|
|
2478
|
-
modifiedDatetime: ts,
|
|
2479
|
-
counts: {}
|
|
2480
|
-
};
|
|
2481
|
-
};
|
|
2482
|
-
const nanoid = customAlphabet('1234567890abcdef', 16);
|
|
2483
|
-
const createCohortId = ()=>nanoid();
|
|
2484
|
-
const cohortsAdapter = createEntityAdapter({
|
|
2485
|
-
sortComparer: (a, b)=>{
|
|
2486
|
-
if (a.modifiedDatetime <= b.modifiedDatetime) return 1;
|
|
2487
|
-
else return -1;
|
|
2488
|
-
},
|
|
2489
|
-
selectId: (cohort)=>cohort.id
|
|
2490
|
-
});
|
|
2491
|
-
// Create an initial unsaved cohort
|
|
2492
|
-
const initialCohort = newCohort({
|
|
2493
|
-
customName: DEFAULT_COHORT_NAME
|
|
2494
|
-
});
|
|
2495
|
-
const emptyInitialState = cohortsAdapter.getInitialState({
|
|
2496
|
-
currentCohortId: initialCohort.id,
|
|
2497
|
-
message: undefined
|
|
2498
|
-
});
|
|
2499
|
-
// Set the initial cohort in the adapter state
|
|
2500
|
-
const initialState$7 = cohortsAdapter.setOne(emptyInitialState, initialCohort);
|
|
2501
|
-
const getCurrentCohortId = (state)=>state.currentCohortId;
|
|
2502
|
-
/**
|
|
2503
|
-
* Redux slice for cohort filters
|
|
2504
|
-
*/ const cohortManagerSlice = createSlice({
|
|
2505
|
-
name: 'cohort',
|
|
2506
|
-
initialState: initialState$7,
|
|
2507
|
-
reducers: {
|
|
2508
|
-
createNewCohort: (state, action)=>{
|
|
2509
|
-
const baseName = action.payload.name || `Cohort`;
|
|
2510
|
-
const uniqueName = generateUniqueName(Object.values(state.entities), baseName);
|
|
2511
|
-
const cohort = newCohort({
|
|
2512
|
-
filters: action.payload.filters,
|
|
2513
|
-
customName: uniqueName
|
|
2514
|
-
});
|
|
2515
|
-
cohortsAdapter.addOne(state, cohort);
|
|
2516
|
-
if (action.payload?.setAsCurrent) {
|
|
2517
|
-
state.currentCohortId = cohort.id;
|
|
1834
|
+
const filters = state.entities[currentCohortId]?.filters[index]?.root;
|
|
1835
|
+
if (!filters) {
|
|
1836
|
+
return;
|
|
2518
1837
|
}
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
const { id, name } = action.payload;
|
|
1838
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1839
|
+
const { [field]: _a, ...updated } = filters;
|
|
2522
1840
|
cohortsAdapter.updateOne(state, {
|
|
2523
|
-
id:
|
|
1841
|
+
id: currentCohortId,
|
|
2524
1842
|
changes: {
|
|
2525
|
-
|
|
1843
|
+
filters: {
|
|
1844
|
+
...state.entities[currentCohortId]?.filters,
|
|
1845
|
+
[index]: {
|
|
1846
|
+
mode: state.entities[currentCohortId].filters[index].mode,
|
|
1847
|
+
root: updated
|
|
1848
|
+
}
|
|
1849
|
+
},
|
|
2526
1850
|
modified: true,
|
|
2527
1851
|
modifiedDatetime: new Date().toISOString()
|
|
2528
1852
|
}
|
|
2529
1853
|
});
|
|
2530
1854
|
},
|
|
2531
|
-
|
|
2532
|
-
const
|
|
2533
|
-
const
|
|
2534
|
-
const
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
`deleteCohort|${removedCohortName}|${state.currentCohortId}`
|
|
2546
|
-
];
|
|
1855
|
+
duplicateCohort: (state)=>{
|
|
1856
|
+
const currentCohortId = getCurrentCohortId(state);
|
|
1857
|
+
const currentCohort = state.entities[currentCohortId];
|
|
1858
|
+
const newName = generateUniqueName(Object.values(state.entities), currentCohort.name);
|
|
1859
|
+
const duplicatedCohort = newCohort({
|
|
1860
|
+
filters: {
|
|
1861
|
+
...currentCohort.filters
|
|
1862
|
+
},
|
|
1863
|
+
customName: newName
|
|
1864
|
+
});
|
|
1865
|
+
cohortsAdapter.addOne(state, {
|
|
1866
|
+
...duplicatedCohort,
|
|
1867
|
+
counts: {
|
|
1868
|
+
...currentCohort.counts
|
|
2547
1869
|
}
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
cohortsAdapter.removeOne(state, cohortId);
|
|
2551
|
-
// deleted the current cohort so set to the most recent cohort
|
|
2552
|
-
if (state.currentCohortId === cohortId) {
|
|
2553
|
-
const remainingIds = Object.keys(state.entities);
|
|
2554
|
-
state.currentCohortId = remainingIds[0];
|
|
2555
|
-
}
|
|
2556
|
-
if (action?.payload.shouldShowMessage) {
|
|
2557
|
-
state.message = [
|
|
2558
|
-
`deleteCohort|${removedCohortName}|${state.currentCohortId}`
|
|
2559
|
-
];
|
|
2560
|
-
}
|
|
1870
|
+
});
|
|
1871
|
+
state.currentCohortId = duplicatedCohort.id;
|
|
2561
1872
|
},
|
|
2562
|
-
//
|
|
2563
|
-
|
|
2564
|
-
const { index
|
|
1873
|
+
// removes all filters from the cohort filter set at the given index
|
|
1874
|
+
clearCohortFilters: (state, action)=>{
|
|
1875
|
+
const { index } = action.payload;
|
|
2565
1876
|
const currentCohortId = getCurrentCohortId(state);
|
|
2566
1877
|
if (!state.entities[currentCohortId]) {
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
filters: {
|
|
2573
|
-
...state.entities[currentCohortId].filters,
|
|
2574
|
-
[index]: {
|
|
2575
|
-
mode: state.entities[currentCohortId]?.filters[index]?.mode ?? 'and',
|
|
2576
|
-
root: {
|
|
2577
|
-
...state.entities[currentCohortId]?.filters[index]?.root,
|
|
2578
|
-
[field]: filter
|
|
2579
|
-
}
|
|
2580
|
-
}
|
|
2581
|
-
},
|
|
2582
|
-
modified: true,
|
|
2583
|
-
modifiedDatetime: new Date().toISOString()
|
|
2584
|
-
}
|
|
2585
|
-
});
|
|
2586
|
-
},
|
|
2587
|
-
setCohortFilter: (state, action)=>{
|
|
2588
|
-
const { index, filters } = action.payload;
|
|
2589
|
-
const currentCohortId = getCurrentCohortId(state);
|
|
2590
|
-
if (!state.entities[currentCohortId]) {
|
|
2591
|
-
console.error(`no cohort with id=${currentCohortId} defined`);
|
|
2592
|
-
return;
|
|
2593
|
-
}
|
|
2594
|
-
cohortsAdapter.updateOne(state, {
|
|
2595
|
-
id: currentCohortId,
|
|
2596
|
-
changes: {
|
|
2597
|
-
filters: {
|
|
2598
|
-
...state.entities[currentCohortId].filters,
|
|
2599
|
-
[index]: filters
|
|
2600
|
-
},
|
|
2601
|
-
modified: true,
|
|
2602
|
-
modifiedDatetime: new Date().toISOString()
|
|
2603
|
-
}
|
|
2604
|
-
});
|
|
2605
|
-
},
|
|
2606
|
-
setCohortIndexFilters: (state, action)=>{
|
|
2607
|
-
const currentCohortId = getCurrentCohortId(state);
|
|
2608
|
-
if (!state.entities[currentCohortId]) {
|
|
2609
|
-
console.error(`no cohort with id=${currentCohortId} defined`);
|
|
2610
|
-
return;
|
|
2611
|
-
}
|
|
2612
|
-
cohortsAdapter.updateOne(state, {
|
|
2613
|
-
id: currentCohortId,
|
|
2614
|
-
changes: {
|
|
2615
|
-
filters: action.payload.filters,
|
|
2616
|
-
modified: true,
|
|
2617
|
-
modifiedDatetime: new Date().toISOString()
|
|
2618
|
-
}
|
|
2619
|
-
});
|
|
2620
|
-
},
|
|
2621
|
-
// removes a filter to the cohort filter set at the given index
|
|
2622
|
-
removeCohortFilter: (state, action)=>{
|
|
2623
|
-
const { index, field } = action.payload;
|
|
2624
|
-
const currentCohortId = getCurrentCohortId(state);
|
|
2625
|
-
if (!state.entities[currentCohortId]) {
|
|
2626
|
-
console.error(`no cohort with id=${currentCohortId} defined`);
|
|
2627
|
-
return;
|
|
2628
|
-
}
|
|
2629
|
-
const filters = state.entities[currentCohortId]?.filters[index]?.root;
|
|
2630
|
-
if (!filters) {
|
|
2631
|
-
return;
|
|
2632
|
-
}
|
|
2633
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
2634
|
-
const { [field]: _a, ...updated } = filters;
|
|
2635
|
-
cohortsAdapter.updateOne(state, {
|
|
2636
|
-
id: currentCohortId,
|
|
2637
|
-
changes: {
|
|
2638
|
-
filters: {
|
|
2639
|
-
...state.entities[currentCohortId]?.filters,
|
|
2640
|
-
[index]: {
|
|
2641
|
-
mode: state.entities[currentCohortId].filters[index].mode,
|
|
2642
|
-
root: updated
|
|
2643
|
-
}
|
|
2644
|
-
},
|
|
2645
|
-
modified: true,
|
|
2646
|
-
modifiedDatetime: new Date().toISOString()
|
|
2647
|
-
}
|
|
2648
|
-
});
|
|
2649
|
-
},
|
|
2650
|
-
duplicateCohort: (state)=>{
|
|
2651
|
-
const currentCohortId = getCurrentCohortId(state);
|
|
2652
|
-
const currentCohort = state.entities[currentCohortId];
|
|
2653
|
-
const newName = generateUniqueName(Object.values(state.entities), currentCohort.name);
|
|
2654
|
-
const duplicatedCohort = newCohort({
|
|
2655
|
-
filters: {
|
|
2656
|
-
...currentCohort.filters
|
|
2657
|
-
},
|
|
2658
|
-
customName: newName
|
|
2659
|
-
});
|
|
2660
|
-
cohortsAdapter.addOne(state, {
|
|
2661
|
-
...duplicatedCohort,
|
|
2662
|
-
counts: {
|
|
2663
|
-
...currentCohort.counts
|
|
2664
|
-
}
|
|
2665
|
-
});
|
|
2666
|
-
state.currentCohortId = duplicatedCohort.id;
|
|
2667
|
-
},
|
|
2668
|
-
// removes all filters from the cohort filter set at the given index
|
|
2669
|
-
clearCohortFilters: (state, action)=>{
|
|
2670
|
-
const { index } = action.payload;
|
|
2671
|
-
const currentCohortId = getCurrentCohortId(state);
|
|
2672
|
-
if (!state.entities[currentCohortId]) {
|
|
2673
|
-
console.error(`no cohort with id=${currentCohortId} defined`);
|
|
2674
|
-
return;
|
|
2675
|
-
}
|
|
2676
|
-
const filters = state.entities[currentCohortId]?.filters[index]?.root;
|
|
2677
|
-
if (!filters) {
|
|
1878
|
+
console.error(`no cohort with id=${currentCohortId} defined`);
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
const filters = state.entities[currentCohortId]?.filters[index]?.root;
|
|
1882
|
+
if (!filters) {
|
|
2678
1883
|
return;
|
|
2679
1884
|
}
|
|
2680
1885
|
cohortsAdapter.updateOne(state, {
|
|
@@ -3080,171 +2285,6 @@ const rootReducer = combineReducers({
|
|
|
3080
2285
|
sower: sowerReducer
|
|
3081
2286
|
});
|
|
3082
2287
|
|
|
3083
|
-
/**
|
|
3084
|
-
* Flattens a deep nested JSON object skipping
|
|
3085
|
-
* the first level to avoid potentially flattening
|
|
3086
|
-
* non-nested data.
|
|
3087
|
-
* @param {JSON} json
|
|
3088
|
-
*/ function flattenJson(json) {
|
|
3089
|
-
const flattenedJson = [];
|
|
3090
|
-
Object.keys(json).forEach((key)=>{
|
|
3091
|
-
flattenedJson.push(flatten(json[key], {
|
|
3092
|
-
delimiter: '_'
|
|
3093
|
-
}));
|
|
3094
|
-
});
|
|
3095
|
-
return flattenedJson;
|
|
3096
|
-
}
|
|
3097
|
-
/**
|
|
3098
|
-
* Converts JSON based on a config.
|
|
3099
|
-
* @param {JSON} json
|
|
3100
|
-
* @param {Object} config
|
|
3101
|
-
*/ async function conversion(json, config) {
|
|
3102
|
-
return Papa.unparse(json, config);
|
|
3103
|
-
}
|
|
3104
|
-
/**
|
|
3105
|
-
* Converts JSON to a specified file format.
|
|
3106
|
-
* Defaults to JSON if file format is not supported.
|
|
3107
|
-
* @param {JSON} json
|
|
3108
|
-
* @param {string} format
|
|
3109
|
-
*/ async function jsonToFormat(json, format) {
|
|
3110
|
-
if (Object.keys(FILE_DELIMITERS).includes(format)) {
|
|
3111
|
-
const flatJson = flattenJson(json);
|
|
3112
|
-
const data = await conversion(flatJson, {
|
|
3113
|
-
delimiter: FILE_DELIMITERS[format]
|
|
3114
|
-
});
|
|
3115
|
-
return data;
|
|
3116
|
-
}
|
|
3117
|
-
return json;
|
|
3118
|
-
}
|
|
3119
|
-
|
|
3120
|
-
/**
|
|
3121
|
-
* Prepares a URL for downloading by appending '/download' to the provided apiUrl.
|
|
3122
|
-
*
|
|
3123
|
-
* @param {string} apiUrl - The base URL to be used for preparing the download URL.
|
|
3124
|
-
* @returns {URL} - The prepared download URL as a URL object.
|
|
3125
|
-
*/ const prepareUrl = (apiUrl)=>`${apiUrl}/download`;
|
|
3126
|
-
/**
|
|
3127
|
-
* Prepares a fetch configuration object for downloading files from Guppy.
|
|
3128
|
-
*
|
|
3129
|
-
* @param {GuppyDownloadDataParams} parameters - The parameters to include in the request body.
|
|
3130
|
-
* @param {string} csrfToken - The CSRF token to include in the request headers.
|
|
3131
|
-
* @returns {FetchConfig} - The prepared fetch configuration object.
|
|
3132
|
-
*/ const prepareFetchConfig = (parameters, csrfToken)=>{
|
|
3133
|
-
const headers = new Headers({
|
|
3134
|
-
Accept: 'application/json',
|
|
3135
|
-
'Content-Type': 'application/json',
|
|
3136
|
-
...csrfToken !== undefined && {
|
|
3137
|
-
'X-CSRF-Token': csrfToken
|
|
3138
|
-
}
|
|
3139
|
-
});
|
|
3140
|
-
if (process.env.NODE_ENV === 'development') {
|
|
3141
|
-
// NOTE: This cookie can only be accessed from the client side
|
|
3142
|
-
// in development mode. Otherwise, the cookie is set as httpOnly
|
|
3143
|
-
const accessToken = getCookie('credentials_token');
|
|
3144
|
-
if (accessToken) headers.set('Authorization', `Bearer ${accessToken}`);
|
|
3145
|
-
}
|
|
3146
|
-
return {
|
|
3147
|
-
method: 'POST',
|
|
3148
|
-
headers: headers,
|
|
3149
|
-
body: JSON.stringify({
|
|
3150
|
-
type: parameters.type,
|
|
3151
|
-
filter: convertFilterSetToGqlFilter(parameters.filter),
|
|
3152
|
-
accessibility: parameters.accessibility,
|
|
3153
|
-
fields: parameters?.fields,
|
|
3154
|
-
sort: parameters?.sort
|
|
3155
|
-
})
|
|
3156
|
-
};
|
|
3157
|
-
};
|
|
3158
|
-
/**
|
|
3159
|
-
* Downloads a file from Guppy using the provided parameters.
|
|
3160
|
-
* It will optionally convert the data to the specified format.
|
|
3161
|
-
*
|
|
3162
|
-
* @param {DownloadFromGuppyParams} parameters - The parameters to use for the download request.
|
|
3163
|
-
* @param onStart - The function to call when the download starts.
|
|
3164
|
-
* @param onDone - The function to call when the download is done.
|
|
3165
|
-
* @param onError - The function to call when the download fails.
|
|
3166
|
-
* @param onAbort - The function to call when the download is aborted.
|
|
3167
|
-
* @param signal - AbortSignal to use for the request.
|
|
3168
|
-
*/ const downloadFromGuppyToBlob = async ({ parameters, onStart = ()=>null, onDone = (_)=>null, onError = (_)=>null, onAbort = ()=>null, signal = undefined })=>{
|
|
3169
|
-
const csrfToken = selectCSRFToken(coreStore.getState());
|
|
3170
|
-
onStart?.();
|
|
3171
|
-
const url = prepareUrl(GEN3_GUPPY_API);
|
|
3172
|
-
const fetchConfig = prepareFetchConfig(parameters, csrfToken);
|
|
3173
|
-
fetch(url.toString(), {
|
|
3174
|
-
...fetchConfig,
|
|
3175
|
-
...signal ? {
|
|
3176
|
-
signal: signal
|
|
3177
|
-
} : {}
|
|
3178
|
-
}).then(async (response)=>{
|
|
3179
|
-
if (!response.ok) {
|
|
3180
|
-
throw new Error(response.statusText);
|
|
3181
|
-
}
|
|
3182
|
-
let jsonData = await response.json();
|
|
3183
|
-
if (parameters?.rootPath) {
|
|
3184
|
-
// if rootPath is provided, extract the data from the rootPath
|
|
3185
|
-
jsonData = JSONPath({
|
|
3186
|
-
json: jsonData,
|
|
3187
|
-
path: `$.[${parameters.rootPath}]`,
|
|
3188
|
-
resultType: 'value'
|
|
3189
|
-
});
|
|
3190
|
-
}
|
|
3191
|
-
// convert the data to the specified format and return a Blob
|
|
3192
|
-
let str = '';
|
|
3193
|
-
if (parameters.format === 'json') {
|
|
3194
|
-
str = JSON.stringify(jsonData);
|
|
3195
|
-
} else {
|
|
3196
|
-
const convertedData = await jsonToFormat(jsonData, parameters.format);
|
|
3197
|
-
if (isJSONObject(convertedData)) {
|
|
3198
|
-
str = JSON.stringify(convertedData, null, 2);
|
|
3199
|
-
} else {
|
|
3200
|
-
str = convertedData;
|
|
3201
|
-
}
|
|
3202
|
-
}
|
|
3203
|
-
return new Blob([
|
|
3204
|
-
str
|
|
3205
|
-
], {
|
|
3206
|
-
type: 'application/json'
|
|
3207
|
-
});
|
|
3208
|
-
}).then((blob)=>onDone?.(blob)).catch((error)=>{
|
|
3209
|
-
// Abort is handle as an exception
|
|
3210
|
-
if (error.name == 'AbortError') {
|
|
3211
|
-
// handle abort()
|
|
3212
|
-
onAbort?.();
|
|
3213
|
-
}
|
|
3214
|
-
onError?.(error);
|
|
3215
|
-
});
|
|
3216
|
-
};
|
|
3217
|
-
const downloadJSONDataFromGuppy = async ({ parameters, onAbort = ()=>null, signal = undefined })=>{
|
|
3218
|
-
const csrfToken = selectCSRFToken(coreStore.getState());
|
|
3219
|
-
const url = prepareUrl(GEN3_GUPPY_API);
|
|
3220
|
-
const fetchConfig = prepareFetchConfig(parameters, csrfToken);
|
|
3221
|
-
try {
|
|
3222
|
-
const response = await fetch(url.toString(), {
|
|
3223
|
-
...fetchConfig,
|
|
3224
|
-
...signal ? {
|
|
3225
|
-
signal: signal
|
|
3226
|
-
} : {}
|
|
3227
|
-
});
|
|
3228
|
-
let jsonData = await response.json();
|
|
3229
|
-
if (parameters?.rootPath) {
|
|
3230
|
-
// if rootPath is provided, extract the data from the rootPath
|
|
3231
|
-
jsonData = JSONPath({
|
|
3232
|
-
json: jsonData,
|
|
3233
|
-
path: `$.[${parameters.rootPath}]`,
|
|
3234
|
-
resultType: 'value'
|
|
3235
|
-
});
|
|
3236
|
-
}
|
|
3237
|
-
// convert the data to the specified format and return a Blob
|
|
3238
|
-
return jsonData;
|
|
3239
|
-
} catch (error) {
|
|
3240
|
-
// Abort is handle as an exception
|
|
3241
|
-
if (error.name == 'AbortError') {
|
|
3242
|
-
// handle abort()
|
|
3243
|
-
onAbort?.();
|
|
3244
|
-
}
|
|
3245
|
-
throw new Error(error);
|
|
3246
|
-
}
|
|
3247
|
-
};
|
|
3248
2288
|
const groupSharedFields = (data)=>{
|
|
3249
2289
|
const reverseIndex = {};
|
|
3250
2290
|
// Build reverse index: track which root keys contain each element
|
|
@@ -3882,145 +2922,940 @@ const useGetArrayTypes = ()=>{
|
|
|
3882
2922
|
if (error) {
|
|
3883
2923
|
return {};
|
|
3884
2924
|
}
|
|
3885
|
-
return data ? data['indices'] : {};
|
|
2925
|
+
return data ? data['indices'] : {};
|
|
2926
|
+
}
|
|
2927
|
+
};
|
|
2928
|
+
const useGetIndexFields = (index, indexPrefix = '')=>{
|
|
2929
|
+
const { data } = useGetFieldsForIndexQuery({
|
|
2930
|
+
index: index,
|
|
2931
|
+
indexPrefix: indexPrefix
|
|
2932
|
+
});
|
|
2933
|
+
return data ?? [];
|
|
2934
|
+
};
|
|
2935
|
+
const buildGetAggregationQuery = (type, fields, filters, accessibility = Accessibility.ALL, filterSelf = false, queryId, indexPrefix = '', filterName = 'filter')=>{
|
|
2936
|
+
const queryStart = isFilterEmpty(filters) ? `query getAggs${queryId ? `_${queryId}` : ''} {
|
|
2937
|
+
${indexPrefix}_aggregation {
|
|
2938
|
+
${type} (accessibility: ${accessibility}) {` : `query getAggs ($${filterName}: JSON) {
|
|
2939
|
+
${indexPrefix}_aggregation {
|
|
2940
|
+
${type} (filter: $${filterName}, filterSelf: ${filterSelf ? 'true' : 'false'}, accessibility: ${accessibility}) { _totalCount`;
|
|
2941
|
+
const query = `${queryStart}
|
|
2942
|
+
${fields.map((field)=>histogramQueryStrForEachField(field))}
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
}`;
|
|
2946
|
+
const queryBody = {
|
|
2947
|
+
query: query,
|
|
2948
|
+
variables: {
|
|
2949
|
+
[filterName]: convertFilterSetToGqlFilter(filters)
|
|
2950
|
+
}
|
|
2951
|
+
};
|
|
2952
|
+
return queryBody;
|
|
2953
|
+
};
|
|
2954
|
+
const buildGetStatsAggregationQuery = (type, fields, filters, accessibility = Accessibility.ALL, filterSelf = false, queryId, indexPrefix = '', filterName = 'filter')=>{
|
|
2955
|
+
const queryStart = isFilterEmpty(filters) ? `
|
|
2956
|
+
query getStatsAggs${queryId ? `_${queryId}` : ''} {
|
|
2957
|
+
${indexPrefix}_aggregation {
|
|
2958
|
+
${type} (accessibility: ${accessibility}) {` : `query getStatsAggs${queryId ? `_${queryId}` : ''} ($${filterName}: JSON) {
|
|
2959
|
+
${indexPrefix}_aggregation {
|
|
2960
|
+
${type} (filter: $${filterName}, filterSelf: ${filterSelf ? 'true' : 'false'}, accessibility: ${accessibility}) { _totalCount`;
|
|
2961
|
+
const query = `${queryStart}
|
|
2962
|
+
${fields.map((field)=>statsQueryStrForEachField(field))}
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
}`;
|
|
2966
|
+
const queryBody = {
|
|
2967
|
+
query: query,
|
|
2968
|
+
variables: {
|
|
2969
|
+
[filterName]: convertFilterSetToGqlFilter(filters)
|
|
2970
|
+
}
|
|
2971
|
+
};
|
|
2972
|
+
return queryBody;
|
|
2973
|
+
};
|
|
2974
|
+
const { useGetRawDataAndTotalCountsQuery, useGetAccessibleDataQuery, useGetAllFieldsForTypeQuery, useGetAggsQuery, useLazyGetAggsQuery, useGetStatsAggregationsQuery, useLazyGetStatsAggregationsQuery, useGetSubAggsQuery, useGetCountsQuery, useLazyGetCountsQuery, useGetFieldCountSummaryQuery, useGetFieldsForIndexQuery, useGetSharedFieldsForIndexQuery, useGeneralGQLQuery, useLazyGeneralGQLQuery, useCustomRangeQuery, useLazyCustomRangeQuery, useGetObjectIdsQuery, useLazyGetObjectIdsQuery } = explorerApi;
|
|
2975
|
+
|
|
2976
|
+
/**
|
|
2977
|
+
* Defines coreListeners for adding middleware.
|
|
2978
|
+
* used to update the current Cohort Case count
|
|
2979
|
+
*/ const coreStoreListenerMiddleware = createListenerMiddleware();
|
|
2980
|
+
const startCoreListening = coreStoreListenerMiddleware.startListening;
|
|
2981
|
+
/**
|
|
2982
|
+
* When a getCounts is requested, it might be for a cohort.
|
|
2983
|
+
* If so, then update the cohort's counts for that index
|
|
2984
|
+
*/ startCoreListening({
|
|
2985
|
+
matcher: isAnyOf(explorerApi.endpoints.getCounts.matchFulfilled),
|
|
2986
|
+
effect: async (action, listenerApi)=>{
|
|
2987
|
+
const counts = action.payload;
|
|
2988
|
+
const index = action?.meta?.arg?.originalArgs?.type; // note this is the guppy index name
|
|
2989
|
+
const cohortId = action?.meta?.arg?.originalArgs?.queryId;
|
|
2990
|
+
if (cohortId && index) {
|
|
2991
|
+
listenerApi.dispatch(updateCohortIndexCountById({
|
|
2992
|
+
index,
|
|
2993
|
+
cohortId,
|
|
2994
|
+
counts
|
|
2995
|
+
}));
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
});
|
|
2999
|
+
|
|
3000
|
+
/**
|
|
3001
|
+
* Low-level helper to fetch Arborist resources for the current user.
|
|
3002
|
+
* Adds an Authorization header when a token is provided and normalizes the response
|
|
3003
|
+
* to a simple string[] of resource paths.
|
|
3004
|
+
*
|
|
3005
|
+
* token { string | null } - access token to use for authorization
|
|
3006
|
+
* useService { boolean } - use the arborist service endpoint instead of the public arborist API
|
|
3007
|
+
*/ async function fetchArboristResources(token, useService = false) {
|
|
3008
|
+
const headers = {};
|
|
3009
|
+
if (token) {
|
|
3010
|
+
headers.Authorization = `Bearer ${token}`;
|
|
3011
|
+
}
|
|
3012
|
+
const url = useService ? `${GEN3_AUTHZ_SERVICE}/resource` : `${GEN3_AUTHZ_API}/resources`;
|
|
3013
|
+
const res = await fetch(url, {
|
|
3014
|
+
headers
|
|
3015
|
+
});
|
|
3016
|
+
if (!res.ok) {
|
|
3017
|
+
console.error('@gen3/core:fetchArboristResources /resource failed:', res.status, await res.text());
|
|
3018
|
+
return [];
|
|
3019
|
+
}
|
|
3020
|
+
const data = await res.json();
|
|
3021
|
+
return data.resources ?? [];
|
|
3022
|
+
}
|
|
3023
|
+
|
|
3024
|
+
const persistConfig = {
|
|
3025
|
+
key: `${GEN3_COMMONS_NAME}-root`,
|
|
3026
|
+
version: 1,
|
|
3027
|
+
storage,
|
|
3028
|
+
whitelist: [
|
|
3029
|
+
'cohorts',
|
|
3030
|
+
'activeWorkspace',
|
|
3031
|
+
'cart',
|
|
3032
|
+
'workspaceKernels',
|
|
3033
|
+
'tieredWorkspace'
|
|
3034
|
+
]
|
|
3035
|
+
};
|
|
3036
|
+
const persistedReducer = persistReducer(persistConfig, rootReducer);
|
|
3037
|
+
const setupCoreStore = (preloadedState)=>configureStore({
|
|
3038
|
+
reducer: persistedReducer,
|
|
3039
|
+
preloadedState,
|
|
3040
|
+
middleware: (getDefaultMiddleware)=>getDefaultMiddleware({
|
|
3041
|
+
serializableCheck: {
|
|
3042
|
+
ignoredActions: [
|
|
3043
|
+
FLUSH,
|
|
3044
|
+
REHYDRATE,
|
|
3045
|
+
PAUSE,
|
|
3046
|
+
PERSIST,
|
|
3047
|
+
PURGE,
|
|
3048
|
+
REGISTER
|
|
3049
|
+
]
|
|
3050
|
+
}
|
|
3051
|
+
}).concat(gen3ServicesReducerMiddleware, guppyAPISliceMiddleware, userAuthApiMiddleware).prepend(coreStoreListenerMiddleware.middleware)
|
|
3052
|
+
});
|
|
3053
|
+
const coreStore = setupCoreStore();
|
|
3054
|
+
setupListeners(coreStore.dispatch);
|
|
3055
|
+
|
|
3056
|
+
const persistor = persistStore(coreStore);
|
|
3057
|
+
const CoreProvider = ({ children })=>{
|
|
3058
|
+
return /*#__PURE__*/ React__default.createElement(Provider, {
|
|
3059
|
+
store: coreStore
|
|
3060
|
+
}, /*#__PURE__*/ React__default.createElement(PersistGate, {
|
|
3061
|
+
loading: null,
|
|
3062
|
+
persistor: persistor
|
|
3063
|
+
}, children));
|
|
3064
|
+
};
|
|
3065
|
+
|
|
3066
|
+
/**
|
|
3067
|
+
* @description Creates a externalLoginApi for listing the configured identity providers
|
|
3068
|
+
* in workspace token service. Includes user token expiration time.
|
|
3069
|
+
* @see https://github.com/uc-cdis/workspace-token-service/tree/master
|
|
3070
|
+
*/ const externalLoginApi = gen3Api.injectEndpoints({
|
|
3071
|
+
endpoints: (builder)=>({
|
|
3072
|
+
getExternalLogins: builder.query({
|
|
3073
|
+
query: ()=>({
|
|
3074
|
+
url: `${GEN3_WTS_API}/external_oidc/`
|
|
3075
|
+
})
|
|
3076
|
+
}),
|
|
3077
|
+
isExternalConnected: builder.query({
|
|
3078
|
+
query: (idp)=>({
|
|
3079
|
+
url: `${GEN3_WTS_API}/oauth2/connected?idp=${idp}`
|
|
3080
|
+
}),
|
|
3081
|
+
transformResponse: ()=>{
|
|
3082
|
+
return true; // if success then connected is true
|
|
3083
|
+
}
|
|
3084
|
+
})
|
|
3085
|
+
})
|
|
3086
|
+
});
|
|
3087
|
+
const { useGetExternalLoginsQuery, useLazyGetExternalLoginsQuery, useLazyIsExternalConnectedQuery, useIsExternalConnectedQuery } = externalLoginApi;
|
|
3088
|
+
|
|
3089
|
+
/**
|
|
3090
|
+
* Custom error class for resolving DRS object ID errors
|
|
3091
|
+
*/ class ResolveDRSError extends Error {
|
|
3092
|
+
constructor(message){
|
|
3093
|
+
super(message);
|
|
3094
|
+
this.name = 'ResolveDRSError';
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
/**
|
|
3098
|
+
* Resolves DRS object ID from a list of guids using dataguids.org
|
|
3099
|
+
* Use of this function should be limited
|
|
3100
|
+
* @param guidsForHostnameResolution
|
|
3101
|
+
*/ const resolveDRSWithDataGUISOrg = async (guidsForHostnameResolution)=>{
|
|
3102
|
+
const INVALID_INPUT_ERROR = 'Invalid input: guidsForHostnameResolution must be an array of strings';
|
|
3103
|
+
const RESOLVE_DRS_ERROR = 'Failed to resolve DRS object ID';
|
|
3104
|
+
if (!Array.isArray(guidsForHostnameResolution) || !guidsForHostnameResolution.every((guid)=>typeof guid === 'string')) {
|
|
3105
|
+
throw new Error(INVALID_INPUT_ERROR);
|
|
3106
|
+
}
|
|
3107
|
+
try {
|
|
3108
|
+
const responses = await Promise.allSettled(guidsForHostnameResolution.map(async (guid)=>{
|
|
3109
|
+
try {
|
|
3110
|
+
const response = await fetch(`https://dataguids.org/index/${guid}`);
|
|
3111
|
+
if (response.status === 200) {
|
|
3112
|
+
try {
|
|
3113
|
+
return await response.json();
|
|
3114
|
+
// oxlint-disable-next-line no-unused-vars
|
|
3115
|
+
} catch (error) {
|
|
3116
|
+
throw new ResolveDRSError('Failed to parse response as JSON');
|
|
3117
|
+
}
|
|
3118
|
+
} else {
|
|
3119
|
+
throw new ResolveDRSError(RESOLVE_DRS_ERROR);
|
|
3120
|
+
}
|
|
3121
|
+
// oxlint-disable-next-line no-unused-vars
|
|
3122
|
+
} catch (error) {
|
|
3123
|
+
throw new ResolveDRSError(RESOLVE_DRS_ERROR);
|
|
3124
|
+
}
|
|
3125
|
+
}));
|
|
3126
|
+
return responses.reduce((acc, response, index)=>{
|
|
3127
|
+
if (response.status === 'fulfilled' && response.value.data) {
|
|
3128
|
+
return {
|
|
3129
|
+
...acc,
|
|
3130
|
+
[guidsForHostnameResolution[index]]: response.value.data
|
|
3131
|
+
};
|
|
3132
|
+
} else {
|
|
3133
|
+
throw new ResolveDRSError(RESOLVE_DRS_ERROR);
|
|
3134
|
+
}
|
|
3135
|
+
}, {});
|
|
3136
|
+
// oxlint-disable-next-line no-unused-vars
|
|
3137
|
+
} catch (error) {
|
|
3138
|
+
throw new ResolveDRSError(RESOLVE_DRS_ERROR);
|
|
3139
|
+
}
|
|
3140
|
+
};
|
|
3141
|
+
|
|
3142
|
+
const resolveCachedDRS = async (guidsForHostnameResolution)=>{
|
|
3143
|
+
const state = coreStore.getState();
|
|
3144
|
+
const resolvedIds = {};
|
|
3145
|
+
for (const guid of guidsForHostnameResolution){
|
|
3146
|
+
const hostname = drsHostnamesSelector(guid, state);
|
|
3147
|
+
if (hostname) {
|
|
3148
|
+
resolvedIds[guid] = hostname;
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
return resolvedIds;
|
|
3152
|
+
};
|
|
3153
|
+
|
|
3154
|
+
const DRS_Resolvers = {
|
|
3155
|
+
dataGUISOrg: resolveDRSWithDataGUISOrg,
|
|
3156
|
+
cached: resolveCachedDRS
|
|
3157
|
+
};
|
|
3158
|
+
// TODO - add to config file for DRS resolver
|
|
3159
|
+
const resolveDRSObjectId = (guidsForHostnameResolution, resolver = "cached")=>{
|
|
3160
|
+
return DRS_Resolvers[resolver](guidsForHostnameResolution);
|
|
3161
|
+
};
|
|
3162
|
+
|
|
3163
|
+
const isNotDefined = (x)=>{
|
|
3164
|
+
return x === undefined || x === null || x === void 0;
|
|
3165
|
+
};
|
|
3166
|
+
const isObject = (x)=>{
|
|
3167
|
+
return typeof x === 'object';
|
|
3168
|
+
};
|
|
3169
|
+
const isArray = (x)=>{
|
|
3170
|
+
return Array.isArray(x);
|
|
3171
|
+
};
|
|
3172
|
+
const isString = (x)=>{
|
|
3173
|
+
return typeof x === 'string';
|
|
3174
|
+
};
|
|
3175
|
+
|
|
3176
|
+
/**
|
|
3177
|
+
* Prepares a URL for downloading by appending '/download' to the provided apiUrl.
|
|
3178
|
+
*
|
|
3179
|
+
* @param {string} apiUrl - The base URL to be used for preparing the download URL.
|
|
3180
|
+
* @returns {URL} - The prepared download URL as a URL object.
|
|
3181
|
+
*/ const prepareUrl$1 = (apiUrl)=>new URL(apiUrl + '/download');
|
|
3182
|
+
|
|
3183
|
+
var HttpMethod = /*#__PURE__*/ function(HttpMethod) {
|
|
3184
|
+
HttpMethod["GET"] = "GET";
|
|
3185
|
+
HttpMethod["POST"] = "POST";
|
|
3186
|
+
HttpMethod["PUT"] = "PUT";
|
|
3187
|
+
HttpMethod["DELETE"] = "DELETE";
|
|
3188
|
+
return HttpMethod;
|
|
3189
|
+
}({});
|
|
3190
|
+
const DEFAULT_METHOD = "GET";
|
|
3191
|
+
const CONTENT_TYPE_HEADER = 'Content-Type';
|
|
3192
|
+
const CONTENT_TYPE_JSON = 'application/json';
|
|
3193
|
+
const HTTPErrorMessages = {
|
|
3194
|
+
// 4xx Client Errors
|
|
3195
|
+
400: 'Bad Request',
|
|
3196
|
+
401: 'Unauthorized',
|
|
3197
|
+
402: 'Payment Required',
|
|
3198
|
+
403: 'Forbidden',
|
|
3199
|
+
404: 'Not Found',
|
|
3200
|
+
405: 'Method Not Allowed',
|
|
3201
|
+
406: 'Not Acceptable',
|
|
3202
|
+
407: 'Proxy Authentication Required',
|
|
3203
|
+
408: 'Request Timeout',
|
|
3204
|
+
409: 'Conflict',
|
|
3205
|
+
410: 'Gone',
|
|
3206
|
+
411: 'Length Required',
|
|
3207
|
+
412: 'Precondition Failed',
|
|
3208
|
+
413: 'Payload Too Large',
|
|
3209
|
+
414: 'URI Too Long',
|
|
3210
|
+
415: 'Unsupported Media Type',
|
|
3211
|
+
416: 'Range Not Satisfiable',
|
|
3212
|
+
417: 'Expectation Failed',
|
|
3213
|
+
418: "I'm a teapot",
|
|
3214
|
+
421: 'Misdirected Request',
|
|
3215
|
+
422: 'Unprocessable Entity',
|
|
3216
|
+
423: 'Locked',
|
|
3217
|
+
424: 'Failed Dependency',
|
|
3218
|
+
425: 'Too Early',
|
|
3219
|
+
426: 'Upgrade Required',
|
|
3220
|
+
428: 'Precondition Required',
|
|
3221
|
+
429: 'Too Many Requests',
|
|
3222
|
+
431: 'Request Header Fields Too Large',
|
|
3223
|
+
451: 'Unavailable For Legal Reasons',
|
|
3224
|
+
// 5xx Server Errors
|
|
3225
|
+
500: 'Internal Server Error',
|
|
3226
|
+
501: 'Not Implemented',
|
|
3227
|
+
502: 'Bad Gateway',
|
|
3228
|
+
503: 'Service Unavailable',
|
|
3229
|
+
504: 'Gateway Timeout',
|
|
3230
|
+
505: 'HTTP Version Not Supported',
|
|
3231
|
+
506: 'Variant Also Negotiates',
|
|
3232
|
+
507: 'Insufficient Storage',
|
|
3233
|
+
508: 'Loop Detected',
|
|
3234
|
+
510: 'Not Extended',
|
|
3235
|
+
511: 'Network Authentication Required'
|
|
3236
|
+
};
|
|
3237
|
+
/**
|
|
3238
|
+
* Represents an error that occurs during an HTTP request.
|
|
3239
|
+
* Extends the built-in `Error` class to provide additional information
|
|
3240
|
+
* about the HTTP status code and optional response data.
|
|
3241
|
+
*/ class HTTPError extends Error {
|
|
3242
|
+
constructor(status, message, responseData){
|
|
3243
|
+
super(message), this.status = status, this.responseData = responseData;
|
|
3244
|
+
this.name = 'HTTPError';
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
const fetchFencePresignedURL = async ({ guid, method = 'GET', onAbort = ()=>null, signal = undefined })=>{
|
|
3248
|
+
const csrfToken = selectCSRFToken(coreStore.getState());
|
|
3249
|
+
const headers = new Headers();
|
|
3250
|
+
headers.set('Content-Type', 'application/json');
|
|
3251
|
+
if (process.env.NODE_ENV === 'development') {
|
|
3252
|
+
// NOTE: This cookie can only be accessed from the client side
|
|
3253
|
+
// in development mode. Otherwise, the cookie is set as httpOnly
|
|
3254
|
+
const accessToken = getCookie('credentials_token');
|
|
3255
|
+
if (accessToken) {
|
|
3256
|
+
headers.set('Authorization', `Bearer ${accessToken}`);
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
if (csrfToken) headers.set('X-CSRF-Token', csrfToken);
|
|
3260
|
+
const url = `${GEN3_FENCE_API}/data/download/${guid}`;
|
|
3261
|
+
try {
|
|
3262
|
+
const response = await fetch(url, {
|
|
3263
|
+
method: method,
|
|
3264
|
+
headers: headers,
|
|
3265
|
+
...signal ? {
|
|
3266
|
+
signal: signal
|
|
3267
|
+
} : {}
|
|
3268
|
+
});
|
|
3269
|
+
// Check if the response is ok before proceeding
|
|
3270
|
+
if (!response.ok) {
|
|
3271
|
+
let errorMessage;
|
|
3272
|
+
let errorData;
|
|
3273
|
+
try {
|
|
3274
|
+
// Attempt to parse error response as JSON
|
|
3275
|
+
errorData = await response.json();
|
|
3276
|
+
errorMessage = errorData.message || response.statusText;
|
|
3277
|
+
} catch {
|
|
3278
|
+
// If JSON parsing fails, use status text
|
|
3279
|
+
errorMessage = response.statusText;
|
|
3280
|
+
}
|
|
3281
|
+
throw new HTTPError(response.status, errorMessage, errorData);
|
|
3282
|
+
}
|
|
3283
|
+
return (await response.json())['url'];
|
|
3284
|
+
} catch (error) {
|
|
3285
|
+
if (error instanceof Error) {
|
|
3286
|
+
if (error.name === 'AbortError') {
|
|
3287
|
+
onAbort?.();
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
throw error;
|
|
3291
|
+
}
|
|
3292
|
+
};
|
|
3293
|
+
/**
|
|
3294
|
+
* Retrieves a CSRF token from the server.
|
|
3295
|
+
*
|
|
3296
|
+
* This asynchronous function sends a GET request to the server's status endpoint
|
|
3297
|
+
* to fetch the CSRF token in the response. The token is expected to be included
|
|
3298
|
+
* in the JSON response under the `csrf_token` field.
|
|
3299
|
+
*
|
|
3300
|
+
* @returns {Promise<string | null>} A promise that resolves to the CSRF token as a string if successfully retrieved,
|
|
3301
|
+
* or null if the token is not present in the response.
|
|
3302
|
+
* @throws {HTTPError} Throws an HTTPError if the server response is not successful.
|
|
3303
|
+
*/ const getCSRFToken = async ()=>{
|
|
3304
|
+
const requestHeaders = new Headers({
|
|
3305
|
+
[CONTENT_TYPE_HEADER]: CONTENT_TYPE_JSON
|
|
3306
|
+
});
|
|
3307
|
+
const response = await fetch(`${GEN3_API}/_status`, {
|
|
3308
|
+
headers: requestHeaders
|
|
3309
|
+
});
|
|
3310
|
+
if (!response.ok) {
|
|
3311
|
+
throw new HTTPError(response.status, response.statusText);
|
|
3312
|
+
}
|
|
3313
|
+
const { csrf: csrfToken } = await response.json();
|
|
3314
|
+
return csrfToken || null;
|
|
3315
|
+
};
|
|
3316
|
+
/**
|
|
3317
|
+
* Fetches JSON data from a specified URL using the Fetch API.
|
|
3318
|
+
*
|
|
3319
|
+
* @param {string} url - The URL to fetch the JSON data from.
|
|
3320
|
+
* @param {boolean} [requiresCSRF=false] - Indicates whether a CSRF token is required for the request.
|
|
3321
|
+
* If true, the CSRF token will be added to the request headers.
|
|
3322
|
+
* @param {string} [method=DEFAULT_METHOD] - The HTTP method to use for the request (e.g., 'GET', 'POST').
|
|
3323
|
+
* @param {unknown} [body=undefined] - The request body to send, applicable when using methods like 'POST'.
|
|
3324
|
+
*
|
|
3325
|
+
* @param signal - Abort fetch
|
|
3326
|
+
* @returns {Promise<T>} A promise that resolves to the parsed JSON data from the response.
|
|
3327
|
+
*
|
|
3328
|
+
* @throws {HTTPError} Throws an error if the HTTP response status indicates a failure.
|
|
3329
|
+
*/ const fetchJSONDataFromURL = async (url, requiresCSRF = false, method = DEFAULT_METHOD, body = undefined, signal)=>{
|
|
3330
|
+
const requestHeaders = new Headers({
|
|
3331
|
+
[CONTENT_TYPE_HEADER]: CONTENT_TYPE_JSON
|
|
3332
|
+
});
|
|
3333
|
+
if (requiresCSRF) {
|
|
3334
|
+
const csrfToken = await getCSRFToken();
|
|
3335
|
+
if (csrfToken) {
|
|
3336
|
+
requestHeaders.set('X-CSRF-Token', csrfToken);
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
if (process.env.NODE_ENV === 'development') {
|
|
3340
|
+
const accessToken = getCookie('credentials_token');
|
|
3341
|
+
if (accessToken) {
|
|
3342
|
+
requestHeaders.set('Authorization', `Bearer ${accessToken}`);
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
const response = await fetch(url, {
|
|
3346
|
+
method,
|
|
3347
|
+
headers: requestHeaders,
|
|
3348
|
+
body: [
|
|
3349
|
+
'POST',
|
|
3350
|
+
'PUT',
|
|
3351
|
+
'PATCH'
|
|
3352
|
+
].includes(method.toUpperCase()) ? body : undefined,
|
|
3353
|
+
...signal ? {
|
|
3354
|
+
signal: signal
|
|
3355
|
+
} : {}
|
|
3356
|
+
});
|
|
3357
|
+
if (!response.ok) {
|
|
3358
|
+
const errorBody = await response.json();
|
|
3359
|
+
throw new HTTPError(response.status, response.statusText, errorBody);
|
|
3360
|
+
}
|
|
3361
|
+
if (response.status === 204) {
|
|
3362
|
+
// no content so returns null
|
|
3363
|
+
return null;
|
|
3364
|
+
}
|
|
3365
|
+
return await response.json();
|
|
3366
|
+
};
|
|
3367
|
+
|
|
3368
|
+
const extractValuesFromObject = (jsonPathMappings, obj)=>{
|
|
3369
|
+
const result = {};
|
|
3370
|
+
const extractObjectValue = (jsonPath, obj)=>{
|
|
3371
|
+
const extractedValues = JSONPath({
|
|
3372
|
+
path: jsonPath,
|
|
3373
|
+
json: obj
|
|
3374
|
+
});
|
|
3375
|
+
return extractedValues.length > 0 ? extractedValues[0] : undefined;
|
|
3376
|
+
};
|
|
3377
|
+
for(const key in jsonPathMappings){
|
|
3378
|
+
if (key in Object.keys(jsonPathMappings)) {
|
|
3379
|
+
// Extract value from an object and store it in the result.
|
|
3380
|
+
result[key] = extractObjectValue(jsonPathMappings[key], obj);
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
3383
|
+
return result;
|
|
3384
|
+
};
|
|
3385
|
+
const ExtractValueFromObject = (obj, key, valueIfNotFound)=>{
|
|
3386
|
+
return obj?.[key] ?? valueIfNotFound;
|
|
3387
|
+
};
|
|
3388
|
+
|
|
3389
|
+
// type guard functions
|
|
3390
|
+
const isHistogramRangeData = (key)=>{
|
|
3391
|
+
return Array.isArray(key) && key.length === 2 && key.every((item)=>typeof item === 'number');
|
|
3392
|
+
};
|
|
3393
|
+
const isJSONObject = (data)=>{
|
|
3394
|
+
return typeof data === 'object' && data !== null && !Array.isArray(data);
|
|
3395
|
+
};
|
|
3396
|
+
const isJSONValue = (data)=>{
|
|
3397
|
+
return typeof data === 'string' || typeof data === 'number' || typeof data === 'boolean' || Array.isArray(data) && data.every(isJSONValue) || isJSONObject(data);
|
|
3398
|
+
};
|
|
3399
|
+
const isJSONValueArray = (data)=>{
|
|
3400
|
+
return Array.isArray(data) && data.every(isJSONValue);
|
|
3401
|
+
};
|
|
3402
|
+
const isValidObject = (input)=>typeof input === 'object' && input !== null;
|
|
3403
|
+
const isHistogramData = (data)=>{
|
|
3404
|
+
return isValidObject(data) && 'key' in data && 'count' in data;
|
|
3405
|
+
};
|
|
3406
|
+
const isHistogramDataArray = (input)=>{
|
|
3407
|
+
if (!isValidObject(input) || !Array.isArray(input.histogram)) {
|
|
3408
|
+
return false;
|
|
3409
|
+
}
|
|
3410
|
+
return input.histogram.every(isHistogramData);
|
|
3411
|
+
};
|
|
3412
|
+
const isHistogramDataCollection = (obj)=>{
|
|
3413
|
+
return isValidObject(obj) && 'histogram' in obj && isHistogramData(obj.histogram);
|
|
3414
|
+
};
|
|
3415
|
+
// Type guard function for GuppyAggregationData interface
|
|
3416
|
+
const isGuppyAggregationData = (obj)=>{
|
|
3417
|
+
if (!isValidObject(obj)) return false;
|
|
3418
|
+
for(const key in obj){
|
|
3419
|
+
if (!isHistogramDataCollection(obj[key])) {
|
|
3420
|
+
return false;
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
return true;
|
|
3424
|
+
};
|
|
3425
|
+
const isHistogramDataAnEnum = (data)=>{
|
|
3426
|
+
return typeof data === 'object' && data !== null && 'key' in data && 'count' in data && typeof data.key === 'string' && typeof data.count === 'number';
|
|
3427
|
+
};
|
|
3428
|
+
const isStatsValue = (item)=>{
|
|
3429
|
+
if (typeof item !== 'object' || item === null) {
|
|
3430
|
+
return false;
|
|
3431
|
+
}
|
|
3432
|
+
const obj = item;
|
|
3433
|
+
// Check that all present properties have correct types
|
|
3434
|
+
const numericFields = [
|
|
3435
|
+
'count',
|
|
3436
|
+
'min',
|
|
3437
|
+
'max',
|
|
3438
|
+
'avg',
|
|
3439
|
+
'sum',
|
|
3440
|
+
'stddev',
|
|
3441
|
+
'median'
|
|
3442
|
+
];
|
|
3443
|
+
if (!numericFields.some((field)=>field in obj && typeof obj[field] !== 'number')) {
|
|
3444
|
+
return false;
|
|
3445
|
+
}
|
|
3446
|
+
// Check percentiles structure if present
|
|
3447
|
+
if ('percentiles' in obj) {
|
|
3448
|
+
const percentiles = obj.percentiles;
|
|
3449
|
+
if (typeof percentiles !== 'object' || percentiles === null) {
|
|
3450
|
+
return false;
|
|
3451
|
+
}
|
|
3452
|
+
}
|
|
3453
|
+
return true;
|
|
3454
|
+
};
|
|
3455
|
+
const isStatsValuesArray = (data)=>{
|
|
3456
|
+
return Array.isArray(data) && data.every(isStatsValue);
|
|
3457
|
+
};
|
|
3458
|
+
const isHistogramDataAArray = (data)=>{
|
|
3459
|
+
return Array.isArray(data) && data.every(isHistogramData);
|
|
3460
|
+
};
|
|
3461
|
+
const isHistogramDataArrayAnEnum = (data)=>{
|
|
3462
|
+
return Array.isArray(data) && data.every(isHistogramDataAnEnum);
|
|
3463
|
+
};
|
|
3464
|
+
const isHistogramDataArrayARange = (data)=>{
|
|
3465
|
+
return Array.isArray(data) && data.every((item)=>isHistogramRangeData(item.key));
|
|
3466
|
+
};
|
|
3467
|
+
/**
|
|
3468
|
+
* Type predicate to narrow an unknown error to `FetchBaseQueryError`
|
|
3469
|
+
*/ function isFetchBaseQueryError(error) {
|
|
3470
|
+
return typeof error === 'object' && error != null && 'status' in error;
|
|
3471
|
+
}
|
|
3472
|
+
/**
|
|
3473
|
+
* Type predicate to narrow an unknown error to an object with a string 'message' property
|
|
3474
|
+
*/ function isErrorWithMessage(error) {
|
|
3475
|
+
return typeof error === 'object' && error != null && 'message' in error && typeof error.message === 'string';
|
|
3476
|
+
}
|
|
3477
|
+
function isHttpStatusError(error) {
|
|
3478
|
+
return typeof error === 'object' && error != null && 'status' in error && typeof error.status === 'number';
|
|
3479
|
+
}
|
|
3480
|
+
/**
|
|
3481
|
+
* Type predicate to narrow an unknown error to an object with a string 'message' property
|
|
3482
|
+
*/ function isFetchParseError(error) {
|
|
3483
|
+
return typeof error === 'object' && error != null && 'originalStatus' in error && 'status' in error && error['status'] === 'PARSING_ERROR';
|
|
3484
|
+
}
|
|
3485
|
+
/**
|
|
3486
|
+
* Type predicate to narrow an unknown error to a `SerializedError`
|
|
3487
|
+
*/ function isSerializedError(error) {
|
|
3488
|
+
return typeof error === 'object' && error != null && !('status' in error) && ('message' in error || 'name' in error || 'code' in error || 'stack' in error);
|
|
3489
|
+
}
|
|
3490
|
+
|
|
3491
|
+
const HTTPUserFriendlyErrorMessages = {
|
|
3492
|
+
// 4xx Client Errors
|
|
3493
|
+
400: 'Sorry, there was a problem with your request. Please check the information you provided and try again.',
|
|
3494
|
+
401: 'Please sign in to access this content. If you need to create an account, please contact support.',
|
|
3495
|
+
402: 'This feature requires payment to continue. Please update your subscription.',
|
|
3496
|
+
403: "You don't have permission to access this. If you think this is a mistake, please contact support.",
|
|
3497
|
+
404: "We couldn't find what you're looking for. The link might be broken or the content may have been moved.",
|
|
3498
|
+
405: 'This action is not supported. Please try a different approach.',
|
|
3499
|
+
406: "Your browser sent a request that this server couldn't understand. Try updating your browser.",
|
|
3500
|
+
407: 'Please authenticate with the network proxy first.',
|
|
3501
|
+
408: 'The request took too long to complete. Please check your internet connection and try again.',
|
|
3502
|
+
409: 'There was a conflict with your request. Someone might have modified the same content.',
|
|
3503
|
+
410: 'This content has been permanently removed and is no longer available.',
|
|
3504
|
+
411: 'There was a technical problem with your request. Please try again.',
|
|
3505
|
+
412: "Some conditions for this request weren't met. Please try again.",
|
|
3506
|
+
413: "The file you're trying to upload is too large. Please choose a smaller file.",
|
|
3507
|
+
414: 'The web address is too long. Please try a shorter URL.',
|
|
3508
|
+
415: 'This type of file is not supported. Please try a different format.',
|
|
3509
|
+
416: 'The requested content range is not available.',
|
|
3510
|
+
417: "Your request couldn't be completed as expected. Please try again.",
|
|
3511
|
+
418: 'Unusual error occurred. Please try again or contact support if the problem persists.',
|
|
3512
|
+
421: 'Your request was sent to the wrong server. Please try again.',
|
|
3513
|
+
422: "We couldn't process your request. Please check the information and try again.",
|
|
3514
|
+
423: 'This resource is temporarily locked. Please try again in a few moments.',
|
|
3515
|
+
424: 'The request failed because it depends on another request that failed.',
|
|
3516
|
+
425: 'Your request was processed too early. Please try again in a moment.',
|
|
3517
|
+
426: 'You need to upgrade your browser or app to continue.',
|
|
3518
|
+
428: "Some required conditions weren't met. Please check all requirements and try again.",
|
|
3519
|
+
429: "You've made too many requests. Please wait a while before trying again.",
|
|
3520
|
+
431: 'Request failed because the headers are too large. Please simplify your request.',
|
|
3521
|
+
451: 'This content is not available for legal reasons.',
|
|
3522
|
+
// 5xx Server Errors
|
|
3523
|
+
500: 'Something went wrong with the service. Please try again later.',
|
|
3524
|
+
501: "This feature isn't available yet. Check back later.",
|
|
3525
|
+
502: "We're having trouble connecting to our servers. Please try again in a few minutes.",
|
|
3526
|
+
503: "Our service is temporarily unavailable. We're working to restore it as quickly as possible.",
|
|
3527
|
+
504: 'The server took too long to respond. Please try again later.',
|
|
3528
|
+
505: "Your browser's HTTP version isn't supported. Please try updating your browser.",
|
|
3529
|
+
506: 'We encountered a configuration error.',
|
|
3530
|
+
507: "We're out of storage space.",
|
|
3531
|
+
508: 'We detected an infinite loop in our servers.',
|
|
3532
|
+
510: 'This server requires additional extensions to process your request.',
|
|
3533
|
+
511: 'Please sign in to the network before continuing.'
|
|
3534
|
+
};
|
|
3535
|
+
|
|
3536
|
+
const MAX_ERROR_LENGTH = 256;
|
|
3537
|
+
const DEFAULT_API_ERROR_MESSAGE = 'API Query Error';
|
|
3538
|
+
/**
|
|
3539
|
+
* Checks if a string appears to be an HTML page
|
|
3540
|
+
*/ function isHtmlContent(str) {
|
|
3541
|
+
const trimmed = str.trim().toLowerCase();
|
|
3542
|
+
return trimmed.startsWith('<!doctype html') || trimmed.startsWith('<html') || /<html[\s>]/i.test(str);
|
|
3543
|
+
}
|
|
3544
|
+
/**
|
|
3545
|
+
* Sanitizes an error message - returns DEFAULT_API_ERROR_MESSAGE if
|
|
3546
|
+
* the message is too long or contains HTML content
|
|
3547
|
+
*/ function sanitizeErrorMessage(message) {
|
|
3548
|
+
if (!message) return DEFAULT_API_ERROR_MESSAGE;
|
|
3549
|
+
if (message.length > MAX_ERROR_LENGTH || isHtmlContent(message)) {
|
|
3550
|
+
return DEFAULT_API_ERROR_MESSAGE;
|
|
3551
|
+
}
|
|
3552
|
+
return message;
|
|
3553
|
+
}
|
|
3554
|
+
/** Best-effort message extraction from an HTTP error body */ function extractMessage(data) {
|
|
3555
|
+
if (typeof data === 'string') return data;
|
|
3556
|
+
if (typeof data === 'object' && data != null) {
|
|
3557
|
+
const obj = data;
|
|
3558
|
+
// common API error shapes: { message }, { error }, { detail } (FastAPI)
|
|
3559
|
+
for (const key of [
|
|
3560
|
+
'message',
|
|
3561
|
+
'error',
|
|
3562
|
+
'detail'
|
|
3563
|
+
]){
|
|
3564
|
+
const val = obj[key];
|
|
3565
|
+
if (typeof val === 'string') return val;
|
|
3566
|
+
}
|
|
3567
|
+
}
|
|
3568
|
+
return undefined;
|
|
3569
|
+
}
|
|
3570
|
+
function normalizeRtkError(error) {
|
|
3571
|
+
if (!error) {
|
|
3572
|
+
return {
|
|
3573
|
+
type: 'UNKNOWN_ERROR',
|
|
3574
|
+
message: 'Unknown error'
|
|
3575
|
+
};
|
|
3576
|
+
}
|
|
3577
|
+
if (isFetchBaseQueryError(error)) {
|
|
3578
|
+
// status is number for HTTP errors, string literal for the others
|
|
3579
|
+
if (typeof error.status === 'number') {
|
|
3580
|
+
const extractedMessage = extractMessage(error.data);
|
|
3581
|
+
return {
|
|
3582
|
+
type: 'HTTP_ERROR',
|
|
3583
|
+
status: error.status,
|
|
3584
|
+
message: sanitizeErrorMessage(extractedMessage) !== DEFAULT_API_ERROR_MESSAGE ? sanitizeErrorMessage(extractedMessage) : HTTPUserFriendlyErrorMessages[error.status] ?? `Request failed with status ${error.status}`,
|
|
3585
|
+
data: error.data
|
|
3586
|
+
};
|
|
3587
|
+
}
|
|
3588
|
+
switch(error.status){
|
|
3589
|
+
case 'FETCH_ERROR':
|
|
3590
|
+
return {
|
|
3591
|
+
type: 'FETCH_ERROR',
|
|
3592
|
+
message: error.error
|
|
3593
|
+
};
|
|
3594
|
+
case 'PARSING_ERROR':
|
|
3595
|
+
return {
|
|
3596
|
+
type: 'PARSING_ERROR',
|
|
3597
|
+
status: error.originalStatus,
|
|
3598
|
+
message: HTTPUserFriendlyErrorMessages[error.originalStatus] ?? `Request failed with status ${error.originalStatus}`,
|
|
3599
|
+
data: error.data
|
|
3600
|
+
};
|
|
3601
|
+
case 'TIMEOUT_ERROR':
|
|
3602
|
+
return {
|
|
3603
|
+
type: 'TIMEOUT_ERROR',
|
|
3604
|
+
message: sanitizeErrorMessage(error.error)
|
|
3605
|
+
};
|
|
3606
|
+
case 'CUSTOM_ERROR':
|
|
3607
|
+
return {
|
|
3608
|
+
type: 'CUSTOM_ERROR',
|
|
3609
|
+
message: sanitizeErrorMessage(error.error),
|
|
3610
|
+
data: error.data
|
|
3611
|
+
};
|
|
3612
|
+
}
|
|
3886
3613
|
}
|
|
3614
|
+
// SerializedError — a JS error thrown somewhere in the pipeline
|
|
3615
|
+
if ('message' in error || 'name' in error) {
|
|
3616
|
+
return {
|
|
3617
|
+
type: 'SERIALIZED_ERROR',
|
|
3618
|
+
message: sanitizeErrorMessage(error.message) || 'An error occurred'
|
|
3619
|
+
};
|
|
3620
|
+
}
|
|
3621
|
+
return {
|
|
3622
|
+
type: 'UNKNOWN_ERROR',
|
|
3623
|
+
message: 'Unknown error'
|
|
3624
|
+
};
|
|
3625
|
+
}
|
|
3626
|
+
|
|
3627
|
+
const DAYS_IN_YEAR = 365.25;
|
|
3628
|
+
/**
|
|
3629
|
+
* Converts HistogramData to HistogramDataAsStringKey by ensuring the key is a string.
|
|
3630
|
+
* If the key is already a string, it's used as is.
|
|
3631
|
+
* If the key is a number tuple [min, max], it's converted to a string in the format "min-max".
|
|
3632
|
+
*
|
|
3633
|
+
* @param data The HistogramData object to convert
|
|
3634
|
+
* @returns HistogramDataAsStringKey with the converted key
|
|
3635
|
+
*/ const convertToHistogramDataAsStringKey = (data)=>{
|
|
3636
|
+
return {
|
|
3637
|
+
key: typeof data.key === 'string' ? data.key : `${data.key[0]}-${data.key[1]}`,
|
|
3638
|
+
count: data.count
|
|
3639
|
+
};
|
|
3887
3640
|
};
|
|
3888
|
-
const
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
}`;
|
|
3906
|
-
const queryBody = {
|
|
3907
|
-
query: query,
|
|
3908
|
-
variables: {
|
|
3909
|
-
[filterName]: convertFilterSetToGqlFilter(filters)
|
|
3910
|
-
}
|
|
3641
|
+
const calculatePercentageAsNumber = (count, total)=>count ? count / total * 100 : 0;
|
|
3642
|
+
const calculatePercentageAsString = (count, total)=>`${(count / total * 100).toFixed(2)}%`;
|
|
3643
|
+
const capitalize = (original)=>{
|
|
3644
|
+
const customCapitalizations = {
|
|
3645
|
+
id: 'ID',
|
|
3646
|
+
uuid: 'UUID',
|
|
3647
|
+
dna: 'DNA',
|
|
3648
|
+
dbsnp: 'dbSNP',
|
|
3649
|
+
cosmic: 'COSMIC',
|
|
3650
|
+
civic: 'CIViC',
|
|
3651
|
+
dbgap: 'dbGaP',
|
|
3652
|
+
ecog: 'ECOG',
|
|
3653
|
+
bmi: 'BMI',
|
|
3654
|
+
gdc: 'GDC',
|
|
3655
|
+
cnv: 'CNV',
|
|
3656
|
+
ssm: 'SSM',
|
|
3657
|
+
aa: 'AA'
|
|
3911
3658
|
};
|
|
3912
|
-
return
|
|
3659
|
+
return original.split(' ').map((word)=>customCapitalizations[word.toLowerCase()] || `${word.charAt(0).toUpperCase()}${word.slice(1)}`).join(' ');
|
|
3913
3660
|
};
|
|
3914
|
-
const
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
[filterName]: convertFilterSetToGqlFilter(filters)
|
|
3661
|
+
const humanify = ({ term = '', capitalize: cap = true, facetTerm = false })=>{
|
|
3662
|
+
let original;
|
|
3663
|
+
let humanified;
|
|
3664
|
+
if (facetTerm) {
|
|
3665
|
+
// Splits on capital letters followed by lowercase letters to find
|
|
3666
|
+
// words squished together in a string.
|
|
3667
|
+
original = term?.split(/(?=[A-Z][a-z])/).join(' ');
|
|
3668
|
+
humanified = term?.replace(/\./g, ' ').replace(/_/g, ' ').trim();
|
|
3669
|
+
} else {
|
|
3670
|
+
const split = (original || term)?.split('.');
|
|
3671
|
+
humanified = split[split.length - 1]?.replace(/_/g, ' ').trim();
|
|
3672
|
+
// Special case 'name' to include any parent nested for sake of
|
|
3673
|
+
// specificity in the UI
|
|
3674
|
+
if (humanified === 'name' && split?.length > 1) {
|
|
3675
|
+
humanified = `${split[split?.length - 2]} ${humanified}`;
|
|
3930
3676
|
}
|
|
3931
|
-
}
|
|
3932
|
-
return
|
|
3677
|
+
}
|
|
3678
|
+
return cap ? capitalize(humanified) : humanified;
|
|
3679
|
+
};
|
|
3680
|
+
/*https://github.com/NCI-GDC/portal-ui/blob/develop/src/packages/%40ncigdc/utils/ageDisplay.js*/ /**
|
|
3681
|
+
* Converts age in days into a human-readable format.
|
|
3682
|
+
*
|
|
3683
|
+
* @param ageInDays - The age in days.
|
|
3684
|
+
* @param yearsOnly - If true, only display years.
|
|
3685
|
+
* @defaultValue false
|
|
3686
|
+
* @param defaultValue - The default value to return if ageInDays is falsy.
|
|
3687
|
+
* @defaultValue "--"
|
|
3688
|
+
* @returns The formatted age string.
|
|
3689
|
+
*/ const ageDisplay = (ageInDays, yearsOnly = false, defaultValue = '--')=>{
|
|
3690
|
+
if (ageInDays !== 0 && !ageInDays) {
|
|
3691
|
+
return defaultValue;
|
|
3692
|
+
}
|
|
3693
|
+
const calculateYearsAndDays = (years, days)=>days === 365 ? [
|
|
3694
|
+
years + 1,
|
|
3695
|
+
0
|
|
3696
|
+
] : [
|
|
3697
|
+
years,
|
|
3698
|
+
days
|
|
3699
|
+
];
|
|
3700
|
+
const ABS_AGE_DAYS = Math.abs(ageInDays);
|
|
3701
|
+
const [years, remainingDays] = calculateYearsAndDays(Math.floor(ABS_AGE_DAYS / DAYS_IN_YEAR), Math.ceil(ABS_AGE_DAYS % DAYS_IN_YEAR));
|
|
3702
|
+
const formattedYears = years === 0 ? '' : `${years} ${years === 1 ? 'year' : 'years'}`;
|
|
3703
|
+
const formattedDays = !yearsOnly && remainingDays > 0 ? `${remainingDays} ${remainingDays === 1 ? 'day' : 'days'}` : years === 0 && remainingDays === 0 ? '0 days' : '';
|
|
3704
|
+
const ageString = [
|
|
3705
|
+
formattedYears,
|
|
3706
|
+
formattedDays
|
|
3707
|
+
].filter(Boolean).join(' ');
|
|
3708
|
+
return ageInDays >= 0 ? ageString : `-${ageString}`;
|
|
3933
3709
|
};
|
|
3934
|
-
const { useGetRawDataAndTotalCountsQuery, useGetAccessibleDataQuery, useGetAllFieldsForTypeQuery, useGetAggsQuery, useLazyGetAggsQuery, useGetStatsAggregationsQuery, useLazyGetStatsAggregationsQuery, useGetSubAggsQuery, useGetCountsQuery, useLazyGetCountsQuery, useGetFieldCountSummaryQuery, useGetFieldsForIndexQuery, useGetSharedFieldsForIndexQuery, useGeneralGQLQuery, useLazyGeneralGQLQuery, useCustomRangeQuery, useLazyCustomRangeQuery, useGetObjectIdsQuery, useLazyGetObjectIdsQuery } = explorerApi;
|
|
3935
|
-
|
|
3936
|
-
/**
|
|
3937
|
-
* Defines coreListeners for adding middleware.
|
|
3938
|
-
* used to update the current Cohort Case count
|
|
3939
|
-
*/ const coreStoreListenerMiddleware = createListenerMiddleware();
|
|
3940
|
-
const startCoreListening = coreStoreListenerMiddleware.startListening;
|
|
3941
3710
|
/**
|
|
3942
|
-
*
|
|
3943
|
-
*
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
}
|
|
3711
|
+
* Given an object of JSON, stringify it into a string.
|
|
3712
|
+
* @param obj - the object to stringify
|
|
3713
|
+
* @param defaults - the default value to return if the object is undefined
|
|
3714
|
+
* @category Utility
|
|
3715
|
+
*/ const stringifyJSONParam = (obj, defaults = '{}')=>obj ? JSON.stringify(obj) : defaults;
|
|
3716
|
+
|
|
3717
|
+
const queryWTSFederatedLoginStatus = async (signal)=>{
|
|
3718
|
+
try {
|
|
3719
|
+
const results = await fetchJSONDataFromURL(`${GEN3_WTS_API}/external_oidc/`, false, HttpMethod.GET, undefined, signal);
|
|
3720
|
+
// Handle both null response and response without providers
|
|
3721
|
+
if (!results || !results.providers) {
|
|
3722
|
+
return {
|
|
3723
|
+
providers: []
|
|
3724
|
+
};
|
|
3956
3725
|
}
|
|
3726
|
+
return results;
|
|
3727
|
+
} catch (error) {
|
|
3728
|
+
return {
|
|
3729
|
+
providers: [],
|
|
3730
|
+
error: error instanceof Error ? error : new Error('Unknown error')
|
|
3731
|
+
};
|
|
3957
3732
|
}
|
|
3958
|
-
}
|
|
3733
|
+
};
|
|
3959
3734
|
|
|
3960
3735
|
/**
|
|
3961
|
-
*
|
|
3962
|
-
*
|
|
3963
|
-
*
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3736
|
+
* Input is a list of files selected for download.
|
|
3737
|
+
* Output is a array of GUID prefixes
|
|
3738
|
+
* @param selectedFiles
|
|
3739
|
+
*/ const gatherGUIDPrefixes = (selectedFiles)=>{
|
|
3740
|
+
return selectedFiles.reduce((acc, selectedFile)=>{
|
|
3741
|
+
if (selectedFile.object_id) {
|
|
3742
|
+
const guidDomainPrefix = (selectedFile.object_id.match(GUID_PREFIX_PATTERN) || []).shift();
|
|
3743
|
+
if (guidDomainPrefix) {
|
|
3744
|
+
if (!acc.includes(guidDomainPrefix)) {
|
|
3745
|
+
acc.push(guidDomainPrefix);
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3748
|
+
}
|
|
3749
|
+
return acc;
|
|
3750
|
+
}, []);
|
|
3751
|
+
};
|
|
3752
|
+
/**
|
|
3753
|
+
* Resolves DRS object ID from a list of guids using dataguids.org or other DRS resolver
|
|
3754
|
+
* @param guidPrefixes
|
|
3755
|
+
*/ const resolveDRSPrefixes = async (guidPrefixes)=>{
|
|
3756
|
+
return await resolveDRSObjectId(guidPrefixes);
|
|
3757
|
+
};
|
|
3758
|
+
/**
|
|
3759
|
+
* Given a list of object ids, extracts and DRS prefixes and resolves them to DRS server hostnames
|
|
3760
|
+
* then returns a list of resolved GUIDs, and a list of unresolved GUIDs
|
|
3761
|
+
* @param selectedFiles
|
|
3762
|
+
* @returns - {
|
|
3763
|
+
* resolvedDRSPrefixes: Record<string, string> - map of GUID prefixes to DRS server hostnames
|
|
3764
|
+
* resolvedFiles: ResolvedGUID[] - list of resolved GUIDs
|
|
3765
|
+
* unresolvedFiles: ResolvedGUID[] - list of unresolved GUIDs
|
|
3766
|
+
* }
|
|
3767
|
+
*/ const resolveGUIDsInSelectedFiles = async (selectedFiles)=>{
|
|
3768
|
+
if (!selectedFiles || selectedFiles.length === 0) {
|
|
3769
|
+
return {
|
|
3770
|
+
externalHosts: {},
|
|
3771
|
+
resolvedFiles: [],
|
|
3772
|
+
unresolvedFiles: []
|
|
3773
|
+
};
|
|
3971
3774
|
}
|
|
3972
|
-
const
|
|
3973
|
-
const
|
|
3974
|
-
|
|
3775
|
+
const guidPrefixes = gatherGUIDPrefixes(selectedFiles);
|
|
3776
|
+
const resolvedDRSPrefixes = await resolveDRSPrefixes(guidPrefixes);
|
|
3777
|
+
const resolvedIds = [];
|
|
3778
|
+
const unresolvedIds = [];
|
|
3779
|
+
selectedFiles.forEach((selectedFile)=>{
|
|
3780
|
+
if (selectedFile.object_id) {
|
|
3781
|
+
const guidDomainPrefix = (selectedFile.object_id.match(GUID_PREFIX_PATTERN) || []).shift();
|
|
3782
|
+
if (guidDomainPrefix && resolvedDRSPrefixes[guidDomainPrefix]) {
|
|
3783
|
+
resolvedIds.push({
|
|
3784
|
+
...selectedFile,
|
|
3785
|
+
drsPrefix: guidDomainPrefix,
|
|
3786
|
+
url: resolvedDRSPrefixes[guidDomainPrefix]
|
|
3787
|
+
});
|
|
3788
|
+
}
|
|
3789
|
+
} else {
|
|
3790
|
+
unresolvedIds.push(selectedFile);
|
|
3791
|
+
}
|
|
3975
3792
|
});
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3793
|
+
return {
|
|
3794
|
+
externalHosts: resolvedDRSPrefixes,
|
|
3795
|
+
resolvedFiles: resolvedIds,
|
|
3796
|
+
unresolvedFiles: unresolvedIds
|
|
3797
|
+
};
|
|
3798
|
+
};
|
|
3799
|
+
const fetchExternalLogins = async (externalProviders, selectedFiles)=>{
|
|
3800
|
+
const providers = externalProviders ?? [];
|
|
3801
|
+
// find all the providers that do not have tokens
|
|
3802
|
+
const unauthenticatedProviders = providers.filter((provider)=>!provider.refresh_token_expiration);
|
|
3803
|
+
try {
|
|
3804
|
+
const guidResolutions = await resolveGUIDsInSelectedFiles(selectedFiles);
|
|
3805
|
+
const providersToAuthenticate = unauthenticatedProviders.filter((unauthenticatedProvider)=>Object.values(guidResolutions.externalHosts).includes(new URL(unauthenticatedProvider.base_url).hostname));
|
|
3806
|
+
const missingProviders = providersToAuthenticate.filter((provider)=>!Object.values(guidResolutions.externalHosts).includes(new URL(provider.base_url).hostname));
|
|
3807
|
+
return {
|
|
3808
|
+
providersToAuthenticate: providersToAuthenticate,
|
|
3809
|
+
missingProviders: missingProviders
|
|
3810
|
+
};
|
|
3811
|
+
} catch (error) {
|
|
3812
|
+
if (error instanceof Error) {
|
|
3813
|
+
return {
|
|
3814
|
+
error
|
|
3815
|
+
};
|
|
3816
|
+
}
|
|
3817
|
+
return {
|
|
3818
|
+
error: new Error('Unknown error')
|
|
3819
|
+
};
|
|
3979
3820
|
}
|
|
3980
|
-
const data = await res.json();
|
|
3981
|
-
return data.resources ?? [];
|
|
3982
|
-
}
|
|
3983
|
-
|
|
3984
|
-
const persistConfig = {
|
|
3985
|
-
key: `${GEN3_COMMONS_NAME}-root`,
|
|
3986
|
-
version: 1,
|
|
3987
|
-
storage,
|
|
3988
|
-
whitelist: [
|
|
3989
|
-
'cohorts',
|
|
3990
|
-
'activeWorkspace',
|
|
3991
|
-
'cart',
|
|
3992
|
-
'workspaceKernels',
|
|
3993
|
-
'tieredWorkspace'
|
|
3994
|
-
]
|
|
3995
3821
|
};
|
|
3996
|
-
const
|
|
3997
|
-
const
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
3822
|
+
const useGetFederatedLoginStatus = ({ selectedFiles })=>{
|
|
3823
|
+
const { data: wtsResults, isLoading: wstIsLoading, error: wtsError } = useGetExternalLoginsQuery();
|
|
3824
|
+
// State to manage the asynchronous results
|
|
3825
|
+
const [result, setResult] = useState(null);
|
|
3826
|
+
useEffect(()=>{
|
|
3827
|
+
const fetchData = async ()=>{
|
|
3828
|
+
if (wtsError || !wtsResults) {
|
|
3829
|
+
if (wtsError instanceof Error) setResult({
|
|
3830
|
+
error: wtsError
|
|
3831
|
+
});
|
|
3832
|
+
else setResult({
|
|
3833
|
+
error: new Error('Unknown error')
|
|
3834
|
+
});
|
|
3835
|
+
return;
|
|
3836
|
+
}
|
|
3837
|
+
const results = await fetchExternalLogins(wtsResults.providers, selectedFiles);
|
|
3838
|
+
setResult(results);
|
|
3839
|
+
};
|
|
3840
|
+
// Only run if there's data to act on
|
|
3841
|
+
if (!wstIsLoading && wtsResults) {
|
|
3842
|
+
void fetchData();
|
|
3843
|
+
}
|
|
3844
|
+
}, [
|
|
3845
|
+
selectedFiles,
|
|
3846
|
+
wstIsLoading,
|
|
3847
|
+
wtsError,
|
|
3848
|
+
wtsResults
|
|
3849
|
+
]);
|
|
3850
|
+
return {
|
|
3851
|
+
isLoading: wstIsLoading,
|
|
3852
|
+
data: result,
|
|
3853
|
+
error: result?.error || wtsError
|
|
3854
|
+
};
|
|
3855
|
+
};
|
|
3856
|
+
const getFederatedLoginStatus = async (selectedFiles)=>{
|
|
3857
|
+
const providers = await queryWTSFederatedLoginStatus();
|
|
3858
|
+
return await fetchExternalLogins(providers.providers, selectedFiles);
|
|
4024
3859
|
};
|
|
4025
3860
|
|
|
4026
3861
|
const usePrevious = (value)=>{
|
|
@@ -6018,6 +5853,141 @@ const graphQLWithTags = gen3Api.enhanceEndpoints({
|
|
|
6018
5853
|
});
|
|
6019
5854
|
const { useGraphQLQuery } = graphQLAPI;
|
|
6020
5855
|
|
|
5856
|
+
/**
|
|
5857
|
+
* Flattens a deep nested JSON object skipping
|
|
5858
|
+
* the first level to avoid potentially flattening
|
|
5859
|
+
* non-nested data.
|
|
5860
|
+
* @param {JSON} json
|
|
5861
|
+
*/ function flattenJson(json) {
|
|
5862
|
+
const flattenedJson = [];
|
|
5863
|
+
Object.keys(json).forEach((key)=>{
|
|
5864
|
+
flattenedJson.push(flatten(json[key], {
|
|
5865
|
+
delimiter: '_'
|
|
5866
|
+
}));
|
|
5867
|
+
});
|
|
5868
|
+
return flattenedJson;
|
|
5869
|
+
}
|
|
5870
|
+
/**
|
|
5871
|
+
* Converts JSON based on a config.
|
|
5872
|
+
* @param {JSON} json
|
|
5873
|
+
* @param {Object} config
|
|
5874
|
+
*/ async function conversion(json, config) {
|
|
5875
|
+
return Papa.unparse(json, config);
|
|
5876
|
+
}
|
|
5877
|
+
/**
|
|
5878
|
+
* Converts JSON to a specified file format.
|
|
5879
|
+
* Defaults to JSON if file format is not supported.
|
|
5880
|
+
* @param {JSON} json
|
|
5881
|
+
* @param {string} format
|
|
5882
|
+
*/ async function jsonToFormat(json, format) {
|
|
5883
|
+
if (Object.keys(FILE_DELIMITERS).includes(format)) {
|
|
5884
|
+
const flatJson = flattenJson(json);
|
|
5885
|
+
const data = await conversion(flatJson, {
|
|
5886
|
+
delimiter: FILE_DELIMITERS[format]
|
|
5887
|
+
});
|
|
5888
|
+
return data;
|
|
5889
|
+
}
|
|
5890
|
+
return json;
|
|
5891
|
+
}
|
|
5892
|
+
|
|
5893
|
+
const prepareUrl = (apiUrl)=>`${apiUrl}/download`;
|
|
5894
|
+
const prepareFetchConfig = (parameters, csrfToken)=>{
|
|
5895
|
+
const headers = new Headers({
|
|
5896
|
+
Accept: 'application/json',
|
|
5897
|
+
'Content-Type': 'application/json',
|
|
5898
|
+
...csrfToken !== undefined && {
|
|
5899
|
+
'X-CSRF-Token': csrfToken
|
|
5900
|
+
}
|
|
5901
|
+
});
|
|
5902
|
+
if (process.env.NODE_ENV === 'development') {
|
|
5903
|
+
const accessToken = getCookie('credentials_token');
|
|
5904
|
+
if (accessToken) headers.set('Authorization', `Bearer ${accessToken}`);
|
|
5905
|
+
}
|
|
5906
|
+
return {
|
|
5907
|
+
method: 'POST',
|
|
5908
|
+
headers: headers,
|
|
5909
|
+
body: JSON.stringify({
|
|
5910
|
+
type: parameters.type,
|
|
5911
|
+
filter: convertFilterSetToGqlFilter(parameters.filter),
|
|
5912
|
+
accessibility: parameters.accessibility,
|
|
5913
|
+
fields: parameters?.fields,
|
|
5914
|
+
sort: parameters?.sort
|
|
5915
|
+
})
|
|
5916
|
+
};
|
|
5917
|
+
};
|
|
5918
|
+
const downloadFromGuppyToBlob = async ({ parameters, onStart = ()=>null, onDone = (_)=>null, onError = (_)=>null, onAbort = ()=>null, signal = undefined })=>{
|
|
5919
|
+
const csrfToken = selectCSRFToken(coreStore.getState());
|
|
5920
|
+
onStart?.();
|
|
5921
|
+
const url = prepareUrl(GEN3_GUPPY_API);
|
|
5922
|
+
const fetchConfig = prepareFetchConfig(parameters, csrfToken);
|
|
5923
|
+
fetch(url.toString(), {
|
|
5924
|
+
...fetchConfig,
|
|
5925
|
+
...signal ? {
|
|
5926
|
+
signal: signal
|
|
5927
|
+
} : {}
|
|
5928
|
+
}).then(async (response)=>{
|
|
5929
|
+
if (!response.ok) {
|
|
5930
|
+
throw new Error(response.statusText);
|
|
5931
|
+
}
|
|
5932
|
+
let jsonData = await response.json();
|
|
5933
|
+
if (parameters?.rootPath) {
|
|
5934
|
+
jsonData = JSONPath({
|
|
5935
|
+
json: jsonData,
|
|
5936
|
+
path: `$.[${parameters.rootPath}]`,
|
|
5937
|
+
resultType: 'value'
|
|
5938
|
+
});
|
|
5939
|
+
}
|
|
5940
|
+
let str = '';
|
|
5941
|
+
if (parameters.format === 'json') {
|
|
5942
|
+
str = JSON.stringify(jsonData);
|
|
5943
|
+
} else {
|
|
5944
|
+
const convertedData = await jsonToFormat(jsonData, parameters.format);
|
|
5945
|
+
if (isJSONObject(convertedData)) {
|
|
5946
|
+
str = JSON.stringify(convertedData, null, 2);
|
|
5947
|
+
} else {
|
|
5948
|
+
str = convertedData;
|
|
5949
|
+
}
|
|
5950
|
+
}
|
|
5951
|
+
return new Blob([
|
|
5952
|
+
str
|
|
5953
|
+
], {
|
|
5954
|
+
type: 'application/json'
|
|
5955
|
+
});
|
|
5956
|
+
}).then((blob)=>onDone?.(blob)).catch((error)=>{
|
|
5957
|
+
if (error.name == 'AbortError') {
|
|
5958
|
+
onAbort?.();
|
|
5959
|
+
}
|
|
5960
|
+
onError?.(error);
|
|
5961
|
+
});
|
|
5962
|
+
};
|
|
5963
|
+
const downloadJSONDataFromGuppy = async ({ parameters, onAbort = ()=>null, signal = undefined })=>{
|
|
5964
|
+
const csrfToken = selectCSRFToken(coreStore.getState());
|
|
5965
|
+
const url = prepareUrl(GEN3_GUPPY_API);
|
|
5966
|
+
const fetchConfig = prepareFetchConfig(parameters, csrfToken);
|
|
5967
|
+
try {
|
|
5968
|
+
const response = await fetch(url.toString(), {
|
|
5969
|
+
...fetchConfig,
|
|
5970
|
+
...signal ? {
|
|
5971
|
+
signal: signal
|
|
5972
|
+
} : {}
|
|
5973
|
+
});
|
|
5974
|
+
let jsonData = await response.json();
|
|
5975
|
+
if (parameters?.rootPath) {
|
|
5976
|
+
jsonData = JSONPath({
|
|
5977
|
+
json: jsonData,
|
|
5978
|
+
path: `$.[${parameters.rootPath}]`,
|
|
5979
|
+
resultType: 'value'
|
|
5980
|
+
});
|
|
5981
|
+
}
|
|
5982
|
+
return jsonData;
|
|
5983
|
+
} catch (error) {
|
|
5984
|
+
if (error.name === 'AbortError') {
|
|
5985
|
+
onAbort?.();
|
|
5986
|
+
}
|
|
5987
|
+
throw new Error(error);
|
|
5988
|
+
}
|
|
5989
|
+
};
|
|
5990
|
+
|
|
6021
5991
|
/**
|
|
6022
5992
|
* Creates a Guppy API for fetching bulk (> 10K rows) elasticsearch data
|
|
6023
5993
|
* @see https://github.com/uc-cdis/guppy/blob/master/doc/download.md
|
|
@@ -7049,5 +7019,5 @@ const showNotification = (id, title, message, type, options)=>{
|
|
|
7049
7019
|
notificationService.showNotification(id, title, message, type, options);
|
|
7050
7020
|
};
|
|
7051
7021
|
|
|
7052
|
-
export { Accessibility, CART_LIMIT, CohortStorage, CoreProvider, DAYS_IN_YEAR, DataLibraryStoreMode, EmptyFilterSet, EmptyWorkspaceStatusResponse, EnumValueExtractorHandler, ExtractValueFromObject, FILE_DELIMITERS, FILE_FORMATS, GEN3_ANALYSIS_API, GEN3_API, GEN3_AUTHZ_API, GEN3_AUTHZ_SERVICE, GEN3_COMMONS_NAME, GEN3_CROSSWALK_API, GEN3_DOMAIN, GEN3_DOWNLOADS_ENDPOINT, GEN3_FENCE_API, GEN3_FENCE_SERVICE, GEN3_GUPPY_API, GEN3_INDEXD_API, GEN3_MANIFEST_API, GEN3_MDS_API, GEN3_REDIRECT_URL, GEN3_SOWER_API, GEN3_SUBMISSION_API, GEN3_WORKSPACE_API, HTTPError, HTTPErrorMessages, HTTPUserFriendlyErrorMessages, HttpMethod, MissingServiceConfigurationError, Modals, PodConditionType, PodStatus, RequestedWorkspaceStatus, ToGqlAllNested, ToGqlHandler, ValueExtractorHandler, WorkspaceStatus, addItemsToCart, addJEGActiveKernel, ageDisplay, appendFilterToOperation, buildCohortGqlOperator, buildGetAggregationQuery, buildGetStatsAggregationQuery, buildListItemsGroupedByDataset, buildNestedFilterForOperation, buildNestedGQLFilter, buildNestedWithParentPathGQLFilter, buildRangeQuery, calculatePercentageAsNumber, calculatePercentageAsString, capitalize
|
|
7022
|
+
export { Accessibility, CART_LIMIT, CohortStorage, CoreProvider, DAYS_IN_YEAR, DataLibraryStoreMode, EmptyFilterSet, EmptyWorkspaceStatusResponse, EnumValueExtractorHandler, ExtractValueFromObject, FILE_DELIMITERS, FILE_FORMATS, GEN3_ANALYSIS_API, GEN3_API, GEN3_AUTHZ_API, GEN3_AUTHZ_SERVICE, GEN3_COMMONS_NAME, GEN3_CROSSWALK_API, GEN3_DOMAIN, GEN3_DOWNLOADS_ENDPOINT, GEN3_FENCE_API, GEN3_FENCE_SERVICE, GEN3_GUPPY_API, GEN3_INDEXD_API, GEN3_MANIFEST_API, GEN3_MDS_API, GEN3_REDIRECT_URL, GEN3_SOWER_API, GEN3_SUBMISSION_API, GEN3_WORKSPACE_API, HTTPError, HTTPErrorMessages, HTTPUserFriendlyErrorMessages, HttpMethod, MissingServiceConfigurationError, Modals, PodConditionType, PodStatus, RequestedWorkspaceStatus, ToGqlAllNested, ToGqlHandler, ValueExtractorHandler, WorkspaceStatus, addItemsToCart, addJEGActiveKernel, ageDisplay, appendFilterToOperation, buildCohortGqlOperator, buildGetAggregationQuery, buildGetStatsAggregationQuery, buildListItemsGroupedByDataset, buildNestedFilterForOperation, buildNestedGQLFilter, buildNestedWithParentPathGQLFilter, buildRangeQuery, calculatePercentageAsNumber, calculatePercentageAsString, capitalize, cartReducer, cartReducerPath, cedarApi, clearActiveWorkspaceId, clearCohortFilters, clearJEGActiveKernels, clearJEGActiveWorkspaceId, cohortReducer, configRegistry, conversion, convertFilterSetToGqlFilter, convertFilterSetToNestedGqlFilter, convertFilterSetToOperation, convertFilterToGqlFilter, convertFilterToNestedGqlFilter, convertGqlFilterToFilter, convertToHistogramDataAsStringKey, convertToQueryString, coreStore, createAppApiForRTKQ, createAppStore, createCEDARInstance, createGen3App, createGen3AppWithOwnStore, createNewCohort, createUseCoreDataHook, customQueryStrForField, defaultCohortNameGenerator, downloadFromGuppyToBlob, downloadJSONDataFromGuppy, drsHostnamesReducer, duplicateCohort, explorerApi, explorerTags, extractContents, extractEnumFilterValue, extractFieldNameFromFullFieldName, extractFileDatasetsInRecords, extractFilterValue, extractFiltersWithPrefixFromFilterSet, extractIndexAndFieldNameFromFullFieldName, extractIndexFromDataLibraryCohort, extractIndexFromFullFieldName, fetchArboristResources, fetchFence, fetchFencePresignedURL, fetchJSONDataFromURL, fetchJson, fetchUserState, fieldNameToLabel, filterSetToOperation, formatUptimeInMinutes, gen3Api, generateUniqueName, getCurrentTimestamp, getFederatedLoginStatus, getGen3AppId, getNumberOfItemsInDatalist, getRemoteSupportServiceRegistry, getTimestamp, graphQLAPI, graphQLWithTags, groupSharedFields, guppyAPISliceMiddleware, guppyApi, guppyApiReducer, guppyApiSliceReducerPath, guppyDownloadApi, handleGqlOperation, handleOperation, hideModal, histogramQueryStrForEachField, humanify, ifOperationWithField, isAdditionalDataItem, isArray, isAuthenticated, isCohortItem, isDataLibraryAPIResponse, isDatalistAPI, isErrorWithMessage, isFetchBaseQueryError, isFetchError, isFetchParseError, isFileItem, isFilterEmpty, isFilterSet, isGQLIntersection, isGQLUnion, isGuppyAggregationData, isHistogramData, isHistogramDataAArray, isHistogramDataAnEnum, isHistogramDataArray, isHistogramDataArrayARange, isHistogramDataArrayAnEnum, isHistogramDataCollection, isHistogramRangeData, isHttpStatusError, isIncludes, isIndexedFilterSetEmpty, isIntersection, isIntersectionOrUnion, isJSONObject, isJSONValue, isJSONValueArray, isNameUnique, isNestedFilter, isNotDefined, isObject, isOperandsType, isOperationWithField, isOperatorWithFieldAndArrayOfOperands, isPending, isProgramUrl, isRootUrl, isSerializedError, isStatsValue, isStatsValuesArray, isString, isTimeGreaterThan, isUnion, isWorkspaceActive, isWorkspaceRunningOrStopping, joinFilters, jsonToFormat, listifyMethodsFromMapping, logoutFence, manifestApi, manifestTags, mdsApi, nestedHistogramQueryStrForEachField, normalizeRtkError, notificationService, prepareUrl$1 as prepareUrl, prependIndexToFieldName, printRegistry, processHistogramResponse, projectCodeFromResourcePath, queryMultipleMDSRecords, rawDataQueryStrForEachField$1 as rawDataQueryStrForEachField, registerDefaultRemoteSupport, removeCohort, removeCohortFilter, removeItemsFromCart, removeJEGActiveKernel, removeManyJEGActiveKernels, requestorApi, resetUserState, resourcePathFromProjectID, roundHistogramResponse, selectActiveWorkspaceId, selectActiveWorkspaceStatus, selectAllCohortFiltersCollapsed, selectAllCohorts, selectAllJEGKernels, selectAuthzMappingData, selectAvailableCohortByName, selectAvailableCohorts, selectCSRFToken, selectCSRFTokenData, selectCart, selectCartCount, selectCartItem, selectCartItems, selectCohortById, selectCohortFilterCombineMode, selectCohortFilterExpanded, selectCohortFilters, selectCohortIds, selectCurrentCohort, selectCurrentCohortFilters, selectCurrentCohortId, selectCurrentCohortModified, selectCurrentCohortName, selectCurrentCohortSaved, selectCurrentMessage, selectCurrentModal, selectGen3AppByName, selectGen3AppMetadataByName, selectHeadersWithCSRFToken, selectIndexFilters, selectIndexedFilterByName, selectJEGActiveWorkspaceId, selectJEGActiveWorkspaceStatus, selectJEGKernelById, selectJEGKernelIds, selectJEGRequestedWorkspaceStatus, selectJEGRequestedWorkspaceStatusTimestamp, selectPaymodelStatus, selectRequestedWorkspaceStatus, selectRequestedWorkspaceStatusTimestamp, selectSharedFilters, selectSharedFiltersForFields, selectShouldShareFilters, selectSowerJobDatetimeCache, selectTotalCohorts, selectUser, selectUserAuthStatus, selectUserData, selectUserDetails, selectUserLoginStatus, selectWorkspaceFullscreen, selectWorkspaceStatus, selectWorkspaceStatusFromService, selectWorkspaceTier, setActiveWorkspace, setActiveWorkspaceId, setActiveWorkspaceStatus, setCohortFilter, setCohortFilterCombineMode, setCohortIndexFilters, setCohortList, setCurrentCohortId, setDRSHostnames, setJEGActiveWorkspace, setJEGActiveWorkspaceId, setJEGActiveWorkspaceStatus, setJEGRequestedWorkspaceStatus, setRequestedWorkspaceStatus, setSharedFilters, setShouldShareFilters, setWorkspaceFullscreen, setWorkspaceTier, setupCoreStore, showModal, showNotification, statsQueryStrForEachField, stringifyJSONParam, submissionApi, toggleCohortBuilderAllFilters, toggleCohortBuilderCategoryFilter, trimFirstfieldNameToLabel, updateCohortFilter, updateCohortName, updateJEGActionKernelStatus, updateStudyInMDS, upsertManyJEGActiveKernels, useAddCohortManifestMutation, useAddFileManifestMutation, useAddMetadataManifestMutation, useAddNewCredentialMutation, useAskQuestionMutation, useAuthorizeFromCredentialsMutation, useCohortFacetsQuery, useCoreDispatch, useCoreSelector, useCreateAuthzResourceMutation, useCreateRequestMutation, useCustomRangeQuery, useDataLibrary, useDownloadFromGuppyQuery, useFetchUserDetailsQuery, useGeneralGQLQuery, useGetAISearchStatusQuery, useGetAISearchVersionQuery, useGetAccessibleDataQuery, useGetActivePayModelQuery, useGetAggMDSQuery, useGetAggsQuery, useGetAllFieldsForTypeQuery, useGetArrayTypes, useGetAuthzMappingsQuery, useGetAuthzResourcesQuery, useGetCSRFQuery, useGetCohortManifestQuery, useGetCountsQuery, useGetCredentialsQuery, useGetCrosswalkDataQuery, useGetDataQuery, useGetDictionaryFromUrlQuery, useGetDictionaryQuery, useGetDownloadQuery, useGetExternalLoginsQuery, useGetFederatedLoginStatus, useGetFieldCountSummaryQuery, useGetFieldsForIndexQuery, useGetFileFromManifestQuery, useGetFileManifestQuery, useGetIndexAggMDSQuery, useGetIndexFields, useGetIndexObjectQuery, useGetIndexdMetdataQuery, useGetJWKKeysQuery, useGetLoginProvidersQuery, useGetMDSQuery, useGetManifestServiceStatusQuery, useGetMetadataByIdQuery, useGetMetadataByUrlQuery, useGetMetadataFromManifestQuery, useGetMetadataManifestQuery, useGetObjectIdsQuery, useGetPresignedUrlQuery, useGetProjectsDetailsQuery, useGetProjectsQuery, useGetRawDataAndTotalCountsQuery, useGetSharedFieldsForIndexQuery, useGetSowerJobListQuery, useGetSowerJobStatusQuery, useGetSowerOutputQuery, useGetSowerServiceStatusQuery, useGetStatsAggregationsQuery, useGetStatus, useGetSubAggsQuery, useGetSubmissionGraphQLQuery, useGetSubmissionsQuery, useGetTagsQuery, useGetUserDetailsRequestStatus, useGetWorkspaceOptionsQuery, useGetWorkspacePayModelsQuery, useGetWorkspaceStatusQuery, useGraphQLQuery, useIsExternalConnectedQuery, useIsUserLoggedIn, useLaunchWorkspaceMutation, useLazyCustomRangeQuery, useLazyDownloadFromGuppyQuery, useLazyFetchUserDetailsQuery, useLazyGeneralGQLQuery, useLazyGetAggsQuery, useLazyGetAuthzMappingsQuery, useLazyGetAuthzResourcesQuery, useLazyGetCSRFQuery, useLazyGetCountsQuery, useLazyGetCrosswalkDataQuery, useLazyGetDownloadQuery, useLazyGetExternalLoginsQuery, useLazyGetIndexObjectQuery, useLazyGetIndexdMetdataQuery, useLazyGetJWKKeysQuery, useLazyGetManifestServiceStatusQuery, useLazyGetMultipleSowerJobStatusQuery, useLazyGetObjectIdsQuery, useLazyGetPresignedUrlQuery, useLazyGetProjectsQuery, useLazyGetSowerJobListQuery, useLazyGetSowerJobStatusQuery, useLazyGetSowerOutputQuery, useLazyGetStatsAggregationsQuery, useLazyGetSubmissionGraphQLQuery, useLazyIsExternalConnectedQuery, useLazyRequestQuery, usePValueQuery, usePrevious, useRemoveCredentialMutation, useRequestByIdQuery, useRequestQuery, useRequestorStatusQuery, useSetCurrentPayModelMutation, useSubmitSowerJobMutation, useTerminateWorkspaceMutation, useUpdateStudyInMDSMutation, useUserAuth, useUserRequestQuery, useVennDiagramQuery, userHasCreateOrUpdateOnAnyProject, userHasDataUpload, userHasMethodForServiceOnProject, userHasMethodForServiceOnResource, userHasMethodOnAnyProject, userHasSheepdogProgramAdmin, userHasSheepdogProjectAdmin };
|
|
7053
7023
|
//# sourceMappingURL=index.js.map
|