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