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