@gen3/core 0.10.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/dist/cjs/index.js +2243 -0
  2. package/dist/cjs/index.js.map +1 -0
  3. package/dist/dts/api.d.ts +8 -0
  4. package/dist/dts/constants.d.ts +32 -0
  5. package/dist/dts/dataAccess.d.ts +35 -0
  6. package/dist/dts/features/aiSearch/aiSearchSlice.d.ts +38 -0
  7. package/dist/dts/features/aiSearch/index.d.ts +2 -0
  8. package/dist/dts/features/app/store.d.ts +6 -0
  9. package/dist/dts/features/authz/authzMappingSlice.d.ts +141 -0
  10. package/dist/dts/features/authz/index.d.ts +3 -0
  11. package/dist/dts/features/authz/types.d.ts +5 -0
  12. package/dist/dts/features/cohort/cohortBuilderConfigSlice.d.ts +0 -0
  13. package/dist/dts/features/cohort/cohortSlice.d.ts +95 -0
  14. package/dist/dts/features/cohort/index.d.ts +2 -0
  15. package/dist/dts/features/cohort/types.d.ts +0 -0
  16. package/dist/dts/features/download/constants.d.ts +14 -0
  17. package/dist/dts/features/download/downloadStatusApi.d.ts +19 -0
  18. package/dist/dts/features/download/index.d.ts +2 -0
  19. package/dist/dts/features/download/types.d.ts +20 -0
  20. package/dist/dts/features/drsResolver/drsHostnameSlice.d.ts +5 -0
  21. package/dist/dts/features/drsResolver/index.d.ts +2 -0
  22. package/dist/dts/features/drsResolver/resolvers/cachedDRSResolver.d.ts +1 -0
  23. package/dist/dts/features/drsResolver/resolvers/dataGUIDSDotOrg.d.ts +6 -0
  24. package/dist/dts/features/drsResolver/resolvers/tests/dataGUIDSDotOrg.unit.test.d.ts +1 -0
  25. package/dist/dts/features/drsResolver/types.d.ts +0 -0
  26. package/dist/dts/features/drsResolver/utils.d.ts +6 -0
  27. package/dist/dts/features/fence/credentialsApi.d.ts +35 -0
  28. package/dist/dts/features/fence/fenceApi.d.ts +37 -0
  29. package/dist/dts/features/fence/index.d.ts +6 -0
  30. package/dist/dts/features/fence/jwtApi.d.ts +12 -0
  31. package/dist/dts/features/fence/types.d.ts +12 -0
  32. package/dist/dts/features/fence/utils.d.ts +12 -0
  33. package/dist/dts/features/filters/filters.d.ts +122 -0
  34. package/dist/dts/features/filters/index.d.ts +4 -0
  35. package/dist/dts/features/filters/tests/utils.unit.test.d.ts +1 -0
  36. package/dist/dts/features/filters/types.d.ts +113 -0
  37. package/dist/dts/features/filters/utils.d.ts +26 -0
  38. package/dist/dts/features/gen3/gen3Api.d.ts +11 -0
  39. package/dist/dts/features/gen3/index.d.ts +2 -0
  40. package/dist/dts/features/gen3Apps/Gen3App.d.ts +48 -0
  41. package/dist/dts/features/gen3Apps/gen3AppRegistry.d.ts +7 -0
  42. package/dist/dts/features/gen3Apps/gen3AppsSlice.d.ts +19 -0
  43. package/dist/dts/features/gen3Apps/index.d.ts +3 -0
  44. package/dist/dts/features/graphQL/graphQLSlice.d.ts +12 -0
  45. package/dist/dts/features/graphQL/index.d.ts +1 -0
  46. package/dist/dts/features/guppy/conversion.d.ts +23 -0
  47. package/dist/dts/features/guppy/guppyApi.d.ts +41 -0
  48. package/dist/dts/features/guppy/guppyDownloadSlice.d.ts +21 -0
  49. package/dist/dts/features/guppy/guppySlice.d.ts +120 -0
  50. package/dist/dts/features/guppy/index.d.ts +6 -0
  51. package/dist/dts/features/guppy/tests/downloadFromGuppy.unit.test.d.ts +1 -0
  52. package/dist/dts/features/guppy/types.d.ts +33 -0
  53. package/dist/dts/features/guppy/utils.d.ts +28 -0
  54. package/dist/dts/features/metadata/index.d.ts +3 -0
  55. package/dist/dts/features/metadata/metadataSlice.d.ts +56 -0
  56. package/dist/dts/features/metadata/types.d.ts +12 -0
  57. package/dist/dts/features/modals/index.d.ts +2 -0
  58. package/dist/dts/features/modals/modalsSlice.d.ts +26 -0
  59. package/dist/dts/features/submission/authMappingUtils.d.ts +15 -0
  60. package/dist/dts/features/submission/index.d.ts +3 -0
  61. package/dist/dts/features/submission/submissionApi.d.ts +43 -0
  62. package/dist/dts/features/submission/types.d.ts +46 -0
  63. package/dist/dts/features/user/externalLoginsSlice.d.ts +14 -0
  64. package/dist/dts/features/user/hooks.d.ts +18 -0
  65. package/dist/dts/features/user/index.d.ts +5 -0
  66. package/dist/dts/features/user/test/useGetExternalLoginsQuery.unit.test.d.ts +1 -0
  67. package/dist/dts/features/user/types.d.ts +50 -0
  68. package/dist/dts/features/user/userSlice.d.ts +53 -0
  69. package/dist/dts/features/user/userSliceRTK.d.ts +484 -0
  70. package/dist/dts/features/workspace/index.d.ts +2 -0
  71. package/dist/dts/features/workspace/types.d.ts +0 -0
  72. package/dist/dts/features/workspace/workspacesSlice.d.ts +6 -0
  73. package/dist/dts/hooks.d.ts +36 -0
  74. package/dist/dts/index.d.ts +23 -0
  75. package/dist/dts/provider.d.ts +2 -0
  76. package/dist/dts/reducers.d.ts +22 -0
  77. package/dist/dts/store.d.ts +53 -0
  78. package/dist/dts/store.unit.test.d.ts +2 -0
  79. package/dist/dts/types/index.d.ts +51 -0
  80. package/dist/dts/utils/extractvalues.d.ts +9 -0
  81. package/dist/dts/utils/index.d.ts +2 -0
  82. package/dist/dts/utils/ts-utils.d.ts +4 -0
  83. package/dist/esm/index.js +2083 -0
  84. package/dist/esm/index.js.map +1 -0
  85. package/dist/index.d.ts +1604 -0
  86. package/package.json +70 -0
@@ -0,0 +1,2083 @@
1
+ import { createSelector, createAsyncThunk, createSlice, combineReducers, configureStore } from '@reduxjs/toolkit';
2
+ import { buildCreateApi, coreModule, reactHooksModule } from '@reduxjs/toolkit/query/react';
3
+ import React, { useRef, useEffect } from 'react';
4
+ import { createSelectorHook, createDispatchHook, createStoreHook, Provider } from 'react-redux';
5
+ import { fetchBaseQuery } from '@reduxjs/toolkit/dist/query/react';
6
+ import { getCookie } from 'cookies-next';
7
+ import { QueryStatus, setupListeners } from '@reduxjs/toolkit/query';
8
+ import { isEqual } from 'lodash';
9
+ import Queue from 'queue';
10
+ import { JSONPath } from 'jsonpath-plus';
11
+ import { v5 } from 'uuid';
12
+ import 'redux-persist';
13
+ import { CookiesProvider } from 'react-cookie';
14
+ import useSWR from 'swr';
15
+ import { flatten } from 'flat';
16
+ import Papa from 'papaparse';
17
+
18
+ const GEN3_COMMONS_NAME = process.env.GEN3_COMMONS_NAME || 'gen3';
19
+ const GEN3_API = process.env.NEXT_PUBLIC_GEN3_API || '';
20
+ const GEN3_DOMAIN = process.env.NEXT_PUBLIC_GEN3_DOMAIN || '';
21
+ /**
22
+ * Service Specific Constants
23
+ */ const GEN3_GUPPY_API = process.env.NEXT_PUBLIC_GEN3_GUPPY_API || `${GEN3_API}/guppy`;
24
+ const GEN3_MDS_API = process.env.NEXT_PUBLIC_GEN3_MDS_API || `${GEN3_API}/mds`;
25
+ const GEN3_DOWNLOADS_ENDPOINT = process.env.NEXT_PUBLIC_GEN3_DOWNLOADS_ENDPOINT || 'downloads';
26
+ const GEN3_FENCE_API = process.env.NEXT_PUBLIC_GEN3_FENCE_API || GEN3_API;
27
+ const GEN3_AI_SEARCH_API = process.env.NEXT_PUBLIC_GEN3_AI_SEARCH_API || `${GEN3_API}/ai-search`;
28
+ const GEN3_AUTHZ_API = process.env.NEXT_PUBLIC_GEN3_AUTHZ_API || `${GEN3_API}/authz`;
29
+ const GEN3_REDIRECT_URL = process.env.NEXT_PUBLIC_GEN3_REDIRECT_URL || GEN3_API;
30
+ const GEN3_WORKSPACE_STATUS_API = process.env.NEXT_PUBLIC_GEN3_WORKSPACE_STATUS_API || `${GEN3_API}/lw-workspace`;
31
+ const GEN3_SUBMISSION_API = process.env.NEXT_PUBLIC_GEN3_SUBMISSION_API || `${GEN3_API}/api/v0/submission`;
32
+ const GEN3_WTS_API = process.env.NEXT_PUBLIC_GEN3_WTS_API || `${GEN3_API}/wts`;
33
+ const GEN3_CROSSWALK_API = process.env.NEXT_PUBLIC_GEN3_CROSSWALK_API || `${GEN3_API}/mds`;
34
+ var Accessibility;
35
+ (function(Accessibility) {
36
+ Accessibility["ACCESSIBLE"] = "accessible";
37
+ Accessibility["UNACCESSIBLE"] = "unaccessible";
38
+ Accessibility["ALL"] = "all";
39
+ })(Accessibility || (Accessibility = {}));
40
+ const FILE_DELIMITERS = {
41
+ tsv: '\t',
42
+ csv: ','
43
+ };
44
+
45
+ // From here down is react-related code. If we wanted to create a UI-agnotic core,
46
+ // we could need to move the following code and the provider into a new workspace,
47
+ // such as core-react.
48
+ /**
49
+ * The initial context is never used in practice. A little casting voodoo to satisfy TS.
50
+ *
51
+ * Note: Should the action type be AnyAction (from redux) or PayloadAction (from redux-toolkit)?
52
+ * If we are creating all of our actions through RTK, then PayloadAction might be the
53
+ * correct opinionated type.
54
+ */ const CoreContext = React.createContext(undefined);
55
+ const useCoreSelector = createSelectorHook(CoreContext);
56
+ const useCoreDispatch = createDispatchHook(CoreContext);
57
+ const useCoreStore = createStoreHook(CoreContext);
58
+
59
+ /**
60
+ * Creates a custom Redux Toolkit core API
61
+ * See: https://redux-toolkit.js.org/rtk-query/usage/customizing-create-api
62
+ * @returns: created core API.
63
+ */ const coreCreateApi = buildCreateApi(coreModule(), reactHooksModule({
64
+ useSelector: useCoreSelector,
65
+ useStore: useCoreStore,
66
+ useDispatch: useCoreDispatch
67
+ }));
68
+
69
+ /**
70
+ * Template for fence error response dict
71
+ * @returns: An error dict response from a RESTFUL API request
72
+ */ const buildFetchError = async (res, request)=>{
73
+ return {
74
+ url: res.url,
75
+ status: res.status,
76
+ statusText: res.statusText,
77
+ text: await res.text(),
78
+ request: request
79
+ };
80
+ };
81
+ /**
82
+ * Template for a standard fence request
83
+ * @returns: response data
84
+ */ const fetchFence = async ({ endpoint, headers, body = {}, method = 'GET', isJSON = true })=>{
85
+ const res = await fetch(`${GEN3_FENCE_API}${endpoint}`, {
86
+ method: method,
87
+ headers: headers,
88
+ body: 'POST' === method ? JSON.stringify(body) : null
89
+ });
90
+ if (res.ok) return {
91
+ data: isJSON ? await res.json() : await res.text(),
92
+ status: res.status
93
+ };
94
+ throw await buildFetchError(res, {
95
+ endpoint,
96
+ method,
97
+ headers,
98
+ body
99
+ });
100
+ };
101
+
102
+ const userAuthApi = coreCreateApi({
103
+ reducerPath: 'userAuthApi',
104
+ refetchOnMountOrArgChange: 1800,
105
+ refetchOnReconnect: true,
106
+ baseQuery: async ({ endpoint }, { getState })=>{
107
+ let results;
108
+ const csrfToken = selectCSRFToken(getState());
109
+ let accessToken = undefined;
110
+ if (process.env.NODE_ENV === 'development') {
111
+ accessToken = getCookie('credentials_token');
112
+ }
113
+ const headers = {
114
+ Accept: 'application/json',
115
+ 'Content-Type': 'application/json',
116
+ ...csrfToken ? {
117
+ 'X-CSRF-Token': csrfToken
118
+ } : {},
119
+ ...accessToken ? {
120
+ Authorization: `Bearer ${accessToken}`
121
+ } : {},
122
+ credentials: 'include'
123
+ };
124
+ try {
125
+ results = await fetchFence({
126
+ endpoint,
127
+ headers
128
+ });
129
+ } catch (e) {
130
+ /*
131
+ Because an "error" response is valid for the auth requests we don't want to
132
+ put the request in an error state, or it will attempt the request over and over again
133
+ */ return {
134
+ data: {}
135
+ };
136
+ }
137
+ return {
138
+ data: results
139
+ };
140
+ },
141
+ endpoints: (builder)=>({
142
+ fetchUserDetails: builder.query({
143
+ query: ()=>({
144
+ endpoint: '/user/user'
145
+ }),
146
+ transformResponse (response) {
147
+ return {
148
+ data: response.data,
149
+ // TODO: check if this is the correct status code
150
+ loginStatus: response.status === 200 && response.data?.username ? 'authenticated' : 'unauthenticated'
151
+ };
152
+ }
153
+ }),
154
+ getCSRF: builder.query({
155
+ query: ()=>({
156
+ endpoint: '/_status'
157
+ }),
158
+ transformResponse: (response)=>{
159
+ return {
160
+ csrfToken: response?.data?.csrf ?? ''
161
+ };
162
+ }
163
+ })
164
+ })
165
+ });
166
+ const EMPTY_USER = {
167
+ username: undefined
168
+ };
169
+ const { useFetchUserDetailsQuery, useLazyFetchUserDetailsQuery, useGetCSRFQuery } = userAuthApi;
170
+ const userAuthApiMiddleware = userAuthApi.middleware;
171
+ const userAuthApiReducerPath = userAuthApi.reducerPath;
172
+ const userAuthApiReducer = userAuthApi.reducer;
173
+ const selectUserDetailsFromState = userAuthApi.endpoints.fetchUserDetails.select();
174
+ const selectUserDetails = createSelector(selectUserDetailsFromState, (userDetails)=>userDetails?.data?.data ?? EMPTY_USER);
175
+ const selectUserAuthStatus = createSelector(selectUserDetailsFromState, (userLoginState)=>userLoginState.status === QueryStatus.pending ? 'pending' : userLoginState.status === QueryStatus.uninitialized ? 'not present' : userLoginState?.data?.loginStatus ?? 'unauthenticated');
176
+ const selectCSRFTokenData = userAuthApi.endpoints.getCSRF.select();
177
+ const passThroughTheState = (state)=>state.gen3Services;
178
+ const selectCSRFToken = createSelector([
179
+ selectCSRFTokenData,
180
+ passThroughTheState
181
+ ], (state)=>state?.data?.csrfToken);
182
+ const selectHeadersWithCSRFToken = createSelector([
183
+ selectCSRFToken,
184
+ passThroughTheState
185
+ ], (csrfToken)=>({
186
+ Accept: 'application/json',
187
+ 'Content-Type': 'application/json',
188
+ ...csrfToken && {
189
+ 'X-CSRF-Token': csrfToken
190
+ }
191
+ }));
192
+
193
+ /**
194
+ * Creates a base class core API for building other API endpoints on top of.
195
+ * @param reducerPath - The root key name that the other slices will be derived from
196
+ * @param baseQuery: - The template query which the slices will addon to
197
+ * @param endpoints - Base API endpoints that should exist in every slice
198
+ * @returns: The generated base API
199
+ */ const gen3Api = coreCreateApi({
200
+ reducerPath: 'gen3Services',
201
+ baseQuery: fetchBaseQuery({
202
+ baseUrl: `${GEN3_API}`,
203
+ prepareHeaders: (headers, { getState })=>{
204
+ const csrfToken = selectCSRFToken(getState());
205
+ headers.set('Content-Type', 'application/json');
206
+ let accessToken = undefined;
207
+ if (process.env.NODE_ENV === 'development') {
208
+ // NOTE: This cookie can only be accessed from the client side
209
+ // in development mode. Otherwise, the cookie is set as httpOnly
210
+ accessToken = getCookie('credentials_token');
211
+ }
212
+ if (csrfToken) headers.set('X-CSRF-Token', csrfToken);
213
+ if (accessToken) headers.set('Authorization', `Bearer ${accessToken}`);
214
+ return headers;
215
+ }
216
+ }),
217
+ endpoints: ()=>({})
218
+ });
219
+ const gen3ServicesReducer = gen3Api.reducer;
220
+ const gen3ServicesReducerMiddleware = gen3Api.middleware;
221
+
222
+ /**
223
+ * Creates a fence API endpoint for handling login processes
224
+ * @param endpoints - defined endpoint query for logging in
225
+ * @returns: The generated fence login API slice
226
+ */ const loginProvidersApi = gen3Api.injectEndpoints({
227
+ endpoints: (builder)=>({
228
+ getLoginProviders: builder.query({
229
+ query: ()=>`${GEN3_FENCE_API}/user/login`
230
+ })
231
+ })
232
+ });
233
+ const { useGetLoginProvidersQuery } = loginProvidersApi;
234
+ /**
235
+ * Logout from fence
236
+ */ const logoutFence = async (redirect = '/')=>await fetchFence({
237
+ endpoint: `${GEN3_FENCE_API}/user/logout?next=${GEN3_REDIRECT_URL}${redirect}`,
238
+ method: 'GET'
239
+ });
240
+
241
+ // extending the gen3API to add a tag to the endpoints
242
+ const credentialsWithTags$1 = gen3Api.enhanceEndpoints({
243
+ addTagTypes: [
244
+ 'Credentials'
245
+ ]
246
+ });
247
+ /**
248
+ * Adds a credentialsApi into the base gen3Api
249
+ * @endpoints Includes get, add, and remove credential operations
250
+ * @see https://github.com/uc-cdis/fence/blob/master/openapis/swagger.yaml#L972-L1033
251
+ * @param getCredentials - List all the API keys for the current user
252
+ * @param addNewCredential - Get a new API key for the current user
253
+ * @param removeCredential - Delete API access key with given ID for current user
254
+ * @returns: A fence credential API for manipulating user credentials
255
+ */ const credentialsApi = credentialsWithTags$1.injectEndpoints({
256
+ endpoints: (builder)=>({
257
+ getCredentials: builder.query({
258
+ query: ()=>`${GEN3_FENCE_API}/user/credentials/api`,
259
+ transformResponse: (response)=>response['jtis'],
260
+ // "jtis", which is an array of API keys
261
+ // no need to transform the response, since the API returns the correct format
262
+ providesTags: [
263
+ 'Credentials'
264
+ ]
265
+ }),
266
+ addNewCredential: builder.mutation({
267
+ query: (csrfToken)=>({
268
+ url: `${GEN3_FENCE_API}/user/credentials/api`,
269
+ method: 'POST',
270
+ headers: {
271
+ 'Content-Type': 'application/json',
272
+ 'x-csrf-token': csrfToken
273
+ },
274
+ body: {
275
+ scope: [
276
+ 'user',
277
+ 'data'
278
+ ]
279
+ }
280
+ }),
281
+ invalidatesTags: [
282
+ 'Credentials'
283
+ ]
284
+ }),
285
+ removeCredential: builder.mutation({
286
+ query: ({ csrfToken, id })=>({
287
+ url: `${GEN3_FENCE_API}/user/credentials/api/${id}`,
288
+ method: 'DELETE',
289
+ headers: {
290
+ 'Content-Type': 'application/json',
291
+ ...csrfToken && {
292
+ 'x-csrf-token': csrfToken
293
+ }
294
+ }
295
+ }),
296
+ invalidatesTags: [
297
+ 'Credentials'
298
+ ]
299
+ }),
300
+ authorizeFromCredentials: builder.mutation({
301
+ query: (params)=>({
302
+ url: `${GEN3_FENCE_API}/user/credentials/api/access_token`,
303
+ method: 'POST',
304
+ headers: {
305
+ 'Content-Type': 'application/json'
306
+ },
307
+ body: {
308
+ api_key: params.api_key,
309
+ key_id: params.key_id
310
+ }
311
+ })
312
+ })
313
+ })
314
+ });
315
+ const { useGetCredentialsQuery, useAddNewCredentialMutation, useRemoveCredentialMutation, useAuthorizeFromCredentialsMutation } = credentialsApi;
316
+
317
+ // extending the gen3API to add a tag to the endpoints
318
+ const credentialsWithTags = gen3Api.enhanceEndpoints({
319
+ addTagTypes: [
320
+ 'fenceJWT'
321
+ ]
322
+ });
323
+ /**
324
+ * A fence API for getting the public keys which can be used to validate
325
+ * JWTs issued and signed by fence
326
+ * @returns: Fence public keys
327
+ */ const jwtApi = credentialsWithTags.injectEndpoints({
328
+ endpoints: (builder)=>({
329
+ getJWKKeys: builder.query({
330
+ query: ()=>'user/jwt/keys',
331
+ providesTags: [
332
+ 'fenceJWT'
333
+ ]
334
+ })
335
+ })
336
+ });
337
+ const { useGetJWKKeysQuery } = jwtApi;
338
+
339
+ const usePrevious = (value)=>{
340
+ const ref = useRef();
341
+ useEffect(()=>{
342
+ ref.current = value;
343
+ });
344
+ return ref.current;
345
+ };
346
+ const createUseCoreDataHook = (fetchDataActionCreator, dataSelector)=>{
347
+ return (...params)=>{
348
+ const coreDispatch = useCoreDispatch();
349
+ const { data, status, error } = useCoreSelector(dataSelector);
350
+ const action = fetchDataActionCreator(...params);
351
+ const prevParams = usePrevious(params);
352
+ useEffect(()=>{
353
+ if (status === 'uninitialized' || !isEqual(prevParams, params)) {
354
+ // createDispatchHook types forces the input to AnyAction, which is
355
+ // not compatible with thunk actions. hence, the `as any` cast. ;(
356
+ coreDispatch(action); // eslint-disable-line
357
+ }
358
+ }, [
359
+ status,
360
+ coreDispatch,
361
+ action,
362
+ params,
363
+ prevParams
364
+ ]);
365
+ return {
366
+ data,
367
+ error,
368
+ isUninitialized: status === 'uninitialized',
369
+ isFetching: status === 'pending',
370
+ isSuccess: status === 'fulfilled',
371
+ isError: status === 'rejected'
372
+ };
373
+ };
374
+ };
375
+
376
+ /**
377
+ * Creates an async thunk for fetching user permissions details from fence
378
+ * @see https://redux-toolkit.js.org/api/createAsyncThunk
379
+ * @returns: A fence response dict containing user details
380
+ */ const fetchUserState = createAsyncThunk('fence/user/user', async (_, meta)=>{
381
+ // Get an access token from a cookie if in development mode
382
+ const csrfToken = selectCSRFToken(meta.getState());
383
+ let accessToken = undefined;
384
+ if (process.env.NODE_ENV === 'development') {
385
+ accessToken = getCookie('credentials_token');
386
+ }
387
+ return await fetchFence({
388
+ endpoint: '/user/user',
389
+ method: 'GET',
390
+ headers: {
391
+ Accept: 'application/json',
392
+ 'Content-Type': 'application/json',
393
+ ...csrfToken ? {
394
+ 'X-CSRF-Token': csrfToken
395
+ } : {},
396
+ credentials: 'include',
397
+ ...accessToken ? {
398
+ Authorization: `Bearer ${accessToken}`
399
+ } : {}
400
+ }
401
+ });
402
+ });
403
+ const isAuthenticated = (loginStatus)=>loginStatus === 'authenticated';
404
+ const isPending = (loginStatus)=>loginStatus === 'pending';
405
+ const initialState$3 = {
406
+ status: 'uninitialized',
407
+ loginStatus: 'unauthenticated',
408
+ error: undefined
409
+ };
410
+ /**
411
+ * Wraps a slice on top of fetchUserState async thunk to keep track of
412
+ * query state. authenticated/not-authenticated vs. ejected/fulfilled/pending
413
+ * @returns: status messages wrapped around fetchUserState response dict
414
+ */ const slice$3 = createSlice({
415
+ name: 'fence/user',
416
+ initialState: initialState$3,
417
+ reducers: {
418
+ resetUserState: ()=>initialState$3
419
+ },
420
+ extraReducers: (builder)=>{
421
+ builder.addCase(fetchUserState.fulfilled, (_, action)=>{
422
+ const response = action.payload;
423
+ if (response.status !== 200) {
424
+ return {
425
+ status: 'rejected',
426
+ loginStatus: 'unauthenticated'
427
+ };
428
+ }
429
+ return {
430
+ data: {
431
+ ...response.data
432
+ },
433
+ status: 'fulfilled',
434
+ loginStatus: 'authenticated'
435
+ };
436
+ }).addCase(fetchUserState.pending, ()=>{
437
+ return {
438
+ status: 'pending',
439
+ loginStatus: 'unauthenticated'
440
+ };
441
+ }).addCase(fetchUserState.rejected, ()=>{
442
+ return {
443
+ status: 'rejected',
444
+ loginStatus: 'unauthenticated'
445
+ };
446
+ });
447
+ }
448
+ });
449
+ const userReducer = slice$3.reducer;
450
+ const { resetUserState } = slice$3.actions;
451
+ const selectUserData = (state)=>{
452
+ return state.user;
453
+ };
454
+ const selectUser = (state)=>state.user;
455
+ const selectUserLoginStatus = (state)=>state.user.loginStatus;
456
+ const useUser = createUseCoreDataHook(fetchUserState, selectUserData);
457
+ const useIsUserLoggedIn = ()=>{
458
+ return useCoreSelector((state)=>isAuthenticated(selectUserLoginStatus(state)));
459
+ };
460
+ /**
461
+ * Hook to return get the authenticated state of the user and if logged in,
462
+ * the user's profile and access api.
463
+ * Note that if fetchUserState gets called, the user's session is renewed.
464
+ */ const useUserAuth = (renew = false)=>{
465
+ const coreDispatch = useCoreDispatch();
466
+ const { data, status, loginStatus, error } = useCoreSelector(selectUserData);
467
+ useEffect(()=>{
468
+ if (status === 'uninitialized' || renew) {
469
+ // TODO: need to determine what other states require dispatch
470
+ coreDispatch(fetchUserState());
471
+ }
472
+ }, [
473
+ status,
474
+ coreDispatch,
475
+ renew
476
+ ]);
477
+ return {
478
+ data: data,
479
+ error,
480
+ loginStatus,
481
+ isUninitialized: status === 'uninitialized',
482
+ isFetching: status === 'pending',
483
+ isSuccess: status === 'fulfilled',
484
+ isError: status === 'rejected'
485
+ };
486
+ };
487
+
488
+ /**
489
+ * @description Creates a externalLoginApi for listing the configured identity providers
490
+ * in workspace token service. Includes user token expiration time.
491
+ * @see https://github.com/uc-cdis/workspace-token-service/tree/master
492
+ */ const externalLoginApi = gen3Api.injectEndpoints({
493
+ endpoints: (builder)=>({
494
+ getExternalLogins: builder.query({
495
+ query: ()=>({
496
+ url: `${GEN3_WTS_API}/external_oidc/`
497
+ })
498
+ }),
499
+ isExternalConnected: builder.query({
500
+ query: (idp)=>({
501
+ url: `${GEN3_WTS_API}/oauth2/connected?idp=${idp}`
502
+ }),
503
+ transformResponse: ()=>{
504
+ return true; // if success then connected is true
505
+ }
506
+ })
507
+ })
508
+ });
509
+ const { useGetExternalLoginsQuery, useLazyGetExternalLoginsQuery, useLazyIsExternalConnectedQuery, useIsExternalConnectedQuery } = externalLoginApi;
510
+
511
+ // type guard functions
512
+ const isHistogramRangeData = (key)=>{
513
+ return Array.isArray(key) && key.length === 2 && key.every((item)=>typeof item === 'number');
514
+ };
515
+ const isJSONObject = (data)=>{
516
+ return typeof data === 'object' && data !== null && !Array.isArray(data);
517
+ };
518
+ const isJSONValue = (data)=>{
519
+ return typeof data === 'string' || typeof data === 'number' || typeof data === 'boolean' || Array.isArray(data) && data.every(isJSONValue) || isJSONObject(data);
520
+ };
521
+ const isJSONValueArray = (data)=>{
522
+ return Array.isArray(data) && data.every(isJSONValue);
523
+ };
524
+ const isValidObject = (input)=>typeof input === 'object' && input !== null;
525
+ const isHistogramData = (data)=>{
526
+ return isValidObject(data) && 'key' in data && 'count' in data;
527
+ };
528
+ const isHistogramDataArray = (input)=>{
529
+ if (!isValidObject(input) || !Array.isArray(input.histogram)) {
530
+ return false;
531
+ }
532
+ return input.histogram.every(isHistogramData);
533
+ };
534
+ const isHistogramDataCollection = (obj)=>{
535
+ return isValidObject(obj) && 'histogram' in obj && isHistogramData(obj.histogram);
536
+ };
537
+ // Type guard function for GuppyAggregationData interface
538
+ const isGuppyAggregationData = (obj)=>{
539
+ if (!isValidObject(obj)) return false;
540
+ for(const key in obj){
541
+ if (!isHistogramDataCollection(obj[key])) {
542
+ return false;
543
+ }
544
+ }
545
+ return true;
546
+ };
547
+ const isHistogramDataAnEnum = (data)=>{
548
+ return typeof data === 'object' && data !== null && 'key' in data && 'count' in data && typeof data.key === 'string' && typeof data.count === 'number';
549
+ };
550
+ const isHistogramDataAArray = (data)=>{
551
+ return Array.isArray(data) && data.every(isHistogramData);
552
+ };
553
+ const isHistogramDataArrayAnEnum = (data)=>{
554
+ return Array.isArray(data) && data.every(isHistogramDataAnEnum);
555
+ };
556
+ const isHistogramDataArrayARange = (data)=>{
557
+ return Array.isArray(data) && data.every((item)=>isHistogramRangeData(item.key));
558
+ };
559
+ /**
560
+ * Type predicate to narrow an unknown error to `FetchBaseQueryError`
561
+ */ function isFetchBaseQueryError(error) {
562
+ return typeof error === 'object' && error != null && 'status' in error;
563
+ }
564
+ /**
565
+ * Type predicate to narrow an unknown error to an object with a string 'message' property
566
+ */ function isErrorWithMessage(error) {
567
+ return typeof error === 'object' && error != null && 'message' in error && typeof error.message === 'string';
568
+ }
569
+ /**
570
+ * Type predicate to narrow an unknown error to an object with a string 'message' property
571
+ */ function isFetchParseError(error) {
572
+ return typeof error === 'object' && error != null && 'originalStatus' in error && 'status' in error && error['status'] === 'PARSING_ERROR';
573
+ }
574
+
575
+ /**
576
+ * A registry for the Gen3 Apps.
577
+ */ const REGISTRY = {};
578
+ const registerGen3App = (id, gen3App)=>{
579
+ REGISTRY[id] = gen3App;
580
+ };
581
+ const lookupGen3App = (id)=>{
582
+ return REGISTRY[id];
583
+ };
584
+
585
+ const initialState$2 = {
586
+ gen3Apps: {}
587
+ };
588
+ const slice$2 = createSlice({
589
+ name: 'gen3Apps',
590
+ initialState: initialState$2,
591
+ reducers: {
592
+ addGen3AppMetadata: (state, action)=>{
593
+ const { id, requiredEntityTypes } = action.payload;
594
+ state.gen3Apps[id] = {
595
+ ...action.payload,
596
+ // need to turn a ReadonlyArray into a mutable array for immer's WritableDraft
597
+ requiredEntityTypes: [
598
+ ...requiredEntityTypes
599
+ ]
600
+ };
601
+ }
602
+ }
603
+ });
604
+ const gen3AppReducer = slice$2.reducer;
605
+ const { addGen3AppMetadata } = slice$2.actions;
606
+ const selectGen3AppMetadataById = (state, appId)=>state.gen3Apps.gen3Apps[appId];
607
+ const selectGen3AppById = (appId)=>lookupGen3App(appId); // TODO: memoize this selector
608
+
609
+ const initialState$1 = {};
610
+ // TODO: document what this does
611
+ const slice$1 = createSlice({
612
+ name: 'drsResolver',
613
+ initialState: initialState$1,
614
+ reducers: {
615
+ setDRSHostnames: (_state, action)=>{
616
+ return action.payload;
617
+ }
618
+ }
619
+ });
620
+ const drsHostnamesReducer = slice$1.reducer;
621
+ const { setDRSHostnames } = slice$1.actions;
622
+
623
+ var Modals;
624
+ (function(Modals) {
625
+ Modals["FirstTimeModal"] = "FirstTimeModal";
626
+ Modals["SessionExpireModal"] = "SessionExpireModal";
627
+ Modals["NoAccessModal"] = "NoAccessModal";
628
+ Modals["CreateCredentialsAPIKeyModal"] = "CreateCredentialsAPIKeyModal";
629
+ Modals["GeneralErrorModal"] = "GeneralErrorModal";
630
+ })(Modals || (Modals = {}));
631
+ const initialState = {
632
+ currentModal: null
633
+ };
634
+ //Creates a modal slice for tracking showModal and hideModal state.
635
+ const slice = createSlice({
636
+ name: 'modals',
637
+ initialState,
638
+ reducers: {
639
+ showModal: (state, action)=>{
640
+ state.currentModal = action.payload.modal;
641
+ state.message = action.payload.message;
642
+ return state;
643
+ },
644
+ hideModal: (state)=>{
645
+ state.currentModal = null;
646
+ return state;
647
+ }
648
+ }
649
+ });
650
+ const modalReducer = slice.reducer;
651
+ const { showModal, hideModal } = slice.actions;
652
+ const selectCurrentModal = (state)=>state.modals.currentModal;
653
+ const selectCurrentMessage = (state)=>state.modals.message;
654
+
655
+ const initialCohortState = {
656
+ cohort: {
657
+ id: 'default',
658
+ name: 'Filters',
659
+ filters: {},
660
+ modified_datetime: new Date().toISOString()
661
+ }
662
+ };
663
+ // TODO: start using this adapter
664
+ /*
665
+ const cohortsAdapter = createEntityAdapter<Cohort>({
666
+ sortComparer: (a, b) => {
667
+ if (a.modified_datetime <= b.modified_datetime) return 1;
668
+ else return -1;
669
+ },
670
+ });
671
+ */ /**
672
+ * Redux slice for cohort filters
673
+ */ const cohortSlice = createSlice({
674
+ name: 'cohort',
675
+ initialState: initialCohortState,
676
+ reducers: {
677
+ // adds a filter to the cohort filter set at the given index
678
+ updateCohortFilter: (state, action)=>{
679
+ const { index, field, filter } = action.payload;
680
+ return {
681
+ cohort: {
682
+ ...state.cohort,
683
+ filters: {
684
+ ...state.cohort.filters,
685
+ [index]: {
686
+ mode: state.cohort.filters?.[index]?.mode ?? 'and',
687
+ root: {
688
+ ...state.cohort.filters?.[index]?.root ?? {},
689
+ [field]: filter
690
+ }
691
+ }
692
+ }
693
+ }
694
+ };
695
+ },
696
+ // removes a filter to the cohort filter set at the given index
697
+ removeCohortFilter: (state, action)=>{
698
+ const { index, field } = action.payload;
699
+ const filters = state.cohort?.filters?.[index]?.root;
700
+ if (!filters) {
701
+ return;
702
+ }
703
+ const { [field]: _a, ...updated } = filters;
704
+ return {
705
+ cohort: {
706
+ ...state.cohort,
707
+ filters: {
708
+ ...state.cohort.filters,
709
+ [index]: {
710
+ mode: state.cohort.filters[index].mode,
711
+ root: updated
712
+ }
713
+ }
714
+ }
715
+ };
716
+ },
717
+ // removes all filters from the cohort filter set at the given index
718
+ clearCohortFilters: (state, action)=>{
719
+ const { index } = action.payload;
720
+ return {
721
+ cohort: {
722
+ ...state.cohort,
723
+ filters: {
724
+ ...state.cohort.filters,
725
+ [index]: {
726
+ // empty filter set
727
+ mode: 'and',
728
+ root: {}
729
+ }
730
+ }
731
+ }
732
+ };
733
+ }
734
+ },
735
+ extraReducers: {}
736
+ });
737
+ // Filter actions: addFilter, removeFilter, updateFilter
738
+ const { updateCohortFilter, removeCohortFilter, clearCohortFilters } = cohortSlice.actions;
739
+ const selectCohortFilters = (state)=>state.cohorts.cohort.filters;
740
+ const selectCurrentCohortId = (state)=>state.cohorts.cohort.id;
741
+ const selectCurrentCohort = (state)=>state.cohorts.cohort;
742
+ const selectCurrentCohortName = (state)=>state.cohorts.cohort.name;
743
+ /**
744
+ * Select a filter by its name from the current cohort. If the filter is not found
745
+ * returns undefined.
746
+ * @param state - Core
747
+ * @param index which cohort index to select from
748
+ * @param name name of the filter to select
749
+ */ const selectIndexedFilterByName = (state, index, name)=>{
750
+ return state.cohorts.cohort.filters[index]?.root[name];
751
+ };
752
+ const EmptyFilterSet = {
753
+ mode: 'and',
754
+ root: {}
755
+ };
756
+ /**
757
+ * Select a filter from the index.
758
+ * returns undefined.
759
+ * @param state - Core
760
+ * @param index which cohort index to select from
761
+ */ const selectIndexFilters = (state, index)=>{
762
+ return state.cohorts.cohort.filters?.[index] ?? EmptyFilterSet; // TODO: check if this is undefined
763
+ };
764
+ const cohortReducer = cohortSlice.reducer;
765
+
766
+ /**
767
+ * Creates a base class core API for guppy API calls.
768
+ * @returns: guppy core API with guppyAPIFetch base query
769
+ */ const guppyApi = coreCreateApi({
770
+ reducerPath: 'guppy',
771
+ // TODO: refactor to use fetchBaseQuery
772
+ baseQuery: async (query, api)=>{
773
+ const csrfToken = selectCSRFToken(api.getState());
774
+ let accessToken = undefined;
775
+ if (process.env.NODE_ENV === 'development') {
776
+ // NOTE: This cookie can only be accessed from the client side
777
+ // in development mode. Otherwise, the cookie is set as httpOnly
778
+ accessToken = getCookie('credentials_token');
779
+ }
780
+ const headers = {
781
+ Accept: 'application/json',
782
+ 'Content-Type': 'application/json',
783
+ 'Access-Control-Allow-Origin': '*',
784
+ ...csrfToken && {
785
+ 'X-CSRF-Token': csrfToken
786
+ },
787
+ ...accessToken && {
788
+ Authorization: `Bearer ${accessToken}`
789
+ }
790
+ };
791
+ try {
792
+ const response = await fetch(`${GEN3_GUPPY_API}/graphql`, {
793
+ headers: headers,
794
+ method: 'POST',
795
+ body: JSON.stringify(query)
796
+ });
797
+ return {
798
+ data: await response.json()
799
+ };
800
+ } catch (e) {
801
+ if (e instanceof Error) return {
802
+ error: e.message
803
+ };
804
+ return {
805
+ error: e
806
+ };
807
+ }
808
+ },
809
+ endpoints: ()=>({})
810
+ });
811
+ const guppyAPISliceMiddleware = guppyApi.middleware;
812
+ const guppyApiSliceReducerPath = guppyApi.reducerPath;
813
+ const guppyApiReducer = guppyApi.reducer;
814
+
815
+ const rootReducer = combineReducers({
816
+ gen3Services: gen3ServicesReducer,
817
+ user: userReducer,
818
+ gen3Apps: gen3AppReducer,
819
+ drsHostnames: drsHostnamesReducer,
820
+ modals: modalReducer,
821
+ cohorts: cohortReducer,
822
+ [guppyApiSliceReducerPath]: guppyApiReducer,
823
+ [userAuthApiReducerPath]: userAuthApiReducer
824
+ });
825
+
826
+ const coreStore = configureStore({
827
+ reducer: rootReducer,
828
+ middleware: (getDefaultMiddleware)=>getDefaultMiddleware().concat(gen3ServicesReducerMiddleware, guppyAPISliceMiddleware, userAuthApiMiddleware)
829
+ });
830
+ setupListeners(coreStore.dispatch);
831
+
832
+ const CoreProvider = ({ children })=>{
833
+ return /*#__PURE__*/ React.createElement(Provider, {
834
+ store: coreStore,
835
+ context: CoreContext
836
+ }, children);
837
+ };
838
+
839
+ /**
840
+ * Creates the authzApi for checking arborist permissions for a selected user
841
+ * @see https://petstore.swagger.io/?url=https://raw.githubusercontent.com/uc-cdis/arborist/master/docs/openapi.yaml#/auth/get_auth_mapping
842
+ * @see https://github.com/uc-cdis/arborist/blob/master/docs/relationships.simplified.png
843
+ * @returns: An arborist response dict of user permissions {method, service} for each resource path.
844
+ */ const authzApi = gen3Api.injectEndpoints({
845
+ endpoints: (builder)=>({
846
+ getAuthzMappings: builder.query({
847
+ query: ()=>`${GEN3_AUTHZ_API}/mapping`
848
+ })
849
+ })
850
+ });
851
+ const { useGetAuthzMappingsQuery } = authzApi;
852
+ const selectAuthzMapping = authzApi.endpoints.getAuthzMappings.select();
853
+ const selectAuthzMappingData = createSelector(selectAuthzMapping, (authzMapping)=>authzMapping?.data ?? {
854
+ mappings: []
855
+ });
856
+
857
+ const HasEnoughData = (data, keys, limit)=>{
858
+ const numEmptyKeys = keys.filter((k)=>Object.hasOwn(data, k) && typeof data[k] === 'string' && data[k].trim() === '').length;
859
+ return numEmptyKeys < limit;
860
+ };
861
+ /**
862
+ * Defines metadataApi service using a base URL and expected endpoints. Derived from gen3Api core API.
863
+ *
864
+ * @param endpoints - Defines endpoints used in discovery page
865
+ * @param getAggMDS - Queries aggregate metadata service
866
+ * @see https://github.com/uc-cdis/metadata-service/blob/master/docs/agg_mds.md
867
+ * @see https://petstore.swagger.io/?url=https://raw.githubusercontent.com/uc-cdis/metadata-service/master/docs/openapi.yaml#/Aggregate/get_aggregate_metadata_aggregate_metadata_get
868
+ * @param getMDS - Queries normal metadata service
869
+ * @see https://petstore.swagger.io/?url=https://raw.githubusercontent.com/uc-cdis/metadata-service/master/docs/openapi.yaml#/Query/search_metadata_metadata_get
870
+ * @param getIndexAggMDS - queries the Aggregate Metadata service and returns all common passed in indexKeys
871
+ * @param getTags - Probably refering to Aggregate metadata service summary statistics query
872
+ * @see https://petstore.swagger.io/?url=https://raw.githubusercontent.com/uc-cdis/metadata-service/master/docs/openapi.yaml#/Aggregate/get_aggregate_tags_aggregate_tags_get
873
+ * @param getData - Looks like a duplicate of getMDS handler. unused in ./frontend package
874
+ * @param getCrosswalkData - Maps ids from one source to another
875
+ * @returns: A guppy download API for fetching bulk metadata
876
+ */ const metadataApi = gen3Api.injectEndpoints({
877
+ endpoints: (builder)=>({
878
+ getAggMDS: builder.query({
879
+ query: ({ offset, pageSize })=>{
880
+ return `${GEN3_MDS_API}/aggregate/metadata?flatten=true&pagination=true&offset=${offset}&limit=${pageSize}`;
881
+ },
882
+ transformResponse: (response, _meta, params)=>{
883
+ return {
884
+ data: response.results.map((x)=>{
885
+ const objValues = Object.values(x);
886
+ const firstValue = objValues ? objValues.at(0) : undefined;
887
+ return firstValue ? firstValue[params.studyField] : undefined;
888
+ }),
889
+ hits: response.pagination.hits
890
+ };
891
+ }
892
+ }),
893
+ getIndexAggMDS: builder.query({
894
+ query: ({ pageSize })=>{
895
+ return `${GEN3_MDS_API}/aggregate/metadata?limit=${pageSize}`;
896
+ },
897
+ transformResponse: (response, _meta, params)=>{
898
+ const dataFromIndexes = params.indexKeys.reduce((acc, key)=>{
899
+ if (response[key]) {
900
+ acc.push(...response[key]);
901
+ }
902
+ return acc;
903
+ }, []);
904
+ return {
905
+ data: dataFromIndexes.map((x)=>{
906
+ const objValues = Object.values(x);
907
+ const objIds = Object.keys(x);
908
+ let firstValue = objValues ? objValues.at(0) : undefined;
909
+ if (params?.filterEmpty) {
910
+ // remove any data that has < limit if defined
911
+ if (firstValue && !HasEnoughData(firstValue[params.studyField], params.filterEmpty.keys, params.filterEmpty.limit)) firstValue = undefined;
912
+ }
913
+ return firstValue ? {
914
+ gen3MDSGUID: objIds.at(0),
915
+ ...firstValue[params.studyField]
916
+ } : undefined;
917
+ }).filter((x)=>x !== undefined),
918
+ hits: dataFromIndexes.length
919
+ };
920
+ }
921
+ }),
922
+ getMDS: builder.query({
923
+ query: ({ guidType, offset, pageSize })=>{
924
+ return `${GEN3_MDS_API}/metadata?data=True&_guid_type=${guidType}&limit=${pageSize}&offset=${offset}`;
925
+ },
926
+ transformResponse: (response, _meta)=>{
927
+ return {
928
+ data: Object.keys(response).map((guid)=>response[guid]),
929
+ hits: Object.keys(response).length
930
+ };
931
+ }
932
+ }),
933
+ getTags: builder.query({
934
+ query: ()=>'tags'
935
+ }),
936
+ getData: builder.query({
937
+ query: (params)=>({
938
+ url: `metadata?${params}`
939
+ })
940
+ }),
941
+ // TODO: Move this to own slice
942
+ getCrosswalkData: builder.query({
943
+ queryFn: async (arg, _queryApi, _extraOptions, fetchWithBQ)=>{
944
+ const queryMultiple = async ()=>{
945
+ let result = [];
946
+ const queue = Queue({
947
+ concurrency: 15
948
+ });
949
+ for (const id of arg.ids){
950
+ queue.push(async (callback)=>{
951
+ const response = await fetchWithBQ({
952
+ url: `${GEN3_CROSSWALK_API}/metadata/${id}`
953
+ });
954
+ if (response.error) {
955
+ return {
956
+ error: response.error
957
+ };
958
+ }
959
+ const toData = arg.toPaths.reduce((acc, path)=>{
960
+ acc[path.id] = JSONPath({
961
+ json: response.data,
962
+ path: `$.[${path.dataPath}]`,
963
+ resultType: 'value'
964
+ })?.[0] ?? 'n/a';
965
+ return acc;
966
+ }, {});
967
+ result = [
968
+ ...result,
969
+ {
970
+ from: id,
971
+ to: toData
972
+ }
973
+ ];
974
+ callback && callback();
975
+ return result;
976
+ });
977
+ }
978
+ return new Promise((resolve, reject)=>{
979
+ queue.start((err)=>{
980
+ if (err) {
981
+ reject(err);
982
+ } else {
983
+ resolve(result);
984
+ }
985
+ });
986
+ });
987
+ };
988
+ const result = await queryMultiple();
989
+ return {
990
+ data: result
991
+ };
992
+ }
993
+ })
994
+ })
995
+ });
996
+ const { useGetAggMDSQuery, useGetMDSQuery, useGetTagsQuery, useGetDataQuery, useGetCrosswalkDataQuery, useLazyGetCrosswalkDataQuery, useGetIndexAggMDSQuery } = metadataApi;
997
+
998
+ // using a random uuid v4 as the namespace
999
+ const GEN3_APP_NAMESPACE = '7bfaa818-c69c-457e-8d87-413cf60c25f0';
1000
+ const getGen3AppId = (name, version)=>{
1001
+ const nameVersion = `${name}::${version}`;
1002
+ return v5(nameVersion, GEN3_APP_NAMESPACE);
1003
+ };
1004
+ /**
1005
+ * TODO: can't tell what anything in this directory is doing.
1006
+ */ const createGen3App = ({ App, name, version, requiredEntityTypes })=>{
1007
+ // create a stable id for this app
1008
+ const nameVersion = `${name}::${version}`;
1009
+ const id = v5(nameVersion, GEN3_APP_NAMESPACE);
1010
+ // need to create store and provider.
1011
+ // return a component representing this app
1012
+ // if component gets added to a list, then the list can be iterated in index.js and each provider component can be added
1013
+ // a route can be setup for the app
1014
+ // need to register its name, category, path, data requirements
1015
+ // this will be used to build page3
1016
+ // click app link
1017
+ const store = configureStore({
1018
+ // TODO allow user to pass in a reducer in CreateGen3AppOptions?
1019
+ reducer: (state)=>state,
1020
+ devTools: {
1021
+ name: `${nameVersion}::${id}`
1022
+ }
1023
+ });
1024
+ const Gen3AppWrapper = ()=>{
1025
+ useEffect(()=>{
1026
+ document.title = `GEN3 - ${name}`;
1027
+ });
1028
+ return /*#__PURE__*/ React.createElement(Provider, {
1029
+ store: store
1030
+ }, /*#__PURE__*/ React.createElement(CookiesProvider, null, /*#__PURE__*/ React.createElement(App, null)));
1031
+ };
1032
+ // add the app to the store
1033
+ coreStore.dispatch(addGen3AppMetadata({
1034
+ id,
1035
+ name,
1036
+ version,
1037
+ requiredEntityTypes
1038
+ }));
1039
+ registerGen3App(id, Gen3AppWrapper);
1040
+ return Gen3AppWrapper;
1041
+ };
1042
+
1043
+ const graphQLWithTags = gen3Api.enhanceEndpoints({
1044
+ addTagTypes: [
1045
+ 'graphQL'
1046
+ ]
1047
+ });
1048
+ /**
1049
+ * Creates a graphQLAPI for graphql queries to elasticsearch indices via guppy
1050
+ * @see https://github.com/uc-cdis/guppy/blob/master/doc/queries.md
1051
+ * @param query - Resolver function which configures the graphql query with graphQLParams argument
1052
+ * @returns: A guppy search API for fetching metadata
1053
+ */ const graphQLAPI = graphQLWithTags.injectEndpoints({
1054
+ endpoints: (builder)=>({
1055
+ graphQL: builder.query({
1056
+ query: (graphQLParams)=>({
1057
+ url: `${GEN3_GUPPY_API}/graphql`,
1058
+ method: 'POST',
1059
+ credentials: 'include',
1060
+ body: JSON.stringify(graphQLParams)
1061
+ })
1062
+ })
1063
+ })
1064
+ });
1065
+ const { useGraphQLQuery } = graphQLAPI;
1066
+
1067
+ const isOperationWithField = (operation)=>{
1068
+ return operation?.field !== undefined;
1069
+ };
1070
+ const extractFilterValue = (op)=>{
1071
+ const valueExtractorHandler = new ValueExtractorHandler();
1072
+ return handleOperation(valueExtractorHandler, op);
1073
+ };
1074
+ const extractEnumFilterValue = (op)=>{
1075
+ const enumValueExtractorHandler = new EnumValueExtractorHandler();
1076
+ const results = handleOperation(enumValueExtractorHandler, op);
1077
+ return results ?? [];
1078
+ };
1079
+ const assertNever = (x)=>{
1080
+ throw Error(`Exhaustive comparison did not handle: ${x}`);
1081
+ };
1082
+ const handleOperation = (handler, op)=>{
1083
+ switch(op.operator){
1084
+ case '=':
1085
+ return handler.handleEquals(op);
1086
+ case '!=':
1087
+ return handler.handleNotEquals(op);
1088
+ case '<':
1089
+ return handler.handleLessThan(op);
1090
+ case '<=':
1091
+ return handler.handleLessThanOrEquals(op);
1092
+ case '>':
1093
+ return handler.handleGreaterThan(op);
1094
+ case '>=':
1095
+ return handler.handleGreaterThanOrEquals(op);
1096
+ case 'and':
1097
+ return handler.handleIntersection(op);
1098
+ case 'or':
1099
+ return handler.handleUnion(op);
1100
+ case 'nested':
1101
+ return handler.handleNestedFilter(op);
1102
+ case 'in':
1103
+ return handler.handleIncludes(op);
1104
+ case 'excludeifany':
1105
+ return handler.handleExcludeIfAny(op);
1106
+ case 'excludes':
1107
+ return handler.handleExcludes(op);
1108
+ default:
1109
+ return assertNever(op);
1110
+ }
1111
+ };
1112
+ /**
1113
+ * Return true if a FilterSet's root value is an empty object
1114
+ * @param fs - FilterSet to test
1115
+ */ const isFilterEmpty = (fs)=>isEqual({}, fs);
1116
+ class ToGqlHandler {
1117
+ constructor(){
1118
+ this.handleEquals = (op)=>({
1119
+ '=': {
1120
+ [op.field]: op.operand
1121
+ }
1122
+ });
1123
+ this.handleNotEquals = (op)=>({
1124
+ '!=': {
1125
+ [op.field]: op.operand
1126
+ }
1127
+ });
1128
+ this.handleLessThan = (op)=>({
1129
+ '<': {
1130
+ [op.field]: op.operand
1131
+ }
1132
+ });
1133
+ this.handleLessThanOrEquals = (op)=>({
1134
+ '<=': {
1135
+ [op.field]: op.operand
1136
+ }
1137
+ });
1138
+ this.handleGreaterThan = (op)=>({
1139
+ '>': {
1140
+ [op.field]: op.operand
1141
+ }
1142
+ });
1143
+ this.handleGreaterThanOrEquals = (op)=>({
1144
+ '>=': {
1145
+ [op.field]: op.operand
1146
+ }
1147
+ });
1148
+ this.handleIncludes = (op)=>({
1149
+ in: {
1150
+ [op.field]: op.operands
1151
+ }
1152
+ });
1153
+ this.handleExcludes = (op)=>({
1154
+ exclude: {
1155
+ [op.field]: op.operands
1156
+ }
1157
+ });
1158
+ this.handleExcludeIfAny = (op)=>({
1159
+ excludeifany: {
1160
+ [op.field]: op.operands
1161
+ }
1162
+ });
1163
+ this.handleIntersection = (op)=>({
1164
+ and: op.operands.map((x)=>convertFilterToGqlFilter(x))
1165
+ });
1166
+ this.handleUnion = (op)=>({
1167
+ or: op.operands.map((x)=>convertFilterToGqlFilter(x))
1168
+ });
1169
+ this.handleNestedFilter = (op)=>{
1170
+ const child = convertFilterToGqlFilter(op.operand);
1171
+ return {
1172
+ nested: {
1173
+ path: op.path,
1174
+ ...child
1175
+ }
1176
+ };
1177
+ };
1178
+ }
1179
+ }
1180
+ const convertFilterToGqlFilter = (filter)=>{
1181
+ const handler = new ToGqlHandler();
1182
+ return handleOperation(handler, filter);
1183
+ };
1184
+ const convertFilterSetToGqlFilter = (fs, toplevelOp = 'and')=>{
1185
+ const fsKeys = Object.keys(fs.root);
1186
+ // if no keys return undefined
1187
+ if (fsKeys.length === 0) return {
1188
+ and: []
1189
+ };
1190
+ return toplevelOp === 'and' ? {
1191
+ and: fsKeys.map((key)=>convertFilterToGqlFilter(fs.root[key]))
1192
+ } : {
1193
+ or: fsKeys.map((key)=>convertFilterToGqlFilter(fs.root[key]))
1194
+ };
1195
+ };
1196
+ /**
1197
+ * Extract the operand values, if operands themselves have values, otherwise undefined.
1198
+ */ class ValueExtractorHandler {
1199
+ constructor(){
1200
+ this.handleEquals = (op)=>op.operand;
1201
+ this.handleNotEquals = (op)=>op.operand;
1202
+ this.handleIncludes = (op)=>op.operands;
1203
+ this.handleExcludes = (op)=>op.operands;
1204
+ this.handleExcludeIfAny = (op)=>op.operands;
1205
+ this.handleGreaterThanOrEquals = (op)=>op.operand;
1206
+ this.handleGreaterThan = (op)=>op.operand;
1207
+ this.handleLessThan = (op)=>op.operand;
1208
+ this.handleLessThanOrEquals = (op)=>op.operand;
1209
+ this.handleIntersection = (_)=>undefined;
1210
+ this.handleUnion = (_)=>undefined;
1211
+ this.handleNestedFilter = (_)=>undefined;
1212
+ }
1213
+ }
1214
+ /**
1215
+ * Extract the operand values, if operands themselves have values, otherwise undefined.
1216
+ */ class EnumValueExtractorHandler {
1217
+ constructor(){
1218
+ this.handleEquals = (_)=>undefined;
1219
+ this.handleNotEquals = (_)=>undefined;
1220
+ this.handleIncludes = (op)=>op.operands;
1221
+ this.handleExcludes = (op)=>op.operands;
1222
+ this.handleExcludeIfAny = (op)=>op.operands;
1223
+ this.handleGreaterThanOrEquals = (_)=>undefined;
1224
+ this.handleGreaterThan = (_)=>undefined;
1225
+ this.handleLessThan = (_)=>undefined;
1226
+ this.handleLessThanOrEquals = (_)=>undefined;
1227
+ this.handleIntersection = (_)=>undefined;
1228
+ this.handleUnion = (_)=>undefined;
1229
+ this.handleNestedFilter = (op)=>{
1230
+ return extractEnumFilterValue(op.operand);
1231
+ };
1232
+ }
1233
+ }
1234
+
1235
+ const FieldNameOverrides = {};
1236
+ const COMMON_PREPOSITIONS = [
1237
+ 'a',
1238
+ 'an',
1239
+ 'and',
1240
+ 'at',
1241
+ 'but',
1242
+ 'by',
1243
+ 'for',
1244
+ 'in',
1245
+ 'is',
1246
+ 'nor',
1247
+ 'of',
1248
+ 'on',
1249
+ 'or',
1250
+ 'out',
1251
+ 'so',
1252
+ 'the',
1253
+ 'to',
1254
+ 'up',
1255
+ 'yet'
1256
+ ];
1257
+ const capitalize = (s)=>s.length > 0 ? s[0].toUpperCase() + s.slice(1) : '';
1258
+ const trimFirstFieldNameToTitle = (fieldName, trim = false)=>{
1259
+ if (trim) {
1260
+ const source = fieldName.slice(fieldName.indexOf('.') + 1);
1261
+ return fieldNameToTitle(source ? source : fieldName, 0);
1262
+ }
1263
+ return fieldNameToTitle(fieldName);
1264
+ };
1265
+ /**
1266
+ * Converts a filter name to a title,
1267
+ * For example files.input.experimental_strategy will get converted to Experimental Strategy
1268
+ * if sections == 2 then the output would be Input Experimental Strategy
1269
+ * @param fieldName input filter expected to be: string.firstpart_secondpart
1270
+ * @param sections number of "sections" string.string.string to got back from the end of the field
1271
+ */ const fieldNameToTitle = (fieldName, sections = 1)=>{
1272
+ if (fieldName in FieldNameOverrides) {
1273
+ return FieldNameOverrides[fieldName];
1274
+ }
1275
+ if (fieldName === undefined) return 'No Title';
1276
+ return fieldName.split('.').slice(-sections).map((s)=>s.split('_')).flat().map((word)=>COMMON_PREPOSITIONS.includes(word) ? word : capitalize(word)).join(' ');
1277
+ };
1278
+ /**
1279
+ * Extracts the index name from the field name
1280
+ * @param fieldName
1281
+ */ const extractIndexFromFullFieldName = (fieldName)=>fieldName.split('.')[0];
1282
+ /**
1283
+ * prepend the index name to the field name
1284
+ */ const prependIndexToFieldName = (fieldName, index)=>`${index}.${fieldName}`;
1285
+ /**
1286
+ * extract the field name from the index.field name
1287
+ */ const extractFieldNameFromFullFieldName = (fieldName)=>fieldName.split('.').slice(1).join('.');
1288
+ /**
1289
+ * extract the field name and the index from the index.field name returning as a tuple
1290
+ */ const extractIndexAndFieldNameFromFullFieldName = (fieldName)=>{
1291
+ const [index, ...rest] = fieldName.split('.');
1292
+ return [
1293
+ index,
1294
+ rest.join('.')
1295
+ ];
1296
+ };
1297
+
1298
+ const statusEndpoint = '/_status';
1299
+ const processHistogramResponse = (data)=>{
1300
+ const pathData = JSONPath({
1301
+ json: data,
1302
+ path: '$..histogram',
1303
+ resultType: 'all'
1304
+ });
1305
+ const results = pathData.reduce((acc, element)=>{
1306
+ const key = element.pointer.slice(1).replace(/\/histogram/g, '').replace(/\//g, '.');
1307
+ return {
1308
+ ...acc,
1309
+ [key]: element.value
1310
+ };
1311
+ }, {});
1312
+ return results;
1313
+ };
1314
+ const fetchJson = async (url)=>{
1315
+ const res = await fetch(url, {
1316
+ method: 'GET',
1317
+ headers: {
1318
+ 'Content-type': 'application/json'
1319
+ }
1320
+ });
1321
+ if (!res.ok) throw new Error('An error occurred while fetching the data.');
1322
+ return await res.json();
1323
+ };
1324
+ const useGetStatus = ()=>{
1325
+ const fetcher = ()=>fetchJson(`${GEN3_GUPPY_API}${statusEndpoint}`);
1326
+ return useSWR('explorerStatus', fetcher);
1327
+ };
1328
+ /**
1329
+ * The main endpoint used in templating Exploration page queries.
1330
+ * Includes table, filter and aggregation query types and leverages guppyApi defined in ./gupplApi.ts
1331
+ * Query templates support filters where applicable
1332
+ *
1333
+ * @param endpoints - Defines endpoints used in Exploration page:
1334
+ * @param getAllFieldsForType - A mapping query that returns all property key names vertex types specified.
1335
+ * @see https://github.com/uc-cdis/guppy/blob/master/doc/queries.md#mapping-query
1336
+ * @param getAccessibleData - An aggregation histogram counts query that filters based on access type
1337
+ * @see https://github.com/uc-cdis/guppy/blob/master/doc/queries.md#accessibility-argument-for-regular-tier-access-level
1338
+ * @param getRawDataAndTotalCounts - Queries both _totalCount for selected vertex types and
1339
+ * tabular results containing the raw data in the rows of selected vertex types
1340
+ * @see https://github.com/uc-cdis/guppy/blob/master/doc/queries.md#1-total-count-aggregation
1341
+ * @param getAggs - An aggregated histogram counts query which outputs vertex property frequencies
1342
+ * @param getSubAggs - TODO: not sure what this one does. Looks like nested aggregation
1343
+ * @param getCounts - Returns total counts of a vertex type
1344
+ * @returns: A guppy API endpoint for templating queriable data displayed on the exploration page
1345
+ */ const explorerApi = guppyApi.injectEndpoints({
1346
+ endpoints: (builder)=>({
1347
+ getAllFieldsForType: builder.query({
1348
+ query: (type)=>({
1349
+ query: `{ _mapping ${type} } }`
1350
+ }),
1351
+ transformResponse: (response, _meta, params)=>{
1352
+ return response[params.type];
1353
+ }
1354
+ }),
1355
+ getAccessibleData: builder.query({
1356
+ query: ({ type, fields, accessType })=>{
1357
+ const fieldParts = fields.map((field)=>`${field} { histogram { key count } }`);
1358
+ return {
1359
+ query: `_aggregation {
1360
+ ${type} (accessibility: ${accessType}) {
1361
+ ${fieldParts.join(',')}
1362
+ }
1363
+ }`
1364
+ };
1365
+ }
1366
+ }),
1367
+ getRawDataAndTotalCounts: builder.query({
1368
+ query: ({ type, fields, filters, sort, offset = 0, size = 20, accessibility = Accessibility.ALL, format = undefined })=>{
1369
+ const gqlFilter = convertFilterSetToGqlFilter(filters);
1370
+ const params = [
1371
+ ...sort ? [
1372
+ '$sort: JSON'
1373
+ ] : [],
1374
+ ...gqlFilter ? [
1375
+ '$filter: JSON'
1376
+ ] : [],
1377
+ ...format ? [
1378
+ '$format: Format'
1379
+ ] : []
1380
+ ].join(',');
1381
+ const queryLine = `query getRawDataAndTotalCounts (${params}) {`;
1382
+ const dataParams = [
1383
+ ...format ? [
1384
+ 'format: $format'
1385
+ ] : [],
1386
+ ...sort ? [
1387
+ 'sort: $sort'
1388
+ ] : [],
1389
+ ...gqlFilter ? [
1390
+ 'filter: $filter'
1391
+ ] : []
1392
+ ].join(',');
1393
+ const dataTypeLine = `${type} (accessibility: ${accessibility}, offset: ${offset}, first: ${size},
1394
+ ${dataParams}) {`;
1395
+ const typeAggsLine = `${type} (${gqlFilter && 'filter: $filter,'} accessibility: ${accessibility}) {`;
1396
+ const processedFields = fields.map((field)=>rawDataQueryStrForEachField(field));
1397
+ const query = `${queryLine}
1398
+ ${dataTypeLine}
1399
+ ${processedFields.join(' ')}
1400
+ }
1401
+ _aggregation {
1402
+ ${typeAggsLine}
1403
+ _totalCount
1404
+ }
1405
+ }
1406
+ }`;
1407
+ const variables = {
1408
+ ...sort && {
1409
+ sort
1410
+ },
1411
+ ...gqlFilter && {
1412
+ filter: gqlFilter
1413
+ },
1414
+ ...format && {
1415
+ format
1416
+ }
1417
+ };
1418
+ return {
1419
+ query,
1420
+ variables
1421
+ };
1422
+ }
1423
+ }),
1424
+ getAggs: builder.query({
1425
+ query: ({ type, fields, filters, accessibility = Accessibility.ALL })=>{
1426
+ const queryStart = isFilterEmpty(filters) ? `
1427
+ query getAggs {
1428
+ _aggregation {
1429
+ ${type} (accessibility: ${accessibility}) {` : `query getAggs ($filter: JSON) {
1430
+ _aggregation {
1431
+ ${type} (filter: $filter, filterSelf: false, accessibility: ${accessibility}) {`;
1432
+ const query = `${queryStart}
1433
+ ${fields.map((field)=>histogramQueryStrForEachField(field))}
1434
+ }
1435
+ }
1436
+ }`;
1437
+ const queryBody = {
1438
+ query: query,
1439
+ variables: {
1440
+ filter: convertFilterSetToGqlFilter(filters)
1441
+ }
1442
+ };
1443
+ return queryBody;
1444
+ },
1445
+ transformResponse: (response, _meta, args)=>{
1446
+ return processHistogramResponse(response.data._aggregation[args.type]);
1447
+ }
1448
+ }),
1449
+ getSubAggs: builder.query({
1450
+ query: ({ type, mainField, termsFields = undefined, missingFields = undefined, numericAggAsText = false, gqlFilter = undefined, accessibility = Accessibility.ALL })=>{
1451
+ const nestedAggFields = {
1452
+ termsFields: termsFields,
1453
+ missingFields: missingFields
1454
+ };
1455
+ const query = `query getSubAggs ( ${gqlFilter ?? '$filter: JSON,'} $nestedAggFields: JSON) {
1456
+ _aggregation {
1457
+ ${type} ( ${gqlFilter ?? 'filter: $filter, filterSelf: false,'} nestedAggFields: $nestedAggFields, accessibility: ${accessibility}) {
1458
+ ${nestedHistogramQueryStrForEachField(mainField, numericAggAsText)}
1459
+ }`;
1460
+ return {
1461
+ query: query,
1462
+ variables: {
1463
+ ...gqlFilter && {
1464
+ filter: convertFilterSetToGqlFilter(gqlFilter)
1465
+ },
1466
+ nestedAggFields: nestedAggFields
1467
+ }
1468
+ };
1469
+ },
1470
+ transformResponse: (response, _meta, args)=>{
1471
+ return processHistogramResponse(response.data._aggregation[args.type]);
1472
+ }
1473
+ }),
1474
+ getCounts: builder.query({
1475
+ query: ({ type, filters, accessibility = Accessibility.ALL })=>{
1476
+ const gqlFilters = convertFilterSetToGqlFilter(filters);
1477
+ const queryLine = `query totalCounts ${gqlFilters ? '($filter: JSON)' : ''}{`;
1478
+ const typeAggsLine = `${type} ${gqlFilters ? '(filter: $filter, ' : '('} accessibility: ${accessibility}) {`;
1479
+ const query = `${queryLine} _aggregation {
1480
+ ${typeAggsLine}
1481
+ _totalCount
1482
+ }
1483
+ }
1484
+ }`;
1485
+ return {
1486
+ query: query,
1487
+ variables: {
1488
+ ...gqlFilters && {
1489
+ filter: gqlFilters
1490
+ }
1491
+ }
1492
+ };
1493
+ },
1494
+ transformResponse: (response, _meta, args)=>{
1495
+ return response.data._aggregation[args.type]._totalCount;
1496
+ }
1497
+ }),
1498
+ getFieldCountSummary: builder.query({
1499
+ query: ({ type, field, filters })=>{
1500
+ const gqlFilters = convertFilterSetToGqlFilter(filters);
1501
+ const query = `query ($filter: JSON) {
1502
+ _aggregation {
1503
+ ${type} (filter: $filter) {
1504
+ ${field} {
1505
+ histogram {
1506
+ sum
1507
+ }
1508
+ }
1509
+ }
1510
+ }
1511
+ }`;
1512
+ return {
1513
+ query: query,
1514
+ variables: {
1515
+ ...gqlFilters && {
1516
+ filter: gqlFilters
1517
+ }
1518
+ }
1519
+ };
1520
+ }
1521
+ }),
1522
+ getFieldsForIndex: builder.query({
1523
+ query: (index)=>{
1524
+ return {
1525
+ query: `{
1526
+ _mapping ${index}
1527
+ }`
1528
+ };
1529
+ },
1530
+ transformResponse: (response)=>{
1531
+ return response['_mapping'];
1532
+ }
1533
+ }),
1534
+ generalGQL: builder.query({
1535
+ query: ({ query, variables })=>{
1536
+ return {
1537
+ query: query,
1538
+ variables: variables
1539
+ };
1540
+ }
1541
+ })
1542
+ })
1543
+ });
1544
+ // query for aggregate data
1545
+ // convert the function below to typescript
1546
+ const histogramQueryStrForEachField = (field)=>{
1547
+ const splittedFieldArray = field.split('.');
1548
+ const splittedField = splittedFieldArray.shift();
1549
+ if (splittedFieldArray.length === 0) {
1550
+ return `
1551
+ ${splittedField} {
1552
+ histogram {
1553
+ key
1554
+ count
1555
+ }
1556
+ }`;
1557
+ }
1558
+ return `
1559
+ ${splittedField} {
1560
+ ${histogramQueryStrForEachField(splittedFieldArray.join('.'))}
1561
+ }`;
1562
+ };
1563
+ const nestedHistogramQueryStrForEachField = (mainField, numericAggAsText)=>`
1564
+ ${mainField} {
1565
+ ${numericAggAsText ? 'asTextHistogram' : 'histogram'} {
1566
+ key
1567
+ count
1568
+ missingFields {
1569
+ field
1570
+ count
1571
+ }
1572
+ termsFields {
1573
+ field
1574
+ count
1575
+ terms {
1576
+ key
1577
+ count
1578
+ }
1579
+ }
1580
+ }
1581
+ }`;
1582
+ const rawDataQueryStrForEachField = (field)=>{
1583
+ const splitFieldArray = field.split('.');
1584
+ const splitField = splitFieldArray.shift();
1585
+ if (splitFieldArray.length === 0) {
1586
+ return `
1587
+ ${splitField}
1588
+ `;
1589
+ }
1590
+ return `
1591
+ ${splitField} {
1592
+ ${rawDataQueryStrForEachField(splitFieldArray.join('.'))}
1593
+ }`;
1594
+ };
1595
+ const useGetArrayTypes = ()=>{
1596
+ {
1597
+ const { data, error } = useGetStatus();
1598
+ if (error) {
1599
+ return {};
1600
+ }
1601
+ return data ? data['indices'] : {};
1602
+ }
1603
+ };
1604
+ const { useGetRawDataAndTotalCountsQuery, useGetAccessibleDataQuery, useGetAllFieldsForTypeQuery, useGetAggsQuery, useGetSubAggsQuery, useGetCountsQuery, useGetFieldCountSummaryQuery, useGetFieldsForIndexQuery, useGeneralGQLQuery, useLazyGeneralGQLQuery } = explorerApi;
1605
+ const EmptyAggData = {};
1606
+ const selectAggDataForIndex = (state, index, field)=>{
1607
+ const data = state.guppyApi.aggs[index]?.[field]?.histogram;
1608
+ return data ?? EmptyAggData;
1609
+ };
1610
+
1611
+ /**
1612
+ * Flattens a deep nested JSON object skipping
1613
+ * the first level to avoid potentially flattening
1614
+ * non-nested data.
1615
+ * @param {JSON} json
1616
+ */ function flattenJson(json) {
1617
+ const flattenedJson = [];
1618
+ Object.keys(json).forEach((key)=>{
1619
+ flattenedJson.push(flatten(json[key], {
1620
+ delimiter: '_'
1621
+ }));
1622
+ });
1623
+ return flattenedJson;
1624
+ }
1625
+ /**
1626
+ * Converts JSON based on a config.
1627
+ * @param {JSON} json
1628
+ * @param {Object} config
1629
+ */ async function conversion(json, config) {
1630
+ return Papa.unparse(json, config);
1631
+ }
1632
+ /**
1633
+ * Converts JSON to a specified file format.
1634
+ * Defaults to JSON if file format is not supported.
1635
+ * @param {JSON} json
1636
+ * @param {string} format
1637
+ */ async function jsonToFormat(json, format) {
1638
+ if (Object.keys(FILE_DELIMITERS).includes(format)) {
1639
+ const flatJson = await flattenJson(json);
1640
+ const data = await conversion(flatJson, {
1641
+ delimiter: FILE_DELIMITERS[format]
1642
+ });
1643
+ return data;
1644
+ }
1645
+ return json;
1646
+ }
1647
+
1648
+ /**
1649
+ * Prepares a URL for downloading by appending '/download' to the provided apiUrl.
1650
+ *
1651
+ * @param {string} apiUrl - The base URL to be used for preparing the download URL.
1652
+ * @returns {URL} - The prepared download URL as a URL object.
1653
+ */ const prepareUrl = (apiUrl)=>new URL(apiUrl + '/download');
1654
+ /**
1655
+ * Prepares a fetch configuration object for downloading files from Guppy.
1656
+ *
1657
+ * @param {GuppyFileDownloadRequestParams} parameters - The parameters to include in the request body.
1658
+ * @param {string} csrfToken - The CSRF token to include in the request headers.
1659
+ * @returns {FetchConfig} - The prepared fetch configuration object.
1660
+ */ const prepareFetchConfig = (parameters, csrfToken)=>{
1661
+ return {
1662
+ method: 'POST',
1663
+ headers: {
1664
+ 'Content-Type': 'application/json',
1665
+ ...csrfToken !== undefined && {
1666
+ 'X-CSRF-Token': csrfToken
1667
+ }
1668
+ },
1669
+ body: JSON.stringify({
1670
+ type: parameters.type,
1671
+ filter: convertFilterSetToGqlFilter(parameters.filter),
1672
+ accessibility: parameters.accessibility,
1673
+ fields: parameters?.fields,
1674
+ sort: parameters?.sort
1675
+ })
1676
+ };
1677
+ };
1678
+ /**
1679
+ * Downloads a file from Guppy using the provided parameters.
1680
+ * It will optionally convert the data to the specified format.
1681
+ *
1682
+ * @param {DownloadFromGuppyParams} parameters - The parameters to use for the download request.
1683
+ * @param onStart - The function to call when the download starts.
1684
+ * @param onDone - The function to call when the download is done.
1685
+ * @param onError - The function to call when the download fails.
1686
+ * @param onAbort - The function to call when the download is aborted.
1687
+ * @param signal - AbortSignal to use for the request.
1688
+ */ const downloadFromGuppyToBlob = async ({ parameters, onStart = ()=>null, onDone = (_)=>null, onError = (_)=>null, onAbort = ()=>null, signal = undefined })=>{
1689
+ const csrfToken = selectCSRFToken(coreStore.getState());
1690
+ onStart?.();
1691
+ const url = prepareUrl(GEN3_GUPPY_API);
1692
+ const fetchConfig = prepareFetchConfig(parameters, csrfToken);
1693
+ fetch(url.toString(), {
1694
+ ...fetchConfig,
1695
+ ...signal ? {
1696
+ signal: signal
1697
+ } : {}
1698
+ }).then(async (response)=>{
1699
+ if (!response.ok) {
1700
+ throw new Error(response.statusText);
1701
+ }
1702
+ let jsonData = await response.json();
1703
+ if (parameters?.rootPath && parameters.rootPath) {
1704
+ // if rootPath is provided, extract the data from the rootPath
1705
+ jsonData = JSONPath({
1706
+ json: jsonData,
1707
+ path: `$.[${parameters.rootPath}]`,
1708
+ resultType: 'value'
1709
+ });
1710
+ }
1711
+ // convert the data to the specified format and return a Blob
1712
+ let str = '';
1713
+ if (parameters.format === 'json') {
1714
+ str = JSON.stringify(jsonData);
1715
+ } else {
1716
+ const convertedData = await jsonToFormat(jsonData, parameters.format);
1717
+ if (isJSONObject(convertedData)) {
1718
+ str = JSON.stringify(convertedData, null, 2);
1719
+ } else {
1720
+ str = convertedData;
1721
+ }
1722
+ }
1723
+ return new Blob([
1724
+ str
1725
+ ], {
1726
+ type: 'application/json'
1727
+ });
1728
+ }).then((blob)=>onDone?.(blob)).catch((error)=>{
1729
+ // Abort is handle as an exception
1730
+ if (error.name == 'AbortError') {
1731
+ // handle abort()
1732
+ onAbort?.();
1733
+ }
1734
+ onError?.(error);
1735
+ });
1736
+ };
1737
+ const downloadJSONDataFromGuppy = async ({ parameters, onAbort = ()=>null, signal = undefined })=>{
1738
+ const csrfToken = selectCSRFToken(coreStore.getState());
1739
+ const url = prepareUrl(GEN3_GUPPY_API);
1740
+ const fetchConfig = prepareFetchConfig(parameters, csrfToken);
1741
+ try {
1742
+ const response = await fetch(url.toString(), {
1743
+ ...fetchConfig,
1744
+ ...signal ? {
1745
+ signal: signal
1746
+ } : {}
1747
+ });
1748
+ let jsonData = await response.json();
1749
+ if (parameters?.rootPath && parameters.rootPath) {
1750
+ // if rootPath is provided, extract the data from the rootPath
1751
+ jsonData = JSONPath({
1752
+ json: jsonData,
1753
+ path: `$.[${parameters.rootPath}]`,
1754
+ resultType: 'value'
1755
+ });
1756
+ }
1757
+ // convert the data to the specified format and return a Blob
1758
+ return jsonData;
1759
+ } catch (error) {
1760
+ // Abort is handle as an exception
1761
+ if (error.name == 'AbortError') {
1762
+ // handle abort()
1763
+ onAbort?.();
1764
+ }
1765
+ throw new Error(error);
1766
+ }
1767
+ };
1768
+ const useGetIndexFields = (index)=>{
1769
+ const { data } = useGetFieldsForIndexQuery(index);
1770
+ return data ?? [];
1771
+ };
1772
+
1773
+ /**
1774
+ * Creates a Guppy API for fetching bulk (> 10K rows) elasticsearch data
1775
+ * @see https://github.com/uc-cdis/guppy/blob/master/doc/download.md
1776
+ * @param endpoints - Resolver function which configures the query with
1777
+ * type, filter, accessibility, fields, and sort arguments
1778
+ * @returns: A guppy download API for fetching bulk metadata
1779
+ */ const downloadRequestApi = gen3Api.injectEndpoints({
1780
+ endpoints: (builder)=>({
1781
+ downloadFromGuppy: builder.mutation({
1782
+ query: ({ type, filter, accessibility, fields, sort })=>{
1783
+ const queryBody = {
1784
+ filter: convertFilterSetToGqlFilter(filter),
1785
+ ...{
1786
+ type,
1787
+ accessibility,
1788
+ fields,
1789
+ sort
1790
+ }
1791
+ };
1792
+ return {
1793
+ url: `${GEN3_GUPPY_API}/download`,
1794
+ method: 'POST',
1795
+ queryBody,
1796
+ cache: 'no-cache'
1797
+ };
1798
+ },
1799
+ transformResponse: (response)=>{
1800
+ return response;
1801
+ }
1802
+ })
1803
+ })
1804
+ });
1805
+ const { useDownloadFromGuppyMutation } = downloadRequestApi;
1806
+
1807
+ /**
1808
+ * returns a response from the AI search service
1809
+ * @param searchParams - the parameters for the AI search service
1810
+ * @returns the response from the AI search service
1811
+ */ const aiSearchApi = gen3Api.injectEndpoints({
1812
+ endpoints: (builder)=>({
1813
+ askQuestion: builder.mutation({
1814
+ query: (searchParams)=>({
1815
+ url: `${GEN3_AI_SEARCH_API}/ask`,
1816
+ method: 'POST',
1817
+ credentials: 'include',
1818
+ headers: {
1819
+ Accept: 'application/json',
1820
+ 'Content-Type': 'application/json'
1821
+ },
1822
+ body: JSON.stringify({
1823
+ query: searchParams.query,
1824
+ ...searchParams.topic ? {
1825
+ topic: searchParams.topic
1826
+ } : {},
1827
+ ...searchParams.conversationId ? {
1828
+ conversation_id: searchParams.conversationId
1829
+ } : {}
1830
+ })
1831
+ }),
1832
+ transformResponse: (data, _, arg)=>{
1833
+ return {
1834
+ query: arg.query,
1835
+ response: data.response,
1836
+ topic: data.topic,
1837
+ conversationId: data.conversation_id,
1838
+ documents: data.documents
1839
+ };
1840
+ }
1841
+ }),
1842
+ /**
1843
+ * returns the status of the AI search service
1844
+ * @returns {
1845
+ * status: string
1846
+ * timestamp: string
1847
+ * }
1848
+ */ getAISearchStatus: builder.query({
1849
+ query: ()=>`${GEN3_AI_SEARCH_API}/_status`
1850
+ }),
1851
+ getAISearchVersion: builder.query({
1852
+ query: ()=>`${GEN3_AI_SEARCH_API}/_version`
1853
+ })
1854
+ })
1855
+ });
1856
+ // Add more endpoints here
1857
+ const { useAskQuestionMutation, useGetAISearchStatusQuery, useGetAISearchVersionQuery } = aiSearchApi;
1858
+
1859
+ const workspacesApi = gen3Api.injectEndpoints({
1860
+ endpoints: (builder)=>({
1861
+ getWorkspaceOptions: builder.query({
1862
+ query: ()=>`${GEN3_WORKSPACE_STATUS_API}/options`
1863
+ }),
1864
+ getWorkspacePayModels: builder.query({
1865
+ query: ()=>`${GEN3_WORKSPACE_STATUS_API}/allpaymodels`
1866
+ }),
1867
+ getWorkspaceStatus: builder.query({
1868
+ query: ()=>`${GEN3_WORKSPACE_STATUS_API}/status`
1869
+ })
1870
+ })
1871
+ });
1872
+ const { useGetWorkspaceOptionsQuery, useGetWorkspacePayModelsQuery, useGetWorkspaceStatusQuery } = workspacesApi;
1873
+
1874
+ const resourcePathFromProjectID = (projectID)=>{
1875
+ const split = projectID.split('-');
1876
+ const program = split[0];
1877
+ const project = split.slice(1).join('-');
1878
+ const resourcePath = [
1879
+ '/programs',
1880
+ program,
1881
+ 'projects',
1882
+ project
1883
+ ].join('/');
1884
+ return resourcePath;
1885
+ };
1886
+ const isRootUrl = (urlFragment)=>urlFragment === '_root';
1887
+ const isProgramUrl = (urlFragment)=>urlFragment !== '_root' && !urlFragment.includes('-');
1888
+ const userHasSheepdogProgramAdmin = (userAuthMapping = {})=>userAuthMapping['/services/sheepdog/submission/program'] !== undefined;
1889
+ const userHasSheepdogProjectAdmin = (userAuthMapping = {})=>userAuthMapping['/services/sheepdog/submission/project'] !== undefined;
1890
+ const projectCodeFromResourcePath = (resourcePath)=>{
1891
+ const split = resourcePath.split('/');
1892
+ return split.length < 5 || split[1] !== 'programs' || split[3] !== 'projects' ? '' : split[4];
1893
+ };
1894
+ const listifyMethodsFromMapping = (actions)=>{
1895
+ const reducer = (accumulator, currval)=>accumulator.concat([
1896
+ currval.method
1897
+ ]);
1898
+ return actions.reduce(reducer, []);
1899
+ };
1900
+ const userHasDataUpload = (userAuthMapping = {})=>{
1901
+ const actionIsFileUpload = (x)=>x.method === 'file_upload';
1902
+ const resource = userAuthMapping['/data_file'];
1903
+ return resource !== undefined && resource.some(actionIsFileUpload);
1904
+ };
1905
+ const userHasMethodForServiceOnResource = (method, service, resourcePath, userAuthMapping = {})=>{
1906
+ const actions = userAuthMapping[resourcePath];
1907
+ return actions !== undefined && actions.some((x)=>(x.service === service || x.service === '*') && (x.method === method || x.method === '*'));
1908
+ };
1909
+ const userHasMethodForServiceOnProject = (method, service, projectID, userAuthMapping = {})=>{
1910
+ const resourcePath = resourcePathFromProjectID(projectID);
1911
+ return userHasMethodForServiceOnResource(method, service, resourcePath, userAuthMapping);
1912
+ };
1913
+ const userHasMethodOnAnyProject = (method, userAuthMapping = {})=>{
1914
+ const actionHasMethod = (x)=>x.method === method;
1915
+ const actionArrays = Object.values(userAuthMapping);
1916
+ const hasMethod = actionArrays.some((x)=>x.some(actionHasMethod));
1917
+ return hasMethod;
1918
+ };
1919
+ const userHasCreateOrUpdateOnAnyProject = (userAuthMapping = {})=>userHasMethodOnAnyProject('create', userAuthMapping) || userHasMethodOnAnyProject('update', userAuthMapping);
1920
+
1921
+ const extractValuesFromObject = (jsonPathMappings, obj)=>{
1922
+ const result = {};
1923
+ const extractObjectValue = (jsonPath, obj)=>{
1924
+ const extractedValues = JSONPath({
1925
+ path: jsonPath,
1926
+ json: obj
1927
+ });
1928
+ return extractedValues.length > 0 ? extractedValues[0] : undefined;
1929
+ };
1930
+ for(const key in jsonPathMappings){
1931
+ if (key in Object.keys(jsonPathMappings)) {
1932
+ // Extract value from object and store it in the result.
1933
+ result[key] = extractObjectValue(jsonPathMappings[key], obj);
1934
+ }
1935
+ }
1936
+ return result;
1937
+ };
1938
+
1939
+ const SubmissionGraphqlQuery = `query transactionList {
1940
+ transactionList: transaction_log(last: 20) {
1941
+ id
1942
+ submitter
1943
+ project_id
1944
+ created_datetime
1945
+ documents {
1946
+ doc_size
1947
+ doc
1948
+ id
1949
+ }
1950
+ state
1951
+ }
1952
+ }`;
1953
+ /**
1954
+ * Defines submissionApi service using a base URL and expected endpoints. Derived from gen3Api core API.
1955
+ * @param endpoints - Defines endpoints used in submission page
1956
+ * @param getProjects - Queries the list of projects
1957
+ * @param getProjectsDetails - Queries the list of projects and their details
1958
+ * @param getSubmissionGraphQL - Queries the submission graphql with a query and variables
1959
+ * @returns: A submission API for fetching project details
1960
+ */ const submissionApi = gen3Api.injectEndpoints({
1961
+ endpoints: (builder)=>({
1962
+ getProjects: builder.query({
1963
+ query: (graphQLParams)=>({
1964
+ url: `${GEN3_SUBMISSION_API}/graphql`,
1965
+ method: 'POST',
1966
+ credentials: 'include',
1967
+ body: JSON.stringify(graphQLParams)
1968
+ }),
1969
+ transformResponse: (response, _meta)=>{
1970
+ return {
1971
+ projects: response.data.project
1972
+ };
1973
+ }
1974
+ }),
1975
+ getSubmissions: builder.query({
1976
+ query: ()=>({
1977
+ url: `${GEN3_SUBMISSION_API}/graphql`,
1978
+ method: 'POST',
1979
+ credentials: 'include',
1980
+ body: JSON.stringify({
1981
+ query: SubmissionGraphqlQuery
1982
+ })
1983
+ }),
1984
+ transformResponse: (response, _meta)=>{
1985
+ return {
1986
+ transactionList: response.data.transactionList
1987
+ };
1988
+ }
1989
+ }),
1990
+ getProjectsDetails: builder.query({
1991
+ async queryFn (args, _queryApi, _extraOptions, fetchWithBQ) {
1992
+ // get the list of projects
1993
+ const projectsResponse = await fetchWithBQ({
1994
+ url: `${GEN3_SUBMISSION_API}/graphql`,
1995
+ method: 'POST',
1996
+ credentials: 'include',
1997
+ body: JSON.stringify(args.projectQuery)
1998
+ });
1999
+ if (projectsResponse.error) return {
2000
+ error: projectsResponse.error
2001
+ };
2002
+ const projects = projectsResponse?.data.data.project;
2003
+ const projectIds = projects.map((p)=>p.project_id);
2004
+ // given the list of projects, get all of them by executing the projectDetailsQuery for each project
2005
+ const projectDetails = await Promise.all(projectIds.map(async (projectId)=>{
2006
+ const result = await fetchWithBQ({
2007
+ url: `${GEN3_SUBMISSION_API}/graphql`,
2008
+ method: 'POST',
2009
+ credentials: 'include',
2010
+ body: JSON.stringify({
2011
+ ...args.projectDetailsQuery,
2012
+ variables: {
2013
+ name: projectId
2014
+ }
2015
+ })
2016
+ });
2017
+ const { project, ...values } = result.data.data;
2018
+ return result.data ? {
2019
+ data: args.mapping ? {
2020
+ ...extractValuesFromObject(args.mapping, values),
2021
+ project: project[0]
2022
+ } : {
2023
+ ...values,
2024
+ project: project[0]
2025
+ }
2026
+ } : {
2027
+ error: result.error
2028
+ };
2029
+ }));
2030
+ // if any of the projectDetails has an error, return the error
2031
+ if (projectDetails.some((detail)=>'error' in detail)) {
2032
+ return {
2033
+ error: projectDetails.find((detail)=>'error' in detail)
2034
+ };
2035
+ } else return {
2036
+ data: {
2037
+ details: projectDetails.reduce((acc, detail)=>{
2038
+ acc.push(detail.data ?? {
2039
+ project: {
2040
+ name: '',
2041
+ id: '',
2042
+ code: ''
2043
+ }
2044
+ });
2045
+ return acc;
2046
+ }, []),
2047
+ hits: projectIds.length
2048
+ }
2049
+ };
2050
+ }
2051
+ }),
2052
+ getSubmissionGraphQL: builder.query({
2053
+ query: (graphQLParams)=>({
2054
+ url: `${GEN3_SUBMISSION_API}/graphql`,
2055
+ method: 'POST',
2056
+ credentials: 'include',
2057
+ body: JSON.stringify({
2058
+ query: graphQLParams.query,
2059
+ ...graphQLParams.variables ? {
2060
+ variables: graphQLParams.variables
2061
+ } : {}
2062
+ })
2063
+ }),
2064
+ transformResponse: (response, _meta, params)=>{
2065
+ if (params.mapping) {
2066
+ return {
2067
+ data: extractValuesFromObject(params.mapping, response.data)
2068
+ };
2069
+ }
2070
+ return response.data;
2071
+ }
2072
+ }),
2073
+ getDictionary: builder.query({
2074
+ query: ()=>({
2075
+ url: `${GEN3_SUBMISSION_API}/_dictionary/_all/`
2076
+ })
2077
+ })
2078
+ })
2079
+ });
2080
+ const { useGetProjectsQuery, useGetSubmissionGraphQLQuery, useGetProjectsDetailsQuery, useLazyGetProjectsQuery, useLazyGetSubmissionGraphQLQuery, useGetSubmissionsQuery, useGetDictionaryQuery } = submissionApi;
2081
+
2082
+ export { Accessibility, CoreContext, CoreProvider, GEN3_API, GEN3_AUTHZ_API, GEN3_COMMONS_NAME, GEN3_CROSSWALK_API, GEN3_DOMAIN, GEN3_DOWNLOADS_ENDPOINT, GEN3_FENCE_API, GEN3_GUPPY_API, GEN3_MDS_API, GEN3_REDIRECT_URL, GEN3_SUBMISSION_API, GEN3_WORKSPACE_STATUS_API, Modals, clearCohortFilters, cohortReducer, convertFilterSetToGqlFilter, coreStore, createGen3App, createUseCoreDataHook, downloadFromGuppyToBlob, downloadJSONDataFromGuppy, drsHostnamesReducer, extractEnumFilterValue, extractFieldNameFromFullFieldName, extractFilterValue, extractIndexAndFieldNameFromFullFieldName, extractIndexFromFullFieldName, fetchFence, fetchJson, fetchUserState, fieldNameToTitle, gen3Api, getGen3AppId, graphQLAPI, graphQLWithTags, guppyAPISliceMiddleware, guppyApi, guppyApiReducer, guppyApiSliceReducerPath, handleOperation, hideModal, isAuthenticated, isErrorWithMessage, isFetchBaseQueryError, isFetchParseError, isFilterEmpty, isGuppyAggregationData, isHistogramData, isHistogramDataAArray, isHistogramDataAnEnum, isHistogramDataArray, isHistogramDataArrayARange, isHistogramDataArrayAnEnum, isHistogramDataCollection, isHistogramRangeData, isJSONObject, isJSONValue, isJSONValueArray, isOperationWithField, isPending, isProgramUrl, isRootUrl, listifyMethodsFromMapping, logoutFence, prependIndexToFieldName, projectCodeFromResourcePath, rawDataQueryStrForEachField, removeCohortFilter, resetUserState, resourcePathFromProjectID, selectAggDataForIndex, selectAuthzMappingData, selectCSRFToken, selectCSRFTokenData, selectCohortFilters, selectCurrentCohort, selectCurrentCohortId, selectCurrentCohortName, selectCurrentMessage, selectCurrentModal, selectGen3AppById, selectGen3AppMetadataById, selectHeadersWithCSRFToken, selectIndexFilters, selectIndexedFilterByName, selectUser, selectUserAuthStatus, selectUserData, selectUserDetails, selectUserLoginStatus, setDRSHostnames, showModal, submissionApi, trimFirstFieldNameToTitle, updateCohortFilter, useAddNewCredentialMutation, useAskQuestionMutation, useAuthorizeFromCredentialsMutation, useCoreDispatch, useCoreSelector, useCoreStore, useDownloadFromGuppyMutation, useFetchUserDetailsQuery, useGeneralGQLQuery, useGetAISearchStatusQuery, useGetAISearchVersionQuery, useGetAccessibleDataQuery, useGetAggMDSQuery, useGetAggsQuery, useGetAllFieldsForTypeQuery, useGetArrayTypes, useGetAuthzMappingsQuery, useGetCSRFQuery, useGetCountsQuery, useGetCredentialsQuery, useGetCrosswalkDataQuery, useGetDataQuery, useGetDictionaryQuery, useGetExternalLoginsQuery, useGetFieldCountSummaryQuery, useGetFieldsForIndexQuery, useGetIndexAggMDSQuery, useGetIndexFields, useGetJWKKeysQuery, useGetLoginProvidersQuery, useGetMDSQuery, useGetProjectsDetailsQuery, useGetProjectsQuery, useGetRawDataAndTotalCountsQuery, useGetStatus, useGetSubAggsQuery, useGetSubmissionGraphQLQuery, useGetSubmissionsQuery, useGetTagsQuery, useGetWorkspaceOptionsQuery, useGetWorkspacePayModelsQuery, useGetWorkspaceStatusQuery, useGraphQLQuery, useIsExternalConnectedQuery, useIsUserLoggedIn, useLazyFetchUserDetailsQuery, useLazyGeneralGQLQuery, useLazyGetCrosswalkDataQuery, useLazyGetExternalLoginsQuery, useLazyGetProjectsQuery, useLazyGetSubmissionGraphQLQuery, useLazyIsExternalConnectedQuery, usePrevious, useRemoveCredentialMutation, useUser, useUserAuth, userHasCreateOrUpdateOnAnyProject, userHasDataUpload, userHasMethodForServiceOnProject, userHasMethodForServiceOnResource, userHasMethodOnAnyProject, userHasSheepdogProgramAdmin, userHasSheepdogProjectAdmin };
2083
+ //# sourceMappingURL=index.js.map