@gen3/core 0.12.69 → 0.12.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/dist/cjs/index.js +2124 -2154
  2. package/dist/cjs/index.js.map +1 -1
  3. package/dist/dts/constants.d.ts.map +1 -1
  4. package/dist/dts/dataAccess.d.ts.map +1 -1
  5. package/dist/dts/features/aiSearch/aiSearchSlice.d.ts +174 -174
  6. package/dist/dts/features/app/store.d.ts.map +1 -1
  7. package/dist/dts/features/authz/authzMappingSlice.d.ts +403 -403
  8. package/dist/dts/features/cedar/cedarApi.d.ts +54 -54
  9. package/dist/dts/features/cedar/cedarApi.d.ts.map +1 -1
  10. package/dist/dts/features/cedar/createCedarInstance.d.ts.map +1 -1
  11. package/dist/dts/features/cohort/cohortManagerSelector.d.ts +5 -5
  12. package/dist/dts/features/cohort/cohortManagerSelector.d.ts.map +1 -1
  13. package/dist/dts/features/cohort/cohortManagerSlice.d.ts.map +1 -1
  14. package/dist/dts/features/cohort/filterCombineModeSlice.d.ts.map +1 -1
  15. package/dist/dts/features/cohort/filterExpandSlice.d.ts.map +1 -1
  16. package/dist/dts/features/cohort/sharedFiltersSlice.d.ts +1 -1
  17. package/dist/dts/features/cohort/sharedFiltersSlice.d.ts.map +1 -1
  18. package/dist/dts/features/cohort/storage/CohortStorage.d.ts.map +1 -1
  19. package/dist/dts/features/cohort/utils.d.ts.map +1 -1
  20. package/dist/dts/features/cohortComparison/cohortFacetSlice.d.ts +60 -60
  21. package/dist/dts/features/cohortComparison/pValueApi.d.ts +60 -60
  22. package/dist/dts/features/cohortComparison/vennDiagramSlice.d.ts +60 -60
  23. package/dist/dts/features/config/registry.d.ts.map +1 -1
  24. package/dist/dts/features/dataLibrary/storage/APIStorageService.d.ts.map +1 -1
  25. package/dist/dts/features/dataLibrary/storage/CachedAPIStorageService.d.ts.map +1 -1
  26. package/dist/dts/features/dataLibrary/storage/DataLibraryStorageService.d.ts.map +1 -1
  27. package/dist/dts/features/dataLibrary/storage/LocalStorageService.d.ts.map +1 -1
  28. package/dist/dts/features/dataLibrary/storage/tests/data.d.ts.map +1 -1
  29. package/dist/dts/features/dataLibrary/types.d.ts.map +1 -1
  30. package/dist/dts/features/dataLibrary/useDataLibrary.d.ts.map +1 -1
  31. package/dist/dts/features/dataLibrary/utils.d.ts.map +1 -1
  32. package/dist/dts/features/download/constants.d.ts.map +1 -1
  33. package/dist/dts/features/download/downloadStatusApi.d.ts +235 -235
  34. package/dist/dts/features/drsResolver/drsHostnameSlice.d.ts.map +1 -1
  35. package/dist/dts/features/drsResolver/resolvers/cachedDRSResolver.d.ts.map +1 -1
  36. package/dist/dts/features/drsResolver/resolvers/dataGUIDSDotOrg.d.ts.map +1 -1
  37. package/dist/dts/features/drsResolver/utils.d.ts.map +1 -1
  38. package/dist/dts/features/fence/credentialsApi.d.ts +222 -222
  39. package/dist/dts/features/fence/fenceApi.d.ts +300 -300
  40. package/dist/dts/features/fence/fenceApi.d.ts.map +1 -1
  41. package/dist/dts/features/fence/fetchFence.d.ts.map +1 -1
  42. package/dist/dts/features/fence/jwtApi.d.ts +120 -120
  43. package/dist/dts/features/fence/utils.d.ts.map +1 -1
  44. package/dist/dts/features/filters/filters.d.ts +1 -1
  45. package/dist/dts/features/filters/filters.d.ts.map +1 -1
  46. package/dist/dts/features/filters/nestedFilters.d.ts +1 -1
  47. package/dist/dts/features/filters/nestedFilters.d.ts.map +1 -1
  48. package/dist/dts/features/filters/types.d.ts.map +1 -1
  49. package/dist/dts/features/filters/utils.d.ts.map +1 -1
  50. package/dist/dts/features/gen3Apps/Gen3App.d.ts.map +1 -1
  51. package/dist/dts/features/gen3Apps/Gen3AppRTKQ.d.ts +1 -1
  52. package/dist/dts/features/gen3Apps/Gen3AppRTKQ.d.ts.map +1 -1
  53. package/dist/dts/features/gen3Apps/gen3AppRegistry.d.ts.map +1 -1
  54. package/dist/dts/features/gen3Apps/gen3AppsSlice.d.ts.map +1 -1
  55. package/dist/dts/features/graphQL/graphQLSlice.d.ts +61 -61
  56. package/dist/dts/features/guppy/download.d.ts +9 -0
  57. package/dist/dts/features/guppy/download.d.ts.map +1 -0
  58. package/dist/dts/features/guppy/guppyApi.d.ts +2 -2
  59. package/dist/dts/features/guppy/guppyDownloadSlice.d.ts +120 -120
  60. package/dist/dts/features/guppy/guppySlice.d.ts +2141 -2141
  61. package/dist/dts/features/guppy/guppySlice.d.ts.map +1 -1
  62. package/dist/dts/features/guppy/index.d.ts +2 -1
  63. package/dist/dts/features/guppy/index.d.ts.map +1 -1
  64. package/dist/dts/features/guppy/processing.d.ts.map +1 -1
  65. package/dist/dts/features/guppy/queryGenerators.d.ts.map +1 -1
  66. package/dist/dts/features/guppy/range.d.ts.map +1 -1
  67. package/dist/dts/features/guppy/utils.d.ts +1 -27
  68. package/dist/dts/features/guppy/utils.d.ts.map +1 -1
  69. package/dist/dts/features/indexd/indexdSlice.d.ts +240 -240
  70. package/dist/dts/features/manifest/manifestApi.d.ts +585 -585
  71. package/dist/dts/features/metadata/metadataSlice.d.ts +540 -540
  72. package/dist/dts/features/metadata/utils.d.ts.map +1 -1
  73. package/dist/dts/features/modals/modalsSlice.d.ts.map +1 -1
  74. package/dist/dts/features/notifications/notificationService.d.ts.map +1 -1
  75. package/dist/dts/features/requestor/remoteSupport/registeredRemoteSupportServices.d.ts.map +1 -1
  76. package/dist/dts/features/requestor/remoteSupport/types.d.ts.map +1 -1
  77. package/dist/dts/features/requestor/requestorSlice.d.ts +354 -354
  78. package/dist/dts/features/requestor/requestorSlice.d.ts.map +1 -1
  79. package/dist/dts/features/sower/sowerApi.d.ts +558 -558
  80. package/dist/dts/features/sower/sowerApi.d.ts.map +1 -1
  81. package/dist/dts/features/sower/sowerJobDatetime.d.ts.map +1 -1
  82. package/dist/dts/features/submission/authMappingUtils.d.ts.map +1 -1
  83. package/dist/dts/features/submission/submissionApi.d.ts +481 -481
  84. package/dist/dts/features/updateStudyInMDS/mdsApi.d.ts +54 -54
  85. package/dist/dts/features/updateStudyInMDS/updateStudyInMDS.d.ts.map +1 -1
  86. package/dist/dts/features/user/externalLoginsSlice.d.ts +241 -241
  87. package/dist/dts/features/user/externalLoginsSlice.d.ts.map +1 -1
  88. package/dist/dts/features/user/hooks.d.ts +1 -1
  89. package/dist/dts/features/user/hooks.d.ts.map +1 -1
  90. package/dist/dts/features/user/userSlice.d.ts +2 -2
  91. package/dist/dts/features/user/userSlice.d.ts.map +1 -1
  92. package/dist/dts/features/user/userSliceRTK.d.ts +672 -692
  93. package/dist/dts/features/user/userSliceRTK.d.ts.map +1 -1
  94. package/dist/dts/features/user/utils.d.ts.map +1 -1
  95. package/dist/dts/features/workspace/jegKernelSlice.d.ts +83 -83
  96. package/dist/dts/features/workspace/jegWorkspaceSlice.d.ts.map +1 -1
  97. package/dist/dts/features/workspace/tieredWorkspaceSlice.d.ts.map +1 -1
  98. package/dist/dts/features/workspace/utils.d.ts.map +1 -1
  99. package/dist/dts/features/workspace/workspaceApi.d.ts +939 -939
  100. package/dist/dts/features/workspace/workspaceSlice.d.ts.map +1 -1
  101. package/dist/dts/hooks.d.ts +1 -1
  102. package/dist/dts/reducers.d.ts +24 -24
  103. package/dist/dts/storage-persist.d.ts.map +1 -1
  104. package/dist/dts/store.d.ts +3 -3
  105. package/dist/dts/store.d.ts.map +1 -1
  106. package/dist/dts/types/types.d.ts.map +1 -1
  107. package/dist/dts/utils/caseConversion.d.ts.map +1 -1
  108. package/dist/dts/utils/conversions.d.ts.map +1 -1
  109. package/dist/dts/utils/extractvalues.d.ts.map +1 -1
  110. package/dist/dts/utils/fetch.d.ts +2 -2
  111. package/dist/dts/utils/fetch.d.ts.map +1 -1
  112. package/dist/dts/utils/time.d.ts.map +1 -1
  113. package/dist/dts/utils/ts-utils.d.ts.map +1 -1
  114. package/dist/dts/utils/url.d.ts.map +1 -1
  115. package/dist/esm/index.js +2124 -2154
  116. package/dist/esm/index.js.map +1 -1
  117. package/dist/index.d.ts +8363 -8394
  118. 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 flat = require('flat');
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
- * @description Creates a externalLoginApi for listing the configured identity providers
682
- * in workspace token service. Includes user token expiration time.
683
- * @see https://github.com/uc-cdis/workspace-token-service/tree/master
684
- */ const externalLoginApi = gen3Api.injectEndpoints({
685
- endpoints: (builder)=>({
686
- getExternalLogins: builder.query({
687
- query: ()=>({
688
- url: `${GEN3_WTS_API}/external_oidc/`
689
- })
690
- }),
691
- isExternalConnected: builder.query({
692
- query: (idp)=>({
693
- url: `${GEN3_WTS_API}/oauth2/connected?idp=${idp}`
694
- }),
695
- transformResponse: ()=>{
696
- return true; // if success then connected is true
697
- }
698
- })
699
- })
700
- });
701
- const { useGetExternalLoginsQuery, useLazyGetExternalLoginsQuery, useLazyIsExternalConnectedQuery, useIsExternalConnectedQuery } = externalLoginApi;
702
-
703
- /**
704
- * Custom error class for resolving DRS object ID errors
705
- */ class ResolveDRSError extends Error {
706
- constructor(message){
707
- super(message);
708
- this.name = 'ResolveDRSError';
709
- }
710
- }
711
- /**
712
- * Resolves DRS object ID from a list of guids using dataguids.org
713
- * Use of this function should be limited
714
- * @param guidsForHostnameResolution
715
- */ const resolveDRSWithDataGUISOrg = async (guidsForHostnameResolution)=>{
716
- const INVALID_INPUT_ERROR = 'Invalid input: guidsForHostnameResolution must be an array of strings';
717
- const RESOLVE_DRS_ERROR = 'Failed to resolve DRS object ID';
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
- } else {
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
- return responses.reduce((acc, response, index)=>{
741
- if (response.status === 'fulfilled' && response.value.data) {
721
+ return {
722
+ data
723
+ };
724
+ } catch (e) {
725
+ if (e instanceof graphql.GraphQLError) {
742
726
  return {
743
- ...acc,
744
- [guidsForHostnameResolution[index]]: response.value.data
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
- // oxlint-disable-next-line no-unused-vars
751
- } catch (error) {
752
- throw new ResolveDRSError(RESOLVE_DRS_ERROR);
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
- return resolvedIds;
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
- const DRS_Resolvers = {
769
- dataGUISOrg: resolveDRSWithDataGUISOrg,
770
- cached: resolveCachedDRS
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
- // TODO - add to config file for DRS resolver
773
- const resolveDRSObjectId = (guidsForHostnameResolution, resolver = "cached")=>{
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
- const isNotDefined = (x)=>{
778
- return x === undefined || x === null || x === void 0;
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 isObject = (x)=>{
781
- return typeof x === 'object';
783
+ const ifOperationWithField = (operation)=>{
784
+ return operation.field !== undefined;
782
785
  };
783
- const isArray = (x)=>{
784
- return Array.isArray(x);
786
+ const isNestedFilter = (operation)=>{
787
+ return operation.operator === 'nested';
785
788
  };
786
- const isString = (x)=>{
787
- return typeof x === 'string';
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
- * Prepares a URL for downloading by appending '/download' to the provided apiUrl.
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
- const extractValuesFromObject = (jsonPathMappings, obj)=>{
983
- const result = {};
984
- const extractObjectValue = (jsonPath, obj)=>{
985
- const extractedValues = jsonpathPlus.JSONPath({
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 result;
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 isJSONValue = (data)=>{
1011
- return typeof data === 'string' || typeof data === 'number' || typeof data === 'boolean' || Array.isArray(data) && data.every(isJSONValue) || isJSONObject(data);
806
+ const extractFilterValue = (op)=>{
807
+ const valueExtractorHandler = new ValueExtractorHandler();
808
+ return handleOperation(valueExtractorHandler, op);
1012
809
  };
1013
- const isJSONValueArray = (data)=>{
1014
- return Array.isArray(data) && data.every(isJSONValue);
810
+ const extractEnumFilterValue = (op)=>{
811
+ const enumValueExtractorHandler = new EnumValueExtractorHandler();
812
+ const results = handleOperation(enumValueExtractorHandler, op);
813
+ return results ?? [];
1015
814
  };
1016
- const isValidObject = (input)=>typeof input === 'object' && input !== null;
1017
- const isHistogramData = (data)=>{
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 isHistogramDataArray = (input)=>{
1021
- if (!isValidObject(input) || !Array.isArray(input.histogram)) {
1022
- return false;
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
- // Type guard function for GuppyAggregationData interface
1030
- const isGuppyAggregationData = (obj)=>{
1031
- if (!isValidObject(obj)) return false;
1032
- for(const key in obj){
1033
- if (!isHistogramDataCollection(obj[key])) {
1034
- return false;
1035
- }
1036
- }
1037
- return true;
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
- const isHistogramDataAnEnum = (data)=>{
1040
- return typeof data === 'object' && data !== null && 'key' in data && 'count' in data && typeof data.key === 'string' && typeof data.count === 'number';
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
- const isStatsValue = (item)=>{
1043
- if (typeof item !== 'object' || item === null) {
1044
- return false;
1045
- }
1046
- const obj = item;
1047
- // Check that all present properties have correct types
1048
- const numericFields = [
1049
- 'count',
1050
- 'min',
1051
- 'max',
1052
- 'avg',
1053
- 'sum',
1054
- 'stddev',
1055
- 'median'
1056
- ];
1057
- if (!numericFields.some((field)=>field in obj && typeof obj[field] !== 'number')) {
1058
- return false;
1059
- }
1060
- // Check percentiles structure if present
1061
- if ('percentiles' in obj) {
1062
- const percentiles = obj.percentiles;
1063
- if (typeof percentiles !== 'object' || percentiles === null) {
1064
- return false;
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 HTTPUserFriendlyErrorMessages = {
1106
- // 4xx Client Errors
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 MAX_ERROR_LENGTH = 256;
1151
- const DEFAULT_API_ERROR_MESSAGE = 'API Query Error';
1152
- /**
1153
- * Checks if a string appears to be an HTML page
1154
- */ function isHtmlContent(str) {
1155
- const trimmed = str.trim().toLowerCase();
1156
- return trimmed.startsWith('<!doctype html') || trimmed.startsWith('<html') || /<html[\s>]/i.test(str);
1157
- }
1158
- /**
1159
- * Sanitizes an error message - returns DEFAULT_API_ERROR_MESSAGE if
1160
- * the message is too long or contains HTML content
1161
- */ function sanitizeErrorMessage(message) {
1162
- if (!message) return DEFAULT_API_ERROR_MESSAGE;
1163
- if (message.length > MAX_ERROR_LENGTH || isHtmlContent(message)) {
1164
- return DEFAULT_API_ERROR_MESSAGE;
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
- return message;
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
- return undefined;
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 (isFetchBaseQueryError(error)) {
1192
- // status is number for HTTP errors, string literal for the others
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
- // SerializedError — a JS error thrown somewhere in the pipeline
1229
- if ('message' in error || 'name' in error) {
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
- return {
1236
- type: 'UNKNOWN_ERROR',
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
- return cap ? capitalize$1(humanified) : humanified;
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
- const calculateYearsAndDays = (years, days)=>days === 365 ? [
1308
- years + 1,
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
- * Input is a list of files selected for download.
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
- * Given a list of object ids, extracts and DRS prefixes and resolves them to DRS server hostnames
1374
- * then returns a list of resolved GUIDs, and a list of unresolved GUIDs
1375
- * @param selectedFiles
1376
- * @returns - {
1377
- * resolvedDRSPrefixes: Record<string, string> - map of GUID prefixes to DRS server hostnames
1378
- * resolvedFiles: ResolvedGUID[] - list of resolved GUIDs
1379
- * unresolvedFiles: ResolvedGUID[] - list of unresolved GUIDs
1380
- * }
1381
- */ const resolveGUIDsInSelectedFiles = async (selectedFiles)=>{
1382
- if (!selectedFiles || selectedFiles.length === 0) {
1383
- return {
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
- } catch (error) {
1426
- if (error instanceof Error) {
1024
+ this.handleNotEquals = (op)=>{
1025
+ const [field, value] = Object.entries(op['!='])[0];
1427
1026
  return {
1428
- error
1027
+ operator: '!=',
1028
+ field: field,
1029
+ operand: value
1429
1030
  };
1430
- }
1431
- return {
1432
- error: new Error('Unknown error')
1433
1031
  };
1434
- }
1435
- };
1436
- const useGetFederatedLoginStatus = ({ selectedFiles })=>{
1437
- const { data: wtsResults, isLoading: wstIsLoading, error: wtsError } = useGetExternalLoginsQuery();
1438
- // State to manage the asynchronous results
1439
- const [result, setResult] = React.useState(null);
1440
- React.useEffect(()=>{
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
- // Only run if there's data to act on
1455
- if (!wstIsLoading && wtsResults) {
1456
- void fetchData();
1457
- }
1458
- }, [
1459
- selectedFiles,
1460
- wstIsLoading,
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
- try {
1500
- const response = await fetch(`${GEN3_GUPPY_API}/graphql`, {
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
- data
1051
+ operator: '>',
1052
+ field: field,
1053
+ operand: value
1518
1054
  };
1519
- } catch (e) {
1520
- if (e instanceof graphql.GraphQLError) {
1521
- return {
1522
- error: {
1523
- message: e.message,
1524
- locations: e.locations,
1525
- path: e.path
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
- error: e
1067
+ operator: 'in',
1068
+ field: field,
1069
+ operands: value
1534
1070
  };
1535
- }
1536
- },
1537
- endpoints: ()=>({})
1538
- });
1539
- const guppyAPISliceMiddleware = guppyApi.middleware;
1540
- const guppyApiSliceReducerPath = guppyApi.reducerPath;
1541
- const guppyApiReducer = guppyApi.reducer;
1542
-
1543
- const isFilterSet = (input)=>{
1544
- if (typeof input !== 'object' || input === null) {
1545
- return false;
1546
- }
1547
- const { root, mode } = input;
1548
- if (typeof root !== 'object' || root === null) {
1549
- return false;
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
- if (![
1552
- 'and',
1553
- 'or'
1554
- ].includes(mode)) {
1555
- return false;
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
- return true;
1558
- };
1137
+ }
1559
1138
  /**
1560
- * Type guard for Includes
1561
- * @param o - operator to check
1562
- * @category Filters
1563
- */ const isIncludes = (o)=>o.operator === 'includes' || o.operator === 'in';
1564
- const isUnion = (value)=>{
1565
- return typeof value === 'object' && value !== null && value.operator === 'or' && Array.isArray(value.operands);
1566
- };
1567
- const isIntersection = (value)=>{
1568
- return typeof value === 'object' && value !== null && value.operator === 'and' && Array.isArray(value.operands);
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
- * Type guard for Union or Intersection
1572
- * @param o - operator to check
1573
- * @category Filters
1574
- */ const isIntersectionOrUnion = (o)=>o.operator === 'and' || o.operator === 'or';
1575
- const isOperandsType = (operation)=>{
1576
- return operation?.operands !== undefined;
1577
- };
1578
- const ifOperationWithField = (operation)=>{
1579
- return operation.field !== undefined;
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
- const isNestedFilter = (operation)=>{
1582
- return operation.operator === 'nested';
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
- const isIndexedFilterSetEmpty = (filters)=>Object.values(filters).every((filterSet)=>Object.keys(filterSet).length === 0);
1585
- const EmptyFilterSet = {
1586
- mode: 'and',
1587
- root: {}
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 isOperationWithField = (operation)=>{
1591
- return operation?.field !== undefined;
1255
+ const FieldNameOverrides = {
1256
+ gender: 'Sex',
1257
+ Gender: 'Sex'
1592
1258
  };
1593
- const isOperatorWithFieldAndArrayOfOperands = (operation)=>{
1594
- 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
1595
- ) {
1596
- const { operator } = operation;
1597
- return operator === 'in' || operator === 'includes' || operator === 'excludes' || operator === 'excludeifany';
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 false;
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
- const handleOperation = (handler, op)=>{
1614
- switch(op.operator){
1615
- case '=':
1616
- return handler.handleEquals(op);
1617
- case '!=':
1618
- return handler.handleNotEquals(op);
1619
- case '<':
1620
- return handler.handleLessThan(op);
1621
- case '<=':
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
- * Return true if a FilterSet's root value is an empty object
1650
- * @param fs - FilterSet to test
1651
- */ const isFilterEmpty = (fs)=>lodash.isEqual({}, fs);
1299
+ * Extracts the index name from the field name
1300
+ * @param fieldName
1301
+ */ const extractIndexFromFullFieldName = (fieldName)=>fieldName.split('.')[0];
1652
1302
  /**
1653
- * Type guard to check if an object is a GQLIntersection
1654
- * @param value - The value to check
1655
- * @returns True if the value is a GQLIntersection
1656
- */ const isGQLIntersection = (value)=>{
1657
- return typeof value === 'object' && value !== null && 'and' in value && Array.isArray(value.and);
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
- * Type guard to check if an object is a GQLIntersection
1661
- * @param value - The value to check
1662
- * @returns True if the value is a GQLIntersection
1663
- */ const isGQLUnion = (value)=>{
1664
- return typeof value === 'object' && value !== null && 'or' in value && Array.isArray(value.or);
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
- class ToGqlHandler {
1667
- constructor(){
1668
- this.handleEquals = (op)=>({
1669
- '=': {
1670
- [op.field]: op.operand
1671
- }
1672
- });
1673
- this.handleNotEquals = (op)=>({
1674
- '!=': {
1675
- [op.field]: op.operand
1676
- }
1677
- });
1678
- this.handleLessThan = (op)=>({
1679
- '<': {
1680
- [op.field]: op.operand
1681
- }
1682
- });
1683
- this.handleLessThanOrEquals = (op)=>({
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
- this.handleGreaterThan = (op)=>({
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
- this.handleGreaterThanOrEquals = (op)=>({
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
- this.handleIncludes = (op)=>({
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
- this.handleExcludes = (op)=>({
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
- this.handleExcludeIfAny = (op)=>({
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
- this.handleExists = (op)=>({
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 convertFilterToGqlFilter = (filter)=>{
1741
- const handler = new ToGqlHandler();
1617
+ const convertFilterToNestedGqlFilter = (filter)=>{
1618
+ const handler = new ToGqlAllNested();
1742
1619
  return handleOperation(handler, filter);
1743
1620
  };
1744
- const convertFilterSetToGqlFilter = (fs, toplevelOp = 'and')=>{
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)=>convertFilterToGqlFilter(fs.root[key]))
1628
+ and: fsKeys.map((key)=>convertFilterToNestedGqlFilter(fs.root[key]))
1752
1629
  } : {
1753
- or: fsKeys.map((key)=>convertFilterToGqlFilter(fs.root[key]))
1630
+ or: fsKeys.map((key)=>convertFilterToNestedGqlFilter(fs.root[key]))
1754
1631
  };
1755
1632
  };
1756
- const handleGqlOperation = (handler, op)=>{
1757
- const operationKeys = Object.keys(op);
1758
- if (operationKeys.includes('=')) {
1759
- return handler.handleEquals(op);
1760
- }
1761
- if (operationKeys.includes('!=')) {
1762
- return handler.handleNotEquals(op);
1763
- }
1764
- if (operationKeys.includes('<')) {
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 convertGqlFilterToFilter = (gqlFilter)=>{
1803
- const handler = new ToOperationHandler();
1804
- return handleGqlOperation(handler, gqlFilter);
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
- * Convert GQL to Filterset
1808
- * Note assumes all GqlOperators have one field: value
1809
- */ class ToOperationHandler {
1810
- constructor(){
1811
- this.handleEquals = (op)=>{
1812
- const [field, value] = Object.entries(op['='])[0];
1813
- return {
1814
- operator: '=',
1815
- field: field,
1816
- operand: value
1817
- };
1818
- };
1819
- this.handleNotEquals = (op)=>{
1820
- const [field, value] = Object.entries(op['!='])[0];
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
- * Extract the operand values, if operands themselves have values, otherwise undefined.
1935
- */ class EnumValueExtractorHandler {
1936
- constructor(){
1937
- this.handleEquals = (_)=>undefined;
1938
- this.handleNotEquals = (_)=>undefined;
1939
- this.handleIncludes = (op)=>op.operands;
1940
- this.handleExcludes = (op)=>op.operands;
1941
- this.handleExcludeIfAny = (op)=>op.operands;
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
- ...filter,
1964
- operands: [
1965
- ...filter?.operands || [],
1966
- addition
1967
- ]
1968
- };
1969
- };
1970
- const filterSetToOperation = (fs)=>{
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 FieldNameOverrides = {
2051
- gender: 'Sex',
2052
- Gender: 'Sex'
2053
- };
2054
- const COMMON_PREPOSITIONS = [
2055
- 'a',
2056
- 'an',
2057
- 'and',
2058
- 'at',
2059
- 'but',
2060
- 'by',
2061
- 'for',
2062
- 'in',
2063
- 'is',
2064
- 'nor',
2065
- 'of',
2066
- 'on',
2067
- 'or',
2068
- 'out',
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
- * Constructs a nested operation object based on the provided field and leaf operand.
2115
- * If the field does not contain a dot '.', it either assigns the field to the leaf operand (if applicable)
2116
- * or returns the leaf operand as is. When the field contains dots, it splits the field into parts,
2117
- * creates a "nested" operation for the root field, and recursively constructs the nested structure
2118
- * for the remaining portion of the field.
2119
- *
2120
- * @param {string} field - The hierarchical field path, with segments separated by dots (e.g., "root.child").
2121
- * @param {Operation} leafOperand - The operation to be nested within the specified path.
2122
- * @param parentPath - The parent path of the current field. Guppy nested filters require a parent path.
2123
- * @param depth
2124
- * @returns {Operation} A nested operation object that represents the structured path and operand.
2125
- */ const buildNestedWithParentPathGQLFilter = (field, leafOperand, parentPath = undefined)=>{
2126
- if (!field.includes('.')) {
2127
- return leafOperand;
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
- return {
2180
- // TODO: Replace fixed AND with cohort top level operation like Union or Intersection
2181
- [fs.mode]: fsKeys.map((k)=>{
2182
- return convertFilterToGqlFilter(fs.root[k]);
2183
- })
2184
- };
2185
- case 'or':
2186
- return {
2187
- [fs.mode]: fsKeys.map((k)=>{
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
- this.handleNotEquals = (op)=>{
2246
- if (op.field.includes('.')) {
2247
- const leafField = op.field.split('.').at(-1) ?? 'unset';
2248
- return buildNestedGQLFilter(op.field, {
2249
- '!=': {
2250
- [leafField]: op.operand
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
- return {
2255
- '!=': {
2256
- [op.field]: op.operand
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
- return {
2270
- '<': {
2271
- [op.field]: op.operand
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
- return {
2285
- '<=': {
2286
- [op.field]: op.operand
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
- return {
2300
- '>': {
2301
- [op.field]: op.operand
2302
- }
2303
- };
2304
- };
2305
- this.handleGreaterThanOrEquals = (op)=>{
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
- return {
2315
- '>=': {
2316
- [op.field]: op.operand
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
- this.handleIncludes = (op)=>{
2321
- if (op.field.includes('.')) {
2322
- const leafField = op.field.split('.').at(-1) ?? 'unset';
2323
- return buildNestedGQLFilter(op.field, {
2324
- in: {
2325
- [leafField]: op.operands
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
- return {
2330
- in: {
2331
- [op.field]: op.operands
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
- this.handleExcludes = (op)=>{
2336
- if (op.field.includes('.')) {
2337
- const leafField = op.field.split('.').at(-1) ?? 'unset';
2338
- return buildNestedGQLFilter(op.field, {
2339
- exclude: {
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
- return {
2345
- exclude: {
2346
- [op.field]: op.operands
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
- this.handleMissing = (op)=>{
2372
- if (op.field.includes('.')) {
2373
- const leafField = op.field.split('.').at(-1) ?? 'unset';
2374
- return buildNestedGQLFilter(op.field, {
2375
- is: {
2376
- [leafField]: 'MISSING'
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
- return {
2396
- not: {
2397
- [op.field]: op?.operand ?? null
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
- updateCohortName: (state, action)=>{
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: id,
1861
+ id: currentCohortId,
2544
1862
  changes: {
2545
- name: name,
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
- removeCohort: (state, action)=>{
2552
- const { id: cohortId } = action.payload;
2553
- const removedCohortName = state.entities[cohortId].name;
2554
- const totalCohorts = Object.keys(state.entities).length;
2555
- if (totalCohorts <= 1) {
2556
- cohortsAdapter.removeAll(state);
2557
- const defaultCohort = newCohort({
2558
- filters: {},
2559
- customName: DEFAULT_COHORT_NAME
2560
- });
2561
- cohortsAdapter.addOne(state, defaultCohort);
2562
- state.currentCohortId = defaultCohort.id;
2563
- if (action?.payload.shouldShowMessage) {
2564
- state.message = [
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
- return;
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
- // adds a filter to the cohort filter set at the given index
2583
- updateCohortFilter: (state, action)=>{
2584
- const { index, field, filter } = action.payload;
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
- return;
2588
- }
2589
- cohortsAdapter.updateOne(state, {
2590
- id: currentCohortId,
2591
- changes: {
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 useGetIndexFields = (index, indexPrefix = '')=>{
3909
- const { data } = useGetFieldsForIndexQuery({
3910
- index: index,
3911
- indexPrefix: indexPrefix
3912
- });
3913
- return data ?? [];
3914
- };
3915
- const buildGetAggregationQuery = (type, fields, filters, accessibility = Accessibility.ALL, filterSelf = false, queryId, indexPrefix = '', filterName = 'filter')=>{
3916
- const queryStart = isFilterEmpty(filters) ? `query getAggs${queryId ? `_${queryId}` : ''} {
3917
- ${indexPrefix}_aggregation {
3918
- ${type} (accessibility: ${accessibility}) {` : `query getAggs ($${filterName}: JSON) {
3919
- ${indexPrefix}_aggregation {
3920
- ${type} (filter: $${filterName}, filterSelf: ${filterSelf ? 'true' : 'false'}, accessibility: ${accessibility}) { _totalCount`;
3921
- const query = `${queryStart}
3922
- ${fields.map((field)=>histogramQueryStrForEachField(field))}
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 queryBody;
3679
+ return original.split(' ').map((word)=>customCapitalizations[word.toLowerCase()] || `${word.charAt(0).toUpperCase()}${word.slice(1)}`).join(' ');
3933
3680
  };
3934
- const buildGetStatsAggregationQuery = (type, fields, filters, accessibility = Accessibility.ALL, filterSelf = false, queryId, indexPrefix = '', filterName = 'filter')=>{
3935
- const queryStart = isFilterEmpty(filters) ? `
3936
- query getStatsAggs${queryId ? `_${queryId}` : ''} {
3937
- ${indexPrefix}_aggregation {
3938
- ${type} (accessibility: ${accessibility}) {` : `query getStatsAggs${queryId ? `_${queryId}` : ''} ($${filterName}: JSON) {
3939
- ${indexPrefix}_aggregation {
3940
- ${type} (filter: $${filterName}, filterSelf: ${filterSelf ? 'true' : 'false'}, accessibility: ${accessibility}) { _totalCount`;
3941
- const query = `${queryStart}
3942
- ${fields.map((field)=>statsQueryStrForEachField(field))}
3943
- }
3944
- }
3945
- }`;
3946
- const queryBody = {
3947
- query: query,
3948
- variables: {
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 queryBody;
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
- * When a getCounts is requested, it might be for a cohort.
3963
- * If so, then update the cohort's counts for that index
3964
- */ startCoreListening({
3965
- matcher: toolkit.isAnyOf(explorerApi.endpoints.getCounts.matchFulfilled),
3966
- effect: async (action, listenerApi)=>{
3967
- const counts = action.payload;
3968
- const index = action?.meta?.arg?.originalArgs?.type; // note this is the guppy index name
3969
- const cohortId = action?.meta?.arg?.originalArgs?.queryId;
3970
- if (cohortId && index) {
3971
- listenerApi.dispatch(updateCohortIndexCountById({
3972
- index,
3973
- cohortId,
3974
- counts
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
- * Low-level helper to fetch Arborist resources for the current user.
3982
- * Adds an Authorization header when a token is provided and normalizes the response
3983
- * to a simple string[] of resource paths.
3984
- *
3985
- * token { string | null } - access token to use for authorization
3986
- * useService { boolean } - use the arborist service endpoint instead of the public arborist API
3987
- */ async function fetchArboristResources(token, useService = false) {
3988
- const headers = {};
3989
- if (token) {
3990
- headers.Authorization = `Bearer ${token}`;
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 url = useService ? `${GEN3_AUTHZ_SERVICE}/resource` : `${GEN3_AUTHZ_API}/resources`;
3993
- const res = await fetch(url, {
3994
- headers
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
- if (!res.ok) {
3997
- console.error('@gen3/core:fetchArboristResources /resource failed:', res.status, await res.text());
3998
- return [];
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 persistedReducer = reduxPersist.persistReducer(persistConfig, rootReducer);
4017
- const setupCoreStore = (preloadedState)=>toolkit.configureStore({
4018
- reducer: persistedReducer,
4019
- preloadedState,
4020
- middleware: (getDefaultMiddleware)=>getDefaultMiddleware({
4021
- serializableCheck: {
4022
- ignoredActions: [
4023
- reduxPersist.FLUSH,
4024
- reduxPersist.REHYDRATE,
4025
- reduxPersist.PAUSE,
4026
- reduxPersist.PERSIST,
4027
- reduxPersist.PURGE,
4028
- reduxPersist.REGISTER
4029
- ]
4030
- }
4031
- }).concat(gen3ServicesReducerMiddleware, guppyAPISliceMiddleware, userAuthApiMiddleware).prepend(coreStoreListenerMiddleware.middleware)
4032
- });
4033
- const coreStore = setupCoreStore();
4034
- query.setupListeners(coreStore.dispatch);
4035
-
4036
- const persistor = reduxPersist.persistStore(coreStore);
4037
- const CoreProvider = ({ children })=>{
4038
- return /*#__PURE__*/ React.createElement(reactRedux.Provider, {
4039
- store: coreStore
4040
- }, /*#__PURE__*/ React.createElement(react$1.PersistGate, {
4041
- loading: null,
4042
- persistor: persistor
4043
- }, children));
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$1;
7099
+ exports.capitalize = capitalize;
7130
7100
  exports.cartReducer = cartReducer;
7131
7101
  exports.cartReducerPath = cartReducerPath;
7132
7102
  exports.cedarApi = cedarApi;