From 02682e8076da3476b996e5b1b872766fb1a41e0f Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Tue, 24 Sep 2024 13:44:35 +0200 Subject: [PATCH] refactor: Fetch termsets in SearchCubes query ...to make sure all relevant filter are taken into account, and to not have duplicated logic and two very similar queries. This also aligns the termsets logic with organizations and themes in the Browse page. --- app/browser/dataset-browse.tsx | 101 +++++++-------- app/browser/filters.tsx | 8 +- app/browser/search-page-data.tsx | 24 ---- app/browser/select-dataset-step.tsx | 25 ++-- app/components/graphql-search.stories.tsx | 2 +- .../components/add-dataset-dialog.tsx | 2 +- app/domain/data.ts | 4 + app/graphql/queries/data-cubes.graphql | 19 --- app/graphql/query-hooks.ts | 53 ++------ app/graphql/resolver-types.ts | 45 +++---- app/graphql/resolvers/index.ts | 4 - app/graphql/resolvers/rdf.ts | 16 --- app/graphql/resolvers/sql.ts | 6 - app/graphql/schema.graphql | 22 ++-- app/rdf/parse-search-results.ts | 26 +++- app/rdf/query-search.ts | 118 +++++++++++++----- app/rdf/query-termsets.ts | 70 +---------- 17 files changed, 212 insertions(+), 333 deletions(-) delete mode 100644 app/browser/search-page-data.tsx diff --git a/app/browser/dataset-browse.tsx b/app/browser/dataset-browse.tsx index 6fe2e55b66..38548c2e7f 100644 --- a/app/browser/dataset-browse.tsx +++ b/app/browser/dataset-browse.tsx @@ -21,7 +21,7 @@ import uniqBy from "lodash/uniqBy"; import Link from "next/link"; import { useRouter } from "next/router"; import { stringify } from "qs"; -import React, { ComponentProps, useMemo, useState } from "react"; +import React, { ComponentProps, ReactNode, useMemo, useState } from "react"; import Flex, { FlexProps } from "@/components/flex"; import { @@ -41,15 +41,15 @@ import { } from "@/components/presence"; import Tag from "@/components/tag"; import useDisclosure from "@/components/use-disclosure"; -import { SearchCube, Termset } from "@/domain/data"; +import { SearchCube } from "@/domain/data"; import { truthy } from "@/domain/types"; import { useFlag } from "@/flags"; import { useFormatDate } from "@/formatters"; import { DataCubeOrganization, + DataCubeTermset, DataCubeTheme, SearchCubeResultOrder, - TermsetCount, } from "@/graphql/query-hooks"; import { DataCubePublicationStatus, @@ -355,7 +355,7 @@ const encodeFilter = (filter: BrowseFilter) => { return "organization"; case "DataCubeAbout": return "topic"; - case "Termset": + case "DataCubeTermset": return "termset"; default: const check: never = __typename; @@ -375,7 +375,7 @@ const NavItem = ({ level = 1, disableLink, }: { - children: React.ReactNode; + children: ReactNode; filters: BrowseFilter[]; next: BrowseFilter; count?: number; @@ -611,10 +611,10 @@ const NavSection = ({ }: { label: React.ReactNode; icon: React.ReactNode; - items: (DataCubeTheme | DataCubeOrganization | Termset)[]; + items: (DataCubeTheme | DataCubeOrganization | DataCubeTermset)[]; theme: { backgroundColor: string; borderColor: string }; navItemTheme: NavItemTheme; - currentFilter?: DataCubeTheme | DataCubeOrganization | Termset; + currentFilter?: DataCubeTheme | DataCubeOrganization | DataCubeTermset; filters: BrowseFilter[]; counts: Record; extra?: React.ReactNode; @@ -705,7 +705,7 @@ const NavSection = ({ const navOrder: Record = { DataCubeTheme: 1, DataCubeOrganization: 2, - Termset: 3, + DataCubeTermset: 3, // Not used in the nav DataCubeAbout: 4, }; @@ -714,13 +714,13 @@ export const SearchFilters = ({ cubes, themes, orgs, - termsets: termsetCounts, + termsets, disableNavLinks = false, }: { cubes: SearchCubeResult[]; themes: DataCubeTheme[]; orgs: DataCubeOrganization[]; - termsets: TermsetCount[]; + termsets: DataCubeTermset[]; disableNavLinks?: boolean; }) => { const { filters } = useBrowseContext(); @@ -731,6 +731,7 @@ export const SearchFilters = ({ const countable = [ ...cube.themes, ...cube.subthemes, + ...cube.termsets, cube.creator, ].filter(truthy); @@ -741,22 +742,23 @@ export const SearchFilters = ({ } } - for (const { termset, count } of termsetCounts) { - result[termset.iri] = count; - } - return result; - }, [cubes, termsetCounts]); + }, [cubes]); const { DataCubeTheme: themeFilter, DataCubeOrganization: orgFilter, - Termset: termsetFilter, + DataCubeTermset: termsetFilter, } = useMemo(() => { - return keyBy(filters, (f) => f.__typename) as { - DataCubeTheme?: DataCubeTheme; - DataCubeOrganization?: DataCubeOrganization; - Termset?: Termset; + const result = keyBy(filters, (f) => f.__typename) as { + [K in BrowseFilter["__typename"]]?: BrowseFilter; + }; + return { + DataCubeTheme: result.DataCubeTheme as DataCubeTheme | undefined, + DataCubeOrganization: result.DataCubeOrganization as + | DataCubeOrganization + | undefined, + DataCubeTermset: result.DataCubeTermset as DataCubeTermset | undefined, }; }, [filters]); @@ -792,23 +794,21 @@ export const SearchFilters = ({ return true; }); - const displayedTermsets = termsetCounts - .map((d) => d.termset) - .filter((termset) => { - if (!termset.label) { - return false; - } + const displayedTermsets = termsets.filter((termset) => { + if (!termset.label) { + return false; + } - if (!counts[termset.iri] && termsetFilter?.iri !== termset.iri) { - return false; - } + if (!counts[termset.iri] && termsetFilter?.iri !== termset.iri) { + return false; + } - if (termsetFilter && termsetFilter.iri !== termset.iri) { - return false; - } + if (termsetFilter && termsetFilter.iri !== termset.iri) { + return false; + } - return true; - }); + return true; + }); const themeNav = displayedThemes && displayedThemes.length > 0 ? ( @@ -871,7 +871,7 @@ export const SearchFilters = ({ const termsetFlag = useFlag("search.termsets"); const termsetNav = - termsetCounts.length === 0 || !termsetFlag ? null : ( + termsets.length === 0 || !termsetFlag ? null : ( ); - const navs = sortBy( - [ - { element: themeNav, __typename: "DataCubeTheme" }, - { element: orgNav, __typename: "DataCubeOrganization" }, - { element: termsetNav, __typename: "Termset" }, - ], - (x) => { - const i = filters.findIndex((f) => f.__typename === x.__typename); - return i === -1 - ? // If the filter is not in the list, we want to put it at the end - navOrder[x.__typename as BrowseFilter["__typename"]] + - Object.keys(navOrder).length - : i; - } - ); + const baseNavs: { + element: ReactNode; + __typename: BrowseFilter["__typename"]; + }[] = [ + { element: themeNav, __typename: "DataCubeTheme" }, + { element: orgNav, __typename: "DataCubeOrganization" }, + { element: termsetNav, __typename: "DataCubeTermset" }, + ]; + const navs = sortBy(baseNavs, (x) => { + const i = filters.findIndex((f) => f.__typename === x.__typename); + return i === -1 + ? // If the filter is not in the list, we want to put it at the end + navOrder[x.__typename as BrowseFilter["__typename"]] + + Object.keys(navOrder).length + : i; + }); return ( & { label?: string }); + | DataCubeTermset; /** Builds the state search filters from query params */ @@ -35,7 +35,7 @@ export const getFiltersFromParams = (params: BrowseParams) => { case "organization": return SearchCubeFilterType.DataCubeOrganization; case "termset": - return SearchCubeFilterType.Termset; + return SearchCubeFilterType.DataCubeTermset; } })(); filters.push({ @@ -81,7 +81,7 @@ export const getParamsFromFilters = (filters: BrowseFilter[]) => { case "DataCubeAbout": params.topic = filter.iri; break; - case "Termset": + case "DataCubeTermset": params[typeAttr] = "termset"; params[iriAttr] = filter.iri; break; diff --git a/app/browser/search-page-data.tsx b/app/browser/search-page-data.tsx deleted file mode 100644 index 3e245737f5..0000000000 --- a/app/browser/search-page-data.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { SearchCubeFilter, useSearchPageQuery } from "@/graphql/query-hooks"; -import { useConfiguratorState, useLocale } from "@/src"; - -export const useSearchPageData = ({ - includeDrafts, - filters, -}: { - includeDrafts: boolean; - filters: SearchCubeFilter[]; -}) => { - const locale = useLocale(); - const [configState] = useConfiguratorState(); - const { dataSource } = configState; - const [searchPageQuery] = useSearchPageQuery({ - variables: { - sourceType: dataSource.type, - sourceUrl: dataSource.url, - includeDrafts, - filters, - locale, - }, - }); - return searchPageQuery; -}; diff --git a/app/browser/select-dataset-step.tsx b/app/browser/select-dataset-step.tsx index 41c1c2b5b4..062f1af432 100644 --- a/app/browser/select-dataset-step.tsx +++ b/app/browser/select-dataset-step.tsx @@ -24,7 +24,6 @@ import { } from "@/browser/dataset-browse"; import { DataSetPreview, DataSetPreviewProps } from "@/browser/dataset-preview"; import { BrowseFilter, DataCubeAbout } from "@/browser/filters"; -import { useSearchPageData } from "@/browser/search-page-data"; import { DatasetMetadata } from "@/components/dataset-metadata"; import Flex from "@/components/flex"; import { Footer } from "@/components/footer"; @@ -46,6 +45,7 @@ import { import { truthy } from "@/domain/types"; import { DataCubeOrganization, + DataCubeTermset, DataCubeTheme, SearchCubeFilterType, useDataCubeMetadataQuery, @@ -274,14 +274,17 @@ const SelectDatasetStepContent = ({ ); }, [cubes]); - const searchPageData = useSearchPageData({ - includeDrafts, - filters: queryFilters, - }); - const termsets = useMemo( - () => searchPageData.data?.allTermsets ?? [], - [searchPageData.data?.allTermsets] - ); + const termsets: DataCubeTermset[] = useMemo(() => { + return sortBy( + uniqBy( + cubes.flatMap((d) => + d.cube.termsets.map((d) => ({ ...d, __typename: "DataCubeTermset" })) + ), + (d) => d.iri + ), + (d) => d.label + ); + }, [cubes]); const pageTitle = useMemo(() => { return queryFilters @@ -293,8 +296,8 @@ const SelectDatasetStepContent = ({ return themes; case SearchCubeFilterType.DataCubeOrganization: return orgs; - case SearchCubeFilterType.Termset: - return termsets.map((x) => x.termset); + case SearchCubeFilterType.DataCubeTermset: + return termsets; case SearchCubeFilterType.DataCubeAbout: return []; case SearchCubeFilterType.TemporalDimension: diff --git a/app/components/graphql-search.stories.tsx b/app/components/graphql-search.stories.tsx index 95de0bbea4..69d12f2470 100644 --- a/app/components/graphql-search.stories.tsx +++ b/app/components/graphql-search.stories.tsx @@ -92,7 +92,7 @@ export const Search = () => { filters: [ sharedComponents ? { - type: SearchCubeFilterType.Termset, + type: SearchCubeFilterType.DataCubeTermset, value: sharedComponents .map((x) => cubeSharedDimensionsByIri[x]) .filter(truthy) diff --git a/app/configurator/components/add-dataset-dialog.tsx b/app/configurator/components/add-dataset-dialog.tsx index 70eebb0225..07e275ca30 100644 --- a/app/configurator/components/add-dataset-dialog.tsx +++ b/app/configurator/components/add-dataset-dialog.tsx @@ -723,7 +723,7 @@ export const DatasetDialog = ({ : null, selectedSharedDimensions && selectedSharedDimensions.length > 0 ? { - type: SearchCubeFilterType.Termset, + type: SearchCubeFilterType.DataCubeTermset, value: uniq( selectedSharedDimensions.flatMap((x) => x.termsets.map((x) => x.iri) diff --git a/app/domain/data.ts b/app/domain/data.ts index c230b1260f..40d5c049bf 100644 --- a/app/domain/data.ts +++ b/app/domain/data.ts @@ -406,6 +406,10 @@ export type SearchCube = { iri: string; label: string; }[]; + termsets: { + iri: string; + label: string; + }[]; dimensions?: { iri: string; label: string; diff --git a/app/graphql/queries/data-cubes.graphql b/app/graphql/queries/data-cubes.graphql index 0967c9dfe8..bf7738aa75 100644 --- a/app/graphql/queries/data-cubes.graphql +++ b/app/graphql/queries/data-cubes.graphql @@ -1,22 +1,3 @@ -query SearchPage( - $sourceType: String! - $sourceUrl: String! - $includeDrafts: Boolean - $filters: [SearchCubeFilter!] - $locale: String! -) { - allTermsets( - sourceType: $sourceType - sourceUrl: $sourceUrl - includeDrafts: $includeDrafts - filters: $filters - locale: $locale - ) { - count - termset - } -} - query SearchCubes( $sourceType: String! $sourceUrl: String! diff --git a/app/graphql/query-hooks.ts b/app/graphql/query-hooks.ts index 674662e6a7..c190dd94a0 100644 --- a/app/graphql/query-hooks.ts +++ b/app/graphql/query-hooks.ts @@ -98,6 +98,12 @@ export enum DataCubePublicationStatus { Published = 'PUBLISHED' } +export type DataCubeTermset = { + __typename: 'DataCubeTermset'; + iri: Scalars['String']; + label?: Maybe; +}; + export type DataCubeTermsetFilter = { iri: Scalars['String']; }; @@ -132,7 +138,6 @@ export type Query = { possibleFilters: Array; searchCubes: Array; dataCubeDimensionGeoShapes?: Maybe; - allTermsets: Array; }; @@ -209,15 +214,6 @@ export type QueryDataCubeDimensionGeoShapesArgs = { }; -export type QueryAllTermsetsArgs = { - sourceType: Scalars['String']; - sourceUrl: Scalars['String']; - locale: Scalars['String']; - filters?: Maybe>; - includeDrafts?: Maybe; -}; - - export type RelatedDimension = { __typename: 'RelatedDimension'; type: Scalars['String']; @@ -243,7 +239,7 @@ export enum SearchCubeFilterType { DataCubeTheme = 'DataCubeTheme', DataCubeOrganization = 'DataCubeOrganization', DataCubeAbout = 'DataCubeAbout', - Termset = 'Termset' + DataCubeTermset = 'DataCubeTermset' } export type SearchCubeResult = { @@ -262,12 +258,6 @@ export enum SearchCubeResultOrder { -export type TermsetCount = { - __typename: 'TermsetCount'; - termset: Scalars['Termset']; - count: Scalars['Int']; -}; - export enum TimeUnit { Year = 'Year', Month = 'Month', @@ -280,17 +270,6 @@ export enum TimeUnit { -export type SearchPageQueryVariables = Exact<{ - sourceType: Scalars['String']; - sourceUrl: Scalars['String']; - includeDrafts?: Maybe; - filters?: Maybe | SearchCubeFilter>; - locale: Scalars['String']; -}>; - - -export type SearchPageQuery = { __typename: 'Query', allTermsets: Array<{ __typename: 'TermsetCount', count: number, termset: Termset }> }; - export type SearchCubesQueryVariables = Exact<{ sourceType: Scalars['String']; sourceUrl: Scalars['String']; @@ -383,24 +362,6 @@ export type PossibleFiltersQueryVariables = Exact<{ export type PossibleFiltersQuery = { __typename: 'Query', possibleFilters: Array<{ __typename: 'ObservationFilter', iri: string, type: string, value?: Maybe }> }; -export const SearchPageDocument = gql` - query SearchPage($sourceType: String!, $sourceUrl: String!, $includeDrafts: Boolean, $filters: [SearchCubeFilter!], $locale: String!) { - allTermsets( - sourceType: $sourceType - sourceUrl: $sourceUrl - includeDrafts: $includeDrafts - filters: $filters - locale: $locale - ) { - count - termset - } -} - `; - -export function useSearchPageQuery(options: Omit, 'query'> = {}) { - return Urql.useQuery({ query: SearchPageDocument, ...options }); -}; export const SearchCubesDocument = gql` query SearchCubes($sourceType: String!, $sourceUrl: String!, $locale: String!, $query: String, $order: SearchCubeResultOrder, $includeDrafts: Boolean, $filters: [SearchCubeFilter!]) { searchCubes( diff --git a/app/graphql/resolver-types.ts b/app/graphql/resolver-types.ts index ee5e7d5800..e86fa60571 100644 --- a/app/graphql/resolver-types.ts +++ b/app/graphql/resolver-types.ts @@ -98,6 +98,12 @@ export enum DataCubePublicationStatus { Published = 'PUBLISHED' } +export type DataCubeTermset = { + __typename?: 'DataCubeTermset'; + iri: Scalars['String']; + label?: Maybe; +}; + export type DataCubeTermsetFilter = { iri: Scalars['String']; }; @@ -132,7 +138,6 @@ export type Query = { possibleFilters: Array; searchCubes: Array; dataCubeDimensionGeoShapes?: Maybe; - allTermsets: Array; }; @@ -209,15 +214,6 @@ export type QueryDataCubeDimensionGeoShapesArgs = { }; -export type QueryAllTermsetsArgs = { - sourceType: Scalars['String']; - sourceUrl: Scalars['String']; - locale: Scalars['String']; - filters?: Maybe>; - includeDrafts?: Maybe; -}; - - export type RelatedDimension = { __typename?: 'RelatedDimension'; type: Scalars['String']; @@ -243,7 +239,7 @@ export enum SearchCubeFilterType { DataCubeTheme = 'DataCubeTheme', DataCubeOrganization = 'DataCubeOrganization', DataCubeAbout = 'DataCubeAbout', - Termset = 'Termset' + DataCubeTermset = 'DataCubeTermset' } export type SearchCubeResult = { @@ -262,12 +258,6 @@ export enum SearchCubeResultOrder { -export type TermsetCount = { - __typename?: 'TermsetCount'; - termset: Scalars['Termset']; - count: Scalars['Int']; -}; - export enum TimeUnit { Year = 'Year', Month = 'Month', @@ -362,6 +352,7 @@ export type ResolversTypes = ResolversObject<{ DataCubePreview: ResolverTypeWrapper; DataCubePreviewFilter: DataCubePreviewFilter; DataCubePublicationStatus: DataCubePublicationStatus; + DataCubeTermset: ResolverTypeWrapper; DataCubeTermsetFilter: DataCubeTermsetFilter; DataCubeTheme: ResolverTypeWrapper; DimensionValue: ResolverTypeWrapper; @@ -383,8 +374,6 @@ export type ResolversTypes = ResolversObject<{ SearchCubeResultOrder: SearchCubeResultOrder; SingleFilters: ResolverTypeWrapper; Termset: ResolverTypeWrapper; - TermsetCount: ResolverTypeWrapper; - Int: ResolverTypeWrapper; TimeUnit: TimeUnit; ValueIdentifier: ResolverTypeWrapper; ValuePosition: ResolverTypeWrapper; @@ -407,6 +396,7 @@ export type ResolversParentTypes = ResolversObject<{ DataCubePossibleFiltersCubeFilter: DataCubePossibleFiltersCubeFilter; DataCubePreview: Scalars['DataCubePreview']; DataCubePreviewFilter: DataCubePreviewFilter; + DataCubeTermset: DataCubeTermset; DataCubeTermsetFilter: DataCubeTermsetFilter; DataCubeTheme: DataCubeTheme; DimensionValue: Scalars['DimensionValue']; @@ -425,8 +415,6 @@ export type ResolversParentTypes = ResolversObject<{ Float: Scalars['Float']; SingleFilters: Scalars['SingleFilters']; Termset: Scalars['Termset']; - TermsetCount: TermsetCount; - Int: Scalars['Int']; ValueIdentifier: Scalars['ValueIdentifier']; ValuePosition: Scalars['ValuePosition']; }>; @@ -457,6 +445,12 @@ export interface DataCubePreviewScalarConfig extends GraphQLScalarTypeConfig = ResolversObject<{ + iri?: Resolver; + label?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}>; + export type DataCubeThemeResolvers = ResolversObject<{ iri?: Resolver; label?: Resolver, ParentType, ContextType>; @@ -504,7 +498,6 @@ export type QueryResolvers, ParentType, ContextType, RequireFields>; searchCubes?: Resolver, ParentType, ContextType, RequireFields>; dataCubeDimensionGeoShapes?: Resolver, ParentType, ContextType, RequireFields>; - allTermsets?: Resolver, ParentType, ContextType, RequireFields>; }>; export interface RawObservationScalarConfig extends GraphQLScalarTypeConfig { @@ -537,12 +530,6 @@ export interface TermsetScalarConfig extends GraphQLScalarTypeConfig = ResolversObject<{ - termset?: Resolver; - count?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; -}>; - export interface ValueIdentifierScalarConfig extends GraphQLScalarTypeConfig { name: 'ValueIdentifier'; } @@ -558,6 +545,7 @@ export type Resolvers = ResolversObject<{ DataCubeObservations?: GraphQLScalarType; DataCubeOrganization?: DataCubeOrganizationResolvers; DataCubePreview?: GraphQLScalarType; + DataCubeTermset?: DataCubeTermsetResolvers; DataCubeTheme?: DataCubeThemeResolvers; DimensionValue?: GraphQLScalarType; FilterValue?: GraphQLScalarType; @@ -573,7 +561,6 @@ export type Resolvers = ResolversObject<{ SearchCubeResult?: SearchCubeResultResolvers; SingleFilters?: GraphQLScalarType; Termset?: GraphQLScalarType; - TermsetCount?: TermsetCountResolvers; ValueIdentifier?: GraphQLScalarType; ValuePosition?: GraphQLScalarType; }>; diff --git a/app/graphql/resolvers/index.ts b/app/graphql/resolvers/index.ts index d90298107c..ef12f7f15f 100644 --- a/app/graphql/resolvers/index.ts +++ b/app/graphql/resolvers/index.ts @@ -51,10 +51,6 @@ export const Query: QueryResolvers = { const source = getSource(args.sourceType); return await source.dataCubeDimensionGeoShapes(parent, args, context, info); }, - allTermsets: async (parent, args, context, info) => { - const source = getSource(args.sourceType); - return await source.allTermsets(parent, args, context, info); - }, }; export const resolveDimensionType = ( diff --git a/app/graphql/resolvers/rdf.ts b/app/graphql/resolvers/rdf.ts index d9cbcb20fb..6bd83a3894 100644 --- a/app/graphql/resolvers/rdf.ts +++ b/app/graphql/resolvers/rdf.ts @@ -37,7 +37,6 @@ import { parseHierarchy, queryHierarchies } from "@/rdf/query-hierarchies"; import { queryLatestCubeIri } from "@/rdf/query-latest-cube-iri"; import { getPossibleFilters } from "@/rdf/query-possible-filters"; import { SearchResult, searchCubes as _searchCubes } from "@/rdf/query-search"; -import { queryAllTermsets } from "@/rdf/query-termsets"; import { getSparqlEditorUrl } from "@/rdf/sparql-utils"; export const dataCubeLatestIri: NonNullable< @@ -429,18 +428,3 @@ const getDimensionValuesLoader = ( return loaders.dimensionValues; } }; - -export const allTermsets: NonNullable = async ( - _, - { locale, includeDrafts, filters }, - { setup }, - info -) => { - const { sparqlClient } = await setup(info); - return await queryAllTermsets({ - locale, - includeDrafts: !!includeDrafts, - filters: filters ?? undefined, - sparqlClient, - }); -}; diff --git a/app/graphql/resolvers/sql.ts b/app/graphql/resolvers/sql.ts index aa4e85fed0..18b8e0f777 100644 --- a/app/graphql/resolvers/sql.ts +++ b/app/graphql/resolvers/sql.ts @@ -219,9 +219,3 @@ export const dataCubePreview: NonNullable< observations: [], }; }; - -export const allTermsets: NonNullable< - QueryResolvers["allTermsets"] -> = async () => { - return []; -}; diff --git a/app/graphql/schema.graphql b/app/graphql/schema.graphql index 09f97dbfb4..08b4969894 100644 --- a/app/graphql/schema.graphql +++ b/app/graphql/schema.graphql @@ -14,6 +14,7 @@ scalar DataCubePreview scalar Termset scalar ComponentTermsets scalar GeoShapes +scalar SearchCube enum DataCubePublicationStatus { DRAFT @@ -48,8 +49,6 @@ enum TimeUnit { Second } -scalar SearchCube - type SearchCubeResult { score: Float cube: SearchCube! @@ -67,12 +66,17 @@ type DataCubeOrganization { label: String } +type DataCubeTermset { + iri: String! + label: String +} + enum SearchCubeFilterType { TemporalDimension DataCubeTheme DataCubeOrganization DataCubeAbout - Termset + DataCubeTermset } input SearchCubeFilter { @@ -128,11 +132,6 @@ input DataCubeDimensionGeoShapesCubeFilter { dimensionIri: String! } -type TermsetCount { - termset: Termset! - count: Int! -} - # The "Query" type is special: it lists all of the available queries that # clients can execute, along with the return type for each. type Query { @@ -193,11 +192,4 @@ type Query { locale: String! cubeFilter: DataCubeDimensionGeoShapesCubeFilter! ): GeoShapes - allTermsets( - sourceType: String! - sourceUrl: String! - locale: String! - filters: [SearchCubeFilter!] - includeDrafts: Boolean - ): [TermsetCount!]! } diff --git a/app/rdf/parse-search-results.ts b/app/rdf/parse-search-results.ts index 41f9b36caa..28f8f688ed 100644 --- a/app/rdf/parse-search-results.ts +++ b/app/rdf/parse-search-results.ts @@ -4,6 +4,7 @@ import { Quad } from "rdf-js"; import { SearchCube } from "@/domain/data"; import { DataCubePublicationStatus } from "@/graphql/resolver-types"; import * as ns from "@/rdf/namespace"; +import { GROUP_SEPARATOR } from "@/rdf/query-utils"; const visualizePredicates = { hasDimension: ns.visualizeAdmin`hasDimension`.value, @@ -35,7 +36,10 @@ function buildSearchCubes( for (const iri of iriList) { const cubeQuads = bySubjectAndPredicate.get(iri); if (cubeQuads) { - const themeQuads = cubeQuads.get(ns.dcat.theme.value); + const themeQuads = cubeQuads.get("tag:/themeIris")?.[0]; + const themeIris = themeQuads?.object.value.split(GROUP_SEPARATOR); + const themeLabelQuads = cubeQuads.get("tag:/themeLabels")?.[0]; + const themeLabels = themeLabelQuads?.object.value.split(GROUP_SEPARATOR); const subthemesQuads = cubeQuads.get(ns.schema.about.value); const dimensions = cubeQuads.get(visualizePredicates.hasDimension); const creatorIri = cubeQuads.get(ns.schema.creator.value)?.[0]?.object @@ -43,6 +47,9 @@ function buildSearchCubes( const publicationStatus = cubeQuads.get( ns.schema.creativeWorkStatus.value )?.[0].object.value; + const termsetQuads = byPredicateAndObject + .get("https://cube.link/meta/isUsedIn") + ?.get(iri); const cubeSearchCube: SearchCube = { iri, @@ -67,7 +74,14 @@ function buildSearchCubes( } : null, themes: - themeQuads?.map((x) => { + themeIris?.map((iri, i) => { + return { + iri, + label: themeLabels?.[i] ?? "", + }; + }) ?? [], + subthemes: + subthemesQuads?.map((x) => { return { iri: x.object.value, label: @@ -76,13 +90,13 @@ function buildSearchCubes( ?.get(ns.schema.name.value)?.[0].object.value ?? "", }; }) ?? [], - subthemes: - subthemesQuads?.map((x) => { + termsets: + termsetQuads?.map((x) => { return { - iri: x.object.value, + iri: x.subject.value, label: bySubjectAndPredicate - .get(x.object.value) + .get(x.subject.value) ?.get(ns.schema.name.value)?.[0].object.value ?? "", }; }) ?? [], diff --git a/app/rdf/query-search.ts b/app/rdf/query-search.ts index f50d1456c4..2e0d512216 100644 --- a/app/rdf/query-search.ts +++ b/app/rdf/query-search.ts @@ -13,6 +13,7 @@ import { buildSearchCubes } from "@/rdf/parse-search-results"; import { computeScores, highlight } from "@/rdf/query-search-score-utils"; import { buildLocalizedSubQuery, + GROUP_SEPARATOR, iriToNode, makeVisualizeDatasetFilter, } from "@/rdf/query-utils"; @@ -38,7 +39,7 @@ const fanOutExclusiveFilters = ( } const { exclusive = [], common = [] } = groupBy(filters, (f) => { - return f.type === SearchCubeFilterType.Termset || + return f.type === SearchCubeFilterType.DataCubeTermset || f.type === SearchCubeFilterType.TemporalDimension ? "exclusive" : "common"; @@ -152,21 +153,21 @@ const mkScoresQuery = ( # HOTFIX WRT Stardog v9.2.1 bug see https://control.vshn.net/tickets/sbar-1066 #pragma join.bind off + PREFIX cube: + PREFIX cubeMeta: + PREFIX dcat: + PREFIX dcterms: PREFIX meta: PREFIX rdf: PREFIX schema: PREFIX sh: - PREFIX cube: - PREFIX cubeMeta: - PREFIX dcterms: - PREFIX dcat: - PREFIX visualize: PREFIX time: + PREFIX visualize: CONSTRUCT { - ?iri a cube:Cube ; - cube:observationConstraint ?shape; - dcat:theme ?themeIri; + ?iri + a cube:Cube ; + cube:observationConstraint ?shape ; dcterms:publisher ?publisher ; schema:about ?subthemeIri; schema:creativeWorkStatus ?status ; @@ -175,19 +176,44 @@ const mkScoresQuery = ( schema:description ?description ; schema:name ?title ; schema:workExample ; - visualize:hasDimension ?dimensionIri. + visualize:hasDimension ?dimensionIri ; + ?themeIris ; + ?themeLabels . ?dimensionIri - visualize:hasTermset ?termsetIri ; visualize:hasTimeUnit ?unitType ; schema:name ?dimensionLabel . - - ?termsetIri schema:name ?termsetLabel . + + ?termsetIri + meta:isUsedIn ?iri ; + schema:name ?termsetLabel . + ?creatorIri schema:name ?creatorLabel . - ?themeIri schema:name ?themeLabel . - ?subthemeIri schema:inDefinedTermSet ?subthemeTermset ; + + ?subthemeIri + schema:inDefinedTermSet ?subthemeTermset ; schema:name ?subthemeLabel . - } + } WHERE { + SELECT + ?iri + ?shape + (GROUP_CONCAT(DISTINCT ?themeIri; SEPARATOR="${GROUP_SEPARATOR}") as ?themeIris) + (GROUP_CONCAT(DISTINCT ?themeLabel; SEPARATOR="${GROUP_SEPARATOR}") as ?themeLabels) + ?publisher + ?status + ?creatorIri + ?datePublished + ?description + ?title + ?dimensionIri + ?unitType + ?dimensionLabel + ?termsetIri + ?termsetLabel + ?creatorLabel + ?subthemeIri + ?subthemeTermset + ?subthemeLabel WHERE { ?iri a cube:Cube . ${buildLocalizedSubQuery("iri", "schema:name", "title", { @@ -198,9 +224,9 @@ const mkScoresQuery = ( })} ${filters - ?.map((df) => { - if (df.type === SearchCubeFilterType.TemporalDimension) { - const value = df.value as TimeUnit; + ?.map((filter) => { + if (filter.type === SearchCubeFilterType.TemporalDimension) { + const value = filter.value as TimeUnit; const unitNode = unitsToNode.get(value); if (!unitNode) { throw new Error(`Invalid temporal unit used ${value}`); @@ -222,10 +248,10 @@ const mkScoresQuery = ( } )} `; - } else if (df.type === SearchCubeFilterType.Termset) { - const sharedDimensions = df.value.split(";"); + } else if (filter.type === SearchCubeFilterType.DataCubeTermset) { + const sharedDimensions = filter.value.split(";"); return ` - VALUES (?termsetIri) {${sharedDimensions.map((sd) => `(<${sd}>)`).join(" ")}} + VALUES (?termsetIri) {${sharedDimensions.map((sd) => `( ${iriToNode(sd)} )`).join(" ")}} ?termsetIri meta:isUsedIn ?iri . ${buildLocalizedSubQuery("termsetIri", "schema:name", "termsetLabel", { locale })}`; } @@ -233,6 +259,16 @@ const mkScoresQuery = ( .filter(truthy) .join("\n")} + ${ + !filters?.find((f) => f.type === SearchCubeFilterType.DataCubeTermset) + ? ` + OPTIONAL { + ?termsetIri meta:isUsedIn ?iri . + ${buildLocalizedSubQuery("termsetIri", "schema:name", "termsetLabel", { locale })} + }` + : "" + } + # Publisher, creator status, datePublished ?iri schema:creativeWorkStatus ?status . OPTIONAL { ?iri dcterms:publisher ?publisher . } @@ -274,11 +310,11 @@ const mkScoresQuery = ( } # Add more subtheme termsets here when they are available - ${ - creatorValues.includes( - "https://register.ld.admin.ch/opendataswiss/org/bundesamt-fur-umwelt-bafu" - ) - ? ` + ${ + creatorValues.includes( + "https://register.ld.admin.ch/opendataswiss/org/bundesamt-fur-umwelt-bafu" + ) + ? ` OPTIONAL { ?iri schema:about ?subthemeIri . VALUES (?subthemeGraph ?subthemeTermset) { ( ) } @@ -294,13 +330,13 @@ const mkScoresQuery = ( )} } ` - : "" - } + : "" + } ${makeVisualizeDatasetFilter({ includeDrafts: !!includeDrafts, cubeIriVar: "?iri", - }).toString()} + })} ${ query @@ -345,6 +381,24 @@ const mkScoresQuery = ( )` : "" } - - }`; + } + GROUP BY + ?iri + ?shape + ?publisher + ?status + ?creatorIri + ?datePublished + ?description + ?title + ?dimensionIri + ?unitType + ?dimensionLabel + ?termsetIri + ?termsetLabel + ?creatorLabel + ?subthemeIri + ?subthemeTermset + ?subthemeLabel + }`; }; diff --git a/app/rdf/query-termsets.ts b/app/rdf/query-termsets.ts index 83037939c2..78f3dd4c93 100644 --- a/app/rdf/query-termsets.ts +++ b/app/rdf/query-termsets.ts @@ -2,75 +2,7 @@ import groupBy from "lodash/groupBy"; import ParsingClient from "sparql-http-client/ParsingClient"; import { ComponentTermsets, Termset } from "@/domain/data"; -import { - SearchCubeFilter, - SearchCubeFilterType, -} from "@/graphql/resolver-types"; -import { makeInFilter } from "@/rdf/query-search"; -import { - buildLocalizedSubQuery, - iriToNode, - makeVisualizeDatasetFilter, -} from "@/rdf/query-utils"; - -export const queryAllTermsets = async (options: { - locale: string; - sparqlClient: ParsingClient; - filters?: SearchCubeFilter[]; - includeDrafts?: boolean; -}): Promise<{ termset: Termset; count: number }[]> => { - const { sparqlClient, locale, filters = [], includeDrafts } = options; - const creators = filters - .filter((d) => d.type === SearchCubeFilterType.DataCubeOrganization) - .map((d) => d.value); - const themes = filters - .filter((d) => d.type === SearchCubeFilterType.DataCubeTheme) - .map((d) => d.value); - const qs = await sparqlClient.query.select( - `PREFIX cube: -PREFIX dcat: -PREFIX dcterms: -PREFIX meta: -PREFIX schema: - -SELECT DISTINCT (COUNT(DISTINCT ?cubeIri) as ?count) ?termsetIri ?termsetLabel WHERE { - ?termsetIri meta:isUsedIn ?cubeIri . - ${creators.length ? makeInFilter("creatorIri", creators) : ""} - ${ - themes.length - ? ` - VALUES ?theme { ${themes.map(iriToNode).join(" ")} } - ?cubeIri dcat:theme ?theme . - ` - : "" - } - OPTIONAL { - ?cubeIri dcterms:creator ?creatorIri . - GRAPH { - ?creatorIri a schema:Organization ; - schema:inDefinedTermSet . - } - } - ${makeVisualizeDatasetFilter({ - includeDrafts: !!includeDrafts, - cubeIriVar: "?cubeIri", - })} - ${buildLocalizedSubQuery("termsetIri", "schema:name", "termsetLabel", { locale })} -} GROUP BY ?termsetIri ?termsetLabel`, - { operation: "postUrlencoded" } - ); - - return qs - .map((result) => ({ - count: +result.count.value, - termset: { - __typename: "Termset", - iri: result.termsetIri?.value, - label: result.termsetLabel?.value, - }, - })) - .filter((d) => d.termset.iri) as { termset: Termset; count: number }[]; -}; +import { buildLocalizedSubQuery } from "@/rdf/query-utils"; export const getCubeTermsets = async ( iri: string,