@databiosphere/findable-ui 51.1.0 → 52.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/common/filters/adapters/tanstack/typeGuards.d.ts +7 -0
- package/lib/common/filters/adapters/tanstack/typeGuards.js +11 -0
- package/lib/common/filters/hooks/UseValidateFilterKeys/hook.d.ts +32 -0
- package/lib/common/filters/hooks/UseValidateFilterKeys/hook.js +16 -0
- package/lib/common/filters/hooks/UseValidateFilterKeys/utils.d.ts +18 -0
- package/lib/common/filters/hooks/UseValidateFilterKeys/utils.js +47 -0
- package/lib/common/filters/typeGuards.d.ts +1 -1
- package/lib/components/DataDictionary/dataDictionary.js +8 -0
- package/lib/components/DataDictionary/utils.d.ts +13 -0
- package/lib/components/DataDictionary/utils.js +16 -0
- package/lib/config/entities.d.ts +13 -1
- package/lib/config/entities.js +5 -0
- package/lib/google/service.d.ts +5 -3
- package/lib/google/service.js +12 -36
- package/lib/google/services/authorizationCodeFlow.d.ts +10 -0
- package/lib/google/services/authorizationCodeFlow.js +38 -0
- package/lib/google/services/common.d.ts +23 -0
- package/lib/google/services/common.js +54 -0
- package/lib/google/services/implicitFlow.d.ts +9 -0
- package/lib/google/services/implicitFlow.js +17 -0
- package/lib/google/types.d.ts +11 -0
- package/lib/providers/exploreState/initializer/utils.d.ts +8 -1
- package/lib/providers/exploreState/initializer/utils.js +1 -1
- package/lib/views/DataDictionaryView/dataDictionaryView.js +6 -0
- package/lib/views/ExploreView/hooks/UseValidateFilterParam/hook.d.ts +4 -3
- package/lib/views/ExploreView/hooks/UseValidateFilterParam/hook.js +11 -46
- package/lib/views/ExploreView/hooks/UseValidateFilterParam/utils.d.ts +16 -0
- package/lib/views/ExploreView/hooks/UseValidateFilterParam/utils.js +29 -0
- package/package.json +1 -3
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ColumnFilter } from "@tanstack/react-table";
|
|
2
|
+
/**
|
|
3
|
+
* Returns true if the value is a valid TanStack ColumnFilter shape.
|
|
4
|
+
* @param value - Value to check.
|
|
5
|
+
* @returns true if the value has the expected shape.
|
|
6
|
+
*/
|
|
7
|
+
export declare function isColumnFilter(value: unknown): value is ColumnFilter;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns true if the value is a valid TanStack ColumnFilter shape.
|
|
3
|
+
* @param value - Value to check.
|
|
4
|
+
* @returns true if the value has the expected shape.
|
|
5
|
+
*/
|
|
6
|
+
export function isColumnFilter(value) {
|
|
7
|
+
if (typeof value !== "object" || value === null)
|
|
8
|
+
return false;
|
|
9
|
+
const filter = value;
|
|
10
|
+
return typeof filter.id === "string" && "value" in filter;
|
|
11
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates the filter query parameter shape only (no key validation).
|
|
3
|
+
* Throws a DataExplorerError during render if the filter param is present
|
|
4
|
+
* but contains malformed JSON or an invalid entry shape.
|
|
5
|
+
* @param queryParamKey - URL query parameter key to read (e.g. "filter").
|
|
6
|
+
* @param validKeys - Must be undefined to use this overload.
|
|
7
|
+
* @param entryValidator - Type guard that validates each parsed entry's shape.
|
|
8
|
+
* @returns void; throws DataExplorerError on invalid input.
|
|
9
|
+
*/
|
|
10
|
+
export declare function useValidateFilterKeys(queryParamKey: string, validKeys: undefined, entryValidator: (value: unknown) => boolean): void;
|
|
11
|
+
/**
|
|
12
|
+
* Validates the filter query parameter shape and keys against a valid set.
|
|
13
|
+
* Throws a DataExplorerError during render if the filter param is present
|
|
14
|
+
* but contains malformed JSON, an invalid entry shape, or a key that
|
|
15
|
+
* is not in the valid set.
|
|
16
|
+
* @param queryParamKey - URL query parameter key to read (e.g. "filter").
|
|
17
|
+
* @param validKeys - Set of valid keys.
|
|
18
|
+
* @param entryValidator - Type guard that validates each parsed entry's shape.
|
|
19
|
+
* @param keyExtractor - Extracts the key string from a validated entry.
|
|
20
|
+
* @returns void; throws DataExplorerError on invalid input.
|
|
21
|
+
*/
|
|
22
|
+
export declare function useValidateFilterKeys(queryParamKey: string, validKeys: Set<string>, entryValidator: (value: unknown) => boolean, keyExtractor: (entry: unknown) => string): void;
|
|
23
|
+
/**
|
|
24
|
+
* Validates the filter query parameter shape and, when validKeys is defined,
|
|
25
|
+
* keys against the valid set. Use when validKeys is dynamically determined.
|
|
26
|
+
* @param queryParamKey - URL query parameter key to read (e.g. "filter").
|
|
27
|
+
* @param validKeys - Set of valid keys, or undefined to skip key validation.
|
|
28
|
+
* @param entryValidator - Type guard that validates each parsed entry's shape.
|
|
29
|
+
* @param keyExtractor - Extracts the key string from a validated entry.
|
|
30
|
+
* @returns void; throws DataExplorerError on invalid input.
|
|
31
|
+
*/
|
|
32
|
+
export declare function useValidateFilterKeys(queryParamKey: string, validKeys: Set<string> | undefined, entryValidator: (value: unknown) => boolean, keyExtractor: (entry: unknown) => string): void;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useRouter } from "next/router";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { validateFilterParam } from "./utils";
|
|
4
|
+
export function useValidateFilterKeys(queryParamKey, validKeys, entryValidator, keyExtractor) {
|
|
5
|
+
const { query } = useRouter();
|
|
6
|
+
const filterParam = query[queryParamKey];
|
|
7
|
+
const validationError = useMemo(() => {
|
|
8
|
+
if (validKeys && keyExtractor) {
|
|
9
|
+
return validateFilterParam(filterParam, validKeys, entryValidator, keyExtractor);
|
|
10
|
+
}
|
|
11
|
+
return validateFilterParam(filterParam, undefined, entryValidator);
|
|
12
|
+
}, [entryValidator, filterParam, keyExtractor, validKeys]);
|
|
13
|
+
if (validationError) {
|
|
14
|
+
throw validationError;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { DataExplorerError } from "../../../../types/error";
|
|
2
|
+
/**
|
|
3
|
+
* Validates a filter query parameter value (shape only, no key validation).
|
|
4
|
+
* @param filterParam - Raw URL query param value.
|
|
5
|
+
* @param validKeys - Must be undefined to use this overload.
|
|
6
|
+
* @param entryValidator - Type guard that validates each parsed entry's shape.
|
|
7
|
+
* @returns DataExplorerError if invalid, undefined if valid.
|
|
8
|
+
*/
|
|
9
|
+
export declare function validateFilterParam(filterParam: string | string[] | undefined, validKeys: undefined, entryValidator: (value: unknown) => boolean): DataExplorerError | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Validates a filter query parameter value against a set of valid keys.
|
|
12
|
+
* @param filterParam - Raw URL query param value.
|
|
13
|
+
* @param validKeys - Set of valid keys.
|
|
14
|
+
* @param entryValidator - Type guard that validates each parsed entry's shape.
|
|
15
|
+
* @param keyExtractor - Extracts the key string from a validated entry.
|
|
16
|
+
* @returns DataExplorerError if invalid, undefined if valid.
|
|
17
|
+
*/
|
|
18
|
+
export declare function validateFilterParam(filterParam: string | string[] | undefined, validKeys: Set<string>, entryValidator: (value: unknown) => boolean, keyExtractor: (entry: unknown) => string): DataExplorerError | undefined;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { DataExplorerError } from "../../../../types/error";
|
|
2
|
+
const INVALID_FILTER_PARAM = "Invalid filter parameter in URL";
|
|
3
|
+
const INVALID_FILTER_SHAPE = "Invalid filter entry shape in URL";
|
|
4
|
+
const UNKNOWN_FILTER_KEY = "Unknown filter key in URL";
|
|
5
|
+
export function validateFilterParam(filterParam, validKeys, entryValidator, keyExtractor) {
|
|
6
|
+
if (filterParam === undefined)
|
|
7
|
+
return undefined;
|
|
8
|
+
if (typeof filterParam !== "string") {
|
|
9
|
+
return new DataExplorerError({ message: INVALID_FILTER_PARAM });
|
|
10
|
+
}
|
|
11
|
+
// Try JSON.parse directly first (Next.js router.query already decodes),
|
|
12
|
+
// then fall back to decodeURIComponent + JSON.parse for encoded values.
|
|
13
|
+
let parsed;
|
|
14
|
+
try {
|
|
15
|
+
parsed = JSON.parse(filterParam);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
try {
|
|
19
|
+
parsed = JSON.parse(decodeURIComponent(filterParam));
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return new DataExplorerError({ message: INVALID_FILTER_PARAM });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (!Array.isArray(parsed)) {
|
|
26
|
+
return new DataExplorerError({ message: INVALID_FILTER_PARAM });
|
|
27
|
+
}
|
|
28
|
+
// An empty array (e.g. "[]") is valid — it means no filters selected.
|
|
29
|
+
if (parsed.length === 0)
|
|
30
|
+
return undefined;
|
|
31
|
+
// Validate shape: every entry must match the expected shape.
|
|
32
|
+
if (!parsed.every(entryValidator)) {
|
|
33
|
+
return new DataExplorerError({ message: INVALID_FILTER_SHAPE });
|
|
34
|
+
}
|
|
35
|
+
// Validate keys: every extracted key must be in the valid set.
|
|
36
|
+
if (validKeys && keyExtractor) {
|
|
37
|
+
const invalidKey = parsed
|
|
38
|
+
.map(keyExtractor)
|
|
39
|
+
.find((key) => !validKeys.has(key));
|
|
40
|
+
if (invalidKey !== undefined) {
|
|
41
|
+
return new DataExplorerError({
|
|
42
|
+
message: `${UNKNOWN_FILTER_KEY}: ${invalidKey}`,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Fade } from "@mui/material";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { isColumnFilter } from "../../common/filters/adapters/tanstack/typeGuards";
|
|
5
|
+
import { useValidateFilterKeys } from "../../common/filters/hooks/UseValidateFilterKeys/hook";
|
|
3
6
|
import { PROPERTY } from "../../hooks/useHtmlStyle/constants";
|
|
4
7
|
import { useHtmlStyle } from "../../hooks/useHtmlStyle/hook";
|
|
5
8
|
import { useLayoutSpacing } from "../../hooks/UseLayoutSpacing/hook";
|
|
9
|
+
import { DATA_DICTIONARY_URL_PARAMS } from "../../providers/dataDictionaryState/dictionaries/constants";
|
|
6
10
|
import { Description } from "./components/Description/description";
|
|
7
11
|
import { Entities } from "./components/Entities/entities";
|
|
8
12
|
import { ColumnFilterTags } from "./components/Filters/components/ColumnFilterTags/columnFilterTags";
|
|
@@ -18,6 +22,7 @@ import { Title as DefaultTitle } from "./components/Title/title";
|
|
|
18
22
|
import { View } from "./dataDictionary.styles";
|
|
19
23
|
import { useDataDictionaryConfig } from "./hooks/UseDataDictionaryConfig/hook";
|
|
20
24
|
import { useMeasureFilters } from "./hooks/UseMeasureFilters/hook";
|
|
25
|
+
import { extractColumnId, getValidColumnIds } from "./utils";
|
|
21
26
|
export const DataDictionary = ({ className, dictionary, EntitiesLayout = DefaultEntitiesLayout, FiltersLayout = DefaultFiltersLayout, Outline = DefaultOutline, OutlineLayout = DefaultOutlineLayout, Title = DefaultTitle, TitleLayout = DefaultTitleLayout, }) => {
|
|
22
27
|
// Get dictionary configuration.
|
|
23
28
|
const { classes, description, tableOptions, title } = useDataDictionaryConfig(dictionary);
|
|
@@ -30,6 +35,9 @@ export const DataDictionary = ({ className, dictionary, EntitiesLayout = Default
|
|
|
30
35
|
const entitiesSpacing = { ...spacing, top: dimensions.height };
|
|
31
36
|
// Table instance.
|
|
32
37
|
const table = useTable(dictionary, classes, tableOptions);
|
|
38
|
+
// Validate filter URL param keys against the table's column IDs.
|
|
39
|
+
const validColumnIds = useMemo(() => getValidColumnIds(table), [table]);
|
|
40
|
+
useValidateFilterKeys(DATA_DICTIONARY_URL_PARAMS.COLUMN_FILTERS, validColumnIds, isColumnFilter, extractColumnId);
|
|
33
41
|
// Dictionary outline.
|
|
34
42
|
const outline = buildClassesOutline(table);
|
|
35
43
|
// Update scroll-padding-top on the HTML element.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { RowData, Table } from "@tanstack/react-table";
|
|
2
|
+
/**
|
|
3
|
+
* Extracts the column ID from a validated ColumnFilter entry.
|
|
4
|
+
* @param entry - Validated entry.
|
|
5
|
+
* @returns column ID.
|
|
6
|
+
*/
|
|
7
|
+
export declare function extractColumnId(entry: unknown): string;
|
|
8
|
+
/**
|
|
9
|
+
* Returns the set of valid column IDs from a TanStack table instance.
|
|
10
|
+
* @param table - TanStack table instance.
|
|
11
|
+
* @returns set of valid column IDs.
|
|
12
|
+
*/
|
|
13
|
+
export declare function getValidColumnIds<T extends RowData>(table: Table<T>): Set<string>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts the column ID from a validated ColumnFilter entry.
|
|
3
|
+
* @param entry - Validated entry.
|
|
4
|
+
* @returns column ID.
|
|
5
|
+
*/
|
|
6
|
+
export function extractColumnId(entry) {
|
|
7
|
+
return entry.id;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Returns the set of valid column IDs from a TanStack table instance.
|
|
11
|
+
* @param table - TanStack table instance.
|
|
12
|
+
* @returns set of valid column IDs.
|
|
13
|
+
*/
|
|
14
|
+
export function getValidColumnIds(table) {
|
|
15
|
+
return new Set(table.getAllColumns().map((column) => column.id));
|
|
16
|
+
}
|
package/lib/config/entities.d.ts
CHANGED
|
@@ -219,7 +219,11 @@ export interface ListViewConfig {
|
|
|
219
219
|
rowPreviewView?: ComponentsConfig;
|
|
220
220
|
rowSelectionView?: ComponentsConfig;
|
|
221
221
|
}
|
|
222
|
-
export
|
|
222
|
+
export declare enum OAUTH_FLOW {
|
|
223
|
+
AUTHORIZATION_CODE = "AUTHORIZATION_CODE",
|
|
224
|
+
IMPLICIT = "IMPLICIT"
|
|
225
|
+
}
|
|
226
|
+
interface OAuthProviderBase<P = any> {
|
|
223
227
|
authorization: {
|
|
224
228
|
params: {
|
|
225
229
|
scope: string;
|
|
@@ -232,6 +236,14 @@ export interface OAuthProvider<P = any> {
|
|
|
232
236
|
profile: (profile: P) => UserProfile;
|
|
233
237
|
userinfo: string;
|
|
234
238
|
}
|
|
239
|
+
export interface ImplicitFlowProvider<P = any> extends OAuthProviderBase<P> {
|
|
240
|
+
flow: OAUTH_FLOW.IMPLICIT;
|
|
241
|
+
}
|
|
242
|
+
export interface AuthorizationCodeFlowProvider<P = any> extends OAuthProviderBase<P> {
|
|
243
|
+
authorize: string;
|
|
244
|
+
flow: OAUTH_FLOW.AUTHORIZATION_CODE;
|
|
245
|
+
}
|
|
246
|
+
export type OAuthProvider<P = any> = ImplicitFlowProvider<P> | AuthorizationCodeFlowProvider<P>;
|
|
235
247
|
/**
|
|
236
248
|
* Option Method.
|
|
237
249
|
*/
|
package/lib/config/entities.js
CHANGED
package/lib/google/service.d.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
import { OAuthProvider } from "../config/entities";
|
|
2
|
-
import {
|
|
1
|
+
import { type OAuthProvider } from "../config/entities";
|
|
2
|
+
import { type LoginDispatch } from "./services/common";
|
|
3
|
+
import type { SessionDispatch } from "./types";
|
|
3
4
|
/**
|
|
4
5
|
* Google Sign-In service.
|
|
5
6
|
*/
|
|
6
7
|
export declare const service: {
|
|
7
8
|
/**
|
|
8
9
|
* Login with Google OAuth.
|
|
10
|
+
* Dispatches to the configured flow based on `provider.flow`.
|
|
9
11
|
* @param provider - OAuth provider configuration.
|
|
10
12
|
* @param dispatch - Dispatch functions for auth state.
|
|
11
13
|
*/
|
|
12
|
-
login: (provider: OAuthProvider, dispatch:
|
|
14
|
+
login: (provider: OAuthProvider, dispatch: LoginDispatch) => void;
|
|
13
15
|
/**
|
|
14
16
|
* Logout and clear all auth state.
|
|
15
17
|
* @param dispatch - Dispatch functions for auth state.
|
package/lib/google/service.js
CHANGED
|
@@ -1,54 +1,30 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { AUTHENTICATION_STATUS } from "../auth/types/authentication";
|
|
6
|
-
import { fetchProfile, getAuthenticationRequestOptions } from "./utils/auth";
|
|
1
|
+
import { OAUTH_FLOW } from "../config/entities";
|
|
2
|
+
import { login as authorizationCodeFlowLogin } from "./services/authorizationCodeFlow";
|
|
3
|
+
import { logout } from "./services/common";
|
|
4
|
+
import { login as implicitFlowLogin } from "./services/implicitFlow";
|
|
7
5
|
/**
|
|
8
6
|
* Google Sign-In service.
|
|
9
7
|
*/
|
|
10
8
|
export const service = {
|
|
11
9
|
/**
|
|
12
10
|
* Login with Google OAuth.
|
|
11
|
+
* Dispatches to the configured flow based on `provider.flow`.
|
|
13
12
|
* @param provider - OAuth provider configuration.
|
|
14
13
|
* @param dispatch - Dispatch functions for auth state.
|
|
15
14
|
*/
|
|
16
15
|
login: (provider, dispatch) => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
dispatch.tokenDispatch?.(updateToken({ providerId: id, token }));
|
|
24
|
-
fetchProfile(userinfo, getAuthenticationRequestOptions(token), {
|
|
25
|
-
onError: () => {
|
|
26
|
-
dispatch.authDispatch?.(resetAuthState());
|
|
27
|
-
dispatch.authenticationDispatch?.(updateAuthentication({
|
|
28
|
-
profile: undefined,
|
|
29
|
-
status: AUTHENTICATION_STATUS.SETTLED,
|
|
30
|
-
}));
|
|
31
|
-
dispatch.tokenDispatch?.(resetTokenState());
|
|
32
|
-
},
|
|
33
|
-
onSuccess: (r) => dispatch.authenticationDispatch?.(updateAuthentication({
|
|
34
|
-
profile: profile(r),
|
|
35
|
-
status: AUTHENTICATION_STATUS.PENDING, // Authentication is pending until session controller is resolved.
|
|
36
|
-
})),
|
|
37
|
-
});
|
|
38
|
-
},
|
|
39
|
-
client_id: provider.clientId,
|
|
40
|
-
scope: provider.authorization.params.scope,
|
|
41
|
-
});
|
|
42
|
-
client.requestAccessToken();
|
|
16
|
+
if (provider.flow === OAUTH_FLOW.AUTHORIZATION_CODE) {
|
|
17
|
+
authorizationCodeFlowLogin(provider, dispatch);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
implicitFlowLogin(provider, dispatch);
|
|
21
|
+
}
|
|
43
22
|
},
|
|
44
23
|
/**
|
|
45
24
|
* Logout and clear all auth state.
|
|
46
25
|
* @param dispatch - Dispatch functions for auth state.
|
|
47
26
|
*/
|
|
48
27
|
logout: (dispatch) => {
|
|
49
|
-
dispatch
|
|
50
|
-
dispatch.authenticationDispatch?.(resetAuthenticationState());
|
|
51
|
-
dispatch.credentialsDispatch?.(resetCredentialsState());
|
|
52
|
-
dispatch.tokenDispatch?.(resetTokenState());
|
|
28
|
+
logout(dispatch);
|
|
53
29
|
},
|
|
54
30
|
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AuthorizationCodeFlowProvider } from "../../config/entities";
|
|
2
|
+
import { type LoginDispatch } from "./common";
|
|
3
|
+
/**
|
|
4
|
+
* Login using the OAuth 2.0 authorization code flow.
|
|
5
|
+
* Uses Google's initCodeClient to request an authorization code,
|
|
6
|
+
* then exchanges it for an access token via the configured authorize endpoint.
|
|
7
|
+
* @param provider - OAuth provider configuration.
|
|
8
|
+
* @param dispatch - Dispatch functions for auth state.
|
|
9
|
+
*/
|
|
10
|
+
export declare function login(provider: AuthorizationCodeFlowProvider, dispatch: LoginDispatch): void;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createOnAccessToken, createResetSession, } from "./common";
|
|
2
|
+
/**
|
|
3
|
+
* Login using the OAuth 2.0 authorization code flow.
|
|
4
|
+
* Uses Google's initCodeClient to request an authorization code,
|
|
5
|
+
* then exchanges it for an access token via the configured authorize endpoint.
|
|
6
|
+
* @param provider - OAuth provider configuration.
|
|
7
|
+
* @param dispatch - Dispatch functions for auth state.
|
|
8
|
+
*/
|
|
9
|
+
export function login(provider, dispatch) {
|
|
10
|
+
const { authorize } = provider;
|
|
11
|
+
const resetSession = createResetSession(dispatch);
|
|
12
|
+
const onAccessToken = createOnAccessToken(provider, dispatch, resetSession);
|
|
13
|
+
const client = google.accounts.oauth2.initCodeClient({
|
|
14
|
+
callback: (response) => {
|
|
15
|
+
fetch(authorize, {
|
|
16
|
+
body: JSON.stringify(response),
|
|
17
|
+
headers: { "Content-Type": "application/json" },
|
|
18
|
+
method: "POST",
|
|
19
|
+
})
|
|
20
|
+
.then((r) => {
|
|
21
|
+
if (!r.ok) {
|
|
22
|
+
throw new Error(`authorize request failed (${r.status})`);
|
|
23
|
+
}
|
|
24
|
+
return r.json();
|
|
25
|
+
})
|
|
26
|
+
.then((tokens) => {
|
|
27
|
+
if (!tokens?.access_token) {
|
|
28
|
+
throw new Error("authorize response missing access_token");
|
|
29
|
+
}
|
|
30
|
+
onAccessToken(tokens.access_token);
|
|
31
|
+
})
|
|
32
|
+
.catch(resetSession);
|
|
33
|
+
},
|
|
34
|
+
client_id: provider.clientId,
|
|
35
|
+
scope: provider.authorization.params.scope,
|
|
36
|
+
});
|
|
37
|
+
client.requestCode();
|
|
38
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { OAuthProvider } from "../../config/entities";
|
|
2
|
+
import type { SessionDispatch } from "../types";
|
|
3
|
+
export type LoginDispatch = Pick<SessionDispatch, "authDispatch" | "authenticationDispatch" | "tokenDispatch">;
|
|
4
|
+
/**
|
|
5
|
+
* Creates a function that resets the session state on auth failure.
|
|
6
|
+
* @param dispatch - Dispatch functions for auth state.
|
|
7
|
+
* @returns reset session function.
|
|
8
|
+
*/
|
|
9
|
+
export declare function createResetSession(dispatch: LoginDispatch): () => void;
|
|
10
|
+
/**
|
|
11
|
+
* Creates a function that handles a successful access token.
|
|
12
|
+
* Dispatches auth state updates and fetches the user profile.
|
|
13
|
+
* @param provider - OAuth provider configuration.
|
|
14
|
+
* @param dispatch - Dispatch functions for auth state.
|
|
15
|
+
* @param resetSession - Reset session function.
|
|
16
|
+
* @returns on access token function.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createOnAccessToken(provider: OAuthProvider, dispatch: LoginDispatch, resetSession: () => void): (token: string) => void;
|
|
19
|
+
/**
|
|
20
|
+
* Logout and clear all auth state.
|
|
21
|
+
* @param dispatch - Dispatch functions for auth state.
|
|
22
|
+
*/
|
|
23
|
+
export declare function logout(dispatch: SessionDispatch): void;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { requestAuth, resetAuthState } from "../../auth/dispatch/auth";
|
|
2
|
+
import { requestAuthentication, resetAuthenticationState, updateAuthentication, } from "../../auth/dispatch/authentication";
|
|
3
|
+
import { resetCredentialsState } from "../../auth/dispatch/credentials";
|
|
4
|
+
import { resetTokenState, updateToken } from "../../auth/dispatch/token";
|
|
5
|
+
import { AUTHENTICATION_STATUS } from "../../auth/types/authentication";
|
|
6
|
+
import { fetchProfile, getAuthenticationRequestOptions } from "../utils/auth";
|
|
7
|
+
/**
|
|
8
|
+
* Creates a function that resets the session state on auth failure.
|
|
9
|
+
* @param dispatch - Dispatch functions for auth state.
|
|
10
|
+
* @returns reset session function.
|
|
11
|
+
*/
|
|
12
|
+
export function createResetSession(dispatch) {
|
|
13
|
+
return () => {
|
|
14
|
+
dispatch.authDispatch?.(resetAuthState());
|
|
15
|
+
dispatch.authenticationDispatch?.(updateAuthentication({
|
|
16
|
+
profile: undefined,
|
|
17
|
+
status: AUTHENTICATION_STATUS.SETTLED,
|
|
18
|
+
}));
|
|
19
|
+
dispatch.tokenDispatch?.(resetTokenState());
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Creates a function that handles a successful access token.
|
|
24
|
+
* Dispatches auth state updates and fetches the user profile.
|
|
25
|
+
* @param provider - OAuth provider configuration.
|
|
26
|
+
* @param dispatch - Dispatch functions for auth state.
|
|
27
|
+
* @param resetSession - Reset session function.
|
|
28
|
+
* @returns on access token function.
|
|
29
|
+
*/
|
|
30
|
+
export function createOnAccessToken(provider, dispatch, resetSession) {
|
|
31
|
+
const { id, profile, userinfo } = provider;
|
|
32
|
+
return (token) => {
|
|
33
|
+
dispatch.authDispatch?.(requestAuth());
|
|
34
|
+
dispatch.authenticationDispatch?.(requestAuthentication());
|
|
35
|
+
dispatch.tokenDispatch?.(updateToken({ providerId: id, token }));
|
|
36
|
+
fetchProfile(userinfo, getAuthenticationRequestOptions(token), {
|
|
37
|
+
onError: resetSession,
|
|
38
|
+
onSuccess: (r) => dispatch.authenticationDispatch?.(updateAuthentication({
|
|
39
|
+
profile: profile(r),
|
|
40
|
+
status: AUTHENTICATION_STATUS.PENDING,
|
|
41
|
+
})),
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Logout and clear all auth state.
|
|
47
|
+
* @param dispatch - Dispatch functions for auth state.
|
|
48
|
+
*/
|
|
49
|
+
export function logout(dispatch) {
|
|
50
|
+
dispatch.authDispatch?.(resetAuthState());
|
|
51
|
+
dispatch.authenticationDispatch?.(resetAuthenticationState());
|
|
52
|
+
dispatch.credentialsDispatch?.(resetCredentialsState());
|
|
53
|
+
dispatch.tokenDispatch?.(resetTokenState());
|
|
54
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ImplicitFlowProvider } from "../../config/entities";
|
|
2
|
+
import { type LoginDispatch } from "./common";
|
|
3
|
+
/**
|
|
4
|
+
* Login using the OAuth 2.0 implicit flow.
|
|
5
|
+
* Uses Google's initTokenClient to request an access token directly.
|
|
6
|
+
* @param provider - OAuth provider configuration.
|
|
7
|
+
* @param dispatch - Dispatch functions for auth state.
|
|
8
|
+
*/
|
|
9
|
+
export declare function login(provider: ImplicitFlowProvider, dispatch: LoginDispatch): void;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createOnAccessToken, createResetSession, } from "./common";
|
|
2
|
+
/**
|
|
3
|
+
* Login using the OAuth 2.0 implicit flow.
|
|
4
|
+
* Uses Google's initTokenClient to request an access token directly.
|
|
5
|
+
* @param provider - OAuth provider configuration.
|
|
6
|
+
* @param dispatch - Dispatch functions for auth state.
|
|
7
|
+
*/
|
|
8
|
+
export function login(provider, dispatch) {
|
|
9
|
+
const resetSession = createResetSession(dispatch);
|
|
10
|
+
const onAccessToken = createOnAccessToken(provider, dispatch, resetSession);
|
|
11
|
+
const client = google.accounts.oauth2.initTokenClient({
|
|
12
|
+
callback: (response) => onAccessToken(response.access_token),
|
|
13
|
+
client_id: provider.clientId,
|
|
14
|
+
scope: provider.authorization.params.scope,
|
|
15
|
+
});
|
|
16
|
+
client.requestAccessToken();
|
|
17
|
+
}
|
package/lib/google/types.d.ts
CHANGED
|
@@ -3,6 +3,17 @@ import { AuthAction, AuthContextProps } from "../auth/types/auth";
|
|
|
3
3
|
import { AuthenticationAction, AuthenticationContextProps } from "../auth/types/authentication";
|
|
4
4
|
import { CredentialsAction, CredentialsContextProps } from "../auth/types/credentials";
|
|
5
5
|
import { TokenAction, TokenContextProps, TokenState } from "../auth/types/token";
|
|
6
|
+
/**
|
|
7
|
+
* Authorization code response from Google OAuth.
|
|
8
|
+
*/
|
|
9
|
+
export interface CodeResponse {
|
|
10
|
+
code?: string;
|
|
11
|
+
error?: string;
|
|
12
|
+
error_description?: string;
|
|
13
|
+
error_uri?: string;
|
|
14
|
+
scope?: string;
|
|
15
|
+
state?: string;
|
|
16
|
+
}
|
|
6
17
|
/**
|
|
7
18
|
* Google Sign-In authentication provider props.
|
|
8
19
|
*/
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import { SiteConfig } from "../../../config/entities";
|
|
1
|
+
import { CategoryGroupConfig, EntityConfig, SiteConfig } from "../../../config/entities";
|
|
2
2
|
import { ExploreState } from "../../exploreState";
|
|
3
|
+
/**
|
|
4
|
+
* Returns entity related configured category group config where entity config takes precedence over site config.
|
|
5
|
+
* @param siteConfig - Site config.
|
|
6
|
+
* @param entityConfig - Entity config.
|
|
7
|
+
* @returns entity related category group config.
|
|
8
|
+
*/
|
|
9
|
+
export declare function getEntityCategoryGroupConfig(siteConfig: SiteConfig, entityConfig: EntityConfig): CategoryGroupConfig | undefined;
|
|
3
10
|
/**
|
|
4
11
|
* Returns the explore state reducer initial arguments.
|
|
5
12
|
* @param config - Site config.
|
|
@@ -80,7 +80,7 @@ function columnFiltersToSelectedFilters(columnFilters) {
|
|
|
80
80
|
* @param entityConfig - Entity config.
|
|
81
81
|
* @returns entity related category group config.
|
|
82
82
|
*/
|
|
83
|
-
function getEntityCategoryGroupConfig(siteConfig, entityConfig) {
|
|
83
|
+
export function getEntityCategoryGroupConfig(siteConfig, entityConfig) {
|
|
84
84
|
const siteCategoryGroupConfig = siteConfig.categoryGroupConfig;
|
|
85
85
|
const entityCategoryGroupConfig = entityConfig.categoryGroupConfig;
|
|
86
86
|
return entityCategoryGroupConfig ?? siteCategoryGroupConfig;
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { isColumnFilter } from "../../common/filters/adapters/tanstack/typeGuards";
|
|
3
|
+
import { useValidateFilterKeys } from "../../common/filters/hooks/UseValidateFilterKeys/hook";
|
|
2
4
|
import { DataDictionary } from "../../components/DataDictionary/dataDictionary";
|
|
3
5
|
import { useStateSyncManager } from "../../hooks/stateSyncManager/hook";
|
|
4
6
|
import { DataDictionaryContext } from "../../providers/dataDictionary/context";
|
|
5
7
|
import { clearMeta } from "../../providers/dataDictionaryState/actions/clearMeta/dispatch";
|
|
6
8
|
import { stateToUrl } from "../../providers/dataDictionaryState/actions/stateToUrl/dispatch";
|
|
7
9
|
import { urlToState } from "../../providers/dataDictionaryState/actions/urlToState/dispatch";
|
|
10
|
+
import { DATA_DICTIONARY_URL_PARAMS } from "../../providers/dataDictionaryState/dictionaries/constants";
|
|
8
11
|
import { useDataDictionaryState } from "../../providers/dataDictionaryState/hooks/UseDataDictionaryState/hook";
|
|
9
12
|
import { buildStateSyncManagerContext } from "./utils";
|
|
10
13
|
export const DataDictionaryView = ({ className, dictionary, }) => {
|
|
11
14
|
const { dataDictionaryDispatch, dataDictionaryState } = useDataDictionaryState();
|
|
15
|
+
// Shape-only validation (no key check) — must run before useStateSyncManager
|
|
16
|
+
// to prevent malformed entries from being dispatched into state.
|
|
17
|
+
useValidateFilterKeys(DATA_DICTIONARY_URL_PARAMS.COLUMN_FILTERS, undefined, isColumnFilter);
|
|
12
18
|
useStateSyncManager({
|
|
13
19
|
actions: { clearMeta, stateToUrl, urlToState },
|
|
14
20
|
dispatch: dataDictionaryDispatch,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Validates the filter query parameter from the URL.
|
|
2
|
+
* Validates the filter query parameter from the URL for the ExploreView.
|
|
3
3
|
* Throws a DataExplorerError during render if the filter param is present
|
|
4
|
-
* but contains malformed JSON
|
|
4
|
+
* but contains malformed JSON, an invalid filter shape, or a categoryKey
|
|
5
|
+
* that does not exist in the configured categories, allowing the
|
|
5
6
|
* ErrorBoundary to catch and display the error page.
|
|
6
|
-
* @returns
|
|
7
|
+
* @returns void; throws DataExplorerError on invalid input.
|
|
7
8
|
*/
|
|
8
9
|
export declare function useValidateFilterParam(): void;
|
|
@@ -1,54 +1,19 @@
|
|
|
1
|
-
import { useRouter } from "next/router";
|
|
2
1
|
import { useMemo } from "react";
|
|
3
|
-
import {
|
|
2
|
+
import { useValidateFilterKeys } from "../../../../common/filters/hooks/UseValidateFilterKeys/hook";
|
|
3
|
+
import { isSelectedFilter } from "../../../../common/filters/typeGuards";
|
|
4
|
+
import { useConfig } from "../../../../hooks/useConfig";
|
|
4
5
|
import { EXPLORE_URL_PARAMS } from "../../../../providers/exploreState/constants";
|
|
5
|
-
import {
|
|
6
|
-
const INVALID_FILTER_PARAM_ERROR = "Invalid filter parameter in URL";
|
|
6
|
+
import { extractCategoryKey, getValidCategoryKeys } from "./utils";
|
|
7
7
|
/**
|
|
8
|
-
* Validates the filter query parameter from the URL.
|
|
8
|
+
* Validates the filter query parameter from the URL for the ExploreView.
|
|
9
9
|
* Throws a DataExplorerError during render if the filter param is present
|
|
10
|
-
* but contains malformed JSON
|
|
10
|
+
* but contains malformed JSON, an invalid filter shape, or a categoryKey
|
|
11
|
+
* that does not exist in the configured categories, allowing the
|
|
11
12
|
* ErrorBoundary to catch and display the error page.
|
|
12
|
-
* @returns
|
|
13
|
+
* @returns void; throws DataExplorerError on invalid input.
|
|
13
14
|
*/
|
|
14
15
|
export function useValidateFilterParam() {
|
|
15
|
-
const {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
if (filterParam === undefined)
|
|
19
|
-
return undefined;
|
|
20
|
-
if (typeof filterParam !== "string") {
|
|
21
|
-
return new DataExplorerError({ message: INVALID_FILTER_PARAM_ERROR });
|
|
22
|
-
}
|
|
23
|
-
let decoded;
|
|
24
|
-
try {
|
|
25
|
-
decoded = decodeURIComponent(filterParam);
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
return new DataExplorerError({ message: INVALID_FILTER_PARAM_ERROR });
|
|
29
|
-
}
|
|
30
|
-
// Parse and validate the filter param.
|
|
31
|
-
// An empty array (e.g. "[]") is valid — it means no filters selected.
|
|
32
|
-
// A non-empty array with no valid entries is invalid.
|
|
33
|
-
let parsed;
|
|
34
|
-
try {
|
|
35
|
-
parsed = JSON.parse(decoded);
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
return new DataExplorerError({ message: INVALID_FILTER_PARAM_ERROR });
|
|
39
|
-
}
|
|
40
|
-
if (!Array.isArray(parsed)) {
|
|
41
|
-
return new DataExplorerError({ message: INVALID_FILTER_PARAM_ERROR });
|
|
42
|
-
}
|
|
43
|
-
if (parsed.length === 0)
|
|
44
|
-
return undefined;
|
|
45
|
-
const filters = parseFilterParam(decoded);
|
|
46
|
-
if (filters.length === 0) {
|
|
47
|
-
return new DataExplorerError({ message: INVALID_FILTER_PARAM_ERROR });
|
|
48
|
-
}
|
|
49
|
-
return undefined;
|
|
50
|
-
}, [filterParam]);
|
|
51
|
-
if (validationError) {
|
|
52
|
-
throw validationError;
|
|
53
|
-
}
|
|
16
|
+
const { config, entityConfig } = useConfig();
|
|
17
|
+
const validKeys = useMemo(() => getValidCategoryKeys(config, entityConfig), [config, entityConfig]);
|
|
18
|
+
useValidateFilterKeys(EXPLORE_URL_PARAMS.FILTER, validKeys, isSelectedFilter, extractCategoryKey);
|
|
54
19
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { EntityConfig, SiteConfig } from "../../../../config/entities";
|
|
2
|
+
/**
|
|
3
|
+
* Extracts the categoryKey from a validated SelectedFilter entry.
|
|
4
|
+
* @param entry - Validated entry.
|
|
5
|
+
* @returns category key.
|
|
6
|
+
*/
|
|
7
|
+
export declare function extractCategoryKey(entry: unknown): string;
|
|
8
|
+
/**
|
|
9
|
+
* Returns the set of valid category keys for the given entity, derived from
|
|
10
|
+
* the entity's category group config. Returns undefined if no category group
|
|
11
|
+
* config is available (skipping key validation).
|
|
12
|
+
* @param config - Site config.
|
|
13
|
+
* @param entityConfig - Entity config.
|
|
14
|
+
* @returns set of valid category keys, or undefined.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getValidCategoryKeys(config: SiteConfig, entityConfig: EntityConfig): Set<string> | undefined;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { getEntityCategoryGroupConfig } from "../../../../providers/exploreState/initializer/utils";
|
|
2
|
+
/**
|
|
3
|
+
* Extracts the categoryKey from a validated SelectedFilter entry.
|
|
4
|
+
* @param entry - Validated entry.
|
|
5
|
+
* @returns category key.
|
|
6
|
+
*/
|
|
7
|
+
export function extractCategoryKey(entry) {
|
|
8
|
+
return entry.categoryKey;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Returns the set of valid category keys for the given entity, derived from
|
|
12
|
+
* the entity's category group config. Returns undefined if no category group
|
|
13
|
+
* config is available (skipping key validation).
|
|
14
|
+
* @param config - Site config.
|
|
15
|
+
* @param entityConfig - Entity config.
|
|
16
|
+
* @returns set of valid category keys, or undefined.
|
|
17
|
+
*/
|
|
18
|
+
export function getValidCategoryKeys(config, entityConfig) {
|
|
19
|
+
const categoryGroupConfig = getEntityCategoryGroupConfig(config, entityConfig);
|
|
20
|
+
if (!categoryGroupConfig)
|
|
21
|
+
return undefined;
|
|
22
|
+
const keys = new Set();
|
|
23
|
+
for (const group of categoryGroupConfig.categoryGroups) {
|
|
24
|
+
for (const categoryConfig of group.categoryConfigs) {
|
|
25
|
+
keys.add(categoryConfig.key);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return keys;
|
|
29
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@databiosphere/findable-ui",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "52.1.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
@@ -44,7 +44,6 @@
|
|
|
44
44
|
"@types/jest": "^29.5.14",
|
|
45
45
|
"@types/react": "^19.2.7",
|
|
46
46
|
"@types/react-dom": "^19.2.3",
|
|
47
|
-
"@types/react-gtm-module": "^2.0.1",
|
|
48
47
|
"@types/react-window": "^1.8.5",
|
|
49
48
|
"@typescript-eslint/eslint-plugin": "^8.50.0",
|
|
50
49
|
"eslint": "^8.57.1",
|
|
@@ -88,7 +87,6 @@
|
|
|
88
87
|
"react": "^19.2.3",
|
|
89
88
|
"react-dom": "^19.2.3",
|
|
90
89
|
"react-dropzone": "^14.3.8",
|
|
91
|
-
"react-gtm-module": "^2.0.11",
|
|
92
90
|
"react-idle-timer": "^5.7.2",
|
|
93
91
|
"react-window": "^1.8.11",
|
|
94
92
|
"rehype-raw": "^7.0.0",
|