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