From b549077dda10a010c128c8aebaabfadd1a318060 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Tue, 24 Sep 2024 09:06:31 +0200 Subject: [PATCH 01/12] fix: Termset filtering --- app/rdf/query-search.ts | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/app/rdf/query-search.ts b/app/rdf/query-search.ts index e00e6d6cb..5a7b127db 100644 --- a/app/rdf/query-search.ts +++ b/app/rdf/query-search.ts @@ -226,18 +226,7 @@ const mkScoresQuery = ( const sharedDimensions = df.value.split(";"); return ` VALUES (?termsetIri) {${sharedDimensions.map((sd) => `(<${sd}>)`).join(" ")}} - ?iri cube:observationConstraint/sh:property ?dimension . - ?dimension - sh:path ?dimensionIri ; - a cube:KeyDimension ; - sh:in/rdf:first ?value . - ?value schema:inDefinedTermSet ?termsetIri . - ${buildLocalizedSubQuery( - "dimension", - "schema:name", - "dimensionLabel", - { locale } - )} + ?termsetIri meta:isUsedIn ?iri . ${buildLocalizedSubQuery("termsetIri", "schema:name", "termsetLabel", { locale })}`; } }) From da7569b612ce0384cc1ba770bcf65687e05fcf72 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Tue, 24 Sep 2024 09:23:31 +0200 Subject: [PATCH 02/12] refactor: Rename --- app/rdf/query-dimension-values.ts | 9 +++------ app/rdf/query-geo-shapes.ts | 4 ++-- app/rdf/query-possible-filters.ts | 6 +++--- app/rdf/query-search.ts | 13 +++++++------ app/rdf/query-utils.ts | 2 +- 5 files changed, 16 insertions(+), 18 deletions(-) diff --git a/app/rdf/query-dimension-values.ts b/app/rdf/query-dimension-values.ts index 46f8383d8..852855f4f 100644 --- a/app/rdf/query-dimension-values.ts +++ b/app/rdf/query-dimension-values.ts @@ -18,10 +18,7 @@ import * as ns from "@/rdf/namespace"; import { parseDimensionDatatype } from "@/rdf/parse"; import { dimensionIsVersioned } from "@/rdf/queries"; import { executeWithCache } from "@/rdf/query-cache"; -import { - buildLocalizedSubQuery, - formatIriToQueryNode, -} from "@/rdf/query-utils"; +import { buildLocalizedSubQuery, iriToNode } from "@/rdf/query-utils"; /** * Formats a filter value into the right format, string literal @@ -30,7 +27,7 @@ import { */ const formatFilterValue = (value: string | number, dataType?: Term) => { if (!dataType) { - return formatIriToQueryNode(value as string); + return iriToNode(value as string); } else { return `"${value}"`; } @@ -547,7 +544,7 @@ export const loadMinMaxDimensionValues = async ({ cache: LRUCache | undefined; }) => { const query = SELECT`(MIN(?value) as ?minValue) (MAX(?value) as ?maxValue)` - .WHERE`${formatIriToQueryNode(datasetIri)} ${ns.cube.observationSet}/${ns.cube.observation} ?observation . + .WHERE`${iriToNode(datasetIri)} ${ns.cube.observationSet}/${ns.cube.observation} ?observation . ?observation <${dimensionIri}> ?value . FILTER ( (STRLEN(STR(?value)) > 0) && (STR(?value) != "NaN") )`; diff --git a/app/rdf/query-geo-shapes.ts b/app/rdf/query-geo-shapes.ts index 314f68abb..6d5eec893 100644 --- a/app/rdf/query-geo-shapes.ts +++ b/app/rdf/query-geo-shapes.ts @@ -3,7 +3,7 @@ import { NamedNode } from "rdf-js"; import { ParsingClient } from "sparql-http-client/ParsingClient"; import { MAX_BATCH_SIZE } from "@/graphql/context"; -import { formatIriToQueryNode } from "@/rdf/query-utils"; +import { iriToNode } from "@/rdf/query-utils"; export interface GeoShape { geometryIri: string; @@ -22,7 +22,7 @@ type GeoShapesLoaderProps = { export const createGeoShapesLoader = (props: GeoShapesLoaderProps) => { const { geoSparqlClient } = props; return async (geometryIris: readonly string[]): Promise => { - const geometryIriQueryNodes = geometryIris.map(formatIriToQueryNode); + const geometryIriQueryNodes = geometryIris.map(iriToNode); const wktStringsByGeometry = await getWktStringsByGeometry({ geometryIriQueryNodes, geoSparqlClient, diff --git a/app/rdf/query-possible-filters.ts b/app/rdf/query-possible-filters.ts index 1382db5b5..5d531b610 100644 --- a/app/rdf/query-possible-filters.ts +++ b/app/rdf/query-possible-filters.ts @@ -8,7 +8,7 @@ import { SingleFilters } from "@/config-types"; import { isMostRecentValue } from "@/domain/most-recent-value"; import * as ns from "@/rdf/namespace"; import { loadMaxDimensionValue } from "@/rdf/query-dimension-values"; -import { formatIriToQueryNode } from "@/rdf/query-utils"; +import { iriToNode } from "@/rdf/query-utils"; export const getPossibleFilters = async ( cubeIri: string, @@ -68,7 +68,7 @@ SELECT ?${DIMENSION_IRI} ?${VERSION} ?${NODE_KIND} WHERE { ?dimension sh:path ?${DIMENSION_IRI} . OPTIONAL { ?dimension schema:version ?${VERSION} . } OPTIONAL { ?dimension sh:nodeKind ?${NODE_KIND} . } - ${dimensionIris.length > 0 ? `FILTER(?${DIMENSION_IRI} IN (${dimensionIris.map(formatIriToQueryNode).join(", ")}))` : ""} + ${dimensionIris.length > 0 ? `FILTER(?${DIMENSION_IRI} IN (${dimensionIris.map(iriToNode).join(", ")}))` : ""} }`; const results = await sparqlClient.query.select(query, { operation: "postUrlencoded", @@ -140,7 +140,7 @@ const stringifyDimension = (i: number, isVersioned: boolean) => { }; const getQueryValue = (value: string, isLiteral: boolean) => { - return isLiteral ? `"${value}"` : formatIriToQueryNode(value); + return isLiteral ? `"${value}"` : iriToNode(value); }; type QueryFilter = { diff --git a/app/rdf/query-search.ts b/app/rdf/query-search.ts index 5a7b127db..f50d1456c 100644 --- a/app/rdf/query-search.ts +++ b/app/rdf/query-search.ts @@ -13,15 +13,15 @@ import { buildSearchCubes } from "@/rdf/parse-search-results"; import { computeScores, highlight } from "@/rdf/query-search-score-utils"; import { buildLocalizedSubQuery, - formatIriToQueryNode, + iriToNode, makeVisualizeDatasetFilter, } from "@/rdf/query-utils"; -const makeInFilter = (name: string, values: string[]) => { +export const makeInFilter = (name: string, values: string[]) => { return ` ${ values.length > 0 - ? `FILTER (bound(?${name}) && ?${name} IN (${values.map(formatIriToQueryNode)}))` + ? `FILTER (bound(?${name}) && ?${name} IN (${values.map(iriToNode)}))` : "" }`; }; @@ -186,7 +186,7 @@ const mkScoresQuery = ( ?creatorIri schema:name ?creatorLabel . ?themeIri schema:name ?themeLabel . ?subthemeIri schema:inDefinedTermSet ?subthemeTermset ; - schema:name ?subthemeLabel . + schema:name ?subthemeLabel . } WHERE { ?iri a cube:Cube . @@ -234,8 +234,8 @@ const mkScoresQuery = ( .join("\n")} # Publisher, creator status, datePublished - OPTIONAL { ?iri dcterms:publisher ?publisher . } ?iri schema:creativeWorkStatus ?status . + OPTIONAL { ?iri dcterms:publisher ?publisher . } OPTIONAL { ?iri schema:datePublished ?datePublished . } ${creatorValues.length ? makeInFilter("creatorIri", creatorValues) : ""} @@ -263,10 +263,11 @@ const mkScoresQuery = ( })} } } + ${ themeValues.length ? ` - VALUES ?theme { ${themeValues.map(formatIriToQueryNode).join(" ")} } + VALUES ?theme { ${themeValues.map(iriToNode).join(" ")} } ?iri dcat:theme ?theme . ` : "" diff --git a/app/rdf/query-utils.ts b/app/rdf/query-utils.ts index 85e3a28ee..5be348e4b 100644 --- a/app/rdf/query-utils.ts +++ b/app/rdf/query-utils.ts @@ -2,7 +2,7 @@ import { locales } from "@/locales/locales"; export const GROUP_SEPARATOR = "|||"; -export const formatIriToQueryNode = (iri: string) => { +export const iriToNode = (iri: string) => { return `<${iri}>`; }; From 8a4e87392f7439b9466ae58925db43afd13b9898 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Tue, 24 Sep 2024 09:27:40 +0200 Subject: [PATCH 03/12] fix: Passing includeDrafts --- app/graphql/resolvers/rdf.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/graphql/resolvers/rdf.ts b/app/graphql/resolvers/rdf.ts index 5716ac9fb..5e0f35902 100644 --- a/app/graphql/resolvers/rdf.ts +++ b/app/graphql/resolvers/rdf.ts @@ -432,10 +432,14 @@ const getDimensionValuesLoader = ( export const allTermsets: NonNullable = async ( _, - { locale }, + { locale, includeDrafts }, { setup }, info ) => { const { sparqlClient } = await setup(info); - return await queryAllTermsets({ locale, sparqlClient }); + return await queryAllTermsets({ + locale, + includeDrafts: !!includeDrafts, + sparqlClient, + }); }; From c55f981f32717a61bcda24096e91d37962cf5d91 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Tue, 24 Sep 2024 09:40:30 +0200 Subject: [PATCH 04/12] feat: Make the termset query take other search page filters into account --- app/browser/search-page-data.tsx | 12 +- app/browser/select-dataset-step.tsx | 5 +- app/graphql/queries/data-cubes.graphql | 16 ++- app/graphql/query-hooks.ts | 165 +++++++++++++------------ app/graphql/resolver-types.ts | 1 + app/graphql/resolvers/rdf.ts | 3 +- app/graphql/schema.graphql | 1 + app/rdf/query-termsets.ts | 51 ++++++-- 8 files changed, 161 insertions(+), 93 deletions(-) diff --git a/app/browser/search-page-data.tsx b/app/browser/search-page-data.tsx index 1da1e1c74..3e245737f 100644 --- a/app/browser/search-page-data.tsx +++ b/app/browser/search-page-data.tsx @@ -1,7 +1,13 @@ -import { useSearchPageQuery } from "@/graphql/query-hooks"; +import { SearchCubeFilter, useSearchPageQuery } from "@/graphql/query-hooks"; import { useConfiguratorState, useLocale } from "@/src"; -export const useSearchPageData = () => { +export const useSearchPageData = ({ + includeDrafts, + filters, +}: { + includeDrafts: boolean; + filters: SearchCubeFilter[]; +}) => { const locale = useLocale(); const [configState] = useConfiguratorState(); const { dataSource } = configState; @@ -9,6 +15,8 @@ export const useSearchPageData = () => { variables: { sourceType: dataSource.type, sourceUrl: dataSource.url, + includeDrafts, + filters, locale, }, }); diff --git a/app/browser/select-dataset-step.tsx b/app/browser/select-dataset-step.tsx index 45f1b5586..41c1c2b5b 100644 --- a/app/browser/select-dataset-step.tsx +++ b/app/browser/select-dataset-step.tsx @@ -274,7 +274,10 @@ const SelectDatasetStepContent = ({ ); }, [cubes]); - const searchPageData = useSearchPageData(); + const searchPageData = useSearchPageData({ + includeDrafts, + filters: queryFilters, + }); const termsets = useMemo( () => searchPageData.data?.allTermsets ?? [], [searchPageData.data?.allTermsets] diff --git a/app/graphql/queries/data-cubes.graphql b/app/graphql/queries/data-cubes.graphql index 291e6f307..0967c9dfe 100644 --- a/app/graphql/queries/data-cubes.graphql +++ b/app/graphql/queries/data-cubes.graphql @@ -1,5 +1,17 @@ -query SearchPage($sourceType: String!, $sourceUrl: String!, $locale: String!) { - allTermsets(sourceType: $sourceType, sourceUrl: $sourceUrl, locale: $locale) { +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 } diff --git a/app/graphql/query-hooks.ts b/app/graphql/query-hooks.ts index ddbe69348..674662e6a 100644 --- a/app/graphql/query-hooks.ts +++ b/app/graphql/query-hooks.ts @@ -213,6 +213,7 @@ export type QueryAllTermsetsArgs = { sourceType: Scalars['String']; sourceUrl: Scalars['String']; locale: Scalars['String']; + filters?: Maybe>; includeDrafts?: Maybe; }; @@ -279,6 +280,30 @@ 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']; + locale: Scalars['String']; + query?: Maybe; + order?: Maybe; + includeDrafts?: Maybe; + filters?: Maybe | SearchCubeFilter>; +}>; + + +export type SearchCubesQuery = { __typename: 'Query', searchCubes: Array<{ __typename: 'SearchCubeResult', highlightedTitle?: Maybe, highlightedDescription?: Maybe, cube: SearchCube }> }; + export type DataCubeLatestIriQueryVariables = Exact<{ sourceType: Scalars['String']; sourceUrl: Scalars['String']; @@ -298,6 +323,16 @@ export type DataCubeComponentsQueryVariables = Exact<{ export type DataCubeComponentsQuery = { __typename: 'Query', dataCubeComponents: DataCubeComponents }; +export type DataCubeDimensionGeoShapesQueryVariables = Exact<{ + sourceType: Scalars['String']; + sourceUrl: Scalars['String']; + locale: Scalars['String']; + cubeFilter: DataCubeDimensionGeoShapesCubeFilter; +}>; + + +export type DataCubeDimensionGeoShapesQuery = { __typename: 'Query', dataCubeDimensionGeoShapes?: Maybe }; + export type DataCubeMetadataQueryVariables = Exact<{ sourceType: Scalars['String']; sourceUrl: Scalars['String']; @@ -338,19 +373,6 @@ export type DataCubePreviewQueryVariables = Exact<{ export type DataCubePreviewQuery = { __typename: 'Query', dataCubePreview: DataCubePreview }; -export type SearchCubesQueryVariables = Exact<{ - sourceType: Scalars['String']; - sourceUrl: Scalars['String']; - locale: Scalars['String']; - query?: Maybe; - order?: Maybe; - includeDrafts?: Maybe; - filters?: Maybe | SearchCubeFilter>; -}>; - - -export type SearchCubesQuery = { __typename: 'Query', searchCubes: Array<{ __typename: 'SearchCubeResult', highlightedTitle?: Maybe, highlightedDescription?: Maybe, cube: SearchCube }> }; - export type PossibleFiltersQueryVariables = Exact<{ sourceType: Scalars['String']; sourceUrl: Scalars['String']; @@ -360,26 +382,46 @@ export type PossibleFiltersQueryVariables = Exact<{ export type PossibleFiltersQuery = { __typename: 'Query', possibleFilters: Array<{ __typename: 'ObservationFilter', iri: string, type: string, value?: Maybe }> }; -export type DataCubeDimensionGeoShapesQueryVariables = Exact<{ - sourceType: Scalars['String']; - sourceUrl: Scalars['String']; - locale: Scalars['String']; - cubeFilter: DataCubeDimensionGeoShapesCubeFilter; -}>; - - -export type DataCubeDimensionGeoShapesQuery = { __typename: 'Query', dataCubeDimensionGeoShapes?: Maybe }; - -export type SearchPageQueryVariables = Exact<{ - sourceType: Scalars['String']; - sourceUrl: Scalars['String']; - locale: Scalars['String']; -}>; - -export type SearchPageQuery = { __typename: 'Query', allTermsets: Array<{ __typename: 'TermsetCount', count: number, termset: Termset }> }; +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( + sourceType: $sourceType + sourceUrl: $sourceUrl + locale: $locale + query: $query + order: $order + includeDrafts: $includeDrafts + filters: $filters + ) { + highlightedTitle + highlightedDescription + cube + } +} + `; +export function useSearchCubesQuery(options: Omit, 'query'> = {}) { + return Urql.useQuery({ query: SearchCubesDocument, ...options }); +}; export const DataCubeLatestIriDocument = gql` query DataCubeLatestIri($sourceType: String!, $sourceUrl: String!, $cubeFilter: DataCubeLatestIriFilter!) { dataCubeLatestIri( @@ -407,6 +449,20 @@ export const DataCubeComponentsDocument = gql` export function useDataCubeComponentsQuery(options: Omit, 'query'> = {}) { return Urql.useQuery({ query: DataCubeComponentsDocument, ...options }); }; +export const DataCubeDimensionGeoShapesDocument = gql` + query DataCubeDimensionGeoShapes($sourceType: String!, $sourceUrl: String!, $locale: String!, $cubeFilter: DataCubeDimensionGeoShapesCubeFilter!) { + dataCubeDimensionGeoShapes( + sourceType: $sourceType + sourceUrl: $sourceUrl + locale: $locale + cubeFilter: $cubeFilter + ) +} + `; + +export function useDataCubeDimensionGeoShapesQuery(options: Omit, 'query'> = {}) { + return Urql.useQuery({ query: DataCubeDimensionGeoShapesDocument, ...options }); +}; export const DataCubeMetadataDocument = gql` query DataCubeMetadata($sourceType: String!, $sourceUrl: String!, $locale: String!, $cubeFilter: DataCubeMetadataFilter!) { dataCubeMetadata( @@ -424,10 +480,10 @@ export function useDataCubeMetadataQuery(options: Omit, 'query'> = {}) { return Urql.useQuery({ query: DataCubePreviewDocument, ...options }); }; -export const SearchCubesDocument = gql` - query SearchCubes($sourceType: String!, $sourceUrl: String!, $locale: String!, $query: String, $order: SearchCubeResultOrder, $includeDrafts: Boolean, $filters: [SearchCubeFilter!]) { - searchCubes( - sourceType: $sourceType - sourceUrl: $sourceUrl - locale: $locale - query: $query - order: $order - includeDrafts: $includeDrafts - filters: $filters - ) { - highlightedTitle - highlightedDescription - cube - } -} - `; - -export function useSearchCubesQuery(options: Omit, 'query'> = {}) { - return Urql.useQuery({ query: SearchCubesDocument, ...options }); -}; export const PossibleFiltersDocument = gql` query PossibleFilters($sourceType: String!, $sourceUrl: String!, $cubeFilter: DataCubePossibleFiltersCubeFilter!) { possibleFilters( @@ -500,30 +535,4 @@ export const PossibleFiltersDocument = gql` export function usePossibleFiltersQuery(options: Omit, 'query'> = {}) { return Urql.useQuery({ query: PossibleFiltersDocument, ...options }); -}; -export const DataCubeDimensionGeoShapesDocument = gql` - query DataCubeDimensionGeoShapes($sourceType: String!, $sourceUrl: String!, $locale: String!, $cubeFilter: DataCubeDimensionGeoShapesCubeFilter!) { - dataCubeDimensionGeoShapes( - sourceType: $sourceType - sourceUrl: $sourceUrl - locale: $locale - cubeFilter: $cubeFilter - ) -} - `; - -export function useDataCubeDimensionGeoShapesQuery(options: Omit, 'query'> = {}) { - return Urql.useQuery({ query: DataCubeDimensionGeoShapesDocument, ...options }); -}; -export const SearchPageDocument = gql` - query SearchPage($sourceType: String!, $sourceUrl: String!, $locale: String!) { - allTermsets(sourceType: $sourceType, sourceUrl: $sourceUrl, locale: $locale) { - count - termset - } -} - `; - -export function useSearchPageQuery(options: Omit, 'query'> = {}) { - return Urql.useQuery({ query: SearchPageDocument, ...options }); }; \ No newline at end of file diff --git a/app/graphql/resolver-types.ts b/app/graphql/resolver-types.ts index 26e301d0f..ee5e7d580 100644 --- a/app/graphql/resolver-types.ts +++ b/app/graphql/resolver-types.ts @@ -213,6 +213,7 @@ export type QueryAllTermsetsArgs = { sourceType: Scalars['String']; sourceUrl: Scalars['String']; locale: Scalars['String']; + filters?: Maybe>; includeDrafts?: Maybe; }; diff --git a/app/graphql/resolvers/rdf.ts b/app/graphql/resolvers/rdf.ts index 5e0f35902..d9cbcb20f 100644 --- a/app/graphql/resolvers/rdf.ts +++ b/app/graphql/resolvers/rdf.ts @@ -432,7 +432,7 @@ const getDimensionValuesLoader = ( export const allTermsets: NonNullable = async ( _, - { locale, includeDrafts }, + { locale, includeDrafts, filters }, { setup }, info ) => { @@ -440,6 +440,7 @@ export const allTermsets: NonNullable = async ( return await queryAllTermsets({ locale, includeDrafts: !!includeDrafts, + filters: filters ?? undefined, sparqlClient, }); }; diff --git a/app/graphql/schema.graphql b/app/graphql/schema.graphql index e0897f52d..09f97dbfb 100644 --- a/app/graphql/schema.graphql +++ b/app/graphql/schema.graphql @@ -197,6 +197,7 @@ type Query { sourceType: String! sourceUrl: String! locale: String! + filters: [SearchCubeFilter!] includeDrafts: Boolean ): [TermsetCount!]! } diff --git a/app/rdf/query-termsets.ts b/app/rdf/query-termsets.ts index 1798c5478..83037939c 100644 --- a/app/rdf/query-termsets.ts +++ b/app/rdf/query-termsets.ts @@ -2,24 +2,55 @@ 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, includeDrafts } = options; + 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", @@ -29,14 +60,16 @@ SELECT DISTINCT (COUNT(DISTINCT ?cubeIri) as ?count) ?termsetIri ?termsetLabel W { operation: "postUrlencoded" } ); - return qs.map((result) => ({ - count: +result.count.value, - termset: { - __typename: "Termset", - iri: result.termsetIri.value, - label: result.termsetLabel.value, - }, - })); + 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 }[]; }; export const getCubeTermsets = async ( From cd1516108f7934d93188f0d0c2974c57cc569d2d Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Tue, 24 Sep 2024 10:07:39 +0200 Subject: [PATCH 05/12] feat: Improve reordering logic --- app/browser/dataset-browse.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/browser/dataset-browse.tsx b/app/browser/dataset-browse.tsx index 6d60b0858..041911832 100644 --- a/app/browser/dataset-browse.tsx +++ b/app/browser/dataset-browse.tsx @@ -901,10 +901,14 @@ export const SearchFilters = ({ { element: orgNav, __typename: "DataCubeOrganization" }, { element: termsetNav, __typename: "Termset" }, ], - (x) => - x.__typename === filters[0]?.__typename - ? 0 - : navOrder[x.__typename as keyof typeof navOrder] + (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 ( From b90330f3dec4e24f45a27a5d587ec75bcdc5d1a9 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Tue, 24 Sep 2024 10:08:00 +0200 Subject: [PATCH 06/12] feat: Make the termset panel take other filters into account --- app/browser/dataset-browse.tsx | 80 ++++++++++++++++------------------ 1 file changed, 37 insertions(+), 43 deletions(-) diff --git a/app/browser/dataset-browse.tsx b/app/browser/dataset-browse.tsx index 041911832..6fe2e55b6 100644 --- a/app/browser/dataset-browse.tsx +++ b/app/browser/dataset-browse.tsx @@ -702,44 +702,6 @@ const NavSection = ({ ); }; -const TermsetNavSection = ({ - currentFilter, - termsetCounts, - disableLinks, -}: { - currentFilter: Termset | undefined; - termsetCounts: { termset: Termset; count: number }[]; - disableLinks: boolean; -}) => { - const { counts, termsets } = useMemo(() => { - const termsets = termsetCounts.map((x) => x.termset) ?? []; - const counts = Object.fromEntries( - termsetCounts.map((x) => [x.termset.iri, x.count]) - ); - return { - counts, - termsets: sortBy(termsets, (t) => t.label), - }; - }, [termsetCounts]); - - return ( - } - label={Concepts} - filters={[]} - counts={counts} - disableLinks={disableLinks} - /> - ); -}; - const navOrder: Record = { DataCubeTheme: 1, DataCubeOrganization: 2, @@ -766,21 +728,25 @@ export const SearchFilters = ({ const result: Record = {}; for (const { cube } of cubes) { - const countables = [ + const countable = [ ...cube.themes, ...cube.subthemes, cube.creator, ].filter(truthy); - for (const { iri } of countables) { + for (const { iri } of countable) { if (iri) { result[iri] = (result[iri] ?? 0) + 1; } } } + for (const { termset, count } of termsetCounts) { + result[termset.iri] = count; + } + return result; - }, [cubes]); + }, [cubes, termsetCounts]); const { DataCubeTheme: themeFilter, @@ -826,6 +792,24 @@ export const SearchFilters = ({ return true; }); + const displayedTermsets = termsetCounts + .map((d) => d.termset) + .filter((termset) => { + if (!termset.label) { + return false; + } + + if (!counts[termset.iri] && termsetFilter?.iri !== termset.iri) { + return false; + } + + if (termsetFilter && termsetFilter.iri !== termset.iri) { + return false; + } + + return true; + }); + const themeNav = displayedThemes && displayedThemes.length > 0 ? ( } + label={Concepts} + extra={null} disableLinks={disableNavLinks} /> ); From b671e89a0ce3f797f46ad1b3dceeacd71cbb2efe Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Tue, 24 Sep 2024 10:32:12 +0200 Subject: [PATCH 07/12] feat: Keep third level of browse filters in URL --- app/browser/context.tsx | 28 ++++++++++++++++--- app/browser/filters.tsx | 9 ++++-- app/pages/browse/[type]/[iri]/[subtype].tsx | 3 ++ .../[iri]/[subtype]/[subiri]/[subsubtype].tsx | 3 ++ .../[subiri]/[subsubtype]/[subsubiri].tsx | 3 ++ app/pages/browse/index.tsx | 2 ++ 6 files changed, 41 insertions(+), 7 deletions(-) create mode 100644 app/pages/browse/[type]/[iri]/[subtype].tsx create mode 100644 app/pages/browse/[type]/[iri]/[subtype]/[subiri]/[subsubtype].tsx create mode 100644 app/pages/browse/[type]/[iri]/[subtype]/[subiri]/[subsubtype]/[subsubiri].tsx diff --git a/app/browser/context.tsx b/app/browser/context.tsx index 72626ba9c..4bc04c1ed 100644 --- a/app/browser/context.tsx +++ b/app/browser/context.tsx @@ -34,6 +34,8 @@ export const getBrowseParamsFromQuery = ( "iri", "subtype", "subiri", + "subsubtype", + "subsubiri", "topic", "includeDrafts", "order", @@ -44,8 +46,17 @@ export const getBrowseParamsFromQuery = ( (v) => (Array.isArray(v) ? v[0] : v) ); - const { type, iri, subtype, subiri, topic, includeDrafts, ...values } = - rawValues; + const { + type, + iri, + subtype, + subiri, + subsubtype, + subsubiri, + topic, + includeDrafts, + ...values + } = rawValues; const previous = values.previous ? JSON.parse(values.previous) : undefined; return pickBy( @@ -55,6 +66,8 @@ export const getBrowseParamsFromQuery = ( iri: iri ?? previous?.iri, subtype: subtype ?? previous?.subtype, subiri: subiri ?? previous?.subiri, + subsubtype: subsubtype ?? previous?.subsubtype, + subsubiri: subsubiri ?? previous?.subsubiri, topic: topic ?? previous?.topic, includeDrafts: includeDrafts ? JSON.parse(includeDrafts) : undefined, }, @@ -63,7 +76,8 @@ export const getBrowseParamsFromQuery = ( }; export const buildURLFromBrowseState = (browseState: BrowseParams) => { - const { type, iri, subtype, subiri, ...queryParams } = browseState; + const { type, iri, subtype, subiri, subsubtype, subsubiri, ...queryParams } = + browseState; const typePart = type && iri @@ -73,8 +87,14 @@ export const buildURLFromBrowseState = (browseState: BrowseParams) => { subtype && subiri ? `${encodeURIComponent(subtype)}/${encodeURIComponent(subiri)}` : undefined; + const subsubtypePart = + subsubtype && subsubiri + ? `${encodeURIComponent(subsubtype)}/${encodeURIComponent(subsubiri)}` + : undefined; - const pathname = ["/browse", typePart, subtypePart].filter(Boolean).join("/"); + const pathname = ["/browse", typePart, subtypePart, subsubtypePart] + .filter(Boolean) + .join("/"); return { pathname, query: queryParams, diff --git a/app/browser/filters.tsx b/app/browser/filters.tsx index 0e040c421..6e05d6581 100644 --- a/app/browser/filters.tsx +++ b/app/browser/filters.tsx @@ -21,10 +21,11 @@ export type BrowseFilter = export const getFiltersFromParams = (params: BrowseParams) => { const filters: BrowseFilter[] = []; - const { type, subtype, iri, subiri, topic } = params; + const { type, subtype, subsubtype, iri, subiri, subsubiri, topic } = params; for (const [t, i] of [ [type, iri], [subtype, subiri], + [subsubtype, subsubiri], ]) { if (t && i && (t === "theme" || t === "organization" || t === "termset")) { const __typename = (() => { @@ -58,14 +59,16 @@ export const getParamsFromFilters = (filters: BrowseFilter[]) => { const params: BrowseParams = { type: undefined, subtype: undefined, + subsubtype: undefined, iri: undefined, subiri: undefined, + subsubiri: undefined, topic: undefined, }; let i = 0; for (const filter of filters) { - const typeAttr = i === 0 ? "type" : ("subtype" as const); - const iriAttr = i === 0 ? "iri" : ("subiri" as const); + const typeAttr = i === 0 ? "type" : i === 1 ? "subtype" : "subsubtype"; + const iriAttr = i === 0 ? "iri" : i === 1 ? "subiri" : "subsubiri"; switch (filter.__typename) { case "DataCubeTheme": params[typeAttr] = "theme"; diff --git a/app/pages/browse/[type]/[iri]/[subtype].tsx b/app/pages/browse/[type]/[iri]/[subtype].tsx new file mode 100644 index 000000000..f625bc14b --- /dev/null +++ b/app/pages/browse/[type]/[iri]/[subtype].tsx @@ -0,0 +1,3 @@ +import { DatasetBrowser } from "@/pages/browse"; + +export default DatasetBrowser; diff --git a/app/pages/browse/[type]/[iri]/[subtype]/[subiri]/[subsubtype].tsx b/app/pages/browse/[type]/[iri]/[subtype]/[subiri]/[subsubtype].tsx new file mode 100644 index 000000000..f625bc14b --- /dev/null +++ b/app/pages/browse/[type]/[iri]/[subtype]/[subiri]/[subsubtype].tsx @@ -0,0 +1,3 @@ +import { DatasetBrowser } from "@/pages/browse"; + +export default DatasetBrowser; diff --git a/app/pages/browse/[type]/[iri]/[subtype]/[subiri]/[subsubtype]/[subsubiri].tsx b/app/pages/browse/[type]/[iri]/[subtype]/[subiri]/[subsubtype]/[subsubiri].tsx new file mode 100644 index 000000000..f625bc14b --- /dev/null +++ b/app/pages/browse/[type]/[iri]/[subtype]/[subiri]/[subsubtype]/[subsubiri].tsx @@ -0,0 +1,3 @@ +import { DatasetBrowser } from "@/pages/browse"; + +export default DatasetBrowser; diff --git a/app/pages/browse/index.tsx b/app/pages/browse/index.tsx index 4a0f8efa5..2807b543b 100644 --- a/app/pages/browse/index.tsx +++ b/app/pages/browse/index.tsx @@ -6,8 +6,10 @@ import { SearchCubeResultOrder } from "@/graphql/query-hooks"; export type BrowseParams = { type?: "theme" | "organization" | "dataset" | "termset"; subtype?: "theme" | "organization" | "termset"; + subsubtype?: "theme" | "organization" | "termset"; iri?: string; subiri?: string; + subsubiri?: string; topic?: string; search?: string; order?: SearchCubeResultOrder; From 8fcf5eea8ccbcd6d2d1511e0b0403ae6edd3a481 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Tue, 24 Sep 2024 10:37:41 +0200 Subject: [PATCH 08/12] docs: Update CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60b333118..34e28d22e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ You can also check the - Preview mode now takes the real size of the breakpoint, making the area horizontally scrollable in case the screen size is smaller than the breakpoint + - It's now possible to simultaneously select browse filters from every panel + (themes, organizations, concepts) - Fixes - Interactive filters in the published mode and now only showing combinations of filters that are actually present in the data, taking configurator @@ -27,6 +29,8 @@ You can also check the - Compact table view margins were improved so rows do not overlap - Chart title and dataset tooltips in the user profile now only shows when the title is truncated + - Concepts navigation on the browse page now shows the correct number of + results # [4.8.0] - 2024-09-11 From d61249765c21f4e1cd745b7b34b87e33b07e2a3f Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Tue, 24 Sep 2024 13:45:38 +0200 Subject: [PATCH 09/12] refactor: Fetch termsets in SearchCubes query ...to make sure all relevant filters 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 6fe2e55b6..38548c2e7 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 3e245737f..000000000 --- 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 41c1c2b5b..062f1af43 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 95de0bbea..69d12f247 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 70eebb022..07e275ca3 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 c230b1260..40d5c049b 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 0967c9dfe..bf7738aa7 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 674662e6a..c190dd94a 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 ee5e7d580..e86fa6057 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 d90298107..ef12f7f15 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 d9cbcb20f..6bd83a389 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 aa4e85fed..18b8e0f77 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 09f97dbfb..08b496989 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 41f9b36ca..28f8f688e 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 f50d1456c..2e0d51221 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 83037939c..78f3dd4c9 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, From 986a7e5bf93fc0d8f3ec5a45698534bebf98fefe Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Tue, 24 Sep 2024 14:57:06 +0200 Subject: [PATCH 10/12] fix: Do not ignore first row (header is automatically removed) --- app/rdf/parse-search-results.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/rdf/parse-search-results.spec.ts b/app/rdf/parse-search-results.spec.ts index 8fca413bb..9ec38beab 100644 --- a/app/rdf/parse-search-results.spec.ts +++ b/app/rdf/parse-search-results.spec.ts @@ -18,7 +18,7 @@ const parseCSV = async (filepath: string) => { ); // Parse the rows into subject, predicate, and object - const rows = csvParse(csvData).slice(1); + const rows = csvParse(csvData); return rows.map((row) => { return { subject: { value: row.subject! }, From 1b93f11bbad3ff26a51aa52ae4387c2d48aba5b9 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Tue, 24 Sep 2024 15:03:26 +0200 Subject: [PATCH 11/12] test: Add parsing test --- app/rdf/parse-search-results.spec.ts | 49 ++++++++++++++++++- ...earch-results-photovoltaikanlagen.mock.csv | 17 +++++++ 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 app/rdf/query-search-results-photovoltaikanlagen.mock.csv diff --git a/app/rdf/parse-search-results.spec.ts b/app/rdf/parse-search-results.spec.ts index 9ec38beab..24b601a78 100644 --- a/app/rdf/parse-search-results.spec.ts +++ b/app/rdf/parse-search-results.spec.ts @@ -29,6 +29,54 @@ const parseCSV = async (filepath: string) => { }; describe("parse-search-results", () => { + it("should correctly parse cube", async () => { + const data = await parseCSV( + path.join( + __dirname, + "./query-search-results-photovoltaikanlagen.mock.csv" + ) + ); + const searchCubes = buildSearchCubes(data); + + expect(searchCubes).toMatchInlineSnapshot(` + Array [ + Object { + "creator": Object { + "iri": "https://register.ld.admin.ch/opendataswiss/org/bundesamt-fur-energie-bfe", + "label": "Swiss Federal Office of Energy SFOE", + }, + "datePublished": "2022-08-16", + "description": "Seit 2014 werden Photovoltaikanlagen mit einer Einmalvergütung (EIV) gefördert. Dabei wird abhängig von der Leistung, der Anlagenkategorie und dem Inbetriebnahmedatum ein einmaliger Beitrag an die Anlagenbetreiber ausbezahlt. Hier finden Sie pro Kanton und Auszahlungsjahr einen Überblick über die Anzahl geförderter EIV-Anlagen, die installierte Leistung in Kilowatt (kW) sowie den ausbezahlten EIV-Förderbeitrag. Die dargestellten Daten entsprechen nicht vollständig der offiziellen Statistik der erneuerbaren Energien durch das BFE. Da der Abbau der Wartelisten zeitverzögert stattfindet, können Abweichungen entstehen.", + "dimensions": undefined, + "iri": "https://energy.ld.admin.ch/sfoe/bfe_ogd84_einmalverguetung_fuer_photovoltaikanlagen/10", + "publicationStatus": "PUBLISHED", + "subthemes": Array [], + "termsets": Array [ + Object { + "iri": "https://ld.admin.ch/dimension/office", + "label": "Federal Offices", + }, + Object { + "iri": "https://ld.admin.ch/dimension/canton", + "label": "Cantons", + }, + ], + "themes": Array [ + Object { + "iri": "https://register.ld.admin.ch/opendataswiss/category/energy", + "label": "Energy", + }, + Object { + "iri": "https://register.ld.admin.ch/opendataswiss/category/national-economy", + "label": "National economy", + }, + ], + "title": "Einmalvergütung für Photovoltaikanlagen", + }, + ] + `); + }); + it("should build search cubes from CSV (shared dimension query)", async () => { const data = await parseCSV( path.join(__dirname, "./query-search-results-shared-dimensions.mock.csv") @@ -167,7 +215,6 @@ describe("parse-search-results", () => { const data = await parseCSV( path.join(__dirname, "./query-search-results-temporal.mock.csv") ); - // Build search cubes using the buildSearchCubes function const searchCubes = buildSearchCubes(data); expect(searchCubes.slice(0, 3)).toMatchInlineSnapshot(` diff --git a/app/rdf/query-search-results-photovoltaikanlagen.mock.csv b/app/rdf/query-search-results-photovoltaikanlagen.mock.csv new file mode 100644 index 000000000..550863dc2 --- /dev/null +++ b/app/rdf/query-search-results-photovoltaikanlagen.mock.csv @@ -0,0 +1,17 @@ +"subject","predicate","object" +"https://energy.ld.admin.ch/sfoe/bfe_ogd84_einmalverguetung_fuer_photovoltaikanlagen/10","http://www.w3.org/1999/02/22-rdf-syntax-ns#type","https://cube.link/Cube" +"https://energy.ld.admin.ch/sfoe/bfe_ogd84_einmalverguetung_fuer_photovoltaikanlagen/10","https://cube.link/observationConstraint","https://energy.ld.admin.ch/sfoe/bfe_ogd84_einmalverguetung_fuer_photovoltaikanlagen/10/shape/" +"https://energy.ld.admin.ch/sfoe/bfe_ogd84_einmalverguetung_fuer_photovoltaikanlagen/10","http://purl.org/dc/terms/publisher","Pronovo AG" +"https://energy.ld.admin.ch/sfoe/bfe_ogd84_einmalverguetung_fuer_photovoltaikanlagen/10","http://schema.org/creativeWorkStatus","https://ld.admin.ch/vocabulary/CreativeWorkStatus/Published" +"https://energy.ld.admin.ch/sfoe/bfe_ogd84_einmalverguetung_fuer_photovoltaikanlagen/10","http://schema.org/creator","https://register.ld.admin.ch/opendataswiss/org/bundesamt-fur-energie-bfe" +"https://energy.ld.admin.ch/sfoe/bfe_ogd84_einmalverguetung_fuer_photovoltaikanlagen/10","http://schema.org/datePublished","2022-08-16" +"https://energy.ld.admin.ch/sfoe/bfe_ogd84_einmalverguetung_fuer_photovoltaikanlagen/10","http://schema.org/description","Seit 2014 werden Photovoltaikanlagen mit einer Einmalvergütung (EIV) gefördert. Dabei wird abhängig von der Leistung, der Anlagenkategorie und dem Inbetriebnahmedatum ein einmaliger Beitrag an die Anlagenbetreiber ausbezahlt. Hier finden Sie pro Kanton und Auszahlungsjahr einen Überblick über die Anzahl geförderter EIV-Anlagen, die installierte Leistung in Kilowatt (kW) sowie den ausbezahlten EIV-Förderbeitrag. Die dargestellten Daten entsprechen nicht vollständig der offiziellen Statistik der erneuerbaren Energien durch das BFE. Da der Abbau der Wartelisten zeitverzögert stattfindet, können Abweichungen entstehen." +"https://energy.ld.admin.ch/sfoe/bfe_ogd84_einmalverguetung_fuer_photovoltaikanlagen/10","http://schema.org/name","Einmalvergütung für Photovoltaikanlagen" +"https://energy.ld.admin.ch/sfoe/bfe_ogd84_einmalverguetung_fuer_photovoltaikanlagen/10","http://schema.org/workExample","https://ld.admin.ch/application/visualize" +"https://energy.ld.admin.ch/sfoe/bfe_ogd84_einmalverguetung_fuer_photovoltaikanlagen/10","tag:/themeIris","https://register.ld.admin.ch/opendataswiss/category/energy|||https://register.ld.admin.ch/opendataswiss/category/national-economy" +"https://energy.ld.admin.ch/sfoe/bfe_ogd84_einmalverguetung_fuer_photovoltaikanlagen/10","tag:/themeLabels","Energy|||National economy" +"https://ld.admin.ch/dimension/office","https://cube.link/meta/isUsedIn","https://energy.ld.admin.ch/sfoe/bfe_ogd84_einmalverguetung_fuer_photovoltaikanlagen/10" +"https://ld.admin.ch/dimension/office","http://schema.org/name","Federal Offices" +"https://register.ld.admin.ch/opendataswiss/org/bundesamt-fur-energie-bfe","http://schema.org/name","Swiss Federal Office of Energy SFOE" +"https://ld.admin.ch/dimension/canton","https://cube.link/meta/isUsedIn","https://energy.ld.admin.ch/sfoe/bfe_ogd84_einmalverguetung_fuer_photovoltaikanlagen/10" +"https://ld.admin.ch/dimension/canton","http://schema.org/name","Cantons" \ No newline at end of file From 7d116130d6663faaa321cf3216e8a9b62594ef5b Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Tue, 24 Sep 2024 16:24:41 +0200 Subject: [PATCH 12/12] fix: Fetch dimension termsets when merging cubes --- app/components/graphql-search.stories.tsx | 1 + .../components/add-dataset-dialog.tsx | 1 + app/graphql/queries/data-cubes.graphql | 2 + app/graphql/query-hooks.ts | 5 +- app/graphql/resolver-types.ts | 1 + app/graphql/resolvers/rdf.ts | 3 +- app/graphql/schema.graphql | 1 + app/rdf/parse-search-results.spec.ts | 141 +++++++----------- ...-search-results-shared-dimensions.mock.csv | 4 + .../query-search-results-temporal.mock.csv | 4 + app/rdf/query-search.ts | 50 +++++-- 11 files changed, 114 insertions(+), 99 deletions(-) diff --git a/app/components/graphql-search.stories.tsx b/app/components/graphql-search.stories.tsx index 69d12f247..8bdba770a 100644 --- a/app/components/graphql-search.stories.tsx +++ b/app/components/graphql-search.stories.tsx @@ -108,6 +108,7 @@ export const Search = () => { : null, ].filter(truthy), includeDrafts: false, + fetchDimensionTermsets: true, query, }, }); diff --git a/app/configurator/components/add-dataset-dialog.tsx b/app/configurator/components/add-dataset-dialog.tsx index 07e275ca3..5e5b958e4 100644 --- a/app/configurator/components/add-dataset-dialog.tsx +++ b/app/configurator/components/add-dataset-dialog.tsx @@ -714,6 +714,7 @@ export const DatasetDialog = ({ query: query, order: SearchCubeResultOrder.Score, includeDrafts: false, + fetchDimensionTermsets: true, filters: [ selectedTemporalDimension ? { diff --git a/app/graphql/queries/data-cubes.graphql b/app/graphql/queries/data-cubes.graphql index bf7738aa7..8eabd91c6 100644 --- a/app/graphql/queries/data-cubes.graphql +++ b/app/graphql/queries/data-cubes.graphql @@ -5,6 +5,7 @@ query SearchCubes( $query: String $order: SearchCubeResultOrder $includeDrafts: Boolean + $fetchDimensionTermsets: Boolean $filters: [SearchCubeFilter!] ) { searchCubes( @@ -14,6 +15,7 @@ query SearchCubes( query: $query order: $order includeDrafts: $includeDrafts + fetchDimensionTermsets: $fetchDimensionTermsets filters: $filters ) { highlightedTitle diff --git a/app/graphql/query-hooks.ts b/app/graphql/query-hooks.ts index c190dd94a..ed3d2c97a 100644 --- a/app/graphql/query-hooks.ts +++ b/app/graphql/query-hooks.ts @@ -202,6 +202,7 @@ export type QuerySearchCubesArgs = { query?: Maybe; order?: Maybe; includeDrafts?: Maybe; + fetchDimensionTermsets?: Maybe; filters?: Maybe>; }; @@ -277,6 +278,7 @@ export type SearchCubesQueryVariables = Exact<{ query?: Maybe; order?: Maybe; includeDrafts?: Maybe; + fetchDimensionTermsets?: Maybe; filters?: Maybe | SearchCubeFilter>; }>; @@ -363,7 +365,7 @@ export type PossibleFiltersQuery = { __typename: 'Query', possibleFilters: Array export const SearchCubesDocument = gql` - query SearchCubes($sourceType: String!, $sourceUrl: String!, $locale: String!, $query: String, $order: SearchCubeResultOrder, $includeDrafts: Boolean, $filters: [SearchCubeFilter!]) { + query SearchCubes($sourceType: String!, $sourceUrl: String!, $locale: String!, $query: String, $order: SearchCubeResultOrder, $includeDrafts: Boolean, $fetchDimensionTermsets: Boolean, $filters: [SearchCubeFilter!]) { searchCubes( sourceType: $sourceType sourceUrl: $sourceUrl @@ -371,6 +373,7 @@ export const SearchCubesDocument = gql` query: $query order: $order includeDrafts: $includeDrafts + fetchDimensionTermsets: $fetchDimensionTermsets filters: $filters ) { highlightedTitle diff --git a/app/graphql/resolver-types.ts b/app/graphql/resolver-types.ts index e86fa6057..b6dd0a1e8 100644 --- a/app/graphql/resolver-types.ts +++ b/app/graphql/resolver-types.ts @@ -202,6 +202,7 @@ export type QuerySearchCubesArgs = { query?: Maybe; order?: Maybe; includeDrafts?: Maybe; + fetchDimensionTermsets?: Maybe; filters?: Maybe>; }; diff --git a/app/graphql/resolvers/rdf.ts b/app/graphql/resolvers/rdf.ts index 6bd83a389..76330fef2 100644 --- a/app/graphql/resolvers/rdf.ts +++ b/app/graphql/resolvers/rdf.ts @@ -80,7 +80,7 @@ const sortResults = ( export const searchCubes: NonNullable = async ( _, - { locale, query, order, includeDrafts, filters }, + { locale, query, order, includeDrafts, fetchDimensionTermsets, filters }, { setup }, info ) => { @@ -88,6 +88,7 @@ export const searchCubes: NonNullable = async ( const cubes = await _searchCubes({ locale, includeDrafts, + fetchDimensionTermsets, filters, query, sparqlClient, diff --git a/app/graphql/schema.graphql b/app/graphql/schema.graphql index 08b496989..b83eaf1f8 100644 --- a/app/graphql/schema.graphql +++ b/app/graphql/schema.graphql @@ -184,6 +184,7 @@ type Query { query: String order: SearchCubeResultOrder includeDrafts: Boolean + fetchDimensionTermsets: Boolean filters: [SearchCubeFilter!] ): [SearchCubeResult!]! dataCubeDimensionGeoShapes( diff --git a/app/rdf/parse-search-results.spec.ts b/app/rdf/parse-search-results.spec.ts index 24b601a78..b12d8f0b8 100644 --- a/app/rdf/parse-search-results.spec.ts +++ b/app/rdf/parse-search-results.spec.ts @@ -77,7 +77,7 @@ describe("parse-search-results", () => { `); }); - it("should build search cubes from CSV (shared dimension query)", async () => { + it("should build search cubes from CSV (shared dimension query, all)", async () => { const data = await parseCSV( path.join(__dirname, "./query-search-results-shared-dimensions.mock.csv") ); @@ -86,6 +86,33 @@ describe("parse-search-results", () => { expect(searchCubes.slice(0, 3)).toMatchInlineSnapshot(` Array [ + Object { + "creator": Object { + "iri": "https://register.ld.admin.ch/opendataswiss/org/elcom", + "label": "Federal Electricity Commission ElCom", + }, + "datePublished": "2021-01-01", + "description": "Median electricity tariff per region & consumption profiles", + "dimensions": Array [ + Object { + "iri": "https://energy.ld.admin.ch/elcom/electricityprice/dimension/canton", + "label": "Canton", + "termsets": Array [ + Object { + "iri": "https://ld.admin.ch/dimension/canton", + "label": "Cantons", + }, + ], + "timeUnit": "", + }, + ], + "iri": "https://energy.ld.admin.ch/elcom/electricityprice-canton", + "publicationStatus": "PUBLISHED", + "subthemes": Array [], + "termsets": Array [], + "themes": Array [], + "title": "Median electricity tariff per canton", + }, Object { "creator": Object { "iri": "https://register.ld.admin.ch/opendataswiss/org/bundesamt-fur-energie-bfe", @@ -109,6 +136,7 @@ describe("parse-search-results", () => { "iri": "https://energy.ld.admin.ch/sfoe/bfe_ogd84_einmalverguetung_fuer_photovoltaikanlagen/14", "publicationStatus": "PUBLISHED", "subthemes": Array [], + "termsets": Array [], "themes": Array [ Object { "iri": "https://register.ld.admin.ch/opendataswiss/category/national-economy", @@ -144,6 +172,7 @@ describe("parse-search-results", () => { "iri": "https://energy.ld.admin.ch/sfoe/bfe_ogd18_gebaeudeprogramm_co2wirkung/4", "publicationStatus": "PUBLISHED", "subthemes": Array [], + "termsets": Array [], "themes": Array [ Object { "iri": "https://register.ld.admin.ch/opendataswiss/category/energy", @@ -164,54 +193,11 @@ describe("parse-search-results", () => { ], "title": "Gebäudeprogramm - CO2-Wirkungen je Massnahmenbereich", }, - Object { - "creator": Object { - "iri": "https://register.ld.admin.ch/opendataswiss/org/bundesamt-fur-energie-bfe", - "label": "Swiss Federal Office of Energy SFOE", - }, - "datePublished": "2023-09-12", - "description": "Anzahl Gesuche mit Auszahlungen im jeweiligen Jahr, Gebäudeprogramm ab 2017 (nur direkte Massnahmen)", - "dimensions": Array [ - Object { - "iri": "https://energy.ld.admin.ch/sfoe/bfe_ogd18_gebaeudeprogramm_anzahl_gesuche/region", - "label": "Kanton", - "termsets": Array [ - Object { - "iri": "https://ld.admin.ch/dimension/canton", - "label": "Cantons", - }, - ], - "timeUnit": "", - }, - ], - "iri": "https://energy.ld.admin.ch/sfoe/bfe_ogd18_gebaeudeprogramm_anzahl_gesuche/15", - "publicationStatus": "PUBLISHED", - "subthemes": Array [], - "themes": Array [ - Object { - "iri": "https://register.ld.admin.ch/opendataswiss/category/energy", - "label": "Energy", - }, - Object { - "iri": "https://register.ld.admin.ch/opendataswiss/category/construction", - "label": "Construction and housing", - }, - Object { - "iri": "https://register.ld.admin.ch/opendataswiss/category/population", - "label": "Population", - }, - Object { - "iri": "https://register.ld.admin.ch/opendataswiss/category/statistical-basis", - "label": "Statistical basis", - }, - ], - "title": "Gebäudeprogramm - Anzahl unterstützter Gesuche", - }, ] `); }); - it("should build search cubes from CSV (shared dimension query)", async () => { + it("should build search cubes from CSV (shared dimension query, temporal)", async () => { const data = await parseCSV( path.join(__dirname, "./query-search-results-temporal.mock.csv") ); @@ -219,6 +205,28 @@ describe("parse-search-results", () => { expect(searchCubes.slice(0, 3)).toMatchInlineSnapshot(` Array [ + Object { + "creator": Object { + "iri": "https://register.ld.admin.ch/opendataswiss/org/elcom", + "label": "Eidgenössische Elektrizitätskommission ElCom", + }, + "datePublished": "2021-01-01", + "description": "Strompreise per Stromnetzbetreiber und Gemeinde in der Schweiz", + "dimensions": Array [ + Object { + "iri": "https://energy.ld.admin.ch/elcom/electricityprice/dimension/period", + "label": "", + "termsets": Array [], + "timeUnit": "http://www.w3.org/2006/time#unitYear", + }, + ], + "iri": "https://energy.ld.admin.ch/elcom/electricityprice", + "publicationStatus": "PUBLISHED", + "subthemes": Array [], + "termsets": Array [], + "themes": Array [], + "title": "Strompreis per Stromnetzbetreiber", + }, Object { "creator": Object { "iri": "https://register.ld.admin.ch/opendataswiss/org/bundesamt-fur-energie-bfe", @@ -237,6 +245,7 @@ describe("parse-search-results", () => { "iri": "https://energy.ld.admin.ch/sfoe/OGD84GebTest/1", "publicationStatus": "PUBLISHED", "subthemes": Array [], + "termsets": Array [], "themes": Array [ Object { "iri": "https://register.ld.admin.ch/opendataswiss/category/energy", @@ -246,14 +255,6 @@ describe("parse-search-results", () => { "iri": "https://register.ld.admin.ch/opendataswiss/category/national-economy", "label": "Volkswirtschaft", }, - Object { - "iri": "https://register.ld.admin.ch/opendataswiss/category/energy", - "label": "Energie", - }, - Object { - "iri": "https://register.ld.admin.ch/opendataswiss/category/national-economy", - "label": "Volkswirtschaft", - }, ], "title": "GEB - Einmalvergütung für Photovoltaikanlagen", }, @@ -308,6 +309,7 @@ describe("parse-search-results", () => { "label": "Wirtschaft und Konsum", }, ], + "termsets": Array [], "themes": Array [ Object { "iri": "https://register.ld.admin.ch/opendataswiss/category/territory", @@ -328,39 +330,8 @@ describe("parse-search-results", () => { ], "title": "Statistik der Wasserkraftanlagen (WASTA)", }, - Object { - "creator": Object { - "iri": "https://register.ld.admin.ch/opendataswiss/org/bundesamt-fur-umwelt-bafu", - "label": "Bundesamt für Umwelt BAFU", - }, - "datePublished": "2022-08-17", - "description": "Cf. IIR report Submission 2022.", - "dimensions": Array [ - Object { - "iri": "https://environment.ld.admin.ch/foen/BAFU_LuChem_EMIS_pollutants_KCA_all_years/year", - "label": "", - "termsets": Array [], - "timeUnit": "http://www.w3.org/2006/time#unitYear", - }, - ], - "iri": "https://environment.ld.admin.ch/foen/BAFU_LuChem_EMIS_pollutants_KCA_all_years/7", - "publicationStatus": "PUBLISHED", - "subthemes": Array [ - Object { - "iri": "https://register.ld.admin.ch/foen/theme/11", - "label": "Luft", - }, - ], - "themes": Array [ - Object { - "iri": "https://register.ld.admin.ch/opendataswiss/category/territory", - "label": "Raum und Umwelt", - }, - ], - "title": "Schlüsselkategorien für die wichtigsten Schadstoffe für das Einreichungsjahr 2022", - }, ] - `); +`); }); }); diff --git a/app/rdf/query-search-results-shared-dimensions.mock.csv b/app/rdf/query-search-results-shared-dimensions.mock.csv index 3bfd7df5a..1bd84a395 100644 --- a/app/rdf/query-search-results-shared-dimensions.mock.csv +++ b/app/rdf/query-search-results-shared-dimensions.mock.csv @@ -32,6 +32,8 @@ "https://energy.ld.admin.ch/sfoe/bfe_ogd84_einmalverguetung_fuer_photovoltaikanlagen/14","http://schema.org/name","Einmalvergütung für Photovoltaikanlagen" "https://energy.ld.admin.ch/sfoe/bfe_ogd84_einmalverguetung_fuer_photovoltaikanlagen/14","http://schema.org/workExample","https://ld.admin.ch/application/visualize" "https://energy.ld.admin.ch/sfoe/bfe_ogd84_einmalverguetung_fuer_photovoltaikanlagen/14","https://visualize.admin.ch/hasDimension","https://energy.ld.admin.ch/sfoe/bfe_ogd84_einmalverguetung_fuer_photovoltaikanlagen/Kanton" +"https://energy.ld.admin.ch/sfoe/bfe_ogd84_einmalverguetung_fuer_photovoltaikanlagen/14","tag:/themeIris","https://register.ld.admin.ch/opendataswiss/category/national-economy|||https://register.ld.admin.ch/opendataswiss/category/energy" +"https://energy.ld.admin.ch/sfoe/bfe_ogd84_einmalverguetung_fuer_photovoltaikanlagen/14","tag:/themeLabels","National economy|||Energy" "https://energy.ld.admin.ch/sfoe/bfe_ogd84_einmalverguetung_fuer_photovoltaikanlagen/Kanton","https://visualize.admin.ch/hasTermset","https://ld.admin.ch/dimension/canton" "https://energy.ld.admin.ch/sfoe/bfe_ogd84_einmalverguetung_fuer_photovoltaikanlagen/Kanton","http://schema.org/name","Kanton" "https://register.ld.admin.ch/opendataswiss/org/bundesamt-fur-energie-bfe","http://schema.org/name","Swiss Federal Office of Energy SFOE" @@ -48,6 +50,8 @@ "https://energy.ld.admin.ch/sfoe/bfe_ogd18_gebaeudeprogramm_co2wirkung/4","http://schema.org/name","Gebäudeprogramm - CO2-Wirkungen je Massnahmenbereich" "https://energy.ld.admin.ch/sfoe/bfe_ogd18_gebaeudeprogramm_co2wirkung/4","http://schema.org/workExample","https://ld.admin.ch/application/visualize" "https://energy.ld.admin.ch/sfoe/bfe_ogd18_gebaeudeprogramm_co2wirkung/4","https://visualize.admin.ch/hasDimension","https://energy.ld.admin.ch/sfoe/bfe_ogd18_gebaeudeprogramm_co2wirkung/region" +"https://energy.ld.admin.ch/sfoe/bfe_ogd18_gebaeudeprogramm_co2wirkung/4","tag:/themeIris","https://register.ld.admin.ch/opendataswiss/category/energy|||https://register.ld.admin.ch/opendataswiss/category/construction|||https://register.ld.admin.ch/opendataswiss/category/statistical-basis|||https://register.ld.admin.ch/opendataswiss/category/population" +"https://energy.ld.admin.ch/sfoe/bfe_ogd18_gebaeudeprogramm_co2wirkung/4","tag:/themeLabels","Energy|||Construction and housing|||Statistical basis|||Population" "https://energy.ld.admin.ch/sfoe/bfe_ogd18_gebaeudeprogramm_co2wirkung/region","https://visualize.admin.ch/hasTermset","https://ld.admin.ch/dimension/country" "https://energy.ld.admin.ch/sfoe/bfe_ogd18_gebaeudeprogramm_co2wirkung/region","http://schema.org/name","Region" "https://ld.admin.ch/dimension/country","http://schema.org/name","Countries" diff --git a/app/rdf/query-search-results-temporal.mock.csv b/app/rdf/query-search-results-temporal.mock.csv index 26d61f120..8e3795ca7 100644 --- a/app/rdf/query-search-results-temporal.mock.csv +++ b/app/rdf/query-search-results-temporal.mock.csv @@ -30,6 +30,8 @@ "https://energy.ld.admin.ch/sfoe/OGD84GebTest/1","http://schema.org/name","GEB - Einmalvergütung für Photovoltaikanlagen" "https://energy.ld.admin.ch/sfoe/OGD84GebTest/1","http://schema.org/workExample","https://ld.admin.ch/application/visualize" "https://energy.ld.admin.ch/sfoe/OGD84GebTest/1","https://visualize.admin.ch/hasDimension","https://energy.ld.admin.ch/sfoe/OGD84GebTest/Jahr" +"https://energy.ld.admin.ch/sfoe/OGD84GebTest/1","tag:/themeIris","https://register.ld.admin.ch/opendataswiss/category/energy|||https://register.ld.admin.ch/opendataswiss/category/national-economy" +"https://energy.ld.admin.ch/sfoe/OGD84GebTest/1","tag:/themeLabels","Energie|||Volkswirtschaft" "https://energy.ld.admin.ch/sfoe/OGD84GebTest/Jahr","https://visualize.admin.ch/hasTimeUnit","http://www.w3.org/2006/time#unitYear" "https://register.ld.admin.ch/opendataswiss/org/bundesamt-fur-energie-bfe","http://schema.org/name","Bundesamt für Energie BFE" "https://register.ld.admin.ch/opendataswiss/category/energy","http://schema.org/name","Energie" @@ -53,6 +55,8 @@ "https://energy.ld.admin.ch/sfoe/bfe_ogd40_wasta/7","http://schema.org/name","Statistik der Wasserkraftanlagen (WASTA)" "https://energy.ld.admin.ch/sfoe/bfe_ogd40_wasta/7","http://schema.org/workExample","https://ld.admin.ch/application/visualize" "https://energy.ld.admin.ch/sfoe/bfe_ogd40_wasta/7","https://visualize.admin.ch/hasDimension","https://energy.ld.admin.ch/sfoe/bfe_ogd40_wasta/YearOfStatistic" +"https://energy.ld.admin.ch/sfoe/bfe_ogd40_wasta/7","tag:/themeIris","https://register.ld.admin.ch/opendataswiss/category/territory|||https://register.ld.admin.ch/opendataswiss/category/energy|||https://register.ld.admin.ch/opendataswiss/category/geography|||https://register.ld.admin.ch/opendataswiss/category/culture" +"https://energy.ld.admin.ch/sfoe/bfe_ogd40_wasta/7","tag:/themeLabels","Raum und Umwelt|||Energie|||Geographie|||Kultur, Medien, Informationsgesellschaft, Sport" "https://energy.ld.admin.ch/sfoe/bfe_ogd40_wasta/YearOfStatistic","https://visualize.admin.ch/hasTimeUnit","http://www.w3.org/2006/time#unitYear" "https://register.ld.admin.ch/opendataswiss/category/territory","http://schema.org/name","Raum und Umwelt" "https://register.ld.admin.ch/foen/theme/15","http://schema.org/inDefinedTermSet","https://register.ld.admin.ch/foen/theme" diff --git a/app/rdf/query-search.ts b/app/rdf/query-search.ts index 2e0d51221..dc606fa76 100644 --- a/app/rdf/query-search.ts +++ b/app/rdf/query-search.ts @@ -68,12 +68,14 @@ export const searchCubes = async ({ locale: _locale, filters, includeDrafts, + fetchDimensionTermsets, sparqlClient, }: { query?: string | null; locale?: string | null; filters?: SearchCubeFilter[] | null; includeDrafts?: Boolean | null; + fetchDimensionTermsets?: boolean | null; sparqlClient: ParsingClient; }) => { const locale = _locale ?? defaultLocale; @@ -95,7 +97,8 @@ export const searchCubes = async ({ creatorValues, themeValues, includeDrafts, - query + query, + fetchDimensionTermsets ) ); @@ -146,7 +149,8 @@ const mkScoresQuery = ( creatorValues: string[], themeValues: string[], includeDrafts: Boolean | null | undefined, - query: string | null | undefined + query: string | null | undefined, + fetchDimensionTermsets: boolean | null | undefined ) => { return ` ${pragmas} @@ -182,7 +186,10 @@ const mkScoresQuery = ( ?dimensionIri visualize:hasTimeUnit ?unitType ; - schema:name ?dimensionLabel . + schema:name ?dimensionLabel ; + visualize:hasTermset ?dimensionTermsetIri . + + ?dimensionTermsetIri schema:name ?dimensionTermsetLabel . ?termsetIri meta:isUsedIn ?iri ; @@ -206,8 +213,10 @@ const mkScoresQuery = ( ?description ?title ?dimensionIri - ?unitType ?dimensionLabel + ?dimensionTermsetIri + ?dimensionTermsetLabel + ?unitType ?termsetIri ?termsetLabel ?creatorLabel @@ -249,11 +258,26 @@ const mkScoresQuery = ( )} `; } else if (filter.type === SearchCubeFilterType.DataCubeTermset) { - const sharedDimensions = filter.value.split(";"); - return ` - VALUES (?termsetIri) {${sharedDimensions.map((sd) => `( ${iriToNode(sd)} )`).join(" ")}} - ?termsetIri meta:isUsedIn ?iri . - ${buildLocalizedSubQuery("termsetIri", "schema:name", "termsetLabel", { locale })}`; + if (fetchDimensionTermsets) { + const sharedDimensions = filter.value.split(";"); + return ` + VALUES (?dimensionTermsetIri) {${sharedDimensions.map((sd) => `(${iriToNode(sd)})`).join(" ")}} + ?iri cube:observationConstraint/sh:property ?dimension . + ${buildLocalizedSubQuery("dimension", "schema:name", "dimensionLabel", { locale })} + ?dimension + sh:path ?dimensionIri ; + a cube:KeyDimension ; + sh:in/rdf:first ?value . + ?value schema:inDefinedTermSet ?dimensionTermsetIri . + ${buildLocalizedSubQuery("dimensionTermsetIri", "schema:name", "dimensionTermsetLabel", { locale })} + `; + } else { + const termsets = filter.value.split(";"); + return ` + VALUES (?termsetIri) {${termsets.map((termset) => `(${iriToNode(termset)})`).join(" ")}} + ?termsetIri meta:isUsedIn ?iri . + ${buildLocalizedSubQuery("termsetIri", "schema:name", "termsetLabel", { locale })}`; + } } }) .filter(truthy) @@ -263,8 +287,8 @@ const mkScoresQuery = ( !filters?.find((f) => f.type === SearchCubeFilterType.DataCubeTermset) ? ` OPTIONAL { - ?termsetIri meta:isUsedIn ?iri . - ${buildLocalizedSubQuery("termsetIri", "schema:name", "termsetLabel", { locale })} + ?termsetIri meta:isUsedIn ?iri . + ${buildLocalizedSubQuery("termsetIri", "schema:name", "termsetLabel", { locale })} }` : "" } @@ -392,8 +416,10 @@ const mkScoresQuery = ( ?description ?title ?dimensionIri - ?unitType ?dimensionLabel + ?dimensionTermsetIri + ?dimensionTermsetLabel + ?unitType ?termsetIri ?termsetLabel ?creatorLabel