Skip to content

Commit

Permalink
Remove use of eda_study_id and url-based cache for permissions (#302)
Browse files Browse the repository at this point in the history
* Remove use of eda_study_id and url-based cache for permissions

* Request data in parallel, when possible

* Add a non-hook way to get WDK datasets for use in EDA

* Use AllDatasets to populate map, for now
  • Loading branch information
dmfalke authored Aug 31, 2023
1 parent 13c28c0 commit 0da6d6e
Show file tree
Hide file tree
Showing 13 changed files with 261 additions and 151 deletions.
82 changes: 39 additions & 43 deletions packages/libs/eda/src/lib/core/hooks/study.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ import SubsettingClient from '../api/SubsettingClient';

// Hooks
import { useStudyRecord } from '..';
import { useStudyAccessApi } from '@veupathdb/study-data-access/lib/study-access/studyAccessHooks';
import { getWdkStudyRecords } from '../utils/study-records';
import { useDeepValue } from './immutability';
import { usePermissions } from '@veupathdb/study-data-access/lib/data-restriction/permissionsHooks';

const STUDY_RECORD_CLASS_NAME = 'dataset';

Expand Down Expand Up @@ -61,7 +65,12 @@ export function useWdkStudyRecord(datasetId: string): HookValue | undefined {
)
.map(getNodeId)
.toArray()
.concat(['bulk_download_url', 'request_needs_approval', 'is_public'])
.concat([
'dataset_id',
'bulk_download_url',
'request_needs_approval',
'is_public',
])
.filter((attribute) => attribute in studyRecordClass.attributesMap);
const studyRecord = await wdkService
.getRecord(
Expand Down Expand Up @@ -99,44 +108,30 @@ export function useWdkStudyRecord(datasetId: string): HookValue | undefined {
);
}

const DEFAULT_STUDY_ATTRIBUTES = ['dataset_id', 'eda_study_id'];
const DEFAULT_STUDY_TABLES: string[] = [];
const EMPTY_ARRAY: string[] = [];
interface WdkStudyRecordsOptions {
attributes?: AnswerJsonFormatConfig['attributes'];
tables?: AnswerJsonFormatConfig['tables'];
searchName?: string;
}

export function useWdkStudyRecords(
attributes: AnswerJsonFormatConfig['attributes'] = EMPTY_ARRAY,
tables: AnswerJsonFormatConfig['tables'] = EMPTY_ARRAY
subsettingClient: SubsettingClient,
options?: WdkStudyRecordsOptions
): StudyRecord[] | undefined {
const studyAccessApi = useStudyAccessApi();
const stableOptions = useDeepValue(options);
return useWdkService(
async (wdkService) => {
const recordClass = await wdkService.findRecordClass('dataset');
const finalAttributes = DEFAULT_STUDY_ATTRIBUTES.concat(
attributes
).filter((attribute) => attribute in recordClass.attributesMap);
const finalTables = DEFAULT_STUDY_TABLES.concat(tables).filter(
(table) => table in recordClass.tablesMap
);
return wdkService.getAnswerJson(
(wdkService) =>
getWdkStudyRecords(
{
searchName: 'Studies',
searchConfig: {
parameters: {},
},
studyAccessApi,
subsettingClient,
wdkService,
},
{
attributes: finalAttributes,
tables: finalTables,
sorting: [
{
attributeName: 'display_name',
direction: 'ASC',
},
],
}
);
},
[attributes, tables]
)?.records.filter((record) => record.attributes.eda_study_id != null);
stableOptions
),
[studyAccessApi, subsettingClient, stableOptions]
);
}

/**
Expand Down Expand Up @@ -191,10 +186,13 @@ export function isStubEntity(entity: StudyEntity) {
}

export function useStudyMetadata(datasetId: string, client: SubsettingClient) {
const permissionsResponse = usePermissions();
return useWdkServiceWithRefresh(
async (wdkService) => {
if (permissionsResponse.loading) return;
const { permissions } = permissionsResponse;
const recordClass = await wdkService.findRecordClass('dataset');
const attributes = ['dataset_id', 'eda_study_id', 'study_access'].filter(
const attributes = ['dataset_id', 'study_access'].filter(
(attribute) => attribute in recordClass.attributesMap
);
const studyRecord = await wdkService
Expand Down Expand Up @@ -224,22 +222,20 @@ export function useStudyMetadata(datasetId: string, client: SubsettingClient) {
tableErrors: [],
} as RecordInstance;
});
if (typeof studyRecord.attributes.eda_study_id !== 'string')
throw new Error(
'Could not find study with associated dataset id `' + datasetId + '`.'
);
const studyId =
permissions.perDataset[studyRecord.attributes.dataset_id as string]
?.studyId;
if (studyId == null) throw new Error('Not an eda study');
try {
return await client.getStudyMetadata(
studyRecord.attributes.eda_study_id
);
return await client.getStudyMetadata(studyId);
} catch (error) {
console.error(error);
return {
id: studyRecord.attributes.eda_study_id,
id: studyId,
rootEntity: STUB_ENTITY,
};
}
},
[datasetId, client]
[datasetId, client, permissionsResponse]
);
}
76 changes: 76 additions & 0 deletions packages/libs/eda/src/lib/core/utils/study-records.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// utils for getting study records

import { cachedPermissionCheck } from '@veupathdb/study-data-access/lib/data-restriction/permissionsHooks';
import { getStudyId } from '@veupathdb/study-data-access/lib/shared/studies';
import { StudyAccessApi } from '@veupathdb/study-data-access/lib/study-access/api';
import { WdkService } from '@veupathdb/wdk-client/lib/Core';
import { AnswerJsonFormatConfig } from '@veupathdb/wdk-client/lib/Utils/WdkModel';
import { SubsettingClient } from '../api';
import { StudyRecord } from '../types/study';

interface WdkStudyRecordsDeps {
wdkService: WdkService;
subsettingClient: SubsettingClient;
studyAccessApi: StudyAccessApi;
}

interface WdkStudyRecordsOptions {
attributes?: AnswerJsonFormatConfig['attributes'];
tables?: AnswerJsonFormatConfig['tables'];
searchName?: string;
}

const DEFAULT_STUDY_ATTRIBUTES = ['dataset_id'];
const DEFAULT_STUDY_TABLES: string[] = [];
const EMPTY_ARRAY: string[] = [];

export async function getWdkStudyRecords(
deps: WdkStudyRecordsDeps,
options?: WdkStudyRecordsOptions
): Promise<StudyRecord[]> {
const { wdkService, subsettingClient, studyAccessApi } = deps;
const attributes = options?.attributes ?? EMPTY_ARRAY;
const tables = options?.tables ?? EMPTY_ARRAY;
const searchName = options?.searchName ?? 'Studies';

const [permissions, recordClass] = await Promise.all([
cachedPermissionCheck(await wdkService.getCurrentUser(), studyAccessApi),
wdkService.findRecordClass('dataset'),
]);
const finalAttributes = DEFAULT_STUDY_ATTRIBUTES.concat(attributes).filter(
(attribute) => attribute in recordClass.attributesMap
);
const finalTables = DEFAULT_STUDY_TABLES.concat(tables).filter(
(table) => table in recordClass.tablesMap
);
const [edaStudies, answer] = await Promise.all([
subsettingClient.getStudies(),
wdkService.getAnswerJson(
{
searchName,
searchConfig: {
parameters: {},
},
},
{
attributes: finalAttributes,
tables: finalTables,
sorting: [
{
attributeName: 'display_name',
direction: 'ASC',
},
],
}
),
]);
const studyIds = new Set(edaStudies.map((s) => s.id));
return answer.records.filter((record) => {
const datasetId = getStudyId(record);
if (datasetId == null) {
return false;
}
const studyId = permissions.perDataset[datasetId]?.studyId;
return studyId && studyIds.has(studyId);
});
}
13 changes: 11 additions & 2 deletions packages/libs/eda/src/lib/map/MapVeuContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,19 @@ export function MapVeuContainer(mapVeuContainerProps: Props) {
/>
)}
/>
<Route path={`${path}/studies`} exact render={() => <StudyList />} />
<Route
path={`${path}/studies`}
exact
render={() => <StudyList subsettingClient={edaClient} />}
/>
<Route
path={`${path}/public`}
render={() => <PublicAnalysesRoute analysisClient={analysisClient} />}
render={() => (
<PublicAnalysesRoute
analysisClient={analysisClient}
subsettingClient={edaClient}
/>
)}
/>
<Route
path={[`${path}/:studyId/new`, `${path}/:studyId/:analysisId`]}
Expand Down
9 changes: 7 additions & 2 deletions packages/libs/eda/src/lib/map/StudyList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@ import { Link } from 'react-router-dom';
import { safeHtml } from '@veupathdb/wdk-client/lib/Utils/ComponentUtils';

import { useWdkStudyRecords } from '../core/hooks/study';
import { SubsettingClient } from '../core/api';

export function StudyList() {
const studies = useWdkStudyRecords();
interface Props {
subsettingClient: SubsettingClient;
}

export function StudyList({ subsettingClient }: Props) {
const studies = useWdkStudyRecords(subsettingClient);
if (studies == null) return <div>Loading...</div>;
return (
<div>
Expand Down
6 changes: 5 additions & 1 deletion packages/libs/eda/src/lib/workspace/AllAnalyses.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
AnalysisSummary,
useAnalysisList,
usePinnedAnalyses,
useSubsettingClient,
} from '../core';
import SubsettingClient from '../core/api/SubsettingClient';
import { useDebounce } from '../core/hooks/debouncing';
Expand Down Expand Up @@ -169,7 +170,10 @@ export function AllAnalyses(props: Props) {
removePinnedAnalysis,
} = usePinnedAnalyses(analysisClient);

const datasets = useWdkStudyRecords(WDK_STUDY_RECORD_ATTRIBUTES);
const subsettingClient = useSubsettingClient();
const datasets = useWdkStudyRecords(subsettingClient, {
attributes: WDK_STUDY_RECORD_ATTRIBUTES,
});

const { analyses, deleteAnalyses, updateAnalysis, loading, error } =
useAnalysisList(analysisClient);
Expand Down
5 changes: 4 additions & 1 deletion packages/libs/eda/src/lib/workspace/PublicAnalysesRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,21 @@ import { AnalysisClient, usePublicAnalysisList } from '../core';
import { useWdkStudyRecords } from '../core/hooks/study';

import { PublicAnalyses } from './PublicAnalyses';
import SubsettingClient from '../core/api/SubsettingClient';

export interface Props {
analysisClient: AnalysisClient;
subsettingClient: SubsettingClient;
exampleAnalysesAuthor?: number;
}

export function PublicAnalysesRoute({
analysisClient,
subsettingClient,
exampleAnalysesAuthor,
}: Props) {
const publicAnalysisListState = usePublicAnalysisList(analysisClient);
const studyRecords = useWdkStudyRecords();
const studyRecords = useWdkStudyRecords(subsettingClient);

const location = useLocation();
const makeAnalysisLink = useCallback(
Expand Down
30 changes: 16 additions & 14 deletions packages/libs/eda/src/lib/workspace/StudyList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,23 @@ import { usePermissions } from '@veupathdb/study-data-access/lib/data-restrictio

import { useWdkStudyRecords } from '../core/hooks/study';
import { getStudyAccess } from '@veupathdb/study-data-access/lib/shared/studies';
import { SubsettingClient } from '../core/api';

interface StudyListProps {
baseUrl: string;
subsettingClient: SubsettingClient;
}
/**
* Displays a list of links to various available studies.
*/
export function StudyList(props: StudyListProps) {
const { baseUrl } = props;
const { baseUrl, subsettingClient } = props;

const studyRecordAttributes = useMemo(() => ['study_access'], []);

const datasets = useWdkStudyRecords(studyRecordAttributes);
const datasets = useWdkStudyRecords(subsettingClient, {
attributes: studyRecordAttributes,
});

const permissions = usePermissions();

Expand All @@ -34,18 +38,16 @@ export function StudyList(props: StudyListProps) {
<h1>EDA Workspace</h1>
<h2>Choose a study</h2>
<ul>
{datasets
.filter((dataset) => dataset.attributes.eda_study_id != null)
.map((dataset) => {
return (
<li key={dataset.attributes.dataset_id as string}>
<Link to={`${baseUrl}/${dataset.attributes.dataset_id}/new`}>
{safeHtml(dataset.displayName)} [
<b>{getStudyAccess(dataset)}</b>]
</Link>
</li>
);
})}
{datasets.map((dataset) => {
return (
<li key={dataset.attributes.dataset_id as string}>
<Link to={`${baseUrl}/${dataset.attributes.dataset_id}/new`}>
{safeHtml(dataset.displayName)} [
<b>{getStudyAccess(dataset)}</b>]
</Link>
</li>
);
})}
</ul>
</div>
);
Expand Down
5 changes: 4 additions & 1 deletion packages/libs/eda/src/lib/workspace/WorkspaceRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -183,13 +183,16 @@ export function WorkspaceRouter({
<Route
path={`${path}/studies`}
exact
render={() => <StudyList baseUrl={url} />}
render={() => (
<StudyList baseUrl={url} subsettingClient={subsettingClient} />
)}
/>
<Route
path={`${path}/public`}
render={() => (
<PublicAnalysesRoute
analysisClient={analysisClient}
subsettingClient={subsettingClient}
exampleAnalysesAuthor={exampleAnalysesAuthor}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ function DataRestrictionDaemon(props) {

useEffect(() => {
clearRestrictions();
}, [location.pathname]);
}, [location.pathname, clearRestrictions]);

const permissionsValue = usePermissions();
const permissionsValue = usePermissions({ force: true });

if (dataRestriction == null || user == null || permissionsValue.loading)
return null;
Expand Down
Loading

0 comments on commit 0da6d6e

Please sign in to comment.