Skip to content

Commit

Permalink
kids-first#3346 duplicate freactal fences provider into redux
Browse files Browse the repository at this point in the history
  • Loading branch information
evans-g-crsj committed Jul 14, 2021
1 parent a4550db commit 00517ea
Show file tree
Hide file tree
Showing 14 changed files with 570 additions and 0 deletions.
40 changes: 40 additions & 0 deletions src/hooks/useFenceConnections.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';

import {
concatAllFencesAcls,
fetchAllFencesConnectionsIfNeeded,
} from 'store/actionCreators/fenceConnections';
import { Api } from 'store/apiTypes';
import { DispatchFenceConnections, FenceConnections } from 'store/fenceConnectionsTypes';
import { FencesAllConcatenatedAcls } from 'store/fenceTypes';
import { RootState } from 'store/rootState';
import {
selectFenceConnections,
selectIsFetchingAllFenceConnections,
} from 'store/selectors/fenceConnections';

type Output = {
isFetchingAllFenceConnections: boolean;
fenceConnections: FenceConnections;
fencesAllAcls: FencesAllConcatenatedAcls;
};

const useFenceConnections = (api: Api, fences: string[]): Output => {
const fenceConnections = useSelector((state: RootState) => selectFenceConnections(state));
const isFetchingAllFenceConnections = useSelector((state: RootState) =>
selectIsFetchingAllFenceConnections(state),
);
const dispatch: DispatchFenceConnections = useDispatch();

useEffect(() => {
dispatch(fetchAllFencesConnectionsIfNeeded(api, fences));
}, [fences, dispatch, api]);

return {
isFetchingAllFenceConnections,
fenceConnections,
fencesAllAcls: concatAllFencesAcls(fenceConnections),
};
};
export default useFenceConnections;
47 changes: 47 additions & 0 deletions src/hooks/useFenceStudies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';

import { computeAclsByFence } from 'store/actionCreators/fenceConnections';
import {
computeAllFencesAuthStudies,
fetchAllFenceStudiesIfNeeded,
} from 'store/actionCreators/fenceStudies';
import { Api } from 'store/apiTypes';
import { DispatchFenceStudies, FenceStudies, FenceStudy } from 'store/fenceStudiesTypes';
import { RootState } from 'store/rootState';
import { selectFenceConnections } from 'store/selectors/fenceConnections';
import { selectFenceStudies, selectIsFetchingAllFenceStudies } from 'store/selectors/fenceStudies';

const { keys } = Object;

type Output = {
isFetchingAllFenceStudies: boolean;
fenceStudies: FenceStudies;
fenceAuthStudies: FenceStudy[];
};

const useFenceStudies = (api: Api): Output => {
const dispatch: DispatchFenceStudies = useDispatch();

const isFetchingAllFenceStudies = useSelector((state: RootState) =>
selectIsFetchingAllFenceStudies(state),
);
const fenceStudies = useSelector((state: RootState) => selectFenceStudies(state));
const fenceConnections = useSelector((state: RootState) => selectFenceConnections(state));

useEffect(() => {
const fences = keys(fenceConnections);
const shouldFetch = fences.length > 0;
if (shouldFetch) {
const aclsByFence = computeAclsByFence(fenceConnections);
dispatch(fetchAllFenceStudiesIfNeeded(api, fences, aclsByFence));
}
}, [fenceConnections, dispatch, api]);

return {
isFetchingAllFenceStudies,
fenceStudies,
fenceAuthStudies: computeAllFencesAuthStudies(fenceStudies),
};
};
export default useFenceStudies;
99 changes: 99 additions & 0 deletions src/services/fenceStudies.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import keys from 'lodash/keys';

import { graphql } from 'services/arranger';

export const getAuthStudiesIdAndCount = async (api, fence, userAcl) =>
graphql(api)({
query: `
query AuthorizedStudyIdsAndCount($sqon: JSON) {
file {
aggregations(filters: $sqon, aggregations_filter_themselves: true){
participants__study__external_id{
buckets{
key
doc_count}
}
}
}
}
`,
variables: {
sqon: {
op: 'and',
content: [
{ op: 'in', content: { field: 'acl', value: userAcl } },
{ op: 'in', content: { field: 'repository', value: fence } },
],
},
},
}).then(
({
data: {
file: {
aggregations: {
participants__study__external_id: { buckets },
},
},
},
}) =>
buckets.reduce((obj, { key, doc_count }) => {
obj[key] = { authorizedFiles: doc_count };
return obj;
}, {}),
);

export const getStudiesCountByNameAndAcl = async (api, studies, userAcl) => {
const studyIds = keys(studies);

const sqons = studyIds.reduce((obj, studyId) => {
obj[`${studyId}_sqon`] = {
op: 'in',
content: { field: 'participants.study.external_id', value: [studyId] },
};
return obj;
}, {});

return graphql(api)({
query: `
query StudyCountByNamesAndAcl(${studyIds.map(
(studyId) => `$${studyId}_sqon: JSON`,
)}) {
file {
${studyIds
.map(
(studyId) => `
${studyId}: aggregations(filters: $${studyId}_sqon, aggregations_filter_themselves: true) {
acl {
buckets {
key
}
}
participants__study__short_name{
buckets{
key
doc_count
}
}
}
`,
)
.join('')}
}
}
`,
variables: sqons,
}).then(({ data: { file } }) =>
studyIds.map((id) => {
let study = {};
const agg = file[id];
study['acl'] = agg['acl']['buckets'].map((b) => b.key).filter((a) => userAcl.includes(a));
study['studyShortName'] = agg['participants__study__short_name']['buckets'][0]['key'];
study['totalFiles'] = agg['participants__study__short_name']['buckets'][0]['doc_count'];
study['id'] = id;
study['authorizedFiles'] = studies[id]['authorizedFiles'];

return study;
}),
);
};
83 changes: 83 additions & 0 deletions src/store/actionCreators/fenceConnections.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import isEmpty from 'lodash/isEmpty';
import { ThunkAction } from 'redux-thunk';

import { getFenceUser } from 'services/fence';

import { Api } from '../apiTypes';
import {
Connection,
FenceConnections,
FenceConnectionsActions,
FenceConnectionsActionTypes,
} from '../fenceConnectionsTypes';
import { FenceName } from '../fenceTypes';
import { RootState } from '../rootState';
import { selectFenceConnections } from '../selectors/fenceConnections';

const { entries, keys } = Object;

const addFenceConnection = (
fenceName: FenceName,
connection: Connection,
): FenceConnectionsActionTypes => ({
type: FenceConnectionsActions.addFenceConnection,
fenceName,
connection,
});

const toggleIsFetchingAllFenceConnections = (isLoading: boolean): FenceConnectionsActionTypes => ({
type: FenceConnectionsActions.toggleIsFetchingAllFenceConnections,
isLoading,
});

const shouldFetchConnections = (fenceName: FenceName, state: RootState): boolean =>
isEmpty(selectFenceConnections(state)[fenceName]);

const fetchFencesConnections = (
api: Api,
fenceName: FenceName,
): ThunkAction<void, RootState, null, FenceConnectionsActionTypes> => async (dispatch) => {
try {
const connection = await getFenceUser(api, fenceName);
dispatch(addFenceConnection(fenceName, connection));
} catch (error) {
console.error(`Error fetching fence connection for '${fenceName}': ${error}`);
}
};

export const fetchFencesConnectionsIfNeeded = (
api: Api,
fenceName: FenceName,
): ThunkAction<void, RootState, null, FenceConnectionsActionTypes> => async (
dispatch,
getState,
) => {
if (shouldFetchConnections(fenceName, getState())) {
return dispatch(fetchFencesConnections(api, fenceName));
}
};

export const fetchAllFencesConnectionsIfNeeded = (
api: Api,
fencesName: FenceName[],
): ThunkAction<void, RootState, null, FenceConnectionsActionTypes> => async (dispatch) => {
dispatch(toggleIsFetchingAllFenceConnections(true));
for (const fenceName of fencesName) {
await dispatch(fetchFencesConnectionsIfNeeded(api, fenceName));
}
dispatch(toggleIsFetchingAllFenceConnections(false));
};

export const concatAllFencesAcls = (fenceConnections: FenceConnections) => {
const fenceNames = keys(fenceConnections);
return fenceNames.map((fence: FenceName) => keys(fenceConnections[fence].projects)).flat();
};

export const computeAclsByFence = (fenceConnections: FenceConnections) =>
entries(fenceConnections).reduce(
(acc, [fenceName, connection]) => ({
...acc,
[fenceName]: keys(connection.projects || {}),
}),
{},
);
81 changes: 81 additions & 0 deletions src/store/actionCreators/fenceStudies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import flatMap from 'lodash/flatMap';
import isEmpty from 'lodash/isEmpty';
import { ThunkAction } from 'redux-thunk';

import { getAuthStudiesIdAndCount, getStudiesCountByNameAndAcl } from 'services/fenceStudies';

import { Api } from '../apiTypes';
import { FenceStudies, FenceStudiesActions, FenceStudiesActionTypes } from '../fenceStudiesTypes';
import { AclsByFence, FenceName, UserAcls } from '../fenceTypes';
import { RootState } from '../rootState';
import { selectFenceStudies } from '../selectors/fenceStudies';

const addWildCardToAcls = (acls: UserAcls) => [...(acls || []), '*'];

const toggleIsFetchingAllFenceStudies = (isLoading: boolean): FenceStudiesActionTypes => ({
type: FenceStudiesActions.toggleIsFetchingAllFenceStudies,
isLoading,
});

const addFenceStudies = (fenceAuthorizedStudies: FenceStudies): FenceStudiesActionTypes => ({
type: FenceStudiesActions.addFenceStudies,
fenceAuthorizedStudies: fenceAuthorizedStudies,
});

const shouldFetchFenceStudies = (fenceName: FenceName, state: RootState) => {
const studiesForFence = selectFenceStudies(state)[fenceName];
return isEmpty(studiesForFence) || isEmpty(studiesForFence.authorizedStudies);
};

const fetchFenceStudies = (
api: Api,
fenceName: FenceName,
userAcls: UserAcls,
): ThunkAction<void, RootState, null, FenceStudiesActionTypes> => async (dispatch) => {
try {
const aclsWithWildCard = addWildCardToAcls(userAcls);
const studies = await getAuthStudiesIdAndCount(api, fenceName, aclsWithWildCard);
const authorizedStudies = isEmpty(studies)
? []
: await getStudiesCountByNameAndAcl(api, studies, aclsWithWildCard);
dispatch(
addFenceStudies({
[fenceName]: {
authorizedStudies: authorizedStudies || [],
},
}),
);
} catch (error) {
console.error(`Error fetching fence studies for '${fenceName}': ${error}`);
}
};

const fetchFenceStudiesIfNeeded = (
api: Api,
fenceName: FenceName,
aclsByFence: AclsByFence,
): ThunkAction<void, RootState, null, FenceStudiesActionTypes> => async (dispatch, getState) => {
if (shouldFetchFenceStudies(fenceName, getState())) {
const userAcls = aclsByFence[fenceName];
return dispatch(fetchFenceStudies(api, fenceName, userAcls));
}
};

export const fetchAllFenceStudiesIfNeeded = (
api: Api,
fencesName: FenceName[],
aclsByFence: AclsByFence,
): ThunkAction<void, RootState, null, FenceStudiesActionTypes> => async (dispatch) => {
dispatch(toggleIsFetchingAllFenceStudies(true));
for (const fenceName of fencesName) {
await dispatch(fetchFenceStudiesIfNeeded(api, fenceName, aclsByFence));
}
dispatch(toggleIsFetchingAllFenceStudies(false));
};

export const computeAllFencesAuthStudies = (fenceStudies: FenceStudies) => {
if (isEmpty(fenceStudies)) {
return [];
}
return flatMap(Object.values(fenceStudies), (studies) => studies.authorizedStudies);
};
Loading

0 comments on commit 00517ea

Please sign in to comment.