@gen3/core 0.11.20 → 0.11.22

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 (47) hide show
  1. package/dist/cjs/index.js +1902 -1364
  2. package/dist/cjs/index.js.map +1 -1
  3. package/dist/dts/features/cohort/cohortManagerSelector.d.ts +193 -0
  4. package/dist/dts/features/cohort/cohortManagerSelector.d.ts.map +1 -0
  5. package/dist/dts/features/cohort/cohortManagerSlice.d.ts +112 -0
  6. package/dist/dts/features/cohort/cohortManagerSlice.d.ts.map +1 -0
  7. package/dist/dts/features/cohort/index.d.ts +9 -6
  8. package/dist/dts/features/cohort/index.d.ts.map +1 -1
  9. package/dist/dts/features/cohort/reducers.d.ts +2 -2
  10. package/dist/dts/features/cohort/storage/CohortStorage.d.ts +70 -0
  11. package/dist/dts/features/cohort/storage/CohortStorage.d.ts.map +1 -0
  12. package/dist/dts/features/cohort/tests/cohortManager.unit.test.d.ts +2 -0
  13. package/dist/dts/features/cohort/tests/cohortManager.unit.test.d.ts.map +1 -0
  14. package/dist/dts/features/cohort/types.d.ts +28 -0
  15. package/dist/dts/features/cohort/types.d.ts.map +1 -1
  16. package/dist/dts/features/cohort/utils.d.ts +3 -0
  17. package/dist/dts/features/cohort/utils.d.ts.map +1 -1
  18. package/dist/dts/features/dataLibrary/index.d.ts +3 -2
  19. package/dist/dts/features/dataLibrary/index.d.ts.map +1 -1
  20. package/dist/dts/features/dataLibrary/storage/LocalStorageService.d.ts +1 -1
  21. package/dist/dts/features/dataLibrary/storage/LocalStorageService.d.ts.map +1 -1
  22. package/dist/dts/features/dataLibrary/utils.d.ts +1 -2
  23. package/dist/dts/features/dataLibrary/utils.d.ts.map +1 -1
  24. package/dist/dts/features/facets/index.d.ts +2 -0
  25. package/dist/dts/features/facets/index.d.ts.map +1 -0
  26. package/dist/dts/features/facets/types.d.ts +20 -0
  27. package/dist/dts/features/facets/types.d.ts.map +1 -0
  28. package/dist/dts/features/filters/index.d.ts +1 -2
  29. package/dist/dts/features/filters/index.d.ts.map +1 -1
  30. package/dist/dts/features/filters/types.d.ts +4 -6
  31. package/dist/dts/features/filters/types.d.ts.map +1 -1
  32. package/dist/dts/features/user/userSliceRTK.d.ts +3 -3
  33. package/dist/dts/hooks.d.ts +4 -4
  34. package/dist/dts/index.d.ts +2 -1
  35. package/dist/dts/index.d.ts.map +1 -1
  36. package/dist/dts/reducers.d.ts +3 -3
  37. package/dist/dts/store.d.ts +4 -4
  38. package/dist/dts/utils/index.d.ts +4 -3
  39. package/dist/dts/utils/index.d.ts.map +1 -1
  40. package/dist/dts/utils/time.d.ts +1 -0
  41. package/dist/dts/utils/time.d.ts.map +1 -1
  42. package/dist/esm/index.js +1885 -1362
  43. package/dist/esm/index.js.map +1 -1
  44. package/dist/index.d.ts +265 -58
  45. package/package.json +2 -2
  46. package/dist/dts/features/cohort/cohortSlice.d.ts +0 -204
  47. package/dist/dts/features/cohort/cohortSlice.d.ts.map +0 -1
package/dist/cjs/index.js CHANGED
@@ -10,9 +10,9 @@ var reduxPersist = require('redux-persist');
10
10
  var createWebStorage = require('redux-persist/lib/storage/createWebStorage');
11
11
  var lodash = require('lodash');
12
12
  var react$1 = require('redux-persist/integration/react');
13
+ var idb = require('idb');
13
14
  var useDeepCompare = require('use-deep-compare');
14
15
  var graphql = require('graphql');
15
- var idb = require('idb');
16
16
  var uuid = require('uuid');
17
17
  var reactCookie = require('react-cookie');
18
18
  var useSWR = require('swr');
@@ -188,7 +188,7 @@ const userAuthApi = react.createApi({
188
188
  } catch (error) {
189
189
  if (error instanceof Error) {
190
190
  return {
191
- error: error
191
+ error: error.message
192
192
  };
193
193
  } else {
194
194
  return {
@@ -717,6 +717,9 @@ const isTimeGreaterThan = (startTime, minutes)=>{
717
717
  const diffMinutes = diffMs / (1000 * 60);
718
718
  return diffMinutes > minutes;
719
719
  };
720
+ const getTimestamp = ()=>{
721
+ return new Date(Date.now()).toLocaleString();
722
+ };
720
723
 
721
724
  const NO_WORKSPACE_ID = 'none';
722
725
  const initialState$4 = {
@@ -817,15 +820,39 @@ const guppyAPISliceMiddleware = guppyApi.middleware;
817
820
  const guppyApiSliceReducerPath = guppyApi.reducerPath;
818
821
  const guppyApiReducer = guppyApi.reducer;
819
822
 
823
+ const defaultCohortNameGenerator = ()=>`Custom cohort ${new Date().toLocaleString('en-CA', {
824
+ timeZone: 'America/Chicago',
825
+ hour12: false
826
+ }).replace(',', '')}`;
827
+ const isNameUnique = (entities, name, excludeId)=>{
828
+ const trimmedName = name.trim();
829
+ if (!trimmedName) return false;
830
+ return !entities.some((cohort)=>cohort && cohort.id !== excludeId && cohort.name.trim().toLowerCase() === trimmedName.toLowerCase());
831
+ };
832
+ const generateUniqueName = (entities, baseName)=>{
833
+ const trimmedBaseName = baseName.trim();
834
+ // If base name is unique, use it
835
+ if (isNameUnique(entities, trimmedBaseName)) {
836
+ return trimmedBaseName;
837
+ }
838
+ // Find a unique name by appending numbers
839
+ let counter = 1;
840
+ let uniqueName;
841
+ do {
842
+ uniqueName = `${trimmedBaseName} (${counter})`;
843
+ counter++;
844
+ }while (!isNameUnique(entities, uniqueName))
845
+ return uniqueName;
846
+ };
847
+
820
848
  /**
821
849
  * Cohorts in Gen3 are defined as a set of filters for each index in the data.
822
850
  * This means one cohort id defined for all "tabs" in CohortBuilder (explorer)
823
851
  * Switching a cohort is means that all the cohorts for the index changes.
824
- */ const UNSAVED_COHORT_NAME = 'Cohort';
825
- const NULL_COHORT_ID = 'null_cohort_id';
852
+ */ const DEFAULT_COHORT_NAME = 'Cohort';
826
853
  const newCohort = ({ filters = {}, customName })=>{
827
- const ts = new Date();
828
- const newName = customName;
854
+ const ts = new Date().toISOString();
855
+ const newName = customName ?? defaultCohortNameGenerator();
829
856
  const newId = createCohortId();
830
857
  return {
831
858
  name: newName,
@@ -833,73 +860,92 @@ const newCohort = ({ filters = {}, customName })=>{
833
860
  filters: filters ?? {},
834
861
  modified: false,
835
862
  saved: false,
836
- modified_datetime: ts.toISOString(),
863
+ createdDatetime: ts,
864
+ modifiedDatetime: ts,
837
865
  counts: {}
838
866
  };
839
867
  };
840
868
  const createCohortId = ()=>toolkit.nanoid();
841
869
  const cohortsAdapter = toolkit.createEntityAdapter({
842
870
  sortComparer: (a, b)=>{
843
- if (a.modified_datetime <= b.modified_datetime) return 1;
871
+ if (a.modifiedDatetime <= b.modifiedDatetime) return 1;
844
872
  else return -1;
845
873
  },
846
874
  selectId: (cohort)=>cohort.id
847
875
  });
848
876
  // Create an initial unsaved cohort
849
877
  const initialCohort = newCohort({
850
- customName: UNSAVED_COHORT_NAME
878
+ customName: DEFAULT_COHORT_NAME
851
879
  });
852
880
  const emptyInitialState = cohortsAdapter.getInitialState({
853
- currentCohort: initialCohort.id,
881
+ currentCohortId: initialCohort.id,
854
882
  message: undefined
855
883
  });
856
884
  // Set the initial cohort in the adapter state
857
885
  const initialState$3 = cohortsAdapter.setOne(emptyInitialState, initialCohort);
858
- const getCurrentCohort = (state)=>{
859
- if (state.currentCohort) {
860
- return state.currentCohort;
861
- }
862
- return NULL_COHORT_ID;
863
- };
886
+ const getCurrentCohortId = (state)=>state.currentCohortId;
864
887
  /**
865
888
  * Redux slice for cohort filters
866
- */ const cohortSlice = toolkit.createSlice({
889
+ */ const cohortManagerSlice = toolkit.createSlice({
867
890
  name: 'cohort',
868
891
  initialState: initialState$3,
869
892
  reducers: {
870
- addNewDefaultUnsavedCohort: (state)=>{
893
+ createNewCohort: (state, action)=>{
894
+ const baseName = action.payload.name || `Cohort`;
895
+ const uniqueName = generateUniqueName(Object.values(state.entities), baseName);
871
896
  const cohort = newCohort({
872
- customName: UNSAVED_COHORT_NAME
897
+ filters: action.payload.filters,
898
+ customName: uniqueName
873
899
  });
874
900
  cohortsAdapter.addOne(state, cohort);
875
- state.currentCohort = cohort.id;
876
- state.message = [
877
- `newCohort|${cohort.name}|${cohort.id}`
878
- ];
901
+ state.currentCohortId = cohort.id;
879
902
  },
880
903
  updateCohortName: (state, action)=>{
904
+ const { id, name } = action.payload;
881
905
  cohortsAdapter.updateOne(state, {
882
- id: getCurrentCohort(state),
906
+ id: id,
883
907
  changes: {
884
- name: action.payload,
908
+ name: name,
885
909
  modified: true,
886
- modified_datetime: new Date().toISOString()
910
+ modifiedDatetime: new Date().toISOString()
887
911
  }
888
912
  });
889
913
  },
890
914
  removeCohort: (state, action)=>{
891
- const removedCohort = state.entities[action?.payload?.id || getCurrentCohort(state)];
892
- cohortsAdapter.removeOne(state, action?.payload?.id || getCurrentCohort(state));
915
+ const { id: cohortId } = action.payload;
916
+ const removedCohortName = state.entities[cohortId].name;
917
+ const totalCohorts = Object.keys(state.entities).length;
918
+ if (totalCohorts <= 1) {
919
+ cohortsAdapter.removeAll(state);
920
+ const defaultCohort = newCohort({
921
+ filters: {},
922
+ customName: DEFAULT_COHORT_NAME
923
+ });
924
+ cohortsAdapter.addOne(state, defaultCohort);
925
+ state.currentCohortId = defaultCohort.id;
926
+ if (action?.payload.shouldShowMessage) {
927
+ state.message = [
928
+ `deleteCohort|${removedCohortName}|${state.currentCohortId}`
929
+ ];
930
+ }
931
+ return;
932
+ }
933
+ cohortsAdapter.removeOne(state, cohortId);
934
+ // deleted the current cohort so set to most recent cohort
935
+ if (state.currentCohortId === cohortId) {
936
+ const remainingIds = Object.keys(state.entities);
937
+ state.currentCohortId = remainingIds[0];
938
+ }
893
939
  if (action?.payload.shouldShowMessage) {
894
940
  state.message = [
895
- `deleteCohort|${removedCohort?.name}|${state.currentCohort}`
941
+ `deleteCohort|${removedCohortName}|${state.currentCohortId}`
896
942
  ];
897
943
  }
898
944
  },
899
945
  // adds a filter to the cohort filter set at the given index
900
946
  updateCohortFilter: (state, action)=>{
901
947
  const { index, field, filter } = action.payload;
902
- const currentCohortId = getCurrentCohort(state);
948
+ const currentCohortId = getCurrentCohortId(state);
903
949
  if (!state.entities[currentCohortId]) {
904
950
  return;
905
951
  }
@@ -917,13 +963,13 @@ const getCurrentCohort = (state)=>{
917
963
  }
918
964
  },
919
965
  modified: true,
920
- modified_datetime: new Date().toISOString()
966
+ modifiedDatetime: new Date().toISOString()
921
967
  }
922
968
  });
923
969
  },
924
970
  setCohortFilter: (state, action)=>{
925
971
  const { index, filters } = action.payload;
926
- const currentCohortId = getCurrentCohort(state);
972
+ const currentCohortId = getCurrentCohortId(state);
927
973
  if (!state.entities[currentCohortId]) {
928
974
  console.error(`no cohort with id=${currentCohortId} defined`);
929
975
  return;
@@ -936,12 +982,12 @@ const getCurrentCohort = (state)=>{
936
982
  [index]: filters
937
983
  },
938
984
  modified: true,
939
- modified_datetime: new Date().toISOString()
985
+ modifiedDatetime: new Date().toISOString()
940
986
  }
941
987
  });
942
988
  },
943
989
  setCohortIndexFilters: (state, action)=>{
944
- const currentCohortId = getCurrentCohort(state);
990
+ const currentCohortId = getCurrentCohortId(state);
945
991
  if (!state.entities[currentCohortId]) {
946
992
  console.error(`no cohort with id=${currentCohortId} defined`);
947
993
  return;
@@ -951,14 +997,14 @@ const getCurrentCohort = (state)=>{
951
997
  changes: {
952
998
  filters: action.payload.filters,
953
999
  modified: true,
954
- modified_datetime: new Date().toISOString()
1000
+ modifiedDatetime: new Date().toISOString()
955
1001
  }
956
1002
  });
957
1003
  },
958
1004
  // removes a filter to the cohort filter set at the given index
959
1005
  removeCohortFilter: (state, action)=>{
960
1006
  const { index, field } = action.payload;
961
- const currentCohortId = getCurrentCohort(state);
1007
+ const currentCohortId = getCurrentCohortId(state);
962
1008
  if (!state.entities[currentCohortId]) {
963
1009
  console.error(`no cohort with id=${currentCohortId} defined`);
964
1010
  return;
@@ -980,14 +1026,25 @@ const getCurrentCohort = (state)=>{
980
1026
  }
981
1027
  },
982
1028
  modified: true,
983
- modified_datetime: new Date().toISOString()
1029
+ modifiedDatetime: new Date().toISOString()
984
1030
  }
985
1031
  });
986
1032
  },
1033
+ duplicateCohort: (state)=>{
1034
+ const currentCohortId = getCurrentCohortId(state);
1035
+ const currentCohort = state.entities[currentCohortId];
1036
+ const newName = generateUniqueName(Object.values(state.entities), currentCohort.name);
1037
+ const duplicatedCohort = newCohort({
1038
+ filters: currentCohort.filters,
1039
+ customName: newName
1040
+ });
1041
+ cohortsAdapter.addOne(state, duplicatedCohort);
1042
+ state.currentCohortId = duplicatedCohort.id;
1043
+ },
987
1044
  // removes all filters from the cohort filter set at the given index
988
1045
  clearCohortFilters: (state, action)=>{
989
1046
  const { index } = action.payload;
990
- const currentCohortId = getCurrentCohort(state);
1047
+ const currentCohortId = getCurrentCohortId(state);
991
1048
  if (!state.entities[currentCohortId]) {
992
1049
  console.error(`no cohort with id=${currentCohortId} defined`);
993
1050
  return;
@@ -1007,30 +1064,12 @@ const getCurrentCohort = (state)=>{
1007
1064
  }
1008
1065
  },
1009
1066
  modified: true,
1010
- modified_datetime: new Date().toISOString()
1011
- }
1012
- });
1013
- },
1014
- discardCohortChanges: (state, action)=>{
1015
- const { index, id, filters } = action.payload;
1016
- const cohortId = id ?? getCurrentCohort(state);
1017
- cohortsAdapter.updateOne(state, {
1018
- id: cohortId,
1019
- changes: {
1020
- filters: {
1021
- ...state.entities[cohortId].filters,
1022
- [index]: filters || {
1023
- mode: 'and',
1024
- root: {}
1025
- }
1026
- },
1027
- modified: false,
1028
- modified_datetime: new Date().toISOString()
1067
+ modifiedDatetime: new Date().toISOString()
1029
1068
  }
1030
1069
  });
1031
1070
  },
1032
1071
  setCurrentCohortId: (state, action)=>{
1033
- state.currentCohort = action.payload;
1072
+ state.currentCohortId = action.payload;
1034
1073
  },
1035
1074
  /** @hidden */ setCohortList: (state, action)=>{
1036
1075
  if (!action.payload) {
@@ -1048,104 +1087,10 @@ const getCurrentCohort = (state)=>{
1048
1087
  * @param state - the CoreState
1049
1088
  *
1050
1089
  * @hidden
1051
- */ const cohortSelectors = cohortsAdapter.getSelectors((state)=>state.cohorts.cohort);
1052
- /**
1053
- * Returns an array of all the cohorts
1054
- * @param state - the CoreState
1055
- * @category Cohort
1056
- * @category Selectors
1057
- */ const selectAllCohorts = (state)=>cohortSelectors.selectEntities(state);
1058
- const getCurrentCohortFromCoreState = (state)=>{
1059
- if (state.cohorts.cohort.currentCohort) {
1060
- return state.cohorts.cohort.currentCohort;
1061
- }
1062
- return NULL_COHORT_ID;
1063
- };
1090
+ */ const cohortSelectors = cohortsAdapter.getSelectors((state)=>state.cohorts.cohortManager);
1064
1091
  // Filter actions: addFilter, removeFilter, updateFilter
1065
- const { updateCohortFilter, setCohortFilter, setCohortIndexFilters, removeCohortFilter, clearCohortFilters, removeCohort, discardCohortChanges, addNewDefaultUnsavedCohort, setCurrentCohortId, setCohortList } = cohortSlice.actions;
1066
- const selectCohortFilters = (state)=>{
1067
- const currentCohortId = getCurrentCohortFromCoreState(state);
1068
- return state.cohorts.cohort.entities[currentCohortId]?.filters;
1069
- };
1070
- const selectCurrentCohortFilters = (state)=>{
1071
- const currentCohortId = getCurrentCohortFromCoreState(state);
1072
- return state.cohorts.cohort.entities[currentCohortId]?.filters;
1073
- };
1074
- const selectCurrentCohortId = (state)=>{
1075
- return getCurrentCohort(state.cohorts.cohort);
1076
- };
1077
- const selectCurrentCohort = (state)=>cohortSelectors.selectById(state, getCurrentCohortFromCoreState(state));
1078
- const selectCurrentCohortName = (state)=>cohortSelectors.selectById(state, getCurrentCohortFromCoreState(state)).name;
1079
- /**
1080
- * Select a filter by its name from the current cohort. If the filter is not found
1081
- * returns undefined.
1082
- * @param state - Core
1083
- * @param index which cohort index to select from
1084
- * @param name name of the filter to select
1085
- */ const selectIndexedFilterByName = (state, index, name)=>{
1086
- return cohortSelectors.selectById(state, getCurrentCohortFromCoreState(state)).filters[index]?.root[name];
1087
- };
1088
- /**
1089
- * a thunk to optionally create a caseSet when switching cohorts.
1090
- * Note the assumption if the caseset member has ids then the caseset has previously been created.
1091
- */ const setActiveCohort = (cohortId)=>async (dispatch /* getState */ )=>{
1092
- dispatch(setCurrentCohortId(cohortId));
1093
- };
1094
- /**
1095
- * Returns all the cohorts in the state
1096
- * @param state - the CoreState
1097
- *
1098
- * @category Cohort
1099
- * @category Selectors
1100
- */ const selectAvailableCohorts = (state)=>cohortSelectors.selectAll(state);
1101
- /**
1102
- * Returns if the current cohort is modified
1103
- * @param state - the CoreState
1104
- * @category Cohort
1105
- * @category Selectors
1106
- * @hidden
1107
- */ const selectCurrentCohortModified = (state)=>{
1108
- const cohort = cohortSelectors.selectById(state, getCurrentCohortFromCoreState(state));
1109
- return cohort?.modified;
1110
- };
1111
- /**
1112
- * Returns if the current cohort has been saved
1113
- * @param state - the CoreState
1114
- * @category Cohort
1115
- * @category Selectors
1116
- * @hidden
1117
- */ const selectCurrentCohortSaved = (state)=>{
1118
- const cohort = cohortSelectors.selectById(state, getCurrentCohortFromCoreState(state));
1119
- return cohort?.saved;
1120
- };
1121
- const EmptyFilterSet = {
1122
- mode: 'and',
1123
- root: {}
1124
- };
1125
- /**
1126
- * Select a filter from the index.
1127
- * returns undefined.
1128
- * @param state - Core
1129
- * @param index which cohort index to select from
1130
- */ const selectIndexFilters = (state, index)=>{
1131
- const cohort = cohortSelectors.selectById(state, getCurrentCohortFromCoreState(state));
1132
- if (!cohort) {
1133
- console.error('No Cohort Defined');
1134
- }
1135
- return cohort?.filters?.[index] ?? EmptyFilterSet;
1136
- };
1137
- const setActiveCohortList = (cohorts)=>async (dispatch, getState)=>{
1138
- // set the list of all cohorts
1139
- if (cohorts) {
1140
- dispatch(setCohortList(cohorts));
1141
- return;
1142
- }
1143
- const availableCohorts = selectAllCohorts(getState());
1144
- if (Object.keys(availableCohorts).length === 0) {
1145
- dispatch(addNewDefaultUnsavedCohort());
1146
- }
1147
- };
1148
- const cohortReducer = cohortSlice.reducer;
1092
+ const { createNewCohort, updateCohortFilter, setCohortFilter, setCohortIndexFilters, duplicateCohort, removeCohortFilter, clearCohortFilters, removeCohort, setCurrentCohortId, updateCohortName, setCohortList } = cohortManagerSlice.actions;
1093
+ const cohortReducer = cohortManagerSlice.reducer;
1149
1094
 
1150
1095
  const initialState$2 = {};
1151
1096
  const expandSlice$1 = toolkit.createSlice({
@@ -1226,7 +1171,7 @@ const cohortReducers = toolkit.combineReducers({
1226
1171
  filtersExpanded: cohortBuilderFiltersExpandedReducer,
1227
1172
  filtersCombineMode: cohortBuilderFiltersCombineModeReducer,
1228
1173
  sharedFilters: cohortSharedFiltersReducer,
1229
- cohort: cohortReducer
1174
+ cohortManager: cohortReducer
1230
1175
  });
1231
1176
 
1232
1177
  const rootReducer = toolkit.combineReducers({
@@ -1879,424 +1824,212 @@ const selectAuthzMappingData = toolkit.createSelector(selectAuthzMapping, (authz
1879
1824
  mappings: []
1880
1825
  });
1881
1826
 
1882
- const isFileItem = (item)=>{
1883
- return item && 'guid' in item;
1884
- };
1885
- const isAdditionalDataItem = (item)=>{
1886
- return item.itemType === 'AdditionalData'; // TODO resolve this with type from the api
1887
- };
1888
- // Type guard for CohortItem
1889
- const isCohortItem = (item)=>{
1890
- return item && 'data' in item && 'schemaVersion' in item && item.itemType === 'Gen3GraphQL';
1891
- };
1892
- // Type guard for DatalistAPI
1893
- const isDatalistAPI = (value)=>{
1894
- if (typeof value !== 'object' || value === null) {
1895
- return false;
1827
+ class CohortStorage {
1828
+ constructor(config){
1829
+ this.databaseName = config.databaseName;
1830
+ this.storeName = config.storeName;
1831
+ this.schemaVersion = config.schemaVersion || 1;
1896
1832
  }
1897
- const data = value;
1898
- // Check required properties in DataItemBaseData
1899
- if (typeof data.name !== 'string' || typeof data.created_time !== 'string' || typeof data.updated_time !== 'string' || typeof data.version !== 'number' || typeof data.authz !== 'object' || data.authz === null || !Array.isArray(data.authz.authz)) {
1900
- return false;
1833
+ getDb() {
1834
+ try {
1835
+ const storeName = this.storeName;
1836
+ return idb.openDB(this.databaseName, this.schemaVersion, {
1837
+ upgrade (db) {
1838
+ if (!db.objectStoreNames.contains(storeName)) {
1839
+ db.createObjectStore(storeName, {
1840
+ keyPath: 'id'
1841
+ });
1842
+ }
1843
+ }
1844
+ });
1845
+ } catch (error) {
1846
+ let errorMessage = 'Unknown error';
1847
+ if (error instanceof Error) {
1848
+ errorMessage = error.message;
1849
+ }
1850
+ throw new Error(`Database initialization failed: ${errorMessage}`);
1851
+ }
1901
1852
  }
1902
- // Check required properties in DatalistAsItems
1903
- if (typeof data.items !== 'object' || data.items === null || typeof data.items !== 'object') {
1904
- return false;
1853
+ // ===== CREATE OPERATIONS =====
1854
+ /**
1855
+ * Save a single cohort to the database
1856
+ */ async saveCohort(cohort) {
1857
+ try {
1858
+ const db = await this.getDb();
1859
+ const tx = db.transaction(this.storeName, 'readwrite');
1860
+ tx.objectStore(this.storeName).put(cohort);
1861
+ await tx.done;
1862
+ return {
1863
+ status: 200,
1864
+ message: 'cohort added'
1865
+ };
1866
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1867
+ } catch (_error) {
1868
+ return {
1869
+ isError: true,
1870
+ status: 500,
1871
+ message: 'unable to save cohort'
1872
+ };
1873
+ }
1905
1874
  }
1906
- return true;
1907
- };
1908
- /**
1909
- * Type guard for DataLibraryAPIResponse
1910
- */ const isDataLibraryAPIResponse = (obj)=>{
1911
- return typeof obj === 'object' && obj !== null && 'lists' in obj && typeof obj.lists === 'object';
1912
- };
1913
- var DataLibraryStoreMode = /*#__PURE__*/ function(DataLibraryStoreMode) {
1914
- DataLibraryStoreMode["ApiOnly"] = "apiOnly";
1915
- DataLibraryStoreMode["ApiAndLocal"] = "apiAndLocal";
1916
- DataLibraryStoreMode["LocalOnly"] = "localOnly";
1917
- return DataLibraryStoreMode;
1918
- }({});
1919
-
1920
- const processItem = (id, data)=>{
1921
- if (data?.type === 'AdditionalData') {
1922
- return {
1923
- name: data.name,
1924
- itemType: 'AdditionalData',
1925
- description: data?.description,
1926
- documentationUrl: data?.documentationUrl,
1927
- url: data?.url
1875
+ /**
1876
+ * Save multiple cohorts in a single transaction (bulk operation)
1877
+ */ async saveCohorts(cohorts) {
1878
+ if (cohorts.length === 0) return {
1879
+ isError: true,
1880
+ status: 400,
1881
+ message: 'cannot add an empty array'
1928
1882
  };
1883
+ try {
1884
+ const db = await this.getDb();
1885
+ const tx = db.transaction(this.storeName, 'readwrite');
1886
+ // Batch all operations in a single transaction for better performance
1887
+ await Promise.all([
1888
+ ...cohorts.map((cohort)=>tx.store.put({
1889
+ ...cohort,
1890
+ saved: true
1891
+ })),
1892
+ tx.done
1893
+ ]);
1894
+ return {
1895
+ status: 200,
1896
+ message: 'cohorts added'
1897
+ };
1898
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1899
+ } catch (_error) {
1900
+ return {
1901
+ isError: true,
1902
+ status: 500,
1903
+ message: 'unable to save cohort'
1904
+ };
1905
+ }
1929
1906
  }
1930
- return {
1931
- ...data,
1932
- itemType: 'Data',
1933
- guid: data.id,
1934
- id: id
1935
- };
1936
- };
1937
- const buildListItemsGroupedByDataset = (listData)=>{
1938
- const items = Object.entries(listData).reduce((acc, [id, data])=>{
1939
- if (data?.type === 'Gen3GraphQL') {
1940
- const cohortData = data;
1941
- acc[id] = {
1942
- itemType: 'Gen3GraphQL',
1943
- id: data.guid,
1944
- schemaVersion: cohortData.schema_version,
1945
- data: cohortData.data,
1946
- name: data.name,
1947
- index: cohortData.index
1907
+ // ===== READ OPERATIONS =====
1908
+ /**
1909
+ * Get a specific cohort by ID
1910
+ */ async getCohort(id) {
1911
+ try {
1912
+ const db = await this.getDb();
1913
+ const tx = db.transaction(this.storeName, 'readonly');
1914
+ const store = tx.objectStore(this.storeName);
1915
+ const cohort = await store.get(id);
1916
+ return {
1917
+ status: 200,
1918
+ message: 'success',
1919
+ data: cohort
1948
1920
  };
1949
- } else {
1950
- // Dataset
1951
- if (!(data?.dataset_guid && data.dataset_guid in acc)) {
1952
- acc[data.dataset_guid] = {
1953
- id: data.dataset_guid,
1954
- name: '',
1955
- members: {
1956
- [id]: processItem(id, data)
1957
- }
1921
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1922
+ } catch (_error) {
1923
+ return {
1924
+ isError: true,
1925
+ status: 401,
1926
+ message: `cannot find cohort ${id}`
1927
+ };
1928
+ }
1929
+ }
1930
+ /**
1931
+ * Get all cohorts from the database
1932
+ */ async getAllCohorts() {
1933
+ try {
1934
+ const db = await this.getDb();
1935
+ const tx = db.transaction(this.storeName, 'readonly');
1936
+ const store = tx.objectStore(this.storeName);
1937
+ const savedCohorts = await store.getAll();
1938
+ if (!savedCohorts) {
1939
+ return {
1940
+ isError: true,
1941
+ status: 500,
1942
+ message: 'no cohorts returned'
1958
1943
  };
1959
- } else {
1960
- acc[data.dataset_guid].members[id] = processItem(id, data);
1961
1944
  }
1945
+ const cohorts = savedCohorts.reduce((acc, cohort)=>{
1946
+ const { id } = cohort;
1947
+ acc[id] = cohort;
1948
+ return acc;
1949
+ }, {});
1950
+ return {
1951
+ status: 200,
1952
+ message: 'success',
1953
+ data: cohorts
1954
+ };
1955
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1956
+ } catch (_error) {
1957
+ return {
1958
+ isError: true,
1959
+ status: 401,
1960
+ message: 'cannot return cohorts'
1961
+ };
1962
1962
  }
1963
- return acc;
1964
- }, {});
1965
- return items;
1966
- };
1967
- const BuildList = (listId, listData)=>{
1968
- if (!Object.keys(listData).includes('items')) return undefined;
1969
- const items = buildListItemsGroupedByDataset(listData?.items ?? {});
1970
- return {
1971
- items: items,
1972
- version: listData?.version ?? 0,
1973
- created_time: listData?.created_time,
1974
- updated_time: listData?.updated_time,
1975
- name: listData?.name ?? listId,
1976
- id: listId,
1977
- authz: listData?.authz
1978
- };
1979
- };
1980
- /**
1981
- * Constructs a `DataLibrary` object by transforming the input `DataLibraryAPIResponse`.
1982
- *
1983
- * This function takes an API response containing lists and processes each list entry.
1984
- * It uses `BuildList` to build individual list objects for each entry in the provided data.
1985
- * The resulting lists are accumulated and structured into a `DataLibrary` object. which
1986
- * groups File Object by dataset_guid.
1987
- *
1988
- * @param {DataLibraryAPIResponse} data - The API response containing the lists to process.
1989
- * @returns {DataLibrary} A structured `DataLibrary` object containing the processed lists.
1990
- */ const BuildLists = (data)=>{
1991
- return Object.entries(data?.lists).reduce((acc, [listId, listData])=>{
1992
- const list = BuildList(listId, listData);
1993
- if (list) acc[listId] = list;
1994
- return acc;
1995
- }, {});
1996
- };
1997
- /**
1998
- * Calculates the total number of items within a DataList object.
1999
- *
2000
- * @param {DataList} dataList - The DataList object to count items from.
2001
- * @return {number} The total number of items in the DataList.
2002
- */ const getNumberOfItemsInDatalist = (dataList)=>{
2003
- if (!dataList?.items) return 0;
2004
- return Object.values(dataList.items).reduce((count, item)=>{
2005
- if (isCohortItem(item)) {
2006
- return count + 1;
2007
- } else {
2008
- return count + Object.values(item?.members ?? {}).reduce((fileCount, x)=>{
2009
- if (isFileItem(x)) {
2010
- return fileCount + 1;
2011
- }
2012
- return fileCount;
2013
- }, 0);
2014
- }
2015
- }, 0);
2016
- };
2017
- const getTimestamp = ()=>{
2018
- return new Date(Date.now()).toLocaleString();
2019
- };
2020
- const flattenDataList = (dataList)=>{
2021
- // convert datalist into user-data-library API for for updating.
2022
- const items = Object.entries(dataList.items).reduce((acc, [id, value])=>{
2023
- if (isCohortItem(value)) {
2024
- acc[id] = value;
2025
- } else {
1963
+ }
1964
+ /**
1965
+ * Search cohorts by name (case-insensitive partial match)
1966
+ */ async searchCohortsByName(searchTerm) {
1967
+ try {
1968
+ const db = await this.getDb();
1969
+ const tx = db.transaction(this.storeName, 'readonly');
1970
+ const store = tx.objectStore(this.storeName);
1971
+ const allCohorts = await store.getAll(this.storeName);
1972
+ // Filter in memory for partial name matching
1973
+ const searchLower = searchTerm.toLowerCase();
2026
1974
  return {
2027
- ...acc,
2028
- ...value.members
2029
- }; // TODO: might need to convert this to the API version
2030
- }
2031
- return acc;
2032
- }, {});
2033
- return {
2034
- name: dataList.name,
2035
- items: items
2036
- };
2037
- };
2038
- const convertDatasetOrCohortToLibraryListItemsAPI = (list)=>{
2039
- const result = {};
2040
- // Iterate through each entry in the DatasetOrCohort object
2041
- Object.entries(list).forEach(([datasetId, item])=>{
2042
- if (isCohortItem(item)) {
2043
- // Handle cohort items
2044
- result[datasetId] = {
2045
- itemType: 'Gen3GraphQL',
2046
- id: item.id,
2047
- schemaVersion: item.schemaVersion,
2048
- data: item.data,
2049
- name: item.name,
2050
- index: item.index
1975
+ status: 200,
1976
+ message: 'success',
1977
+ data: allCohorts.filter((cohort)=>cohort.name.toLowerCase().includes(searchLower)).reduce((acc, cohort)=>{
1978
+ const { id } = cohort;
1979
+ acc[id] = cohort;
1980
+ return acc;
1981
+ }, {})
2051
1982
  };
2052
- } else {
2053
- // Handle dataset items
2054
- const members = item.members || {};
2055
- // Process each member of the dataset
2056
- Object.entries(members).forEach(([memberId, memberData])=>{
2057
- if (isFileItem(memberData)) {
2058
- result[memberId] = {
2059
- ...memberData.guid && {
2060
- guid: memberData.guid
2061
- },
2062
- ...memberData.name && {
2063
- name: memberData.name
2064
- },
2065
- ...memberData.name && {
2066
- name: memberData.name
2067
- },
2068
- ...memberData.description && {
2069
- description: memberData.description
2070
- },
2071
- ...memberData.type && {
2072
- type: memberData.type
2073
- },
2074
- dataset_guid: datasetId
2075
- };
2076
- } else if (memberData.itemType === 'AdditionalData') {
2077
- // Handle additional data items
2078
- result[memberId] = {
2079
- itemType: 'AdditionalData',
2080
- name: memberData.name,
2081
- description: memberData.description,
2082
- documentationUrl: memberData.documentationUrl,
2083
- url: memberData.url,
2084
- dataset_guid: datasetId
2085
- };
2086
- }
2087
- });
2088
- }
2089
- });
2090
- return result;
2091
- };
2092
- const convertDataLibraryToDataLibraryAPI = (dataLibrary)=>{
2093
- const result = {};
2094
- Object.entries(dataLibrary).forEach(([listId, list])=>{
2095
- result[listId] = {
2096
- name: list.name,
2097
- items: convertDatasetOrCohortToLibraryListItemsAPI(list.items),
2098
- version: list.version,
2099
- created_time: list.created_time,
2100
- updated_time: list.updated_time,
2101
- authz: list.authz
2102
- };
2103
- });
2104
- return result;
2105
- };
2106
- const extractIndexFromDataLibraryCohort = (query)=>{
2107
- try {
2108
- const parsedQuery = graphql.parse(query['query']);
2109
- const aggregationField = parsedQuery.definitions.filter((def)=>def.kind === 'OperationDefinition').flatMap((def)=>def.selectionSet.selections).find((sel)=>sel.kind === 'Field' && sel.name.value === '_aggregation');
2110
- if (aggregationField && 'selectionSet' in aggregationField) {
2111
- const indexField = aggregationField?.selectionSet?.selections.find((sel)=>sel.kind === 'Field');
2112
- return indexField ? indexField.name.value : null;
2113
- }
2114
- } catch (error) {
2115
- console.error('Invalid GraphQL query:', error);
2116
- }
2117
- return null;
2118
- };
2119
- /**
2120
- * Takes a list of file items from anb array of manifest entries
2121
- * and creates an Object of Files grouped by their dataset guid, which is
2122
- * used to add these to a Data Library List
2123
- * @param data
2124
- * @param dataFieldMapping
2125
- * @constructor
2126
- */ const extractFileDatasetsInRecords = (data, dataFieldMapping)=>{
2127
- const items = data.reduce((acc, resource)=>{
2128
- const dataObjects = resource[dataFieldMapping.dataObjectField];
2129
- // Check if dataObjects exists and is an array
2130
- if (!dataObjects || !Array.isArray(dataObjects)) {
2131
- return acc;
2132
- }
2133
- const datasetId = resource[dataFieldMapping.datasetIdField]; // Note: typo still preserved
2134
- if (datasetId === undefined) {
2135
- return acc; // Skip if dataset ID is missing
2136
- }
2137
- const datafiles = dataObjects.reduce((dataAcc, dataObject)=>{
2138
- const fileId = dataObject[dataFieldMapping.dataObjectIdField];
2139
- // Skip items without a valid ID
2140
- if (typeof fileId !== 'string' || !fileId) {
2141
- return dataAcc;
2142
- }
2143
- const name = dataObject[dataFieldMapping?.dataObjectNameField ?? 'name'] ?? 'No Name';
2144
- const size = dataObject[dataFieldMapping?.dataObjectSizeField ?? 'size'];
2145
- let sizeString = 'N/A';
2146
- if (typeof size === 'number') {
2147
- sizeString = size.toString();
2148
- }
2149
- if (typeof size === 'string') {
2150
- sizeString = size;
2151
- }
2152
- const md5Sum = dataObject[dataFieldMapping?.dataObjectMd5sumField ?? 'md5sum'] ?? 'N/A';
2153
- const url = dataObject[dataFieldMapping?.dataObjectUrlField ?? 'url'] ?? 'N/A';
2154
- let fileType = 'GA4GH_DRS';
2155
- if (dataFieldMapping?.dataObjectFileTypeValue) fileType = dataFieldMapping.dataObjectFileTypeValue;
2156
- if (dataFieldMapping?.dataObjectFileTypeField) fileType = dataObject[dataFieldMapping?.dataObjectFileTypeField];
1983
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1984
+ } catch (_error) {
2157
1985
  return {
2158
- ...dataAcc,
2159
- [fileId]: {
2160
- dataset_guid: datasetId,
2161
- id: fileId,
2162
- guid: fileId,
2163
- itemType: 'Data',
2164
- name: name,
2165
- size: sizeString,
2166
- md5sum: md5Sum,
2167
- type: fileType,
2168
- url: url
2169
- }
1986
+ isError: true,
1987
+ status: 401,
1988
+ message: 'cannot find cohorts'
2170
1989
  };
2171
- }, {});
2172
- return {
2173
- ...acc,
2174
- ...datafiles
2175
- };
2176
- }, {});
2177
- return items;
2178
- };
2179
-
2180
- const DATABASE_NAME = 'Gen3DataLibrary';
2181
- const STORE_NAME = 'DataLibraryLists';
2182
- class LocalStorageService {
2183
- getDb() {
2184
- return idb.openDB(DATABASE_NAME, 1, {
2185
- upgrade (db) {
2186
- if (!db.objectStoreNames.contains(STORE_NAME)) {
2187
- db.createObjectStore(STORE_NAME, {
2188
- keyPath: 'id'
2189
- });
2190
- }
2191
- }
2192
- });
1990
+ }
2193
1991
  }
2194
- async getList(id) {
2195
- const db = await this.getDb();
2196
- const tx = db.transaction(STORE_NAME, 'readonly');
2197
- const store = tx.objectStore(STORE_NAME);
2198
- const lists = await store.get(id);
2199
- if (lists) {
1992
+ /**
1993
+ * Count total number of cohorts
1994
+ */ async getCohortCount() {
1995
+ try {
1996
+ const db = await this.getDb();
1997
+ const total = await db.count(this.storeName);
2200
1998
  return {
2201
1999
  status: 200,
2202
2000
  message: 'success',
2203
- lists: {
2204
- [id]: {
2205
- id: id,
2206
- ...lists,
2207
- items: lists.items
2208
- }
2209
- }
2001
+ data: total
2210
2002
  };
2211
- } else {
2003
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
2004
+ } catch (_error) {
2212
2005
  return {
2213
2006
  isError: true,
2214
- status: 500,
2215
- message: `${id} does not exist`
2007
+ status: 401,
2008
+ message: 'cannot find cohort count'
2216
2009
  };
2217
2010
  }
2218
2011
  }
2219
- async getLists() {
2220
- const db = await this.getDb();
2221
- const tx = db.transaction(STORE_NAME, 'readonly');
2222
- const store = tx.objectStore(STORE_NAME);
2223
- const lists = await store.getAll();
2224
- if (!lists) {
2225
- return {
2226
- isError: true,
2227
- status: 500,
2228
- message: 'no lists returned'
2229
- };
2230
- }
2231
- const listMap = lists.reduce((acc, x)=>{
2232
- const { id } = x;
2233
- acc[id] = x;
2234
- return acc;
2235
- }, {});
2236
- const datalists = BuildLists({
2237
- lists: listMap
2238
- });
2239
- return {
2240
- status: 200,
2241
- message: 'success',
2242
- lists: datalists
2243
- };
2244
- }
2245
- async addList(list) {
2246
- const timestamp = getTimestamp();
2247
- try {
2248
- const db = await this.getDb();
2249
- const tx = db.transaction(STORE_NAME, 'readwrite');
2250
- const id = toolkit.nanoid(); // Create an id for the list
2251
- tx.objectStore(STORE_NAME).put({
2252
- id,
2253
- version: 0,
2254
- items: list?.items ?? {},
2255
- creator: '{{subject_id}}',
2256
- authz: {
2257
- version: 0,
2258
- authz: [
2259
- `/users/{{subject_id}}/user-library/lists/${id}`
2260
- ]
2261
- },
2262
- name: list?.name ?? 'New List',
2263
- created_time: timestamp,
2264
- updated_time: timestamp
2265
- });
2266
- await tx.done;
2267
- return {
2268
- status: 200,
2269
- message: 'list added'
2270
- };
2271
- } catch (_error) {
2272
- return {
2273
- isError: true,
2274
- status: 500,
2275
- message: `unable to add list ${list?.name ?? 'New List'}`
2276
- };
2277
- }
2278
- }
2279
- async updateList(id, update) {
2280
- const { name, items } = update;
2012
+ // ===== UPDATE OPERATIONS =====
2013
+ /**
2014
+ * Update an existing cohort (full replacement)
2015
+ */ async updateCohort(cohort) {
2281
2016
  try {
2282
2017
  const db = await this.getDb();
2283
- const tx = db.transaction(STORE_NAME, 'readwrite');
2284
- const store = tx.objectStore(STORE_NAME);
2285
- const listData = await store.get(id);
2286
- if (!listData) {
2287
- throw new Error(`List ${id} does not exist`);
2018
+ // Verify cohort exists before updating
2019
+ const tx = db.transaction(this.storeName, 'readwrite');
2020
+ const store = tx.objectStore(this.storeName);
2021
+ const existing = await store.get(cohort.id);
2022
+ if (!existing) {
2023
+ return {
2024
+ isError: true,
2025
+ status: 401,
2026
+ message: 'cohort not found'
2027
+ };
2288
2028
  }
2289
2029
  const timestamp = getTimestamp();
2290
- const version = listData.version ? listData.version + 1 : 0;
2291
2030
  const updated = {
2292
- ...listData,
2293
- ...{
2294
- name,
2295
- items
2296
- },
2297
- version: version,
2298
- updated_time: timestamp,
2299
- created_time: listData.created_time
2031
+ ...existing,
2032
+ modifiedDatetime: timestamp
2300
2033
  };
2301
2034
  store.put(updated);
2302
2035
  await tx.done;
@@ -2312,18 +2045,26 @@ class LocalStorageService {
2312
2045
  return {
2313
2046
  isError: true,
2314
2047
  status: 500,
2315
- message: `Unable to update list: ${id}. Error: ${errorMessage}`
2048
+ message: `Unable to update cohort: ${cohort.id}. Error: ${errorMessage}`
2316
2049
  };
2317
2050
  }
2318
2051
  }
2319
- async deleteList(id) {
2052
+ // ===== DELETE OPERATIONS =====
2053
+ /**
2054
+ * Delete a specific cohort by ID
2055
+ */ async deleteCohort(id) {
2320
2056
  try {
2321
2057
  const db = await this.getDb();
2322
- const tx = db.transaction(STORE_NAME, 'readwrite');
2323
- const store = tx.objectStore(STORE_NAME);
2324
- const item = await store.get(id);
2325
- if (!item) {
2326
- throw new Error(`List ${id} does not exist`);
2058
+ const tx = db.transaction(this.storeName, 'readwrite');
2059
+ const store = tx.objectStore(this.storeName);
2060
+ // Verify cohort exists before deleting
2061
+ const existing = await db.get(this.storeName, id);
2062
+ if (!existing) {
2063
+ return {
2064
+ isError: true,
2065
+ status: 401,
2066
+ message: 'cohort not found'
2067
+ };
2327
2068
  }
2328
2069
  store.delete(id);
2329
2070
  await tx.done;
@@ -2336,926 +2077,1708 @@ class LocalStorageService {
2336
2077
  return {
2337
2078
  isError: true,
2338
2079
  status: 500,
2339
- message: `Unable to delete list: ${id}. Error: ${errorMessage}`
2080
+ message: `Unable to delete cohort: ${id}. Error: ${errorMessage}`
2340
2081
  };
2341
2082
  }
2342
2083
  }
2343
- async clearLists() {
2084
+ /**
2085
+ * Delete all cohorts from the database
2086
+ */ async deleteAllCohorts() {
2344
2087
  try {
2345
2088
  const db = await this.getDb();
2346
- const tx = db.transaction(STORE_NAME, 'readwrite');
2347
- tx.objectStore(STORE_NAME).clear();
2089
+ const tx = db.transaction(this.storeName, 'readwrite');
2090
+ const store = tx.objectStore(this.storeName);
2091
+ store.clear();
2348
2092
  await tx.done;
2349
2093
  return {
2350
2094
  status: 200,
2351
- message: 'list cleared'
2095
+ message: `all cohorts deleted`
2352
2096
  };
2353
2097
  } catch (error) {
2354
2098
  const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
2355
2099
  return {
2356
2100
  isError: true,
2357
2101
  status: 500,
2358
- message: `Unable to clear library. Error: ${errorMessage}`
2102
+ message: `Unable to delete all cohorts. Error: ${errorMessage}`
2359
2103
  };
2360
2104
  }
2361
2105
  }
2362
- async cacheLists(data) {
2363
- if (!data || typeof data !== 'object') {
2364
- return {
2365
- isError: true,
2366
- status: 500,
2367
- message: 'Invalid or missing lists property in request'
2368
- };
2369
- }
2370
- const allLists = Object.entries(data).reduce((acc, [id, x])=>{
2371
- if (!isDatalistAPI(x)) return acc;
2372
- acc[id] = {
2373
- ...x
2374
- };
2375
- return acc;
2376
- }, {});
2106
+ // ===== UTILITY OPERATIONS =====
2107
+ /**
2108
+ * Check if a cohort exists
2109
+ */ async cohortExists(id) {
2377
2110
  try {
2378
2111
  const db = await this.getDb();
2379
- const tx = db.transaction(STORE_NAME, 'readwrite');
2380
- for (const [id, list] of Object.entries(allLists)){
2381
- tx.objectStore(STORE_NAME).put({
2382
- id,
2383
- ...list
2384
- });
2385
- }
2386
- await tx.done;
2112
+ // Verify cohort exists before deleting
2113
+ const existing = await db.get(this.storeName, id);
2387
2114
  return {
2388
2115
  status: 200,
2389
- message: 'success'
2116
+ message: `${id}: ${existing ? 'true' : 'false'}`
2390
2117
  };
2391
2118
  } catch (error) {
2392
2119
  const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
2393
- return {
2394
- isError: true,
2395
- status: 200,
2396
- message: `unable to cache library to local storage. Error: ${errorMessage}`
2397
- };
2398
- }
2399
- }
2400
- async cacheList(id, data) {
2401
- if (!data || typeof data !== 'object') {
2402
2120
  return {
2403
2121
  isError: true,
2404
2122
  status: 500,
2405
- message: 'Invalid or missing lists property in request'
2123
+ message: `Unable search for cohort. Error: ${errorMessage}`
2406
2124
  };
2407
2125
  }
2126
+ }
2127
+ /**
2128
+ * Export all cohorts as JSON
2129
+ */ async exportCohorts() {
2130
+ return await this.getAllCohorts();
2131
+ }
2132
+ /**
2133
+ * Import cohorts from JSON data
2134
+ */ async importCohorts(cohorts, overwrite = false) {
2408
2135
  try {
2409
- const db = await this.getDb();
2410
- const tx = db.transaction(STORE_NAME, 'readwrite');
2411
- tx.objectStore(STORE_NAME).put({
2412
- id: id,
2413
- ...data
2414
- });
2415
- await tx.done;
2416
- return {
2417
- status: 200,
2418
- message: 'success'
2419
- };
2136
+ if (overwrite) {
2137
+ await this.deleteAllCohorts();
2138
+ }
2139
+ await this.saveCohorts(cohorts);
2420
2140
  } catch (error) {
2421
2141
  const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
2422
2142
  return {
2423
2143
  isError: true,
2424
2144
  status: 500,
2425
- message: `unable to clear library. Error: ${errorMessage}`
2145
+ message: `Failed to import cohorts: ${errorMessage}`
2426
2146
  };
2427
2147
  }
2428
2148
  }
2429
- constructor(){
2430
- this.setAllLists = async (data)=>{
2431
- const timestamp = getTimestamp();
2432
- const allLists = data.reduce((acc, x)=>{
2433
- if (!isJSONObject(x)) return acc;
2434
- const id = toolkit.nanoid(10);
2435
- acc[id] = {
2436
- ...x,
2437
- version: 0,
2438
- created_time: timestamp,
2439
- updated_time: timestamp,
2440
- creator: '{{subject_id}}',
2441
- authz: {
2442
- version: 0,
2443
- authz: [
2444
- `/users/{{subject_id}}/user-library/lists/${id}`
2445
- ]
2446
- }
2447
- };
2448
- return acc;
2449
- }, {});
2450
- try {
2451
- const db = await this.getDb();
2452
- const tx = db.transaction(STORE_NAME, 'readwrite');
2453
- for (const [id, list] of Object.entries(allLists)){
2454
- tx.objectStore(STORE_NAME).put({
2455
- id,
2456
- ...list
2457
- });
2458
- }
2459
- await tx.done;
2460
- return {
2461
- status: 200,
2462
- message: 'success'
2463
- };
2464
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
2465
- } catch (_error) {
2466
- return {
2467
- isError: true,
2468
- status: 500,
2469
- message: 'unable to add lists'
2470
- };
2471
- }
2472
- };
2149
+ /**
2150
+ * Close the database connection
2151
+ */ async close() {
2152
+ try {
2153
+ const db = await this.getDb();
2154
+ db.close();
2155
+ } catch (error) {
2156
+ console.error('Failed to close database:', error);
2157
+ }
2473
2158
  }
2474
2159
  }
2475
2160
 
2476
- const fetchFromDataLibraryAPI = async (url, method = HttpMethod.GET, body = undefined)=>{
2477
- try {
2478
- return {
2479
- data: await fetchJSONDataFromURL(url, true, method, body)
2480
- };
2481
- } catch (error) {
2482
- if (error instanceof HTTPError) {
2483
- return {
2484
- error: {
2485
- status: error.status,
2486
- message: HTTPErrorMessages[error.status] || error.responseData?.message || 'No HTTP Error Message'
2487
- }
2488
- };
2489
- } else {
2490
- return {
2491
- error: {
2492
- status: 500,
2493
- message: 'Unknown Error'
2494
- }
2495
- };
2496
- }
2497
- }
2161
+ const isOperationWithField = (operation)=>{
2162
+ return operation?.field !== undefined;
2498
2163
  };
2499
- const responseFromMutation = (responseReceived)=>{
2500
- if (responseReceived.error) {
2501
- return {
2502
- isError: true,
2503
- ...responseReceived.error
2504
- };
2164
+ const isOperatorWithFieldAndArrayOfOperands = (operation)=>{
2165
+ 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
2166
+ ) {
2167
+ const { operator } = operation.operator;
2168
+ return operator === 'in' || operator === 'exclude' || operator === 'excludeifany';
2505
2169
  }
2506
- return {
2507
- lists: responseReceived.data,
2508
- message: 'success',
2509
- status: 200
2510
- };
2170
+ return false;
2511
2171
  };
2512
- class APIStorageService {
2513
- constructor(apiBaseUrl = `${GEN3_DATA_LIBRARY_API}`){
2514
- this.pendingRequests = new Map();
2515
- this.apiBaseUrl = apiBaseUrl;
2516
- }
2517
- async dedupedRequest(url, method = HttpMethod.GET, body = undefined) {
2518
- // Create a unique key for this request
2519
- const requestKey = `${method}:${url}:${body ? JSON.stringify(body) : ''}`;
2520
- // If this exact request is already in progress, return the pending promise
2521
- if (this.pendingRequests.has(requestKey)) {
2522
- return this.pendingRequests.get(requestKey);
2523
- }
2524
- // Otherwise, make the request and store the promise
2525
- const requestPromise = fetchFromDataLibraryAPI(url, method, body);
2526
- this.pendingRequests.set(requestKey, requestPromise);
2527
- try {
2528
- // Wait for the request to complete
2529
- const result = await requestPromise;
2530
- return result;
2531
- } finally{
2532
- // Remove the request from pending requests after it completes
2533
- this.pendingRequests.delete(requestKey);
2534
- }
2535
- }
2536
- async getLists() {
2537
- const { data, error } = await this.dedupedRequest(`${this.apiBaseUrl}`);
2538
- if (error) {
2539
- return {
2540
- isError: true,
2541
- ...error
2542
- };
2543
- }
2544
- if (data && isDataLibraryAPIResponse(data)) {
2545
- const datalists = BuildLists(data);
2172
+ const extractFilterValue = (op)=>{
2173
+ const valueExtractorHandler = new ValueExtractorHandler();
2174
+ return handleOperation(valueExtractorHandler, op);
2175
+ };
2176
+ const extractEnumFilterValue = (op)=>{
2177
+ const enumValueExtractorHandler = new EnumValueExtractorHandler();
2178
+ const results = handleOperation(enumValueExtractorHandler, op);
2179
+ return results ?? [];
2180
+ };
2181
+ const assertNever = (x)=>{
2182
+ throw Error(`Exhaustive comparison did not handle: ${x}`);
2183
+ };
2184
+ const handleOperation = (handler, op)=>{
2185
+ switch(op.operator){
2186
+ case '=':
2187
+ return handler.handleEquals(op);
2188
+ case '!=':
2189
+ return handler.handleNotEquals(op);
2190
+ case '<':
2191
+ return handler.handleLessThan(op);
2192
+ case '<=':
2193
+ return handler.handleLessThanOrEquals(op);
2194
+ case '>':
2195
+ return handler.handleGreaterThan(op);
2196
+ case '>=':
2197
+ return handler.handleGreaterThanOrEquals(op);
2198
+ case 'and':
2199
+ return handler.handleIntersection(op);
2200
+ case 'or':
2201
+ return handler.handleUnion(op);
2202
+ case 'nested':
2203
+ return handler.handleNestedFilter(op);
2204
+ case 'in':
2205
+ case 'includes':
2206
+ return handler.handleIncludes(op);
2207
+ case 'excludeifany':
2208
+ return handler.handleExcludeIfAny(op);
2209
+ case 'excludes':
2210
+ return handler.handleExcludes(op);
2211
+ case 'exists':
2212
+ return handler.handleExists(op);
2213
+ case 'missing':
2214
+ return handler.handleMissing(op);
2215
+ default:
2216
+ return assertNever(op);
2217
+ }
2218
+ };
2219
+ /**
2220
+ * Return true if a FilterSet's root value is an empty object
2221
+ * @param fs - FilterSet to test
2222
+ */ const isFilterEmpty = (fs)=>lodash.isEqual({}, fs);
2223
+ /**
2224
+ * Type guard to check if an object is a GQLIntersection
2225
+ * @param value - The value to check
2226
+ * @returns True if the value is a GQLIntersection
2227
+ */ const isGQLIntersection = (value)=>{
2228
+ return typeof value === 'object' && value !== null && 'and' in value && Array.isArray(value.and);
2229
+ };
2230
+ /**
2231
+ * Type guard to check if an object is a GQLIntersection
2232
+ * @param value - The value to check
2233
+ * @returns True if the value is a GQLIntersection
2234
+ */ const isGQLUnion = (value)=>{
2235
+ return typeof value === 'object' && value !== null && 'or' in value && Array.isArray(value.or);
2236
+ };
2237
+ class ToGqlHandler {
2238
+ constructor(){
2239
+ this.handleEquals = (op)=>({
2240
+ '=': {
2241
+ [op.field]: op.operand
2242
+ }
2243
+ });
2244
+ this.handleNotEquals = (op)=>({
2245
+ '!=': {
2246
+ [op.field]: op.operand
2247
+ }
2248
+ });
2249
+ this.handleLessThan = (op)=>({
2250
+ '<': {
2251
+ [op.field]: op.operand
2252
+ }
2253
+ });
2254
+ this.handleLessThanOrEquals = (op)=>({
2255
+ '<=': {
2256
+ [op.field]: op.operand
2257
+ }
2258
+ });
2259
+ this.handleGreaterThan = (op)=>({
2260
+ '>': {
2261
+ [op.field]: op.operand
2262
+ }
2263
+ });
2264
+ this.handleGreaterThanOrEquals = (op)=>({
2265
+ '>=': {
2266
+ [op.field]: op.operand
2267
+ }
2268
+ });
2269
+ this.handleIncludes = (op)=>({
2270
+ in: {
2271
+ [op.field]: op.operands
2272
+ }
2273
+ });
2274
+ this.handleExcludes = (op)=>({
2275
+ exclude: {
2276
+ [op.field]: op.operands
2277
+ }
2278
+ });
2279
+ this.handleExcludeIfAny = (op)=>({
2280
+ excludeifany: {
2281
+ [op.field]: op.operands
2282
+ }
2283
+ });
2284
+ this.handleIntersection = (op)=>({
2285
+ and: op.operands.map((x)=>convertFilterToGqlFilter(x))
2286
+ });
2287
+ this.handleUnion = (op)=>({
2288
+ or: op.operands.map((x)=>convertFilterToGqlFilter(x))
2289
+ });
2290
+ this.handleMissing = (op)=>({
2291
+ is: {
2292
+ [op.field]: 'MISSING'
2293
+ }
2294
+ });
2295
+ this.handleExists = (op)=>({
2296
+ not: {
2297
+ [op.field]: op?.operand ?? null
2298
+ }
2299
+ });
2300
+ this.handleNestedFilter = (op)=>{
2301
+ const child = convertFilterToGqlFilter(op.operand);
2546
2302
  return {
2547
- lists: datalists,
2548
- status: 200,
2549
- message: 'success'
2303
+ nested: {
2304
+ path: op.path,
2305
+ ...child
2306
+ }
2550
2307
  };
2551
- }
2552
- return {
2553
- lists: {},
2554
- status: 200,
2555
- message: 'no list returned'
2556
2308
  };
2557
2309
  }
2558
- async addList(list) {
2559
- const response = await fetchFromDataLibraryAPI(`${this.apiBaseUrl}`, HttpMethod.PUT, JSON.stringify({
2560
- lists: [
2561
- list
2562
- ]
2563
- }));
2564
- return responseFromMutation(response);
2310
+ }
2311
+ const convertFilterToGqlFilter = (filter)=>{
2312
+ const handler = new ToGqlHandler();
2313
+ return handleOperation(handler, filter);
2314
+ };
2315
+ const convertFilterSetToGqlFilter = (fs, toplevelOp = 'and')=>{
2316
+ const fsKeys = Object.keys(fs.root);
2317
+ // if no keys return undefined
2318
+ if (fsKeys.length === 0) return {
2319
+ and: []
2320
+ };
2321
+ return toplevelOp === 'and' ? {
2322
+ and: fsKeys.map((key)=>convertFilterToGqlFilter(fs.root[key]))
2323
+ } : {
2324
+ or: fsKeys.map((key)=>convertFilterToGqlFilter(fs.root[key]))
2325
+ };
2326
+ };
2327
+ const handleGqlOperation = (handler, op)=>{
2328
+ const operationKeys = Object.keys(op);
2329
+ if (operationKeys.includes('=')) {
2330
+ return handler.handleEquals(op);
2565
2331
  }
2566
- async updateList(id, list) {
2567
- const response = await fetchFromDataLibraryAPI(`${this.apiBaseUrl}/${id}`, HttpMethod.PUT, JSON.stringify(list));
2568
- return responseFromMutation(response);
2332
+ if (operationKeys.includes('!=')) {
2333
+ return handler.handleNotEquals(op);
2569
2334
  }
2570
- async deleteList(id) {
2571
- const response = await fetchFromDataLibraryAPI(`${this.apiBaseUrl}/${id}`, HttpMethod.DELETE);
2572
- return responseFromMutation(response);
2335
+ if (operationKeys.includes('<')) {
2336
+ return handler.handleLessThan(op);
2573
2337
  }
2574
- async clearLists() {
2575
- const response = await fetchFromDataLibraryAPI(this.apiBaseUrl, HttpMethod.DELETE);
2576
- return responseFromMutation(response);
2338
+ if (operationKeys.includes('<=')) {
2339
+ return handler.handleLessThanOrEquals(op);
2577
2340
  }
2578
- async setAllLists(lists) {
2579
- const response = await fetchFromDataLibraryAPI(this.apiBaseUrl, HttpMethod.POST, JSON.stringify({
2580
- lists: Object.values(lists)
2581
- }));
2582
- return responseFromMutation(response);
2341
+ if (operationKeys.includes('>')) {
2342
+ return handler.handleGreaterThan(op);
2583
2343
  }
2584
- async getList(id) {
2585
- const { data, error } = await fetchFromDataLibraryAPI(`${this.apiBaseUrl}/${id}`);
2586
- if (error) {
2344
+ if (operationKeys.includes('>=')) {
2345
+ return handler.handleGreaterThanOrEquals(op);
2346
+ }
2347
+ if (operationKeys.includes('in')) {
2348
+ return handler.handleIncludes(op);
2349
+ }
2350
+ if (operationKeys.includes('exclude')) {
2351
+ return handler.handleExcludes(op);
2352
+ }
2353
+ if (operationKeys.includes('excludeifany')) {
2354
+ return handler.handleExcludeIfAny(op);
2355
+ }
2356
+ if (operationKeys.includes('and')) {
2357
+ return handler.handleIntersection(op);
2358
+ }
2359
+ if (operationKeys.includes('or')) {
2360
+ return handler.handleUnion(op);
2361
+ }
2362
+ if (operationKeys.includes('nested')) {
2363
+ return handler.handleNestedFilter(op);
2364
+ }
2365
+ if (operationKeys.includes('is')) {
2366
+ return handler.handleExists(op);
2367
+ }
2368
+ if (operationKeys.includes('not')) {
2369
+ return handler.handleMissing(op);
2370
+ }
2371
+ return assertNever(op);
2372
+ };
2373
+ const convertGqlFilterToFilter = (gqlFilter)=>{
2374
+ const handler = new ToOperationHandler();
2375
+ return handleGqlOperation(handler, gqlFilter);
2376
+ };
2377
+ /**
2378
+ * Convert GQL to Filterset
2379
+ * Note assumes all GqlOperators have one field: value
2380
+ */ class ToOperationHandler {
2381
+ constructor(){
2382
+ this.handleEquals = (op)=>{
2383
+ const [field, value] = Object.entries(op['='])[0];
2587
2384
  return {
2588
- isError: true,
2589
- ...error
2385
+ operator: '=',
2386
+ field: field,
2387
+ operand: value
2590
2388
  };
2591
- }
2592
- if (isDataLibraryAPIResponse(data)) {
2593
- const datalists = BuildLists(data);
2389
+ };
2390
+ this.handleNotEquals = (op)=>{
2391
+ const [field, value] = Object.entries(op['!='])[0];
2594
2392
  return {
2595
- lists: datalists,
2596
- status: 200,
2597
- message: 'success'
2393
+ operator: '!=',
2394
+ field: field,
2395
+ operand: value
2598
2396
  };
2599
- }
2600
- return {
2601
- isError: true,
2602
- status: 500,
2603
- message: `Unknown error getting list ${id}`
2604
2397
  };
2605
- }
2606
- }
2607
-
2608
- class CachedAPIService {
2609
- constructor(){
2610
- this.localStorageDataLibrary = new LocalStorageService(); // always update local storage
2611
- this.apiDataLibrary = new APIStorageService();
2612
- }
2613
- async getLists() {
2614
- // do a network request to get the library
2615
- // get the remote list
2616
- const apiResults = await this.apiDataLibrary.getLists();
2617
- if (apiResults.isError) {
2398
+ this.handleLessThan = (op)=>{
2399
+ const [field, value] = Object.entries(op['<'])[0];
2618
2400
  return {
2619
- ...apiResults,
2620
- lists: undefined
2401
+ operator: '<',
2402
+ field: field,
2403
+ operand: value
2621
2404
  };
2622
- }
2623
- const dataLibrary = convertDataLibraryToDataLibraryAPI(apiResults?.lists ?? {});
2624
- await this.localStorageDataLibrary.cacheLists(dataLibrary);
2625
- return apiResults;
2626
- }
2627
- async getList(id) {
2628
- return await this.localStorageDataLibrary.getList(id);
2629
- }
2630
- async getCachedLists(id) {
2631
- return await this.localStorageDataLibrary.getList(id);
2632
- }
2633
- async setAllLists(lists) {
2634
- const apiResults = await this.apiDataLibrary.setAllLists(lists);
2635
- if (apiResults.isError) {
2405
+ };
2406
+ this.handleLessThanOrEquals = (op)=>{
2407
+ const [field, value] = Object.entries(op['<='])[0];
2636
2408
  return {
2637
- ...apiResults,
2638
- lists: undefined
2409
+ operator: '<=',
2410
+ field: field,
2411
+ operand: value
2639
2412
  };
2640
- }
2641
- const dataLibrary = convertDataLibraryToDataLibraryAPI(apiResults?.lists ?? {});
2642
- await this.localStorageDataLibrary.cacheLists(dataLibrary);
2643
- return apiResults;
2644
- }
2645
- async addList(list) {
2646
- // update the API list
2647
- const apiResults = await this.apiDataLibrary.addList(list);
2648
- if (apiResults.isError) {
2413
+ };
2414
+ this.handleGreaterThan = (op)=>{
2415
+ const [field, value] = Object.entries(op['>'])[0];
2649
2416
  return {
2650
- ...apiResults,
2651
- lists: undefined
2417
+ operator: '>',
2418
+ field: field,
2419
+ operand: value
2652
2420
  };
2653
- }
2654
- const cacheResults = await this.localStorageDataLibrary.addList(list);
2655
- return {
2656
- ...cacheResults,
2657
- lists: undefined
2658
2421
  };
2659
- }
2660
- async updateList(id, list) {
2661
- const apiResults = await this.apiDataLibrary.updateList(id, list);
2662
- if (apiResults.isError) {
2422
+ this.handleGreaterThanOrEquals = (op)=>{
2423
+ const [field, value] = Object.entries(op['>='])[0];
2663
2424
  return {
2664
- ...apiResults,
2665
- lists: undefined
2425
+ operator: '>=',
2426
+ field: field,
2427
+ operand: value
2666
2428
  };
2667
- }
2668
- return await this.localStorageDataLibrary.cacheList(id, apiResults.lists ?? {});
2669
- }
2670
- async deleteList(id) {
2671
- const apiResults = await this.apiDataLibrary.deleteList(id);
2672
- if (apiResults.isError) {
2429
+ };
2430
+ this.handleIncludes = (op)=>{
2431
+ const [field, value] = Object.entries(op.in)[0];
2673
2432
  return {
2674
- ...apiResults,
2675
- lists: undefined
2433
+ operator: 'in',
2434
+ field: field,
2435
+ operands: value
2676
2436
  };
2677
- }
2678
- return await this.localStorageDataLibrary.deleteList(id);
2679
- }
2680
- async clearLists() {
2681
- const apiResults = await this.apiDataLibrary.clearLists();
2682
- if (apiResults.isError) {
2437
+ };
2438
+ this.handleExcludes = (op)=>{
2439
+ const [field, value] = Object.entries(op.exclude)[0];
2683
2440
  return {
2684
- ...apiResults,
2685
- lists: undefined
2441
+ operator: 'excludes',
2442
+ field: field,
2443
+ operands: value
2686
2444
  };
2687
- }
2688
- return await this.localStorageDataLibrary.clearLists();
2445
+ };
2446
+ this.handleExcludeIfAny = (op)=>{
2447
+ const [field, value] = Object.entries(op.excludeifany)[0];
2448
+ return {
2449
+ operator: 'excludeifany',
2450
+ field: field,
2451
+ operands: value
2452
+ };
2453
+ };
2454
+ this.handleIntersection = (op)=>({
2455
+ operator: 'and',
2456
+ operands: op.and.map(convertGqlFilterToFilter)
2457
+ });
2458
+ this.handleUnion = (op)=>({
2459
+ operator: 'or',
2460
+ operands: op.or.map(convertGqlFilterToFilter)
2461
+ });
2462
+ this.handleExists = (op)=>{
2463
+ const [field, value] = Object.entries(op.not)[0];
2464
+ return {
2465
+ operator: 'exists',
2466
+ field: field,
2467
+ operand: value
2468
+ };
2469
+ };
2470
+ this.handleMissing = (op)=>{
2471
+ const field = Object.keys(op.is)[0];
2472
+ return {
2473
+ operator: 'missing',
2474
+ field: field
2475
+ };
2476
+ };
2477
+ this.handleNestedFilter = (op)=>({
2478
+ operator: 'nested',
2479
+ path: op.nested.path,
2480
+ operand: convertGqlFilterToFilter(op.nested)
2481
+ });
2689
2482
  }
2690
2483
  }
2691
-
2692
- class DataLibraryStorageService {
2693
- constructor(mode = DataLibraryStoreMode.ApiOnly){
2694
- if (mode === DataLibraryStoreMode.ApiOnly) {
2695
- this.storageService = new APIStorageService();
2696
- } else if (mode === DataLibraryStoreMode.ApiAndLocal) this.storageService = new CachedAPIService();
2697
- else this.storageService = new LocalStorageService();
2698
- }
2699
- async setStorageMode(mode) {
2700
- if (mode === DataLibraryStoreMode.ApiOnly) {
2701
- this.storageService = new APIStorageService();
2702
- } else if (mode === DataLibraryStoreMode.ApiAndLocal) this.storageService = new CachedAPIService();
2703
- else this.storageService = new LocalStorageService();
2704
- }
2705
- // private async syncApiAndLocal() {
2706
- // const { lists: localData, isError: localError } =
2707
- // await this.localStorageDataLibrary.getLists();
2708
- // const { lists: apiData, isError: apiError } =
2709
- // await this.apiDataLibrary.getLists();
2710
- //
2711
- // if (localError || apiError) {
2712
- // return;
2713
- // }
2714
- //
2715
- // const mergedData: Record<string, Datalist> = { ...localData };
2716
- //
2717
- // // First, update any existing items with newer versions from API
2718
- // Object.values(apiData?.lists ?? {}).forEach((apiList) => {
2719
- // const id = apiList.id as keyof DataLibrary;
2720
- // const localList = localData?.[id];
2721
- // if (
2722
- // !localList ||
2723
- // storage Date(apiList.updatedTime) > storage Date(localList.updatedTime)
2724
- // ) {
2725
- // mergedData[id] = apiList;
2726
- // }
2727
- // });
2728
- //
2729
- // // Push local-only changes to API
2730
- // const syncPromises: Promise<any>[] = [];
2731
- //
2732
- // for (const [id, localList] of Object.entries(localData?.lists ?? {})) {
2733
- // if (!apiData?.[id]) {
2734
- // // This list exists locally but not in API, so push it to API
2735
- // syncPromises.push(this.apiDataLibrary.addList(localList));
2736
- // } else if (
2737
- // storage Date(localList.updatedTime) > storage Date(apiData[id].updatedTime)
2738
- // ) {
2739
- // // Local list is newer than API, so update API
2740
- // syncPromises.push(this.apiDataLibrary.updateList(localList));
2741
- // }
2742
- // }
2743
- //
2744
- // // Wait for all API operations to complete
2745
- // await Promise.all(syncPromises);
2746
- // await this.localStorageDataLibrary.cacheLists({ lists: mergedData });
2747
- // }
2748
- async getLists() {
2749
- return await this.storageService.getLists();
2750
- }
2751
- async getList(id) {
2752
- return await this.storageService.getList(id);
2753
- }
2754
- async getCachedLists(id) {
2755
- return await this.storageService.getList(id);
2484
+ /**
2485
+ * Extract the operand values, if operands themselves have values, otherwise undefined.
2486
+ */ class ValueExtractorHandler {
2487
+ constructor(){
2488
+ this.handleEquals = (op)=>op.operand;
2489
+ this.handleNotEquals = (op)=>op.operand;
2490
+ this.handleIncludes = (op)=>op.operands;
2491
+ this.handleExcludes = (op)=>op.operands;
2492
+ this.handleExcludeIfAny = (op)=>op.operands;
2493
+ this.handleGreaterThanOrEquals = (op)=>op.operand;
2494
+ this.handleGreaterThan = (op)=>op.operand;
2495
+ this.handleLessThan = (op)=>op.operand;
2496
+ this.handleLessThanOrEquals = (op)=>op.operand;
2497
+ this.handleIntersection = (_arg)=>undefined;
2498
+ this.handleUnion = (_)=>undefined;
2499
+ this.handleNestedFilter = (_)=>undefined;
2500
+ this.handleExists = (_)=>undefined;
2501
+ this.handleMissing = (_)=>undefined;
2756
2502
  }
2757
- async setAllLists(lists) {
2758
- return await this.storageService.setAllLists(lists ?? {});
2503
+ }
2504
+ /**
2505
+ * Extract the operand values, if operands themselves have values, otherwise undefined.
2506
+ */ class EnumValueExtractorHandler {
2507
+ constructor(){
2508
+ this.handleEquals = (_)=>undefined;
2509
+ this.handleNotEquals = (_)=>undefined;
2510
+ this.handleIncludes = (op)=>op.operands;
2511
+ this.handleExcludes = (op)=>op.operands;
2512
+ this.handleExcludeIfAny = (op)=>op.operands;
2513
+ this.handleGreaterThanOrEquals = (_)=>undefined;
2514
+ this.handleGreaterThan = (_)=>undefined;
2515
+ this.handleLessThan = (_)=>undefined;
2516
+ this.handleLessThanOrEquals = (_)=>undefined;
2517
+ this.handleIntersection = (_)=>undefined;
2518
+ this.handleUnion = (_)=>undefined;
2519
+ this.handleNestedFilter = (op)=>{
2520
+ return extractEnumFilterValue(op.operand);
2521
+ };
2522
+ this.handleExists = (_)=>undefined;
2523
+ this.handleMissing = (_)=>undefined;
2759
2524
  }
2760
- async addList(list) {
2761
- return await this.storageService.addList(list);
2525
+ }
2526
+ const appendFilterToOperation = (filter, addition)=>{
2527
+ if (filter === undefined && addition === undefined) return {
2528
+ operator: 'and',
2529
+ operands: []
2530
+ };
2531
+ if (addition === undefined && filter) return filter;
2532
+ if (filter === undefined && addition) return addition;
2533
+ return {
2534
+ ...filter,
2535
+ operands: [
2536
+ ...filter?.operands || [],
2537
+ addition
2538
+ ]
2539
+ };
2540
+ };
2541
+ const filterSetToOperation = (fs)=>{
2542
+ if (!fs) return undefined;
2543
+ switch(fs.mode){
2544
+ case 'and':
2545
+ return Object.keys(fs.root).length == 0 ? undefined : {
2546
+ operator: fs.mode,
2547
+ operands: Object.keys(fs.root).map((k)=>{
2548
+ return fs.root[k];
2549
+ })
2550
+ };
2762
2551
  }
2763
- async updateList(id, list) {
2764
- return await this.storageService.updateList(id, list);
2552
+ return undefined;
2553
+ };
2554
+
2555
+ const isFilterSet = (input)=>{
2556
+ if (typeof input !== 'object' || input === null) {
2557
+ return false;
2765
2558
  }
2766
- async deleteList(id) {
2767
- return await this.storageService.deleteList(id);
2559
+ const { root, mode } = input;
2560
+ if (typeof root !== 'object' || root === null) {
2561
+ return false;
2768
2562
  }
2769
- async clearLists() {
2770
- return await this.storageService.clearLists();
2563
+ if (![
2564
+ 'and',
2565
+ 'or'
2566
+ ].includes(mode)) {
2567
+ return false;
2771
2568
  }
2772
- }
2569
+ return true;
2570
+ };
2571
+ const isUnion = (value)=>{
2572
+ return typeof value === 'object' && value !== null && value.operator === 'or' && Array.isArray(value.operands);
2573
+ };
2574
+ const isIntersection = (value)=>{
2575
+ return typeof value === 'object' && value !== null && value.operator === 'and' && Array.isArray(value.operands);
2576
+ };
2577
+ const isOperandsType = (operation)=>{
2578
+ return operation?.operands !== undefined;
2579
+ };
2580
+ const isIndexedFilterSetEmpty = (filters)=>Object.values(filters).every((filterSet)=>Object.keys(filterSet).length === 0);
2581
+ const EmptyFilterSet = {
2582
+ mode: 'and',
2583
+ root: {}
2584
+ };
2773
2585
 
2774
- const EMPTY_LIST = {
2775
- items: {},
2776
- version: 0,
2777
- created_time: 'not_set',
2778
- updated_time: 'not_set',
2779
- name: '',
2780
- id: '',
2781
- authz: {
2782
- version: -1,
2783
- authz: []
2586
+ const FieldNameOverrides = {};
2587
+ const COMMON_PREPOSITIONS = [
2588
+ 'a',
2589
+ 'an',
2590
+ 'and',
2591
+ 'at',
2592
+ 'but',
2593
+ 'by',
2594
+ 'for',
2595
+ 'in',
2596
+ 'is',
2597
+ 'nor',
2598
+ 'of',
2599
+ 'on',
2600
+ 'or',
2601
+ 'out',
2602
+ 'so',
2603
+ 'the',
2604
+ 'to',
2605
+ 'up',
2606
+ 'yet'
2607
+ ];
2608
+ const capitalize = (s)=>s.length > 0 ? s[0].toUpperCase() + s.slice(1) : '';
2609
+ const trimFirstFieldNameToTitle = (fieldName, trim = false)=>{
2610
+ if (trim) {
2611
+ const source = fieldName.slice(fieldName.indexOf('.') + 1);
2612
+ return fieldNameToTitle(source ? source : fieldName, 0);
2784
2613
  }
2614
+ return fieldNameToTitle(fieldName);
2785
2615
  };
2786
- const DEFAULT_LIST_NAME = 'List';
2787
- const useDataLibrary = (options = {
2788
- storageMode: DataLibraryStoreMode.ApiOnly
2789
- })=>{
2790
- // State management
2791
- const [isLoggedIn, setIsLoggedIn] = React.useState(false);
2792
- const [isLoading, setIsLoading] = React.useState(false);
2793
- const [isUpdating, setIsUpdating] = React.useState(null);
2794
- const [error, setError] = React.useState(null);
2795
- const [lists, setLists] = React.useState({});
2796
- // Refs
2797
- const initialLoadRef = React.useRef(false);
2798
- // Services
2799
- const dataLibraryStoreAPI = React.useRef(new DataLibraryStorageService(options.storageMode)).current;
2800
- const handleErrorOrSetLists = React.useCallback(async (error)=>{
2801
- if (error.isError) {
2802
- setError(error);
2803
- } else {
2804
- const getListResults = await dataLibraryStoreAPI.getLists();
2805
- if (getListResults.isError) {
2806
- setError(getListResults);
2807
- } else {
2808
- setLists(getListResults.lists ?? {});
2809
- setError(null);
2810
- }
2811
- }
2812
- }, [
2813
- dataLibraryStoreAPI
2814
- ]);
2815
- const generateUniqueName = React.useCallback((baseName = DEFAULT_LIST_NAME)=>{
2816
- let uniqueName = baseName;
2817
- let counter = 1;
2818
- const existingNames = Object.values(lists).map((x)=>x.name);
2819
- while(existingNames.includes(uniqueName)){
2820
- uniqueName = `${baseName} ${counter}`;
2821
- counter++;
2822
- }
2823
- return uniqueName;
2824
- }, [
2825
- lists
2826
- ]);
2827
- const performLibraryOperation = React.useCallback(async (operation, updateId)=>{
2828
- setError(null);
2829
- if (updateId) {
2830
- setIsUpdating(updateId);
2831
- } else setIsLoading(true);
2832
- const operationResults = await operation();
2833
- await handleErrorOrSetLists(operationResults);
2834
- if (updateId) setIsUpdating(null);
2835
- else setIsLoading(false);
2836
- return operationResults;
2837
- }, [
2838
- handleErrorOrSetLists
2839
- ]);
2840
- // Lifecycle effects
2841
- React.useEffect(()=>{
2842
- const initializeData = async ()=>{
2843
- if (!initialLoadRef.current) {
2844
- setError(null);
2845
- setIsLoading(true);
2846
- const results = await dataLibraryStoreAPI.getLists(); // get the initial lists
2847
- if (results.isError) setError(results);
2848
- else setLists(results.lists ?? {});
2849
- setIsLoading(false);
2850
- initialLoadRef.current = true;
2851
- }
2852
- };
2853
- initializeData();
2854
- }, [
2855
- dataLibraryStoreAPI
2856
- ]);
2857
- React.useEffect(()=>{
2858
- const handleLogin = async ()=>{
2859
- // setIsLoading(true);
2860
- // await dataLibraryStoreAPI.setUseAPI(options.requiresAPI && isLoggedIn);
2861
- // setIsLoading(false);
2862
- };
2863
- handleLogin();
2864
- }, [
2865
- dataLibraryStoreAPI,
2866
- isLoggedIn
2867
- ]);
2868
- // CRUD operations
2869
- const addListToDataLibrary = React.useCallback(async (items, name)=>{
2870
- const apiItems = convertDatasetOrCohortToLibraryListItemsAPI(items);
2871
- const namedItems = {
2872
- items: apiItems,
2873
- name: generateUniqueName(name ?? DEFAULT_LIST_NAME)
2874
- };
2875
- return await performLibraryOperation(()=>dataLibraryStoreAPI.addList(namedItems));
2876
- }, [
2877
- dataLibraryStoreAPI,
2878
- generateUniqueName,
2879
- performLibraryOperation
2880
- ]);
2881
- const updateListInDataLibrary = React.useCallback(async (payload)=>{
2882
- const flattened = flattenDataList(payload);
2883
- return await performLibraryOperation(()=>dataLibraryStoreAPI.updateList(payload.id, {
2884
- name: payload.name,
2885
- items: flattened.items
2886
- }), payload.id);
2887
- }, [
2888
- dataLibraryStoreAPI,
2889
- performLibraryOperation
2890
- ]);
2891
- const deleteListFromDataLibrary = React.useCallback(async (id)=>{
2892
- return await performLibraryOperation(()=>dataLibraryStoreAPI.deleteList(id));
2893
- }, [
2894
- dataLibraryStoreAPI,
2895
- performLibraryOperation
2896
- ]);
2897
- const clearLibrary = React.useCallback(async ()=>{
2898
- return await performLibraryOperation(()=>dataLibraryStoreAPI.clearLists());
2899
- }, [
2900
- dataLibraryStoreAPI,
2901
- performLibraryOperation
2902
- ]);
2903
- const setAllListsInDataLibrary = React.useCallback(async (data)=>{
2904
- const flattenedLists = data.map((x)=>flattenDataList(x));
2905
- return await performLibraryOperation(()=>dataLibraryStoreAPI.setAllLists(flattenedLists));
2906
- }, [
2907
- dataLibraryStoreAPI,
2908
- performLibraryOperation
2909
- ]);
2910
- const getDatalist = React.useCallback((id)=>{
2911
- if (id in lists) return lists[id];
2912
- setError({
2913
- isError: true,
2914
- status: 404,
2915
- message: `List not found. Returning empty list.`
2916
- });
2917
- return EMPTY_LIST;
2918
- }, [
2919
- lists
2920
- ]);
2921
- const setLoginState = React.useCallback((loggedIn)=>setIsLoggedIn(loggedIn), []);
2922
- const results = useDeepCompare.useDeepCompareMemo(()=>({
2923
- dataLibrary: lists,
2924
- isLoading,
2925
- isUpdating,
2926
- error,
2927
- addListToDataLibrary,
2928
- updateListInDataLibrary,
2929
- deleteListFromDataLibrary,
2930
- clearLibrary,
2931
- setAllListsInDataLibrary,
2932
- setLoginState,
2933
- getDatalist
2934
- }), [
2935
- addListToDataLibrary,
2936
- clearLibrary,
2937
- deleteListFromDataLibrary,
2938
- error,
2939
- getDatalist,
2940
- isLoading,
2941
- isUpdating,
2942
- lists,
2943
- setAllListsInDataLibrary,
2944
- setLoginState,
2945
- updateListInDataLibrary
2946
- ]);
2947
- return results;
2948
- };
2949
-
2950
- const isOperationWithField = (operation)=>{
2951
- return operation?.field !== undefined;
2952
- };
2953
- const isOperatorWithFieldAndArrayOfOperands = (operation)=>{
2954
- 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
2955
- ) {
2956
- const { operator } = operation.operator;
2957
- return operator === 'in' || operator === 'exclude' || operator === 'excludeifany';
2616
+ /**
2617
+ * Converts a filter name to a title,
2618
+ * For example files.input.experimental_strategy will get converted to Experimental Strategy
2619
+ * if sections == 2 then the output would be Input Experimental Strategy
2620
+ * @param fieldName input filter expected to be: string.firstpart_secondpart
2621
+ * @param sections number of "sections" string.string.string to got back from the end of the field
2622
+ */ const fieldNameToTitle = (fieldName, sections = 1)=>{
2623
+ if (fieldName in FieldNameOverrides) {
2624
+ return FieldNameOverrides[fieldName];
2958
2625
  }
2959
- return false;
2626
+ if (fieldName === undefined) return 'No Title';
2627
+ return fieldName.split('.').slice(-sections).map((s)=>s.split('_')).flat().map((word)=>COMMON_PREPOSITIONS.includes(word) ? word : capitalize(word)).join(' ');
2960
2628
  };
2961
- const extractFilterValue = (op)=>{
2962
- const valueExtractorHandler = new ValueExtractorHandler();
2963
- return handleOperation(valueExtractorHandler, op);
2629
+ /**
2630
+ * Extracts the index name from the field name
2631
+ * @param fieldName
2632
+ */ const extractIndexFromFullFieldName = (fieldName)=>fieldName.split('.')[0];
2633
+ /**
2634
+ * prepend the index name to the field name
2635
+ */ const prependIndexToFieldName = (fieldName, index)=>`${index}.${fieldName}`;
2636
+ /**
2637
+ * extract the field name from the index.field name
2638
+ */ const extractFieldNameFromFullFieldName = (fieldName)=>fieldName.split('.').slice(1).join('.');
2639
+ /**
2640
+ * extract the field name and the index from the index.field name returning as a tuple
2641
+ */ const extractIndexAndFieldNameFromFullFieldName = (fieldName)=>{
2642
+ const [index, ...rest] = fieldName.split('.');
2643
+ return [
2644
+ index,
2645
+ rest.join('.')
2646
+ ];
2964
2647
  };
2965
- const extractEnumFilterValue = (op)=>{
2966
- const enumValueExtractorHandler = new EnumValueExtractorHandler();
2967
- const results = handleOperation(enumValueExtractorHandler, op);
2968
- return results ?? [];
2648
+
2649
+ const { selectAll: selectAllCohorts, selectTotal: selectTotalCohorts, selectById: selectCohortById, selectIds: selectCohortIds } = cohortsAdapter.getSelectors((state)=>state.cohorts.cohortManager);
2650
+ /**
2651
+ * Internally used selector for the exported selectora
2652
+ * @param state
2653
+ */ const getCurrentCohortFromCoreState = (state)=>{
2654
+ return state.cohorts.cohortManager.currentCohortId;
2969
2655
  };
2970
- const assertNever = (x)=>{
2971
- throw Error(`Exhaustive comparison did not handle: ${x}`);
2656
+ const selectCohortFilters = (state)=>{
2657
+ const currentCohortId = getCurrentCohortFromCoreState(state);
2658
+ return state.cohorts.cohortManager.entities[currentCohortId]?.filters;
2972
2659
  };
2973
- const handleOperation = (handler, op)=>{
2974
- switch(op.operator){
2975
- case '=':
2976
- return handler.handleEquals(op);
2977
- case '!=':
2978
- return handler.handleNotEquals(op);
2979
- case '<':
2980
- return handler.handleLessThan(op);
2981
- case '<=':
2982
- return handler.handleLessThanOrEquals(op);
2983
- case '>':
2984
- return handler.handleGreaterThan(op);
2985
- case '>=':
2986
- return handler.handleGreaterThanOrEquals(op);
2987
- case 'and':
2988
- return handler.handleIntersection(op);
2989
- case 'or':
2990
- return handler.handleUnion(op);
2991
- case 'nested':
2992
- return handler.handleNestedFilter(op);
2993
- case 'in':
2994
- case 'includes':
2995
- return handler.handleIncludes(op);
2996
- case 'excludeifany':
2997
- return handler.handleExcludeIfAny(op);
2998
- case 'excludes':
2999
- return handler.handleExcludes(op);
3000
- case 'exists':
3001
- return handler.handleExists(op);
3002
- case 'missing':
3003
- return handler.handleMissing(op);
3004
- default:
3005
- return assertNever(op);
3006
- }
2660
+ const selectCurrentCohortFilters = (state)=>{
2661
+ const currentCohortId = getCurrentCohortFromCoreState(state);
2662
+ return state.cohorts.cohortManager.entities[currentCohortId]?.filters;
3007
2663
  };
3008
- /**
3009
- * Return true if a FilterSet's root value is an empty object
3010
- * @param fs - FilterSet to test
3011
- */ const isFilterEmpty = (fs)=>lodash.isEqual({}, fs);
3012
- /**
3013
- * Type guard to check if an object is a GQLIntersection
3014
- * @param value - The value to check
3015
- * @returns True if the value is a GQLIntersection
3016
- */ const isGQLIntersection = (value)=>{
3017
- return typeof value === 'object' && value !== null && 'and' in value && Array.isArray(value.and);
2664
+ const selectCurrentCohortId = (state)=>{
2665
+ return state.cohorts.cohortManager.currentCohortId;
3018
2666
  };
2667
+ const selectCurrentCohort = (state)=>cohortSelectors.selectById(state, getCurrentCohortFromCoreState(state));
2668
+ const selectCurrentCohortName = (state)=>cohortSelectors.selectById(state, getCurrentCohortFromCoreState(state)).name;
3019
2669
  /**
3020
- * Type guard to check if an object is a GQLIntersection
3021
- * @param value - The value to check
3022
- * @returns True if the value is a GQLIntersection
3023
- */ const isGQLUnion = (value)=>{
3024
- return typeof value === 'object' && value !== null && 'or' in value && Array.isArray(value.or);
3025
- };
3026
- class ToGqlHandler {
3027
- constructor(){
3028
- this.handleEquals = (op)=>({
3029
- '=': {
3030
- [op.field]: op.operand
3031
- }
3032
- });
3033
- this.handleNotEquals = (op)=>({
3034
- '!=': {
3035
- [op.field]: op.operand
3036
- }
3037
- });
3038
- this.handleLessThan = (op)=>({
3039
- '<': {
3040
- [op.field]: op.operand
3041
- }
3042
- });
3043
- this.handleLessThanOrEquals = (op)=>({
3044
- '<=': {
3045
- [op.field]: op.operand
3046
- }
3047
- });
3048
- this.handleGreaterThan = (op)=>({
3049
- '>': {
3050
- [op.field]: op.operand
3051
- }
3052
- });
3053
- this.handleGreaterThanOrEquals = (op)=>({
3054
- '>=': {
3055
- [op.field]: op.operand
3056
- }
3057
- });
3058
- this.handleIncludes = (op)=>({
3059
- in: {
3060
- [op.field]: op.operands
3061
- }
3062
- });
3063
- this.handleExcludes = (op)=>({
3064
- exclude: {
3065
- [op.field]: op.operands
3066
- }
3067
- });
3068
- this.handleExcludeIfAny = (op)=>({
3069
- excludeifany: {
3070
- [op.field]: op.operands
3071
- }
3072
- });
3073
- this.handleIntersection = (op)=>({
3074
- and: op.operands.map((x)=>convertFilterToGqlFilter(x))
3075
- });
3076
- this.handleUnion = (op)=>({
3077
- or: op.operands.map((x)=>convertFilterToGqlFilter(x))
3078
- });
3079
- this.handleMissing = (op)=>({
3080
- is: {
3081
- [op.field]: 'MISSING'
3082
- }
3083
- });
3084
- this.handleExists = (op)=>({
3085
- not: {
3086
- [op.field]: op?.operand ?? null
3087
- }
3088
- });
3089
- this.handleNestedFilter = (op)=>{
3090
- const child = convertFilterToGqlFilter(op.operand);
3091
- return {
3092
- nested: {
3093
- path: op.path,
3094
- ...child
3095
- }
3096
- };
3097
- };
3098
- }
3099
- }
3100
- const convertFilterToGqlFilter = (filter)=>{
3101
- const handler = new ToGqlHandler();
3102
- return handleOperation(handler, filter);
2670
+ * Select a filter by its name from the current cohort. If the filter is not found
2671
+ * returns undefined.
2672
+ * @param state - Core
2673
+ * @param index which cohort index to select from
2674
+ * @param name name of the filter to select
2675
+ */ const selectIndexedFilterByName = (state, index, name)=>{
2676
+ return cohortSelectors.selectById(state, getCurrentCohortFromCoreState(state)).filters[index]?.root[name];
3103
2677
  };
3104
- const convertFilterSetToGqlFilter = (fs, toplevelOp = 'and')=>{
3105
- const fsKeys = Object.keys(fs.root);
3106
- // if no keys return undefined
3107
- if (fsKeys.length === 0) return {
3108
- and: []
3109
- };
3110
- return toplevelOp === 'and' ? {
3111
- and: fsKeys.map((key)=>convertFilterToGqlFilter(fs.root[key]))
3112
- } : {
3113
- or: fsKeys.map((key)=>convertFilterToGqlFilter(fs.root[key]))
3114
- };
2678
+ /**
2679
+ * Returns all the cohorts in the state
2680
+ * @param state - the CoreState
2681
+ *
2682
+ * @category Cohort
2683
+ * @category Selectors
2684
+ */ const selectAvailableCohorts = (state)=>cohortSelectors.selectAll(state);
2685
+ /**
2686
+ * Returns if the current cohort is modified
2687
+ * @param state - the CoreState
2688
+ * @category Cohort
2689
+ * @category Selectors
2690
+ * @hidden
2691
+ */ const selectCurrentCohortModified = (state)=>{
2692
+ const cohort = cohortSelectors.selectById(state, getCurrentCohortFromCoreState(state));
2693
+ return cohort?.modified;
3115
2694
  };
3116
2695
  /**
3117
- * Extract the operand values, if operands themselves have values, otherwise undefined.
3118
- */ class ValueExtractorHandler {
3119
- constructor(){
3120
- this.handleEquals = (op)=>op.operand;
3121
- this.handleNotEquals = (op)=>op.operand;
3122
- this.handleIncludes = (op)=>op.operands;
3123
- this.handleExcludes = (op)=>op.operands;
3124
- this.handleExcludeIfAny = (op)=>op.operands;
3125
- this.handleGreaterThanOrEquals = (op)=>op.operand;
3126
- this.handleGreaterThan = (op)=>op.operand;
3127
- this.handleLessThan = (op)=>op.operand;
3128
- this.handleLessThanOrEquals = (op)=>op.operand;
3129
- this.handleIntersection = (_arg)=>undefined;
3130
- this.handleUnion = (_)=>undefined;
3131
- this.handleNestedFilter = (_)=>undefined;
3132
- this.handleExists = (_)=>undefined;
3133
- this.handleMissing = (_)=>undefined;
3134
- }
3135
- }
2696
+ * Returns if the current cohort has been saved
2697
+ * @param state - the CoreState
2698
+ * @category Cohort
2699
+ * @category Selectors
2700
+ * @hidden
2701
+ */ const selectCurrentCohortSaved = (state)=>{
2702
+ const cohort = cohortSelectors.selectById(state, getCurrentCohortFromCoreState(state));
2703
+ return cohort?.saved;
2704
+ };
3136
2705
  /**
3137
- * Extract the operand values, if operands themselves have values, otherwise undefined.
3138
- */ class EnumValueExtractorHandler {
3139
- constructor(){
3140
- this.handleEquals = (_)=>undefined;
3141
- this.handleNotEquals = (_)=>undefined;
3142
- this.handleIncludes = (op)=>op.operands;
3143
- this.handleExcludes = (op)=>op.operands;
3144
- this.handleExcludeIfAny = (op)=>op.operands;
3145
- this.handleGreaterThanOrEquals = (_)=>undefined;
3146
- this.handleGreaterThan = (_)=>undefined;
3147
- this.handleLessThan = (_)=>undefined;
3148
- this.handleLessThanOrEquals = (_)=>undefined;
3149
- this.handleIntersection = (_)=>undefined;
3150
- this.handleUnion = (_)=>undefined;
3151
- this.handleNestedFilter = (op)=>{
3152
- return extractEnumFilterValue(op.operand);
3153
- };
3154
- this.handleExists = (_)=>undefined;
3155
- this.handleMissing = (_)=>undefined;
3156
- }
3157
- }
3158
- const filterSetToOperation = (fs)=>{
3159
- if (!fs) return undefined;
3160
- switch(fs.mode){
3161
- case 'and':
3162
- return Object.keys(fs.root).length == 0 ? undefined : {
3163
- operator: fs.mode,
3164
- operands: Object.keys(fs.root).map((k)=>{
3165
- return fs.root[k];
3166
- })
3167
- };
2706
+ * Select a filter from the index.
2707
+ * returns undefined.
2708
+ * @param state - Core
2709
+ * @param index which cohort index to select from
2710
+ */ const selectIndexFilters = (state, index)=>{
2711
+ const cohort = cohortSelectors.selectById(state, getCurrentCohortFromCoreState(state));
2712
+ if (!cohort) {
2713
+ console.error('No Cohort Defined');
3168
2714
  }
3169
- return undefined;
2715
+ return cohort?.filters?.[index] ?? EmptyFilterSet;
3170
2716
  };
3171
2717
 
3172
- const isFilterSet = (input)=>{
3173
- if (typeof input !== 'object' || input === null) {
2718
+ const isFileItem = (item)=>{
2719
+ return item && 'guid' in item;
2720
+ };
2721
+ const isAdditionalDataItem = (item)=>{
2722
+ return item.itemType === 'AdditionalData'; // TODO resolve this with type from the api
2723
+ };
2724
+ // Type guard for CohortItem
2725
+ const isCohortItem = (item)=>{
2726
+ return item && 'data' in item && 'schemaVersion' in item && item.itemType === 'Gen3GraphQL';
2727
+ };
2728
+ // Type guard for DatalistAPI
2729
+ const isDatalistAPI = (value)=>{
2730
+ if (typeof value !== 'object' || value === null) {
3174
2731
  return false;
3175
2732
  }
3176
- const { root, mode } = input;
3177
- if (typeof root !== 'object' || root === null) {
2733
+ const data = value;
2734
+ // Check required properties in DataItemBaseData
2735
+ if (typeof data.name !== 'string' || typeof data.created_time !== 'string' || typeof data.updated_time !== 'string' || typeof data.version !== 'number' || typeof data.authz !== 'object' || data.authz === null || !Array.isArray(data.authz.authz)) {
3178
2736
  return false;
3179
2737
  }
3180
- if (![
3181
- 'and',
3182
- 'or'
3183
- ].includes(mode)) {
2738
+ // Check required properties in DatalistAsItems
2739
+ if (typeof data.items !== 'object' || data.items === null || typeof data.items !== 'object') {
3184
2740
  return false;
3185
2741
  }
3186
2742
  return true;
3187
2743
  };
3188
- function isUnion(value) {
3189
- return typeof value === 'object' && value !== null && value.operator === 'or' && Array.isArray(value.operands);
3190
- }
3191
- function isIntersection(value) {
3192
- return typeof value === 'object' && value !== null && value.operator === 'and' && Array.isArray(value.operands);
3193
- }
3194
- const isOperandsType = (operation)=>{
3195
- return operation?.operands !== undefined;
2744
+ /**
2745
+ * Type guard for DataLibraryAPIResponse
2746
+ */ const isDataLibraryAPIResponse = (obj)=>{
2747
+ return typeof obj === 'object' && obj !== null && 'lists' in obj && typeof obj.lists === 'object';
3196
2748
  };
2749
+ var DataLibraryStoreMode = /*#__PURE__*/ function(DataLibraryStoreMode) {
2750
+ DataLibraryStoreMode["ApiOnly"] = "apiOnly";
2751
+ DataLibraryStoreMode["ApiAndLocal"] = "apiAndLocal";
2752
+ DataLibraryStoreMode["LocalOnly"] = "localOnly";
2753
+ return DataLibraryStoreMode;
2754
+ }({});
3197
2755
 
3198
- const FieldNameOverrides = {};
3199
- const COMMON_PREPOSITIONS = [
3200
- 'a',
3201
- 'an',
3202
- 'and',
3203
- 'at',
3204
- 'but',
3205
- 'by',
3206
- 'for',
3207
- 'in',
3208
- 'is',
3209
- 'nor',
3210
- 'of',
3211
- 'on',
3212
- 'or',
3213
- 'out',
3214
- 'so',
3215
- 'the',
3216
- 'to',
3217
- 'up',
3218
- 'yet'
3219
- ];
3220
- const capitalize = (s)=>s.length > 0 ? s[0].toUpperCase() + s.slice(1) : '';
3221
- const trimFirstFieldNameToTitle = (fieldName, trim = false)=>{
3222
- if (trim) {
3223
- const source = fieldName.slice(fieldName.indexOf('.') + 1);
3224
- return fieldNameToTitle(source ? source : fieldName, 0);
3225
- }
3226
- return fieldNameToTitle(fieldName);
3227
- };
3228
- /**
3229
- * Converts a filter name to a title,
3230
- * For example files.input.experimental_strategy will get converted to Experimental Strategy
3231
- * if sections == 2 then the output would be Input Experimental Strategy
3232
- * @param fieldName input filter expected to be: string.firstpart_secondpart
3233
- * @param sections number of "sections" string.string.string to got back from the end of the field
3234
- */ const fieldNameToTitle = (fieldName, sections = 1)=>{
3235
- if (fieldName in FieldNameOverrides) {
3236
- return FieldNameOverrides[fieldName];
2756
+ const processItem = (id, data)=>{
2757
+ if (data?.type === 'AdditionalData') {
2758
+ return {
2759
+ name: data.name,
2760
+ itemType: 'AdditionalData',
2761
+ description: data?.description,
2762
+ documentationUrl: data?.documentationUrl,
2763
+ url: data?.url
2764
+ };
3237
2765
  }
3238
- if (fieldName === undefined) return 'No Title';
3239
- return fieldName.split('.').slice(-sections).map((s)=>s.split('_')).flat().map((word)=>COMMON_PREPOSITIONS.includes(word) ? word : capitalize(word)).join(' ');
2766
+ return {
2767
+ ...data,
2768
+ itemType: 'Data',
2769
+ guid: data.id,
2770
+ id: id
2771
+ };
2772
+ };
2773
+ const buildListItemsGroupedByDataset = (listData)=>{
2774
+ const items = Object.entries(listData).reduce((acc, [id, data])=>{
2775
+ if (data?.type === 'Gen3GraphQL') {
2776
+ const cohortData = data;
2777
+ acc[id] = {
2778
+ itemType: 'Gen3GraphQL',
2779
+ id: data.guid,
2780
+ schemaVersion: cohortData.schema_version,
2781
+ data: cohortData.data,
2782
+ name: data.name,
2783
+ index: cohortData.index
2784
+ };
2785
+ } else {
2786
+ // Dataset
2787
+ if (!(data?.dataset_guid && data.dataset_guid in acc)) {
2788
+ acc[data.dataset_guid] = {
2789
+ id: data.dataset_guid,
2790
+ name: '',
2791
+ members: {
2792
+ [id]: processItem(id, data)
2793
+ }
2794
+ };
2795
+ } else {
2796
+ acc[data.dataset_guid].members[id] = processItem(id, data);
2797
+ }
2798
+ }
2799
+ return acc;
2800
+ }, {});
2801
+ return items;
2802
+ };
2803
+ const BuildList = (listId, listData)=>{
2804
+ if (!Object.keys(listData).includes('items')) return undefined;
2805
+ const items = buildListItemsGroupedByDataset(listData?.items ?? {});
2806
+ return {
2807
+ items: items,
2808
+ version: listData?.version ?? 0,
2809
+ created_time: listData?.created_time,
2810
+ updated_time: listData?.updated_time,
2811
+ name: listData?.name ?? listId,
2812
+ id: listId,
2813
+ authz: listData?.authz
2814
+ };
3240
2815
  };
3241
2816
  /**
3242
- * Extracts the index name from the field name
3243
- * @param fieldName
3244
- */ const extractIndexFromFullFieldName = (fieldName)=>fieldName.split('.')[0];
3245
- /**
3246
- * prepend the index name to the field name
3247
- */ const prependIndexToFieldName = (fieldName, index)=>`${index}.${fieldName}`;
2817
+ * Constructs a `DataLibrary` object by transforming the input `DataLibraryAPIResponse`.
2818
+ *
2819
+ * This function takes an API response containing lists and processes each list entry.
2820
+ * It uses `BuildList` to build individual list objects for each entry in the provided data.
2821
+ * The resulting lists are accumulated and structured into a `DataLibrary` object. which
2822
+ * groups File Object by dataset_guid.
2823
+ *
2824
+ * @param {DataLibraryAPIResponse} data - The API response containing the lists to process.
2825
+ * @returns {DataLibrary} A structured `DataLibrary` object containing the processed lists.
2826
+ */ const BuildLists = (data)=>{
2827
+ return Object.entries(data?.lists).reduce((acc, [listId, listData])=>{
2828
+ const list = BuildList(listId, listData);
2829
+ if (list) acc[listId] = list;
2830
+ return acc;
2831
+ }, {});
2832
+ };
3248
2833
  /**
3249
- * extract the field name from the index.field name
3250
- */ const extractFieldNameFromFullFieldName = (fieldName)=>fieldName.split('.').slice(1).join('.');
2834
+ * Calculates the total number of items within a DataList object.
2835
+ *
2836
+ * @param {DataList} dataList - The DataList object to count items from.
2837
+ * @return {number} The total number of items in the DataList.
2838
+ */ const getNumberOfItemsInDatalist = (dataList)=>{
2839
+ if (!dataList?.items) return 0;
2840
+ return Object.values(dataList.items).reduce((count, item)=>{
2841
+ if (isCohortItem(item)) {
2842
+ return count + 1;
2843
+ } else {
2844
+ return count + Object.values(item?.members ?? {}).reduce((fileCount, x)=>{
2845
+ if (isFileItem(x)) {
2846
+ return fileCount + 1;
2847
+ }
2848
+ return fileCount;
2849
+ }, 0);
2850
+ }
2851
+ }, 0);
2852
+ };
2853
+ const flattenDataList = (dataList)=>{
2854
+ // convert datalist into user-data-library API for for updating.
2855
+ const items = Object.entries(dataList.items).reduce((acc, [id, value])=>{
2856
+ if (isCohortItem(value)) {
2857
+ acc[id] = value;
2858
+ } else {
2859
+ return {
2860
+ ...acc,
2861
+ ...value.members
2862
+ }; // TODO: might need to convert this to the API version
2863
+ }
2864
+ return acc;
2865
+ }, {});
2866
+ return {
2867
+ name: dataList.name,
2868
+ items: items
2869
+ };
2870
+ };
2871
+ const convertDatasetOrCohortToLibraryListItemsAPI = (list)=>{
2872
+ const result = {};
2873
+ // Iterate through each entry in the DatasetOrCohort object
2874
+ Object.entries(list).forEach(([datasetId, item])=>{
2875
+ if (isCohortItem(item)) {
2876
+ // Handle cohort items
2877
+ result[datasetId] = {
2878
+ itemType: 'Gen3GraphQL',
2879
+ id: item.id,
2880
+ schemaVersion: item.schemaVersion,
2881
+ data: item.data,
2882
+ name: item.name,
2883
+ index: item.index
2884
+ };
2885
+ } else {
2886
+ // Handle dataset items
2887
+ const members = item.members || {};
2888
+ // Process each member of the dataset
2889
+ Object.entries(members).forEach(([memberId, memberData])=>{
2890
+ if (isFileItem(memberData)) {
2891
+ result[memberId] = {
2892
+ ...memberData.guid && {
2893
+ guid: memberData.guid
2894
+ },
2895
+ ...memberData.name && {
2896
+ name: memberData.name
2897
+ },
2898
+ ...memberData.name && {
2899
+ name: memberData.name
2900
+ },
2901
+ ...memberData.description && {
2902
+ description: memberData.description
2903
+ },
2904
+ ...memberData.type && {
2905
+ type: memberData.type
2906
+ },
2907
+ dataset_guid: datasetId
2908
+ };
2909
+ } else if (memberData.itemType === 'AdditionalData') {
2910
+ // Handle additional data items
2911
+ result[memberId] = {
2912
+ itemType: 'AdditionalData',
2913
+ name: memberData.name,
2914
+ description: memberData.description,
2915
+ documentationUrl: memberData.documentationUrl,
2916
+ url: memberData.url,
2917
+ dataset_guid: datasetId
2918
+ };
2919
+ }
2920
+ });
2921
+ }
2922
+ });
2923
+ return result;
2924
+ };
2925
+ const convertDataLibraryToDataLibraryAPI = (dataLibrary)=>{
2926
+ const result = {};
2927
+ Object.entries(dataLibrary).forEach(([listId, list])=>{
2928
+ result[listId] = {
2929
+ name: list.name,
2930
+ items: convertDatasetOrCohortToLibraryListItemsAPI(list.items),
2931
+ version: list.version,
2932
+ created_time: list.created_time,
2933
+ updated_time: list.updated_time,
2934
+ authz: list.authz
2935
+ };
2936
+ });
2937
+ return result;
2938
+ };
2939
+ const extractIndexFromDataLibraryCohort = (query)=>{
2940
+ try {
2941
+ const parsedQuery = graphql.parse(query['query']);
2942
+ const aggregationField = parsedQuery.definitions.filter((def)=>def.kind === 'OperationDefinition').flatMap((def)=>def.selectionSet.selections).find((sel)=>sel.kind === 'Field' && sel.name.value === '_aggregation');
2943
+ if (aggregationField && 'selectionSet' in aggregationField) {
2944
+ const indexField = aggregationField?.selectionSet?.selections.find((sel)=>sel.kind === 'Field');
2945
+ return indexField ? indexField.name.value : null;
2946
+ }
2947
+ } catch (error) {
2948
+ console.error('Invalid GraphQL query:', error);
2949
+ }
2950
+ return null;
2951
+ };
3251
2952
  /**
3252
- * extract the field name and the index from the index.field name returning as a tuple
3253
- */ const extractIndexAndFieldNameFromFullFieldName = (fieldName)=>{
3254
- const [index, ...rest] = fieldName.split('.');
3255
- return [
3256
- index,
3257
- rest.join('.')
3258
- ];
2953
+ * Takes a list of file items from anb array of manifest entries
2954
+ * and creates an Object of Files grouped by their dataset guid, which is
2955
+ * used to add these to a Data Library List
2956
+ * @param data
2957
+ * @param dataFieldMapping
2958
+ * @constructor
2959
+ */ const extractFileDatasetsInRecords = (data, dataFieldMapping)=>{
2960
+ const items = data.reduce((acc, resource)=>{
2961
+ const dataObjects = resource[dataFieldMapping.dataObjectField];
2962
+ // Check if dataObjects exists and is an array
2963
+ if (!dataObjects || !Array.isArray(dataObjects)) {
2964
+ return acc;
2965
+ }
2966
+ const datasetId = resource[dataFieldMapping.datasetIdField]; // Note: typo still preserved
2967
+ if (datasetId === undefined) {
2968
+ return acc; // Skip if dataset ID is missing
2969
+ }
2970
+ const datafiles = dataObjects.reduce((dataAcc, dataObject)=>{
2971
+ const fileId = dataObject[dataFieldMapping.dataObjectIdField];
2972
+ // Skip items without a valid ID
2973
+ if (typeof fileId !== 'string' || !fileId) {
2974
+ return dataAcc;
2975
+ }
2976
+ const name = dataObject[dataFieldMapping?.dataObjectNameField ?? 'name'] ?? 'No Name';
2977
+ const size = dataObject[dataFieldMapping?.dataObjectSizeField ?? 'size'];
2978
+ let sizeString = 'N/A';
2979
+ if (typeof size === 'number') {
2980
+ sizeString = size.toString();
2981
+ }
2982
+ if (typeof size === 'string') {
2983
+ sizeString = size;
2984
+ }
2985
+ const md5Sum = dataObject[dataFieldMapping?.dataObjectMd5sumField ?? 'md5sum'] ?? 'N/A';
2986
+ const url = dataObject[dataFieldMapping?.dataObjectUrlField ?? 'url'] ?? 'N/A';
2987
+ let fileType = 'GA4GH_DRS';
2988
+ if (dataFieldMapping?.dataObjectFileTypeValue) fileType = dataFieldMapping.dataObjectFileTypeValue;
2989
+ if (dataFieldMapping?.dataObjectFileTypeField) fileType = dataObject[dataFieldMapping?.dataObjectFileTypeField];
2990
+ return {
2991
+ ...dataAcc,
2992
+ [fileId]: {
2993
+ dataset_guid: datasetId,
2994
+ id: fileId,
2995
+ guid: fileId,
2996
+ itemType: 'Data',
2997
+ name: name,
2998
+ size: sizeString,
2999
+ md5sum: md5Sum,
3000
+ type: fileType,
3001
+ url: url
3002
+ }
3003
+ };
3004
+ }, {});
3005
+ return {
3006
+ ...acc,
3007
+ ...datafiles
3008
+ };
3009
+ }, {});
3010
+ return items;
3011
+ };
3012
+
3013
+ const DATABASE_NAME = 'Gen3DataLibrary';
3014
+ const STORE_NAME = 'DataLibraryLists';
3015
+ class LocalStorageService {
3016
+ getDb() {
3017
+ return idb.openDB(DATABASE_NAME, 1, {
3018
+ // TODO add more complete upgrade
3019
+ upgrade (db) {
3020
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
3021
+ db.createObjectStore(STORE_NAME, {
3022
+ keyPath: 'id'
3023
+ });
3024
+ }
3025
+ }
3026
+ });
3027
+ }
3028
+ async getList(id) {
3029
+ const db = await this.getDb();
3030
+ const tx = db.transaction(STORE_NAME, 'readonly');
3031
+ const store = tx.objectStore(STORE_NAME);
3032
+ const lists = await store.get(id);
3033
+ if (lists) {
3034
+ return {
3035
+ status: 200,
3036
+ message: 'success',
3037
+ lists: {
3038
+ [id]: {
3039
+ id: id,
3040
+ ...lists,
3041
+ items: lists.items
3042
+ }
3043
+ }
3044
+ };
3045
+ } else {
3046
+ return {
3047
+ isError: true,
3048
+ status: 500,
3049
+ message: `${id} does not exist`
3050
+ };
3051
+ }
3052
+ }
3053
+ async getLists() {
3054
+ const db = await this.getDb();
3055
+ const tx = db.transaction(STORE_NAME, 'readonly');
3056
+ const store = tx.objectStore(STORE_NAME);
3057
+ const lists = await store.getAll();
3058
+ if (!lists) {
3059
+ return {
3060
+ isError: true,
3061
+ status: 500,
3062
+ message: 'no lists returned'
3063
+ };
3064
+ }
3065
+ const listMap = lists.reduce((acc, x)=>{
3066
+ const { id } = x;
3067
+ acc[id] = x;
3068
+ return acc;
3069
+ }, {});
3070
+ const datalists = BuildLists({
3071
+ lists: listMap
3072
+ });
3073
+ return {
3074
+ status: 200,
3075
+ message: 'success',
3076
+ lists: datalists
3077
+ };
3078
+ }
3079
+ async addList(list) {
3080
+ const timestamp = getTimestamp();
3081
+ try {
3082
+ const db = await this.getDb();
3083
+ const tx = db.transaction(STORE_NAME, 'readwrite');
3084
+ const id = toolkit.nanoid(); // Create an id for the list
3085
+ tx.objectStore(STORE_NAME).put({
3086
+ id,
3087
+ version: 0,
3088
+ items: list?.items ?? {},
3089
+ creator: '{{subject_id}}',
3090
+ authz: {
3091
+ version: 0,
3092
+ authz: [
3093
+ `/users/{{subject_id}}/user-library/lists/${id}`
3094
+ ]
3095
+ },
3096
+ name: list?.name ?? 'New List',
3097
+ created_time: timestamp,
3098
+ updated_time: timestamp
3099
+ });
3100
+ await tx.done;
3101
+ return {
3102
+ status: 200,
3103
+ message: 'list added'
3104
+ };
3105
+ } catch (_error) {
3106
+ return {
3107
+ isError: true,
3108
+ status: 500,
3109
+ message: `unable to add list ${list?.name ?? 'New List'}`
3110
+ };
3111
+ }
3112
+ }
3113
+ async updateList(id, update) {
3114
+ const { name, items } = update;
3115
+ try {
3116
+ const db = await this.getDb();
3117
+ const tx = db.transaction(STORE_NAME, 'readwrite');
3118
+ const store = tx.objectStore(STORE_NAME);
3119
+ const listData = await store.get(id);
3120
+ if (!listData) {
3121
+ throw new Error(`List ${id} does not exist`);
3122
+ }
3123
+ const timestamp = getTimestamp();
3124
+ const version = listData.version ? listData.version + 1 : 0;
3125
+ const updated = {
3126
+ ...listData,
3127
+ ...{
3128
+ name,
3129
+ items
3130
+ },
3131
+ version: version,
3132
+ updated_time: timestamp,
3133
+ created_time: listData.created_time
3134
+ };
3135
+ store.put(updated);
3136
+ await tx.done;
3137
+ return {
3138
+ status: 200,
3139
+ message: 'success'
3140
+ };
3141
+ } catch (error) {
3142
+ let errorMessage = 'An unknown error occurred';
3143
+ if (error instanceof Error) {
3144
+ errorMessage = error.message;
3145
+ }
3146
+ return {
3147
+ isError: true,
3148
+ status: 500,
3149
+ message: `Unable to update list: ${id}. Error: ${errorMessage}`
3150
+ };
3151
+ }
3152
+ }
3153
+ async deleteList(id) {
3154
+ try {
3155
+ const db = await this.getDb();
3156
+ const tx = db.transaction(STORE_NAME, 'readwrite');
3157
+ const store = tx.objectStore(STORE_NAME);
3158
+ const item = await store.get(id);
3159
+ if (!item) {
3160
+ throw new Error(`List ${id} does not exist`);
3161
+ }
3162
+ store.delete(id);
3163
+ await tx.done;
3164
+ return {
3165
+ status: 200,
3166
+ message: `${id} deleted`
3167
+ };
3168
+ } catch (error) {
3169
+ const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
3170
+ return {
3171
+ isError: true,
3172
+ status: 500,
3173
+ message: `Unable to delete list: ${id}. Error: ${errorMessage}`
3174
+ };
3175
+ }
3176
+ }
3177
+ async clearLists() {
3178
+ try {
3179
+ const db = await this.getDb();
3180
+ const tx = db.transaction(STORE_NAME, 'readwrite');
3181
+ tx.objectStore(STORE_NAME).clear();
3182
+ await tx.done;
3183
+ return {
3184
+ status: 200,
3185
+ message: 'list cleared'
3186
+ };
3187
+ } catch (error) {
3188
+ const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
3189
+ return {
3190
+ isError: true,
3191
+ status: 500,
3192
+ message: `Unable to clear library. Error: ${errorMessage}`
3193
+ };
3194
+ }
3195
+ }
3196
+ async cacheLists(data) {
3197
+ if (!data || typeof data !== 'object') {
3198
+ return {
3199
+ isError: true,
3200
+ status: 500,
3201
+ message: 'Invalid or missing lists property in request'
3202
+ };
3203
+ }
3204
+ const allLists = Object.entries(data).reduce((acc, [id, x])=>{
3205
+ if (!isDatalistAPI(x)) return acc;
3206
+ acc[id] = {
3207
+ ...x
3208
+ };
3209
+ return acc;
3210
+ }, {});
3211
+ try {
3212
+ const db = await this.getDb();
3213
+ const tx = db.transaction(STORE_NAME, 'readwrite');
3214
+ for (const [id, list] of Object.entries(allLists)){
3215
+ tx.objectStore(STORE_NAME).put({
3216
+ id,
3217
+ ...list
3218
+ });
3219
+ }
3220
+ await tx.done;
3221
+ return {
3222
+ status: 200,
3223
+ message: 'success'
3224
+ };
3225
+ } catch (error) {
3226
+ const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
3227
+ return {
3228
+ isError: true,
3229
+ status: 200,
3230
+ message: `unable to cache library to local storage. Error: ${errorMessage}`
3231
+ };
3232
+ }
3233
+ }
3234
+ async cacheList(id, data) {
3235
+ if (!data || typeof data !== 'object') {
3236
+ return {
3237
+ isError: true,
3238
+ status: 500,
3239
+ message: 'Invalid or missing lists property in request'
3240
+ };
3241
+ }
3242
+ try {
3243
+ const db = await this.getDb();
3244
+ const tx = db.transaction(STORE_NAME, 'readwrite');
3245
+ tx.objectStore(STORE_NAME).put({
3246
+ id: id,
3247
+ ...data
3248
+ });
3249
+ await tx.done;
3250
+ return {
3251
+ status: 200,
3252
+ message: 'success'
3253
+ };
3254
+ } catch (error) {
3255
+ const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
3256
+ return {
3257
+ isError: true,
3258
+ status: 500,
3259
+ message: `unable to clear library. Error: ${errorMessage}`
3260
+ };
3261
+ }
3262
+ }
3263
+ constructor(){
3264
+ this.setAllLists = async (data)=>{
3265
+ const timestamp = getTimestamp();
3266
+ const allLists = data.reduce((acc, x)=>{
3267
+ if (!isJSONObject(x)) return acc;
3268
+ const id = toolkit.nanoid(10);
3269
+ acc[id] = {
3270
+ ...x,
3271
+ version: 0,
3272
+ created_time: timestamp,
3273
+ updated_time: timestamp,
3274
+ creator: '{{subject_id}}',
3275
+ authz: {
3276
+ version: 0,
3277
+ authz: [
3278
+ `/users/{{subject_id}}/user-library/lists/${id}`
3279
+ ]
3280
+ }
3281
+ };
3282
+ return acc;
3283
+ }, {});
3284
+ try {
3285
+ const db = await this.getDb();
3286
+ const tx = db.transaction(STORE_NAME, 'readwrite');
3287
+ for (const [id, list] of Object.entries(allLists)){
3288
+ tx.objectStore(STORE_NAME).put({
3289
+ id,
3290
+ ...list
3291
+ });
3292
+ }
3293
+ await tx.done;
3294
+ return {
3295
+ status: 200,
3296
+ message: 'success'
3297
+ };
3298
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
3299
+ } catch (_error) {
3300
+ return {
3301
+ isError: true,
3302
+ status: 500,
3303
+ message: 'unable to add lists'
3304
+ };
3305
+ }
3306
+ };
3307
+ }
3308
+ }
3309
+
3310
+ const fetchFromDataLibraryAPI = async (url, method = HttpMethod.GET, body = undefined)=>{
3311
+ try {
3312
+ return {
3313
+ data: await fetchJSONDataFromURL(url, true, method, body)
3314
+ };
3315
+ } catch (error) {
3316
+ if (error instanceof HTTPError) {
3317
+ return {
3318
+ error: {
3319
+ status: error.status,
3320
+ message: HTTPErrorMessages[error.status] || error.responseData?.message || 'No HTTP Error Message'
3321
+ }
3322
+ };
3323
+ } else {
3324
+ return {
3325
+ error: {
3326
+ status: 500,
3327
+ message: 'Unknown Error'
3328
+ }
3329
+ };
3330
+ }
3331
+ }
3332
+ };
3333
+ const responseFromMutation = (responseReceived)=>{
3334
+ if (responseReceived.error) {
3335
+ return {
3336
+ isError: true,
3337
+ ...responseReceived.error
3338
+ };
3339
+ }
3340
+ return {
3341
+ lists: responseReceived.data,
3342
+ message: 'success',
3343
+ status: 200
3344
+ };
3345
+ };
3346
+ class APIStorageService {
3347
+ constructor(apiBaseUrl = `${GEN3_DATA_LIBRARY_API}`){
3348
+ this.pendingRequests = new Map();
3349
+ this.apiBaseUrl = apiBaseUrl;
3350
+ }
3351
+ async dedupedRequest(url, method = HttpMethod.GET, body = undefined) {
3352
+ // Create a unique key for this request
3353
+ const requestKey = `${method}:${url}:${body ? JSON.stringify(body) : ''}`;
3354
+ // If this exact request is already in progress, return the pending promise
3355
+ if (this.pendingRequests.has(requestKey)) {
3356
+ return this.pendingRequests.get(requestKey);
3357
+ }
3358
+ // Otherwise, make the request and store the promise
3359
+ const requestPromise = fetchFromDataLibraryAPI(url, method, body);
3360
+ this.pendingRequests.set(requestKey, requestPromise);
3361
+ try {
3362
+ // Wait for the request to complete
3363
+ const result = await requestPromise;
3364
+ return result;
3365
+ } finally{
3366
+ // Remove the request from pending requests after it completes
3367
+ this.pendingRequests.delete(requestKey);
3368
+ }
3369
+ }
3370
+ async getLists() {
3371
+ const { data, error } = await this.dedupedRequest(`${this.apiBaseUrl}`);
3372
+ if (error) {
3373
+ return {
3374
+ isError: true,
3375
+ ...error
3376
+ };
3377
+ }
3378
+ if (data && isDataLibraryAPIResponse(data)) {
3379
+ const datalists = BuildLists(data);
3380
+ return {
3381
+ lists: datalists,
3382
+ status: 200,
3383
+ message: 'success'
3384
+ };
3385
+ }
3386
+ return {
3387
+ lists: {},
3388
+ status: 200,
3389
+ message: 'no list returned'
3390
+ };
3391
+ }
3392
+ async addList(list) {
3393
+ const response = await fetchFromDataLibraryAPI(`${this.apiBaseUrl}`, HttpMethod.PUT, JSON.stringify({
3394
+ lists: [
3395
+ list
3396
+ ]
3397
+ }));
3398
+ return responseFromMutation(response);
3399
+ }
3400
+ async updateList(id, list) {
3401
+ const response = await fetchFromDataLibraryAPI(`${this.apiBaseUrl}/${id}`, HttpMethod.PUT, JSON.stringify(list));
3402
+ return responseFromMutation(response);
3403
+ }
3404
+ async deleteList(id) {
3405
+ const response = await fetchFromDataLibraryAPI(`${this.apiBaseUrl}/${id}`, HttpMethod.DELETE);
3406
+ return responseFromMutation(response);
3407
+ }
3408
+ async clearLists() {
3409
+ const response = await fetchFromDataLibraryAPI(this.apiBaseUrl, HttpMethod.DELETE);
3410
+ return responseFromMutation(response);
3411
+ }
3412
+ async setAllLists(lists) {
3413
+ const response = await fetchFromDataLibraryAPI(this.apiBaseUrl, HttpMethod.POST, JSON.stringify({
3414
+ lists: Object.values(lists)
3415
+ }));
3416
+ return responseFromMutation(response);
3417
+ }
3418
+ async getList(id) {
3419
+ const { data, error } = await fetchFromDataLibraryAPI(`${this.apiBaseUrl}/${id}`);
3420
+ if (error) {
3421
+ return {
3422
+ isError: true,
3423
+ ...error
3424
+ };
3425
+ }
3426
+ if (isDataLibraryAPIResponse(data)) {
3427
+ const datalists = BuildLists(data);
3428
+ return {
3429
+ lists: datalists,
3430
+ status: 200,
3431
+ message: 'success'
3432
+ };
3433
+ }
3434
+ return {
3435
+ isError: true,
3436
+ status: 500,
3437
+ message: `Unknown error getting list ${id}`
3438
+ };
3439
+ }
3440
+ }
3441
+
3442
+ class CachedAPIService {
3443
+ constructor(){
3444
+ this.localStorageDataLibrary = new LocalStorageService(); // always update local storage
3445
+ this.apiDataLibrary = new APIStorageService();
3446
+ }
3447
+ async getLists() {
3448
+ // do a network request to get the library
3449
+ // get the remote list
3450
+ const apiResults = await this.apiDataLibrary.getLists();
3451
+ if (apiResults.isError) {
3452
+ return {
3453
+ ...apiResults,
3454
+ lists: undefined
3455
+ };
3456
+ }
3457
+ const dataLibrary = convertDataLibraryToDataLibraryAPI(apiResults?.lists ?? {});
3458
+ await this.localStorageDataLibrary.cacheLists(dataLibrary);
3459
+ return apiResults;
3460
+ }
3461
+ async getList(id) {
3462
+ return await this.localStorageDataLibrary.getList(id);
3463
+ }
3464
+ async getCachedLists(id) {
3465
+ return await this.localStorageDataLibrary.getList(id);
3466
+ }
3467
+ async setAllLists(lists) {
3468
+ const apiResults = await this.apiDataLibrary.setAllLists(lists);
3469
+ if (apiResults.isError) {
3470
+ return {
3471
+ ...apiResults,
3472
+ lists: undefined
3473
+ };
3474
+ }
3475
+ const dataLibrary = convertDataLibraryToDataLibraryAPI(apiResults?.lists ?? {});
3476
+ await this.localStorageDataLibrary.cacheLists(dataLibrary);
3477
+ return apiResults;
3478
+ }
3479
+ async addList(list) {
3480
+ // update the API list
3481
+ const apiResults = await this.apiDataLibrary.addList(list);
3482
+ if (apiResults.isError) {
3483
+ return {
3484
+ ...apiResults,
3485
+ lists: undefined
3486
+ };
3487
+ }
3488
+ const cacheResults = await this.localStorageDataLibrary.addList(list);
3489
+ return {
3490
+ ...cacheResults,
3491
+ lists: undefined
3492
+ };
3493
+ }
3494
+ async updateList(id, list) {
3495
+ const apiResults = await this.apiDataLibrary.updateList(id, list);
3496
+ if (apiResults.isError) {
3497
+ return {
3498
+ ...apiResults,
3499
+ lists: undefined
3500
+ };
3501
+ }
3502
+ return await this.localStorageDataLibrary.cacheList(id, apiResults.lists ?? {});
3503
+ }
3504
+ async deleteList(id) {
3505
+ const apiResults = await this.apiDataLibrary.deleteList(id);
3506
+ if (apiResults.isError) {
3507
+ return {
3508
+ ...apiResults,
3509
+ lists: undefined
3510
+ };
3511
+ }
3512
+ return await this.localStorageDataLibrary.deleteList(id);
3513
+ }
3514
+ async clearLists() {
3515
+ const apiResults = await this.apiDataLibrary.clearLists();
3516
+ if (apiResults.isError) {
3517
+ return {
3518
+ ...apiResults,
3519
+ lists: undefined
3520
+ };
3521
+ }
3522
+ return await this.localStorageDataLibrary.clearLists();
3523
+ }
3524
+ }
3525
+
3526
+ class DataLibraryStorageService {
3527
+ constructor(mode = DataLibraryStoreMode.ApiOnly){
3528
+ if (mode === DataLibraryStoreMode.ApiOnly) {
3529
+ this.storageService = new APIStorageService();
3530
+ } else if (mode === DataLibraryStoreMode.ApiAndLocal) this.storageService = new CachedAPIService();
3531
+ else this.storageService = new LocalStorageService();
3532
+ }
3533
+ async setStorageMode(mode) {
3534
+ if (mode === DataLibraryStoreMode.ApiOnly) {
3535
+ this.storageService = new APIStorageService();
3536
+ } else if (mode === DataLibraryStoreMode.ApiAndLocal) this.storageService = new CachedAPIService();
3537
+ else this.storageService = new LocalStorageService();
3538
+ }
3539
+ // private async syncApiAndLocal() {
3540
+ // const { lists: localData, isError: localError } =
3541
+ // await this.localStorageDataLibrary.getLists();
3542
+ // const { lists: apiData, isError: apiError } =
3543
+ // await this.apiDataLibrary.getLists();
3544
+ //
3545
+ // if (localError || apiError) {
3546
+ // return;
3547
+ // }
3548
+ //
3549
+ // const mergedData: Record<string, Datalist> = { ...localData };
3550
+ //
3551
+ // // First, update any existing items with newer versions from API
3552
+ // Object.values(apiData?.lists ?? {}).forEach((apiList) => {
3553
+ // const id = apiList.id as keyof DataLibrary;
3554
+ // const localList = localData?.[id];
3555
+ // if (
3556
+ // !localList ||
3557
+ // storage Date(apiList.updatedTime) > storage Date(localList.updatedTime)
3558
+ // ) {
3559
+ // mergedData[id] = apiList;
3560
+ // }
3561
+ // });
3562
+ //
3563
+ // // Push local-only changes to API
3564
+ // const syncPromises: Promise<any>[] = [];
3565
+ //
3566
+ // for (const [id, localList] of Object.entries(localData?.lists ?? {})) {
3567
+ // if (!apiData?.[id]) {
3568
+ // // This list exists locally but not in API, so push it to API
3569
+ // syncPromises.push(this.apiDataLibrary.addList(localList));
3570
+ // } else if (
3571
+ // storage Date(localList.updatedTime) > storage Date(apiData[id].updatedTime)
3572
+ // ) {
3573
+ // // Local list is newer than API, so update API
3574
+ // syncPromises.push(this.apiDataLibrary.updateList(localList));
3575
+ // }
3576
+ // }
3577
+ //
3578
+ // // Wait for all API operations to complete
3579
+ // await Promise.all(syncPromises);
3580
+ // await this.localStorageDataLibrary.cacheLists({ lists: mergedData });
3581
+ // }
3582
+ async getLists() {
3583
+ return await this.storageService.getLists();
3584
+ }
3585
+ async getList(id) {
3586
+ return await this.storageService.getList(id);
3587
+ }
3588
+ async getCachedLists(id) {
3589
+ return await this.storageService.getList(id);
3590
+ }
3591
+ async setAllLists(lists) {
3592
+ return await this.storageService.setAllLists(lists ?? {});
3593
+ }
3594
+ async addList(list) {
3595
+ return await this.storageService.addList(list);
3596
+ }
3597
+ async updateList(id, list) {
3598
+ return await this.storageService.updateList(id, list);
3599
+ }
3600
+ async deleteList(id) {
3601
+ return await this.storageService.deleteList(id);
3602
+ }
3603
+ async clearLists() {
3604
+ return await this.storageService.clearLists();
3605
+ }
3606
+ }
3607
+
3608
+ const EMPTY_LIST = {
3609
+ items: {},
3610
+ version: 0,
3611
+ created_time: 'not_set',
3612
+ updated_time: 'not_set',
3613
+ name: '',
3614
+ id: '',
3615
+ authz: {
3616
+ version: -1,
3617
+ authz: []
3618
+ }
3619
+ };
3620
+ const DEFAULT_LIST_NAME = 'List';
3621
+ const useDataLibrary = (options = {
3622
+ storageMode: DataLibraryStoreMode.ApiOnly
3623
+ })=>{
3624
+ // State management
3625
+ const [isLoggedIn, setIsLoggedIn] = React.useState(false);
3626
+ const [isLoading, setIsLoading] = React.useState(false);
3627
+ const [isUpdating, setIsUpdating] = React.useState(null);
3628
+ const [error, setError] = React.useState(null);
3629
+ const [lists, setLists] = React.useState({});
3630
+ // Refs
3631
+ const initialLoadRef = React.useRef(false);
3632
+ // Services
3633
+ const dataLibraryStoreAPI = React.useRef(new DataLibraryStorageService(options.storageMode)).current;
3634
+ const handleErrorOrSetLists = React.useCallback(async (error)=>{
3635
+ if (error.isError) {
3636
+ setError(error);
3637
+ } else {
3638
+ const getListResults = await dataLibraryStoreAPI.getLists();
3639
+ if (getListResults.isError) {
3640
+ setError(getListResults);
3641
+ } else {
3642
+ setLists(getListResults.lists ?? {});
3643
+ setError(null);
3644
+ }
3645
+ }
3646
+ }, [
3647
+ dataLibraryStoreAPI
3648
+ ]);
3649
+ const generateUniqueName = React.useCallback((baseName = DEFAULT_LIST_NAME)=>{
3650
+ let uniqueName = baseName;
3651
+ let counter = 1;
3652
+ const existingNames = Object.values(lists).map((x)=>x.name);
3653
+ while(existingNames.includes(uniqueName)){
3654
+ uniqueName = `${baseName} ${counter}`;
3655
+ counter++;
3656
+ }
3657
+ return uniqueName;
3658
+ }, [
3659
+ lists
3660
+ ]);
3661
+ const performLibraryOperation = React.useCallback(async (operation, updateId)=>{
3662
+ setError(null);
3663
+ if (updateId) {
3664
+ setIsUpdating(updateId);
3665
+ } else setIsLoading(true);
3666
+ const operationResults = await operation();
3667
+ await handleErrorOrSetLists(operationResults);
3668
+ if (updateId) setIsUpdating(null);
3669
+ else setIsLoading(false);
3670
+ return operationResults;
3671
+ }, [
3672
+ handleErrorOrSetLists
3673
+ ]);
3674
+ // Lifecycle effects
3675
+ React.useEffect(()=>{
3676
+ const initializeData = async ()=>{
3677
+ if (!initialLoadRef.current) {
3678
+ setError(null);
3679
+ setIsLoading(true);
3680
+ const results = await dataLibraryStoreAPI.getLists(); // get the initial lists
3681
+ if (results.isError) setError(results);
3682
+ else setLists(results.lists ?? {});
3683
+ setIsLoading(false);
3684
+ initialLoadRef.current = true;
3685
+ }
3686
+ };
3687
+ initializeData();
3688
+ }, [
3689
+ dataLibraryStoreAPI
3690
+ ]);
3691
+ React.useEffect(()=>{
3692
+ const handleLogin = async ()=>{
3693
+ // setIsLoading(true);
3694
+ // await dataLibraryStoreAPI.setUseAPI(options.requiresAPI && isLoggedIn);
3695
+ // setIsLoading(false);
3696
+ };
3697
+ handleLogin();
3698
+ }, [
3699
+ dataLibraryStoreAPI,
3700
+ isLoggedIn
3701
+ ]);
3702
+ // CRUD operations
3703
+ const addListToDataLibrary = React.useCallback(async (items, name)=>{
3704
+ const apiItems = convertDatasetOrCohortToLibraryListItemsAPI(items);
3705
+ const namedItems = {
3706
+ items: apiItems,
3707
+ name: generateUniqueName(name ?? DEFAULT_LIST_NAME)
3708
+ };
3709
+ return await performLibraryOperation(()=>dataLibraryStoreAPI.addList(namedItems));
3710
+ }, [
3711
+ dataLibraryStoreAPI,
3712
+ generateUniqueName,
3713
+ performLibraryOperation
3714
+ ]);
3715
+ const updateListInDataLibrary = React.useCallback(async (payload)=>{
3716
+ const flattened = flattenDataList(payload);
3717
+ return await performLibraryOperation(()=>dataLibraryStoreAPI.updateList(payload.id, {
3718
+ name: payload.name,
3719
+ items: flattened.items
3720
+ }), payload.id);
3721
+ }, [
3722
+ dataLibraryStoreAPI,
3723
+ performLibraryOperation
3724
+ ]);
3725
+ const deleteListFromDataLibrary = React.useCallback(async (id)=>{
3726
+ return await performLibraryOperation(()=>dataLibraryStoreAPI.deleteList(id));
3727
+ }, [
3728
+ dataLibraryStoreAPI,
3729
+ performLibraryOperation
3730
+ ]);
3731
+ const clearLibrary = React.useCallback(async ()=>{
3732
+ return await performLibraryOperation(()=>dataLibraryStoreAPI.clearLists());
3733
+ }, [
3734
+ dataLibraryStoreAPI,
3735
+ performLibraryOperation
3736
+ ]);
3737
+ const setAllListsInDataLibrary = React.useCallback(async (data)=>{
3738
+ const flattenedLists = data.map((x)=>flattenDataList(x));
3739
+ return await performLibraryOperation(()=>dataLibraryStoreAPI.setAllLists(flattenedLists));
3740
+ }, [
3741
+ dataLibraryStoreAPI,
3742
+ performLibraryOperation
3743
+ ]);
3744
+ const getDatalist = React.useCallback((id)=>{
3745
+ if (id in lists) return lists[id];
3746
+ setError({
3747
+ isError: true,
3748
+ status: 404,
3749
+ message: `List not found. Returning empty list.`
3750
+ });
3751
+ return EMPTY_LIST;
3752
+ }, [
3753
+ lists
3754
+ ]);
3755
+ const setLoginState = React.useCallback((loggedIn)=>setIsLoggedIn(loggedIn), []);
3756
+ const results = useDeepCompare.useDeepCompareMemo(()=>({
3757
+ dataLibrary: lists,
3758
+ isLoading,
3759
+ isUpdating,
3760
+ error,
3761
+ addListToDataLibrary,
3762
+ updateListInDataLibrary,
3763
+ deleteListFromDataLibrary,
3764
+ clearLibrary,
3765
+ setAllListsInDataLibrary,
3766
+ setLoginState,
3767
+ getDatalist
3768
+ }), [
3769
+ addListToDataLibrary,
3770
+ clearLibrary,
3771
+ deleteListFromDataLibrary,
3772
+ error,
3773
+ getDatalist,
3774
+ isLoading,
3775
+ isUpdating,
3776
+ lists,
3777
+ setAllListsInDataLibrary,
3778
+ setLoginState,
3779
+ updateListInDataLibrary
3780
+ ]);
3781
+ return results;
3259
3782
  };
3260
3783
 
3261
3784
  // using a random uuid v4 as the namespace
@@ -4922,9 +5445,12 @@ const coreCreateApi = react.buildCreateApi(react.coreModule(), react.reactHooksM
4922
5445
  }));
4923
5446
 
4924
5447
  exports.Accessibility = Accessibility;
5448
+ exports.CohortStorage = CohortStorage;
4925
5449
  exports.CoreProvider = CoreProvider;
4926
5450
  exports.DataLibraryStoreMode = DataLibraryStoreMode;
5451
+ exports.EmptyFilterSet = EmptyFilterSet;
4927
5452
  exports.EmptyWorkspaceStatusResponse = EmptyWorkspaceStatusResponse;
5453
+ exports.EnumValueExtractorHandler = EnumValueExtractorHandler;
4928
5454
  exports.GEN3_API = GEN3_API;
4929
5455
  exports.GEN3_AUTHZ_API = GEN3_AUTHZ_API;
4930
5456
  exports.GEN3_COMMONS_NAME = GEN3_COMMONS_NAME;
@@ -4947,8 +5473,10 @@ exports.Modals = Modals;
4947
5473
  exports.PodConditionType = PodConditionType;
4948
5474
  exports.PodStatus = PodStatus;
4949
5475
  exports.RequestedWorkspaceStatus = RequestedWorkspaceStatus;
5476
+ exports.ToGqlHandler = ToGqlHandler;
5477
+ exports.ValueExtractorHandler = ValueExtractorHandler;
4950
5478
  exports.WorkspaceStatus = WorkspaceStatus;
4951
- exports.addNewDefaultUnsavedCohort = addNewDefaultUnsavedCohort;
5479
+ exports.appendFilterToOperation = appendFilterToOperation;
4952
5480
  exports.buildGetAggregationQuery = buildGetAggregationQuery;
4953
5481
  exports.buildListItemsGroupedByDataset = buildListItemsGroupedByDataset;
4954
5482
  exports.calculatePercentageAsNumber = calculatePercentageAsNumber;
@@ -4958,6 +5486,7 @@ exports.clearCohortFilters = clearCohortFilters;
4958
5486
  exports.cohortReducer = cohortReducer;
4959
5487
  exports.convertFilterSetToGqlFilter = convertFilterSetToGqlFilter;
4960
5488
  exports.convertFilterToGqlFilter = convertFilterToGqlFilter;
5489
+ exports.convertGqlFilterToFilter = convertGqlFilterToFilter;
4961
5490
  exports.convertToHistogramDataAsStringKey = convertToHistogramDataAsStringKey;
4962
5491
  exports.convertToQueryString = convertToQueryString;
4963
5492
  exports.coreCreateApi = coreCreateApi;
@@ -4966,10 +5495,13 @@ exports.createAppApiForRTKQ = createAppApiForRTKQ;
4966
5495
  exports.createAppStore = createAppStore;
4967
5496
  exports.createGen3App = createGen3App;
4968
5497
  exports.createGen3AppWithOwnStore = createGen3AppWithOwnStore;
5498
+ exports.createNewCohort = createNewCohort;
4969
5499
  exports.createUseCoreDataHook = createUseCoreDataHook;
5500
+ exports.defaultCohortNameGenerator = defaultCohortNameGenerator;
4970
5501
  exports.downloadFromGuppyToBlob = downloadFromGuppyToBlob;
4971
5502
  exports.downloadJSONDataFromGuppy = downloadJSONDataFromGuppy;
4972
5503
  exports.drsHostnamesReducer = drsHostnamesReducer;
5504
+ exports.duplicateCohort = duplicateCohort;
4973
5505
  exports.extractEnumFilterValue = extractEnumFilterValue;
4974
5506
  exports.extractFieldNameFromFullFieldName = extractFieldNameFromFullFieldName;
4975
5507
  exports.extractFileDatasetsInRecords = extractFileDatasetsInRecords;
@@ -4985,6 +5517,7 @@ exports.fetchUserState = fetchUserState;
4985
5517
  exports.fieldNameToTitle = fieldNameToTitle;
4986
5518
  exports.filterSetToOperation = filterSetToOperation;
4987
5519
  exports.gen3Api = gen3Api;
5520
+ exports.generateUniqueName = generateUniqueName;
4988
5521
  exports.getCurrentTimestamp = getCurrentTimestamp;
4989
5522
  exports.getFederatedLoginStatus = getFederatedLoginStatus;
4990
5523
  exports.getGen3AppId = getGen3AppId;
@@ -4998,6 +5531,7 @@ exports.guppyAPISliceMiddleware = guppyAPISliceMiddleware;
4998
5531
  exports.guppyApi = guppyApi;
4999
5532
  exports.guppyApiReducer = guppyApiReducer;
5000
5533
  exports.guppyApiSliceReducerPath = guppyApiSliceReducerPath;
5534
+ exports.handleGqlOperation = handleGqlOperation;
5001
5535
  exports.handleOperation = handleOperation;
5002
5536
  exports.hideModal = hideModal;
5003
5537
  exports.histogramQueryStrForEachField = histogramQueryStrForEachField;
@@ -5026,10 +5560,12 @@ exports.isHistogramDataArrayAnEnum = isHistogramDataArrayAnEnum;
5026
5560
  exports.isHistogramDataCollection = isHistogramDataCollection;
5027
5561
  exports.isHistogramRangeData = isHistogramRangeData;
5028
5562
  exports.isHttpStatusError = isHttpStatusError;
5563
+ exports.isIndexedFilterSetEmpty = isIndexedFilterSetEmpty;
5029
5564
  exports.isIntersection = isIntersection;
5030
5565
  exports.isJSONObject = isJSONObject;
5031
5566
  exports.isJSONValue = isJSONValue;
5032
5567
  exports.isJSONValueArray = isJSONValueArray;
5568
+ exports.isNameUnique = isNameUnique;
5033
5569
  exports.isNotDefined = isNotDefined;
5034
5570
  exports.isObject = isObject;
5035
5571
  exports.isOperandsType = isOperandsType;
@@ -5064,6 +5600,7 @@ exports.roundHistogramResponse = roundHistogramResponse;
5064
5600
  exports.selectActiveWorkspaceId = selectActiveWorkspaceId;
5065
5601
  exports.selectActiveWorkspaceStatus = selectActiveWorkspaceStatus;
5066
5602
  exports.selectAllCohortFiltersCollapsed = selectAllCohortFiltersCollapsed;
5603
+ exports.selectAllCohorts = selectAllCohorts;
5067
5604
  exports.selectAuthzMappingData = selectAuthzMappingData;
5068
5605
  exports.selectAvailableCohorts = selectAvailableCohorts;
5069
5606
  exports.selectCSRFToken = selectCSRFToken;
@@ -5097,14 +5634,14 @@ exports.selectUserDetails = selectUserDetails;
5097
5634
  exports.selectUserLoginStatus = selectUserLoginStatus;
5098
5635
  exports.selectWorkspaceStatus = selectWorkspaceStatus;
5099
5636
  exports.selectWorkspaceStatusFromService = selectWorkspaceStatusFromService;
5100
- exports.setActiveCohort = setActiveCohort;
5101
- exports.setActiveCohortList = setActiveCohortList;
5102
5637
  exports.setActiveWorkspace = setActiveWorkspace;
5103
5638
  exports.setActiveWorkspaceId = setActiveWorkspaceId;
5104
5639
  exports.setActiveWorkspaceStatus = setActiveWorkspaceStatus;
5105
5640
  exports.setCohortFilter = setCohortFilter;
5106
5641
  exports.setCohortFilterCombineMode = setCohortFilterCombineMode;
5107
5642
  exports.setCohortIndexFilters = setCohortIndexFilters;
5643
+ exports.setCohortList = setCohortList;
5644
+ exports.setCurrentCohortId = setCurrentCohortId;
5108
5645
  exports.setDRSHostnames = setDRSHostnames;
5109
5646
  exports.setRequestedWorkspaceStatus = setRequestedWorkspaceStatus;
5110
5647
  exports.setSharedFilters = setSharedFilters;
@@ -5116,6 +5653,7 @@ exports.toggleCohortBuilderAllFilters = toggleCohortBuilderAllFilters;
5116
5653
  exports.toggleCohortBuilderCategoryFilter = toggleCohortBuilderCategoryFilter;
5117
5654
  exports.trimFirstFieldNameToTitle = trimFirstFieldNameToTitle;
5118
5655
  exports.updateCohortFilter = updateCohortFilter;
5656
+ exports.updateCohortName = updateCohortName;
5119
5657
  exports.useAddCohortManifestMutation = useAddCohortManifestMutation;
5120
5658
  exports.useAddFileManifestMutation = useAddFileManifestMutation;
5121
5659
  exports.useAddMetadataManifestMutation = useAddMetadataManifestMutation;