diff --git a/app/browser/dataset-preview.tsx b/app/browser/dataset-preview.tsx index 4432352009..3387a2fdd3 100644 --- a/app/browser/dataset-preview.tsx +++ b/app/browser/dataset-preview.tsx @@ -7,13 +7,15 @@ import * as React from "react"; import { DataSetPreviewTable } from "@/browse/datatable"; import { useFootnotesStyles } from "@/components/chart-footnotes"; -import { DataDownloadMenu, RunSparqlQuery } from "@/components/data-download"; +import { DataDownloadMenu } from "@/components/data-download"; import Flex from "@/components/flex"; import { HintRed, Loading, LoadingDataError } from "@/components/hint"; import { DataSource } from "@/config-types"; import { sourceToLabel } from "@/domain/datasource"; -import { useDataCubesComponentsQuery } from "@/graphql/hooks"; -import { useDataCubePreviewQuery } from "@/graphql/query-hooks"; +import { + useDataCubeMetadataQuery, + useDataCubePreviewQuery, +} from "@/graphql/query-hooks"; import { DataCubePublicationStatus } from "@/graphql/resolver-types"; import { useLocale } from "@/locales/use-locale"; @@ -93,55 +95,38 @@ export const DataSetPreview = ({ }) => { const footnotesClasses = useFootnotesStyles({ useMarginTop: false }); const locale = useLocale(); - const cubeFilters = [{ iri: dataSetIri }]; + const variables = { + sourceType: dataSource.type, + sourceUrl: dataSource.url, + locale, + cubeFilter: { iri: dataSetIri }, + }; + const [{ data: metadata, fetching: fetchingMetadata, error: metadataError }] = + useDataCubeMetadataQuery({ variables }); const [ { data: previewData, fetching: fetchingPreview, error: previewError }, - ] = useDataCubePreviewQuery({ - variables: { - iri: dataSetIri, - sourceType: dataSource.type, - sourceUrl: dataSource.url, - locale, - }, - }); - const [ - { - data: componentsData, - fetching: fetchingComponents, - error: componentsError, - }, - ] = useDataCubesComponentsQuery({ - variables: { - sourceType: dataSource.type, - sourceUrl: dataSource.url, - locale, - cubeFilters, - }, - }); + ] = useDataCubePreviewQuery({ variables }); const classes = useStyles({ - descriptionPresent: !!previewData?.dataCubeByIri?.description, + descriptionPresent: !!metadata?.dataCubeMetadata.description, }); React.useEffect(() => { window.scrollTo({ top: 0 }); }, []); - if (fetchingPreview || fetchingComponents) { + if (fetchingMetadata || fetchingPreview) { return ( ); - } else if ( - previewData?.dataCubeByIri && - componentsData?.dataCubesComponents - ) { - const { dataCubeByIri } = previewData; - const { dataCubesComponents } = componentsData; + } else if (metadata?.dataCubeMetadata && previewData?.dataCubePreview) { + const { dataCubeMetadata } = metadata; + const { dataCubePreview } = previewData; return ( - {dataCubeByIri.publicationStatus === + {dataCubeMetadata.publicationStatus === DataCubePublicationStatus.Draft && ( @@ -156,15 +141,15 @@ export const DataSetPreview = ({ - {dataCubeByIri.title} - visualize.admin.ch + {dataCubeMetadata.title} - visualize.admin.ch - {dataCubeByIri.title} + {dataCubeMetadata.title} - {dataCubeByIri.description && ( + {dataCubeMetadata.description && ( - {dataCubeByIri.description} + {dataCubeMetadata.description} )} - {dataCubeByIri.observations.sparqlEditorUrl && ( + {/* {dataCubeByIri.observations.sparqlEditorUrl && ( - )} + )} */} ); diff --git a/app/domain/data.ts b/app/domain/data.ts index a4d214200a..1fb6b0f0f3 100644 --- a/app/domain/data.ts +++ b/app/domain/data.ts @@ -40,21 +40,6 @@ export type HierarchyValue = { children?: HierarchyValue[]; }; -export type Observation = Record; - -export type DataCubeObservations = { - data: Observation[]; - sparqlEditorUrl: string; -}; - -export type DataCubesObservations = { - data: Observation[]; - sparqlEditorUrls: { - cubeIri: string; - url: string; - }[]; -}; - export type DataCubeComponents = { dimensions: Dimension[]; measures: Measure[]; @@ -82,6 +67,27 @@ export type DataCubeMetadata = { workExamples?: string[]; }; +export type Observation = Record; + +export type DataCubeObservations = { + data: Observation[]; + sparqlEditorUrl: string; +}; + +export type DataCubesObservations = { + data: Observation[]; + sparqlEditorUrls: { + cubeIri: string; + url: string; + }[]; +}; + +export type DataCubePreview = { + dimensions: Dimension[]; + measures: Measure[]; + observations: Observation[]; +}; + export type Component = Dimension | Measure; export type BaseComponent = { diff --git a/app/graphql/queries/data-cubes.graphql b/app/graphql/queries/data-cubes.graphql index 4cc9c08f2f..a278e2e526 100644 --- a/app/graphql/queries/data-cubes.graphql +++ b/app/graphql/queries/data-cubes.graphql @@ -40,6 +40,20 @@ query DataCubeObservations( ) } +query DataCubePreview( + $sourceType: String! + $sourceUrl: String! + $locale: String! + $cubeFilter: DataCubePreviewFilter! +) { + dataCubePreview( + sourceType: $sourceType + sourceUrl: $sourceUrl + locale: $locale + cubeFilter: $cubeFilter + ) +} + query SearchCubes( $sourceType: String! $sourceUrl: String! @@ -64,38 +78,6 @@ query SearchCubes( } } -query DataCubePreview( - $iri: String! - $sourceType: String! - $sourceUrl: String! - $locale: String! - $latest: Boolean - $disableValuesLoad: Boolean = true -) { - dataCubeByIri( - iri: $iri - sourceType: $sourceType - sourceUrl: $sourceUrl - locale: $locale - latest: $latest - disableValuesLoad: $disableValuesLoad - ) { - iri - title - description - publicationStatus - observations( - sourceType: $sourceType - sourceUrl: $sourceUrl - preview: true - limit: 10 - ) { - data - sparqlEditorUrl - } - } -} - query GeoCoordinatesByDimensionIri( $dataCubeIri: String! $dimensionIri: String! diff --git a/app/graphql/query-hooks.ts b/app/graphql/query-hooks.ts index 436d9a7067..1acad2aa53 100644 --- a/app/graphql/query-hooks.ts +++ b/app/graphql/query-hooks.ts @@ -1,6 +1,7 @@ import { DataCubeComponents } from '../domain/data'; import { DataCubeMetadata } from '../domain/data'; import { DataCubeObservations } from '../domain/data'; +import { DataCubePreview } from '../domain/data'; import { DimensionValue } from '../domain/data'; import { Filters } from '../configurator'; import { HierarchyValue } from '../domain/data'; @@ -24,6 +25,7 @@ export type Scalars = { DataCubeComponents: DataCubeComponents; DataCubeMetadata: DataCubeMetadata; DataCubeObservations: DataCubeObservations; + DataCubePreview: DataCubePreview; DimensionValue: DimensionValue; FilterValue: any; Filters: Filters; @@ -111,7 +113,6 @@ export type DataCubeMetadataFilter = { export type DataCubeObservationFilter = { iri: Scalars['String']; latest?: Maybe; - preview?: Maybe; filters?: Maybe; componentIris?: Maybe>; joinBy?: Maybe; @@ -124,6 +125,12 @@ export type DataCubeOrganization = { label?: Maybe; }; + +export type DataCubePreviewFilter = { + iri: Scalars['String']; + latest?: Maybe; +}; + export enum DataCubePublicationStatus { Draft = 'DRAFT', Published = 'PUBLISHED' @@ -384,6 +391,7 @@ export type Query = { dataCubeComponents: Scalars['DataCubeComponents']; dataCubeMetadata: Scalars['DataCubeMetadata']; dataCubeObservations: Scalars['DataCubeObservations']; + dataCubePreview: Scalars['DataCubePreview']; dataCubeByIri?: Maybe; possibleFilters: Array; searchCubes: Array; @@ -414,6 +422,14 @@ export type QueryDataCubeObservationsArgs = { }; +export type QueryDataCubePreviewArgs = { + sourceType: Scalars['String']; + sourceUrl: Scalars['String']; + locale: Scalars['String']; + cubeFilter: DataCubePreviewFilter; +}; + + export type QueryDataCubeByIriArgs = { sourceType: Scalars['String']; sourceUrl: Scalars['String']; @@ -613,30 +629,28 @@ export type DataCubeObservationsQueryVariables = Exact<{ export type DataCubeObservationsQuery = { __typename: 'Query', dataCubeObservations: DataCubeObservations }; -export type SearchCubesQueryVariables = Exact<{ +export type DataCubePreviewQueryVariables = Exact<{ sourceType: Scalars['String']; sourceUrl: Scalars['String']; locale: Scalars['String']; - query?: Maybe; - order?: Maybe; - includeDrafts?: Maybe; - filters?: Maybe | SearchCubeFilter>; + cubeFilter: DataCubePreviewFilter; }>; -export type SearchCubesQuery = { __typename: 'Query', searchCubes: Array<{ __typename: 'SearchCubeResult', highlightedTitle?: Maybe, highlightedDescription?: Maybe, cube: SearchCube }> }; +export type DataCubePreviewQuery = { __typename: 'Query', dataCubePreview: DataCubePreview }; -export type DataCubePreviewQueryVariables = Exact<{ - iri: Scalars['String']; +export type SearchCubesQueryVariables = Exact<{ sourceType: Scalars['String']; sourceUrl: Scalars['String']; locale: Scalars['String']; - latest?: Maybe; - disableValuesLoad?: Maybe; + query?: Maybe; + order?: Maybe; + includeDrafts?: Maybe; + filters?: Maybe | SearchCubeFilter>; }>; -export type DataCubePreviewQuery = { __typename: 'Query', dataCubeByIri?: Maybe<{ __typename: 'DataCube', iri: string, title: string, description?: Maybe, publicationStatus: DataCubePublicationStatus, observations: { __typename: 'ObservationsQuery', data: Array, sparqlEditorUrl?: Maybe } }> }; +export type SearchCubesQuery = { __typename: 'Query', searchCubes: Array<{ __typename: 'SearchCubeResult', highlightedTitle?: Maybe, highlightedDescription?: Maybe, cube: SearchCube }> }; export type GeoCoordinatesByDimensionIriQueryVariables = Exact<{ dataCubeIri: Scalars['String']; @@ -715,6 +729,20 @@ export const DataCubeObservationsDocument = gql` export function useDataCubeObservationsQuery(options: Omit, 'query'> = {}) { return Urql.useQuery({ query: DataCubeObservationsDocument, ...options }); }; +export const DataCubePreviewDocument = gql` + query DataCubePreview($sourceType: String!, $sourceUrl: String!, $locale: String!, $cubeFilter: DataCubePreviewFilter!) { + dataCubePreview( + sourceType: $sourceType + sourceUrl: $sourceUrl + locale: $locale + cubeFilter: $cubeFilter + ) +} + `; + +export function useDataCubePreviewQuery(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( @@ -736,36 +764,6 @@ export const SearchCubesDocument = gql` export function useSearchCubesQuery(options: Omit, 'query'> = {}) { return Urql.useQuery({ query: SearchCubesDocument, ...options }); }; -export const DataCubePreviewDocument = gql` - query DataCubePreview($iri: String!, $sourceType: String!, $sourceUrl: String!, $locale: String!, $latest: Boolean, $disableValuesLoad: Boolean = true) { - dataCubeByIri( - iri: $iri - sourceType: $sourceType - sourceUrl: $sourceUrl - locale: $locale - latest: $latest - disableValuesLoad: $disableValuesLoad - ) { - iri - title - description - publicationStatus - observations( - sourceType: $sourceType - sourceUrl: $sourceUrl - preview: true - limit: 10 - ) { - data - sparqlEditorUrl - } - } -} - `; - -export function useDataCubePreviewQuery(options: Omit, 'query'> = {}) { - return Urql.useQuery({ query: DataCubePreviewDocument, ...options }); -}; export const GeoCoordinatesByDimensionIriDocument = gql` query GeoCoordinatesByDimensionIri($dataCubeIri: String!, $dimensionIri: String!, $sourceType: String!, $sourceUrl: String!, $locale: String!, $latest: Boolean) { dataCubeByIri( diff --git a/app/graphql/resolver-types.ts b/app/graphql/resolver-types.ts index 1d11d17da9..d62abb6e28 100644 --- a/app/graphql/resolver-types.ts +++ b/app/graphql/resolver-types.ts @@ -1,6 +1,7 @@ import { DataCubeComponents } from '../domain/data'; import { DataCubeMetadata } from '../domain/data'; import { DataCubeObservations } from '../domain/data'; +import { DataCubePreview } from '../domain/data'; import { DimensionValue } from '../domain/data'; import { Filters } from '../configurator'; import { HierarchyValue } from '../domain/data'; @@ -25,6 +26,7 @@ export type Scalars = { DataCubeComponents: DataCubeComponents; DataCubeMetadata: DataCubeMetadata; DataCubeObservations: DataCubeObservations; + DataCubePreview: DataCubePreview; DimensionValue: DimensionValue; FilterValue: any; Filters: Filters; @@ -112,7 +114,6 @@ export type DataCubeMetadataFilter = { export type DataCubeObservationFilter = { iri: Scalars['String']; latest?: Maybe; - preview?: Maybe; filters?: Maybe; componentIris?: Maybe>; joinBy?: Maybe; @@ -125,6 +126,12 @@ export type DataCubeOrganization = { label?: Maybe; }; + +export type DataCubePreviewFilter = { + iri: Scalars['String']; + latest?: Maybe; +}; + export enum DataCubePublicationStatus { Draft = 'DRAFT', Published = 'PUBLISHED' @@ -385,6 +392,7 @@ export type Query = { dataCubeComponents: Scalars['DataCubeComponents']; dataCubeMetadata: Scalars['DataCubeMetadata']; dataCubeObservations: Scalars['DataCubeObservations']; + dataCubePreview: Scalars['DataCubePreview']; dataCubeByIri?: Maybe; possibleFilters: Array; searchCubes: Array; @@ -415,6 +423,14 @@ export type QueryDataCubeObservationsArgs = { }; +export type QueryDataCubePreviewArgs = { + sourceType: Scalars['String']; + sourceUrl: Scalars['String']; + locale: Scalars['String']; + cubeFilter: DataCubePreviewFilter; +}; + + export type QueryDataCubeByIriArgs = { sourceType: Scalars['String']; sourceUrl: Scalars['String']; @@ -661,6 +677,8 @@ export type ResolversTypes = ResolversObject<{ DataCubeObservationFilter: DataCubeObservationFilter; DataCubeObservations: ResolverTypeWrapper; DataCubeOrganization: ResolverTypeWrapper; + DataCubePreview: ResolverTypeWrapper; + DataCubePreviewFilter: DataCubePreviewFilter; DataCubePublicationStatus: DataCubePublicationStatus; DataCubeTheme: ResolverTypeWrapper; Dimension: ResolverTypeWrapper; @@ -710,6 +728,8 @@ export type ResolversParentTypes = ResolversObject<{ DataCubeObservationFilter: DataCubeObservationFilter; DataCubeObservations: Scalars['DataCubeObservations']; DataCubeOrganization: DataCubeOrganization; + DataCubePreview: Scalars['DataCubePreview']; + DataCubePreviewFilter: DataCubePreviewFilter; DataCubeTheme: DataCubeTheme; Dimension: ResolvedDimension; DimensionValue: Scalars['DimensionValue']; @@ -784,6 +804,10 @@ export type DataCubeOrganizationResolvers; }>; +export interface DataCubePreviewScalarConfig extends GraphQLScalarTypeConfig { + name: 'DataCubePreview'; +} + export type DataCubeThemeResolvers = ResolversObject<{ iri?: Resolver; label?: Resolver, ParentType, ContextType>; @@ -961,6 +985,7 @@ export type QueryResolvers>; dataCubeMetadata?: Resolver>; dataCubeObservations?: Resolver>; + dataCubePreview?: Resolver>; dataCubeByIri?: Resolver, ParentType, ContextType, RequireFields>; possibleFilters?: Resolver, ParentType, ContextType, RequireFields>; searchCubes?: Resolver, ParentType, ContextType, RequireFields>; @@ -1052,6 +1077,7 @@ export type Resolvers = ResolversObject<{ DataCubeMetadata?: GraphQLScalarType; DataCubeObservations?: GraphQLScalarType; DataCubeOrganization?: DataCubeOrganizationResolvers; + DataCubePreview?: GraphQLScalarType; DataCubeTheme?: DataCubeThemeResolvers; Dimension?: DimensionResolvers; DimensionValue?: GraphQLScalarType; diff --git a/app/graphql/resolvers/index.ts b/app/graphql/resolvers/index.ts index ca20e7f939..7456b87ce4 100644 --- a/app/graphql/resolvers/index.ts +++ b/app/graphql/resolvers/index.ts @@ -9,6 +9,7 @@ import { DataCubeResolvers, QueryResolvers, Resolvers, + ScaleType, } from "@/graphql/resolver-types"; import * as RDF from "@/graphql/resolvers/rdf"; import * as SQL from "@/graphql/resolvers/sql"; @@ -37,6 +38,10 @@ export const Query: QueryResolvers = { const source = getSource(args.sourceType); return await source.dataCubeObservations(parent, args, context, info); }, + dataCubePreview: async (parent, args, context, info) => { + const source = getSource(args.sourceType); + return await source.dataCubePreview(parent, args, context, info); + }, dataCubeByIri: async (parent, args, context, info) => { const source = getSource(args.sourceType); return await source.dataCubeByIri(parent, args, context, info); @@ -83,10 +88,10 @@ const DataCube: DataCubeResolvers = { }; export const resolveDimensionType = ( - component: ResolvedDimension + dataKind: ResolvedDimension["data"]["dataKind"] | undefined, + scaleType: ScaleType | undefined, + related: ResolvedDimension["data"]["related"] ): DimensionType => { - const { dataKind, scaleType, related } = component.data; - if (related.some((d) => d.type === "StandardError")) { return "StandardErrorDimension"; } @@ -113,16 +118,14 @@ export const resolveDimensionType = ( }; export const resolveMeasureType = ( - component: ResolvedDimension + scaleType: ScaleType | undefined ): MeasureType => { - const { scaleType } = component.data; - return scaleType === "Ordinal" ? "OrdinalMeasure" : "NumericalMeasure"; }; const mkDimensionResolvers = (_: string): Resolvers["Dimension"] => ({ - __resolveType(dimension) { - return resolveDimensionType(dimension); + __resolveType({ data: { dataKind, scaleType, related } }) { + return resolveDimensionType(dataKind, scaleType, related); }, iri: ({ data: { iri } }) => iri, label: ({ data: { name } }) => name, @@ -201,8 +204,8 @@ export const resolvers: Resolvers = { }, }, Dimension: { - __resolveType(dimension) { - return resolveDimensionType(dimension); + __resolveType({ data: { dataKind, scaleType, related } }) { + return resolveDimensionType(dataKind, scaleType, related); }, }, NominalDimension: { @@ -273,7 +276,7 @@ export const resolvers: Resolvers = { }, Measure: { __resolveType(dimension) { - return resolveMeasureType(dimension); + return resolveMeasureType(dimension.data.scaleType); }, }, NumericalMeasure: { diff --git a/app/graphql/resolvers/rdf.ts b/app/graphql/resolvers/rdf.ts index d59c72a5a4..69cd3f3a0d 100644 --- a/app/graphql/resolvers/rdf.ts +++ b/app/graphql/resolvers/rdf.ts @@ -30,6 +30,7 @@ import { getLatestCube, } from "@/rdf/queries"; import { getCubeMetadata } from "@/rdf/query-cube-metadata"; +import { getCubePreview } from "@/rdf/query-cube-preview"; import { unversionObservation } from "@/rdf/query-dimension-values"; import { queryHierarchy } from "@/rdf/query-hierarchies"; import { SearchResult, searchCubes as _searchCubes } from "@/rdf/query-search"; @@ -209,7 +210,7 @@ export const dataCubeComponents: NonNullable< if (data.isMeasureDimension) { const result: Measure = { - __typename: resolveMeasureType(component), + __typename: resolveMeasureType(component.data.scaleType), isCurrency: data.isCurrency, isDecimal: data.isDecimal, currencyExponent: data.currencyExponent, @@ -219,7 +220,12 @@ export const dataCubeComponents: NonNullable< measures.push(result); } else { - const dimensionType = resolveDimensionType(component); + const { dataKind, scaleType, related } = component.data; + const dimensionType = resolveDimensionType( + dataKind, + scaleType, + related + ); const hierarchy = true // TODO: make this configurable ? await queryHierarchy( component, @@ -322,6 +328,18 @@ export const dataCubeObservations: NonNullable< }; }; +export const dataCubePreview: NonNullable = + async (_, { locale, cubeFilter }, { setup }, info) => { + const { sparqlClient } = await setup(info); + const { iri, latest } = cubeFilter; + + return await getCubePreview(iri, { + locale, + latest: !!latest, + sparqlClient, + }); + }; + export const dataCubeDimensions: NonNullable = async ({ cube, locale }, { componentIris }, { setup }, info) => { const { sparqlClient, cache } = await setup(info); diff --git a/app/graphql/resolvers/sql.ts b/app/graphql/resolvers/sql.ts index 266b60ebc4..3316469ece 100644 --- a/app/graphql/resolvers/sql.ts +++ b/app/graphql/resolvers/sql.ts @@ -324,3 +324,12 @@ export const dataCubeObservations: NonNullable< sparqlEditorUrl: "", }; }; + +export const dataCubePreview: NonNullable = + async () => { + return { + dimensions: [], + measures: [], + observations: [], + }; + }; diff --git a/app/graphql/schema.graphql b/app/graphql/schema.graphql index e8948abb9b..eea01b77a3 100644 --- a/app/graphql/schema.graphql +++ b/app/graphql/schema.graphql @@ -354,15 +354,20 @@ input DataCubeMetadataFilter { input DataCubeObservationFilter { iri: String! latest: Boolean - preview: Boolean filters: Filters componentIris: [String!] joinBy: String } +input DataCubePreviewFilter { + iri: String! + latest: Boolean +} + scalar DataCubeComponents scalar DataCubeMetadata scalar DataCubeObservations +scalar DataCubePreview # The "Query" type is special: it lists all of the available queries that # clients can execute, along with the return type for each. @@ -385,6 +390,12 @@ type Query { locale: String! cubeFilter: DataCubeObservationFilter! ): DataCubeObservations! + dataCubePreview( + sourceType: String! + sourceUrl: String! + locale: String! + cubeFilter: DataCubePreviewFilter! + ): DataCubePreview! dataCubeByIri( sourceType: String! sourceUrl: String! diff --git a/app/rdf/parse.ts b/app/rdf/parse.ts index 96d67c56dc..6b62c41bc4 100644 --- a/app/rdf/parse.ts +++ b/app/rdf/parse.ts @@ -95,7 +95,7 @@ export const parseCube = ({ }; }; -const timeUnits = new Map([ +export const timeUnits = new Map([ [ns.time.unitYear.value, TimeUnit.Year], [ns.time.unitMonth.value, TimeUnit.Month], [ns.time.unitWeek.value, TimeUnit.Week], @@ -105,16 +105,16 @@ const timeUnits = new Map([ [ns.time.unitSecond.value, TimeUnit.Second], ]); -const timeFormats = new Map([ +export const timeFormats = new Map([ [ns.xsd.gYear.value, "%Y"], [ns.xsd.gYearMonth.value, "%Y-%m"], [ns.xsd.date.value, "%Y-%m-%d"], [ns.xsd.dateTime.value, "%Y-%m-%dT%H:%M:%S"], ]); -export const getScaleType = (dim: CubeDimension): ScaleType | undefined => { - const scaleTypeTerm = dim.out(ns.qudt.scaleType).term; - +export const getScaleType = ( + scaleTypeTerm: Term | undefined +): ScaleType | undefined => { return scaleTypeTerm?.equals(ns.qudt.NominalScale) ? ScaleType.Nominal : scaleTypeTerm?.equals(ns.qudt.OrdinalScale) @@ -126,7 +126,7 @@ export const getScaleType = (dim: CubeDimension): ScaleType | undefined => { : undefined; }; -const getDataKind = (term: Term | undefined) => { +export const getDataKind = (term: Term | undefined) => { return term?.equals(ns.time.GeneralDateTimeDescription) ? "Time" : term?.equals(ns.schema.GeoCoordinates) @@ -285,7 +285,7 @@ export const parseCubeDimension = ({ dataKind: getDataKind(dataKindTerm), timeUnit: timeUnits.get(timeUnitTerm?.value ?? ""), timeFormat: timeFormats.get(dataType?.value ?? ""), - scaleType: getScaleType(dim), + scaleType: getScaleType(dim.out(ns.qudt.scaleType).term), }, }; }; diff --git a/app/rdf/queries.ts b/app/rdf/queries.ts index 4b132c2f47..dbac475c09 100644 --- a/app/rdf/queries.ts +++ b/app/rdf/queries.ts @@ -400,7 +400,7 @@ export const getCubeDimensionValuesWithMetadata = async ({ const result: DimensionValue[] = []; if (namedNodes.length > 0) { - const scaleType = getScaleType(dimension); + const scaleType = getScaleType(dimension.out(ns.qudt.scaleType).term); const [labels, descriptions, literals, unversioned] = await Promise.all([ loadResourceLabels({ ids: namedNodes, diff --git a/app/rdf/query-cube-preview.ts b/app/rdf/query-cube-preview.ts new file mode 100644 index 0000000000..83c88a485b --- /dev/null +++ b/app/rdf/query-cube-preview.ts @@ -0,0 +1,321 @@ +import uniqBy from "lodash/uniqBy"; +import ParsingClient from "sparql-http-client/ParsingClient"; + +import { + BaseComponent, + BaseDimension, + DataCubePreview, + Dimension, + Measure, + TemporalDimension, +} from "@/domain/data"; +import { resolveDimensionType, resolveMeasureType } from "@/graphql/resolvers"; + +import * as ns from "./namespace"; +import { getDataKind, getScaleType, timeFormats, timeUnits } from "./parse"; +import { buildLocalizedSubQuery } from "./query-utils"; + +export const getCubePreview = async ( + iri: string, + options: { + locale: string; + latest: Boolean; + sparqlClient: ParsingClient; + } +): Promise => { + const { sparqlClient, locale } = options; + const quads = await sparqlClient.query.construct( + `PREFIX cube: + PREFIX meta: + PREFIX qudt: + PREFIX rdf: + PREFIX schema: + PREFIX sh: + PREFIX time: + PREFIX xsd: + + CONSTRUCT { + ?cube schema:version ?cubeVersion . + ?cube cube:observationSet ?observationSet . + ?observationSet cube:observation ?observation . + + ?dimension sh:path ?dimensionIri . + ?dimension sh:datatype ?dimensionDataType . + ?dimension rdf:type ?dimensionType . + ?dimension qudt:scaleType ?dimensionScaleType . + ?dimension qudt:unit ?dimensionUnit . + ?dimensionUnit schema:name ?dimensionUnitLabel . + ?dimensionUnit qudt:CurrencyUnit ?dimensionUnitIsCurrency . + ?dimensionUnit qudt:currencyExponent ?dimensionUnitCurrencyExponent . + ?dimension sh:order ?dimensionOrder . + ?dimension meta:dataKind ?dimensionDataKind . + ?dimensionDataKind rdf:type ?dimensionDataKindType . + ?dimensionDataKind time:unitType ?dimensionTimeUnitType . + ?dimension schema:name ?dimensionLabel . + ?dimension schema:description ?dimensionDescription . + + ?observation ?dimensionIri ?observationValue . + ?observation ?dimensionIri ?observationValueLabel . + } WHERE { + VALUES ?cube { <${iri}> } + FILTER(EXISTS { ?cube a cube:Cube . }) {} + UNION { + ?cube cube:observationConstraint/sh:property ?dimension . + ?dimension sh:path ?dimensionIri . + OPTIONAL { ?dimension sh:datatype ?dimensionDataType . } + OPTIONAL { ?dimension rdf:type ?dimensionType . } + OPTIONAL { ?dimension qudt:scaleType ?dimensionScaleType . } + OPTIONAL { + ?dimension qudt:unit ?_dimensionUnit . + OPTIONAL { ?_dimensionUnit rdfs:label ?_dimensionUnitRdfsLabel . } + OPTIONAL { ?_dimensionUnit qudt:symbol ?_dimensionUnitSymbol . } + OPTIONAL { ?_dimensionUnit qudt:ucumCode ?_dimensionUnitUcumCode . } + OPTIONAL { ?_dimensionUnit qudt:expression ?_dimensionUnitExpression . } + OPTIONAL { ?_dimensionUnit ?_isCurrencyUnit qudt:CurrencyUnit . } + OPTIONAL { ?_dimensionUnit qudt:currencyExponent ?_dimensionUnitCurrencyExponent . } + BIND(STR(COALESCE(STR(?_dimensionUnitSymbol), STR(?_dimensionUnitUcumCode), STR(?_dimensionUnitExpression), STR(?_dimensionUnitRdfsLabel), "?")) AS ?_dimensionUnitLabel) + FILTER (LANG(?_dimensionUnitRdfsLabel) = "en") + } + OPTIONAL { + ?dimension qudt:hasUnit ?_dimensionHasUnit . + OPTIONAL { ?_dimensionHasUnit rdfs:label ?_dimensionHasUnitRdfsLabel . } + OPTIONAL { ?_dimensionHasUnit qudt:symbol ?_dimensionHasUnitSymbol . } + OPTIONAL { ?_dimensionHasUnit qudt:ucumCode ?_dimensionHasUnitUcumCode . } + OPTIONAL { ?_dimensionHasUnit qudt:expression ?_dimensionHasUnitExpression . } + OPTIONAL { ?_dimensionHasUnit ?_isCurrencyHasUnit qudt:CurrencyUnit . } + OPTIONAL { ?_dimensionHasUnit qudt:currencyExponent ?_dimensionHasUnitCurrencyExponent . } + BIND(STR(COALESCE(STR(?_dimensionHasUnitSymbol), STR(?_dimensionHasUnitUcumCode), STR(?_dimensionHasUnitExpression), STR(?_dimensionHasUnitRdfsLabel), "?")) AS ?_dimensionHasUnitLabel) + FILTER (LANG(?_dimensionHasUnitRdfsLabel) = "en") + } + BIND(COALESCE(?_dimensionUnit, ?_dimensionHasUnit) as ?dimensionUnit) + BIND(COALESCE(?_dimensionUnitLabel, ?_dimensionHasUnitLabel) as ?dimensionUnitLabel) + BIND(COALESCE(?_dimensionUnitIsCurrency, ?_dimensionHasUnitIsCurrency) as ?dimensionUnitIsCurrency) + BIND(COALESCE(?_dimensionUnitCurrencyExponent, ?_dimensionHasUnitCurrencyExponent) as ?dimensionUnitCurrencyExponent) + OPTIONAL { ?dimension sh:order ?dimensionOrder . } + OPTIONAL { + ?dimension meta:dataKind ?dimensionDataKind . + ?dimensionDataKind rdf:type ?dimensionDataKindType . + } + OPTIONAL { + ?dimension meta:dataKind ?dimensionDataKind . + ?dimensionDataKind time:unitType ?dimensionTimeUnitType . + } + ${buildLocalizedSubQuery("dimension", "schema:name", "dimensionLabel", { + locale, + })} + ${buildLocalizedSubQuery( + "dimension", + "schema:description", + "dimensionDescription", + { locale } + )} + } UNION { + ?cube cube:observationConstraint/sh:property/sh:path ?dimensionIri . + + { SELECT * WHERE { + ?cube cube:observationSet ?observationSet . + ?observationSet cube:observation ?observation . + FILTER(NOT EXISTS { ?cube cube:observationConstraint/sh:property/sh:datatype cube:Undefined . } && NOT EXISTS { ?observation ?p ""^^cube:Undefined . }) + } LIMIT 10 } + + { SELECT * WHERE { + { SELECT * WHERE { + ?cube cube:observationSet/cube:observation ?observation . + FILTER(NOT EXISTS { ?cube cube:observationConstraint/sh:property/sh:datatype cube:Undefined . } && NOT EXISTS { ?observation ?p ""^^cube:Undefined . }) + } LIMIT 10 } + + ?observation ?dimensionIri ?observationValue . + OPTIONAL { ?observationValue schema:name ?observationValueLabel_de . FILTER(LANG(?observationValueLabel_de) = "de") } + OPTIONAL { ?observationValue schema:name ?observationValueLabel_fr . FILTER(LANG(?observationValueLabel_fr) = "fr") } + OPTIONAL { ?observationValue schema:name ?observationValueLabel_it . FILTER(LANG(?observationValueLabel_it) = "it") } + OPTIONAL { ?observationValue schema:name ?observationValueLabel_en . FILTER(LANG(?observationValueLabel_en) = "en") } + OPTIONAL { ?observationValue schema:name ?observationValueLabel_ . FILTER(LANG(?observationValueLabel_) = "") } + BIND(COALESCE(?observationValueLabel_de, ?observationValueLabel_fr, ?observationValueLabel_it, ?observationValueLabel_en, ?observationValueLabel_) AS ?observationValueLabel) + }} + } + }`, + { operation: "postUrlencoded" } + ); + + if (quads.length === 0) { + throw new Error(`No cube found for ${iri}!`); + } + + const dimensionsQuads = quads.filter( + (quad) => + quad.predicate.equals(ns.sh.path) && + !quad.object.equals(ns.cube.observedBy) && + !quad.object.equals(ns.rdf.type) + ); + const dimensions: Dimension[] = []; + const measures: Measure[] = []; + uniqBy(dimensionsQuads, (quad) => quad.object.value).map((quad) => { + const dimensionQuads = quads.filter((q) => q.subject.equals(quad.subject)); + const dimensionIri = quad.object.value; + const labelQuad = dimensionQuads.find((q) => + q.predicate.equals(ns.schema.name) + ); + const descriptionQuad = dimensionQuads.find((q) => + q.predicate.equals(ns.schema.description) + ); + const orderQuad = dimensionQuads.find((q) => + q.predicate.equals(ns.sh.order) + ); + const typeQuads = dimensionQuads.filter((q) => + q.predicate.equals(ns.rdf.type) + ); + const scaleTypeQuad = dimensionQuads.find((q) => + q.predicate.equals(ns.qudt.scaleType) + ); + const scaleType = getScaleType(scaleTypeQuad?.object); + const dataTypeQuad = dimensionQuads.find((q) => + q.predicate.equals(ns.sh.datatype) + ); + const unitQuad = dimensionQuads.find((q) => + q.predicate.equals(ns.qudt.unit) + ); + const unitLabelQuad = quads.find((q) => q.subject.equals(unitQuad?.object)); + const dataKindQuad = dimensionQuads.find((q) => + q.predicate.equals(ns.cube("meta/dataKind")) + ); + const dataKindTypeQuad = quads.find( + (q) => + q.subject.equals(dataKindQuad?.object) && + q.predicate.equals(ns.rdf.type) + ); + const timeUnitTypeQuad = quads.find( + (q) => + q.subject.equals(dataKindQuad?.object) && + q.predicate.equals(ns.time.unitType) + ); + const isCurrencyQuad = dimensionQuads.find((q) => + q.predicate.equals(ns.qudt.CurrencyUnit) + ); + const currencyExponentQuad = dimensionQuads.find((q) => + q.predicate.equals(ns.qudt.currencyExponent) + ); + const isKeyDimension = typeQuads.some((q) => + q.object.equals(ns.cube.KeyDimension) + ); + const isMeasureDimension = typeQuads.some((q) => + q.object.equals(ns.cube.MeasureDimension) + ); + + const baseComponent: BaseComponent = { + cubeIri: iri, + iri: dimensionIri, + label: labelQuad?.object.value ?? "", + description: descriptionQuad?.object.value, + scaleType, + unit: unitLabelQuad?.object.value, + order: + orderQuad?.object.termType === "Literal" + ? parseInt(orderQuad.object.value) + : undefined, + isNumerical: false, + isKeyDimension, + values: [], + }; + + if (isMeasureDimension) { + const isDecimal = dataTypeQuad?.object.equals(ns.xsd.decimal) ?? false; + const result: Measure = { + ...baseComponent, + __typename: resolveMeasureType(scaleType), + isCurrency: isCurrencyQuad ? true : false, + isDecimal, + currencyExponent: currencyExponentQuad + ? parseInt(currencyExponentQuad.object.value) + : undefined, + resolution: + dataTypeQuad?.object.equals(ns.xsd.int) || + dataTypeQuad?.object.equals(ns.xsd.integer) + ? 0 + : undefined, + isNumerical: + dataTypeQuad?.object.equals(ns.xsd.int) || + dataTypeQuad?.object.equals(ns.xsd.integer) || + isDecimal || + dataTypeQuad?.object.equals(ns.xsd.float) || + dataTypeQuad?.object.equals(ns.xsd.double) || + false, + }; + + measures.push(result); + } else { + const dimensionType = resolveDimensionType( + getDataKind(dataKindTypeQuad?.object), + scaleType, + [] + ); + const baseDimension: BaseDimension = baseComponent; + + switch (dimensionType) { + case "TemporalDimension": { + const timeUnit = timeUnits.get(timeUnitTypeQuad?.object.value ?? ""); + const timeFormat = timeFormats.get(dataTypeQuad?.object.value ?? ""); + + if (!timeFormat || !timeUnit) { + throw new Error( + `Temporal dimension ${dimensionIri} is missing timeFormat or timeUnit!` + ); + } + + const dimension: TemporalDimension = { + ...baseDimension, + __typename: dimensionType, + timeFormat, + timeUnit, + }; + dimensions.push(dimension); + break; + } + default: { + const dimension: Exclude = { + ...baseDimension, + __typename: dimensionType, + }; + dimensions.push(dimension); + } + } + } + }); + + const observationQuads = quads.filter((quad) => + quad.predicate.equals(ns.cube.observation) + ); + const observations = uniqBy( + observationQuads, + (quad) => quad.object.value + ).map((quad) => { + const dimensionValueQuads = dimensionsQuads.map((dimensionQuad) => { + return quads.find( + (q) => + q.subject.equals(quad.object) && + q.predicate.equals(dimensionQuad.object) + ); + }); + + return dimensionValueQuads.reduce((acc, quad) => { + if (!quad) return acc; + + if (!acc[quad.predicate.value]) { + const rootObservationId = quads.filter((q) => + q.object.equals(quad.object) + ); + + acc[quad.predicate.value] = + quads.find( + (q) => + q.subject.equals(rootObservationId[0].subject) && + q.predicate.equals(quad.predicate) && + q.object.termType === "Literal" + )?.object.value ?? quad.object.value; + } + + return acc; + }, {} as Record); + }); + + return { dimensions, measures, observations }; +}; diff --git a/app/scripts/cube.ts b/app/scripts/cube.ts index 2728734395..e897529b09 100644 --- a/app/scripts/cube.ts +++ b/app/scripts/cube.ts @@ -130,11 +130,10 @@ const previewCube = async ({ .query( DataCubePreviewDocument, { - iri, sourceType, sourceUrl, locale, - latest, + cubeFilter: { iri, latest }, } ) .toPromise(); @@ -143,7 +142,7 @@ const previewCube = async ({ throw new Error(res.error.message); } - report(res.data?.dataCubeByIri?.observations); + report(res.data?.dataCubePreview?.observations); }; const main = async () => { diff --git a/codegen.yml b/codegen.yml index 186faf866b..af331dbd8d 100644 --- a/codegen.yml +++ b/codegen.yml @@ -25,6 +25,7 @@ generates: DataCubeComponents: "../domain/data#DataCubeComponents" DataCubeMetadata: "../domain/data#DataCubeMetadata" DataCubeObservations: "../domain/data#DataCubeObservations" + DataCubePreview: "../domain/data#DataCubePreview" app/graphql/resolver-types.ts: plugins: - "typescript" @@ -45,6 +46,7 @@ generates: DataCubeComponents: "../domain/data#DataCubeComponents" DataCubeMetadata: "../domain/data#DataCubeMetadata" DataCubeObservations: "../domain/data#DataCubeObservations" + DataCubePreview: "../domain/data#DataCubePreview" mappers: DataCube: "./shared-types#ResolvedDataCube" ObservationsQuery: "./shared-types#ResolvedObservationsQuery"