Skip to content

Commit

Permalink
fix: Redirects to unversioned cubes
Browse files Browse the repository at this point in the history
  • Loading branch information
bprusinowski committed Mar 6, 2024
1 parent 88d51b6 commit a050eff
Show file tree
Hide file tree
Showing 7 changed files with 78 additions and 120 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ You can also check the [release page](https://github.com/visualize-admin/visuali

- Fixes
- SingleURLs layout mode now correctly publishes charts
- Redirecting to latest cube now works correctly for cases of trying to access old cube that didn't look like a versioned cube, but in fact was a versioned cube

# [3.26.3] - 2024-03-05

Expand Down
5 changes: 2 additions & 3 deletions app/browser/select-dataset-step.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
navPresenceProps,
smoothPresenceProps,
} from "@/components/presence";
import { useRedirectToLatestCube } from "@/components/use-redirect-to-latest-cube";
import {
PanelBodyWrapper,
PanelLayout,
Expand All @@ -42,8 +43,6 @@ import {
import { Icon } from "@/icons";
import { useConfiguratorState, useLocale } from "@/src";

import { useRedirectToVersionedCube } from "../components/use-redirect-to-versioned-cube";

import {
BrowseStateProvider,
buildURLFromBrowseState,
Expand Down Expand Up @@ -178,7 +177,7 @@ const SelectDatasetStepContent = () => {
pause: !!dataset,
});

useRedirectToVersionedCube({
useRedirectToLatestCube({
dataSource: configState.dataSource,
datasetIri: dataset,
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { renderHook } from "@testing-library/react";
import { NextRouter, useRouter } from "next/router";

import { useRedirectToLatestCube } from "@/components/use-redirect-to-latest-cube";
import { useLocale } from "@/locales/use-locale";
import { queryLatestPublishedCubeFromUnversionedIri } from "@/rdf/query-cube-metadata";
import { queryLatestCube } from "@/rdf/query-cube-metadata";

import { useRedirectToVersionedCube } from "./use-redirect-to-versioned-cube";
jest.mock("@/rdf/query-cube-metadata", () => ({
queryLatestPublishedCubeFromUnversionedIri: jest.fn(),
queryLatestCube: jest.fn(),
}));

jest.mock("next/router", () => ({
Expand All @@ -26,7 +26,7 @@ describe("use redirect to versioned cube", () => {
datasetIri,
}: {
datasetIri: string;
versionedCube: undefined | { iri: string };
versionedCube: undefined | string;
}) => {
const router = {
route: "",
Expand All @@ -45,15 +45,11 @@ describe("use redirect to versioned cube", () => {
(useLocale as jest.MockedFunction<typeof useLocale>).mockReturnValue("de");

(
queryLatestPublishedCubeFromUnversionedIri as jest.MockedFunction<
typeof queryLatestPublishedCubeFromUnversionedIri
>
).mockImplementation(async () => {
return versionedCube;
});
queryLatestCube as jest.MockedFunction<typeof queryLatestCube>
).mockImplementation(async () => versionedCube);

renderHook(() =>
useRedirectToVersionedCube({
useRedirectToLatestCube({
datasetIri,
dataSource: {
type: "sparql",
Expand All @@ -65,26 +61,14 @@ describe("use redirect to versioned cube", () => {
// Wait for effects to have finished
await sleep(1);

return {
router,
};
return { router };
};

it("should not do anything if initial cube IRI seems to be versioned", async () => {
const { router } = await setup({
datasetIri:
"https://environment.ld.admin.ch/foen/nfi/49-19-None-None-44/cube/1",
versionedCube: { iri: "https://versioned-cube" },
});

expect(router.replace).not.toHaveBeenCalled();
});

it("should redirect to versioned IRI if initial cube IRI does not seem to be versioned", async () => {
const { router } = await setup({
datasetIri:
"https://environment.ld.admin.ch/foen/nfi/49-19-None-None-44/cube",
versionedCube: { iri: "https://versioned-cube" },
versionedCube: "https://versioned-cube",
});
expect(router.replace).toHaveBeenCalledWith({
pathname: "/browse",
Expand All @@ -97,7 +81,7 @@ describe("use redirect to versioned cube", () => {
it("should redirect to versioned IRI if initial cube IRI does not seem to be versioned 2", async () => {
const { router } = await setup({
datasetIri: "https://environment.ld.admin.ch/foen/ubd000501",
versionedCube: { iri: "https://versioned-cube2" },
versionedCube: "https://versioned-cube2",
});
expect(router.replace).toHaveBeenCalledWith({
pathname: "/browse",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,13 @@ import { useRouter } from "next/router";
import { useEffect, useRef } from "react";
import ParsingClient from "sparql-http-client/ParsingClient";

import { ConfiguratorState } from "@/config-types";
import { useLocale } from "@/locales/use-locale";
import { queryLatestPublishedCubeFromUnversionedIri } from "@/rdf/query-cube-metadata";
import { queryLatestCube } from "@/rdf/query-cube-metadata";
import { getErrorQueryParams } from "@/utils/flashes";
import useEvent from "@/utils/use-event";

import { ConfiguratorState } from "../config-types";

/**
* Heuristic to check if a dataset IRI is versioned.
* Versioned iris look like https://blabla/<number/
*/
const isDatasetIriVersioned = (iri: string) => {
return iri.match(/\/\d+\/?$/) !== null;
};
export const useRedirectToVersionedCube = ({
export const useRedirectToLatestCube = ({
datasetIri,
dataSource,
}: {
Expand All @@ -33,37 +25,31 @@ export const useRedirectToVersionedCube = ({
return;
}

if (
datasetIri &&
!Array.isArray(datasetIri) &&
!isDatasetIriVersioned(datasetIri)
) {
const sparqlClient = new ParsingClient({
endpointUrl: dataSourceURL,
});
const resp = await queryLatestPublishedCubeFromUnversionedIri(
sparqlClient,
datasetIri
);
if (datasetIri && !Array.isArray(datasetIri)) {
hasRun.current = true;

if (resp) {
router.replace({
pathname: "/browse",
query: {
...router.query,
...(router.query.iri ? { iri: resp.iri } : { dataset: resp.iri }),
},
});
} else {
router.replace({
const sparqlClient = new ParsingClient({ endpointUrl: dataSourceURL });
const latestIri = await queryLatestCube(sparqlClient, datasetIri);

if (!latestIri) {
return router.replace({
pathname: `/`,
query: getErrorQueryParams("CANNOT_FIND_CUBE", {
...router.query,
iri: datasetIri,
}),
});
}
hasRun.current = true;

if (datasetIri !== latestIri) {
return router.replace({
pathname: "/browse",
query: {
...router.query,
...(router.query.iri ? { iri: latestIri } : { dataset: latestIri }),
},
});
}
}
});

Expand Down
17 changes: 2 additions & 15 deletions app/rdf/light-cube.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { getCubeComponentsMetadata } from "@/rdf/query-cube-components";
import { getCubeMetadata } from "@/rdf/query-cube-metadata";
import { getCubePreview } from "@/rdf/query-cube-preview";
import { getLatestCubeIriQuery } from "@/rdf/query-latest-cube-iri";

type LightCubeOptions = {
iri: string;
Expand Down Expand Up @@ -39,21 +40,7 @@ export class LightCube {
return this;
}

const query = `PREFIX schema: <http://schema.org/>
SELECT ?iri WHERE {
VALUES ?oldIri { <${this.iri}> }
?versionHistory schema:hasPart ?oldIri .
?versionHistory schema:hasPart ?iri .
?iri schema:version ?version .
?iri schema:creativeWorkStatus ?status .
?oldIri schema:creativeWorkStatus ?oldStatus .
FILTER(NOT EXISTS { ?iri schema:expires ?expires . } && ?status IN (?oldStatus, <https://ld.admin.ch/vocabulary/CreativeWorkStatus/Published>))
}
ORDER BY DESC(?version)
LIMIT 1`;

const query = getLatestCubeIriQuery(this.iri);
const result = await this.sparqlClient.query.select(query);
const latestIri = result[0]?.iri?.value;

Expand Down
53 changes: 11 additions & 42 deletions app/rdf/query-cube-metadata.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,13 @@
import RDF from "@rdfjs/data-model";
import { SELECT } from "@tpluscode/sparql-builder";
import { Literal, NamedNode } from "rdf-js";
import { ParsingClient } from "sparql-http-client/ParsingClient";

import { DataCubeMetadata } from "@/domain/data";
import { DataCubePublicationStatus } from "@/graphql/query-hooks";
import { pragmas } from "@/rdf/create-source";
import * as ns from "@/rdf/namespace";

import { cube, schema } from "../../app/rdf/namespace";
import { DataCubePublicationStatus } from "../graphql/query-hooks";

import { pragmas } from "./create-source";
import {
GROUP_SEPARATOR,
buildLocalizedSubQuery,
makeVisualizeDatasetFilter,
} from "./query-utils";
import { getLatestCubeIriQuery } from "@/rdf/query-latest-cube-iri";
import { GROUP_SEPARATOR, buildLocalizedSubQuery } from "@/rdf/query-utils";

type RawDataCubeMetadata = {
iri: NamedNode;
Expand Down Expand Up @@ -164,38 +157,14 @@ export const getCubeMetadata = async (
return parseRawMetadata(result);
};

export const queryLatestPublishedCubeFromUnversionedIri = async (
export const queryLatestCube = async (
sparqlClient: ParsingClient,
unversionedIri: string
) => {
const iri = RDF.variable("iri");
// Check if it is a versioned cube
const query = SELECT`${iri}`.WHERE`
<${unversionedIri}> ${schema.hasPart} ${iri}.
${makeVisualizeDatasetFilter({ includeDrafts: true })}
`
.ORDER()
.BY(iri, true);
const results = await sparqlClient.query.select(query.build(), {
iri: string
): Promise<string | undefined> => {
const query = getLatestCubeIriQuery(iri);
const results = await sparqlClient.query.select(query, {
operation: "postUrlencoded",
});
if (results.length !== 1) {
// Check if it is an unversioned cube
const query = SELECT`*`.WHERE`
<${unversionedIri}> ${cube.observationConstraint} ?shape.
${makeVisualizeDatasetFilter({ includeDrafts: true })}
`;
const results = await sparqlClient.query.select(query.build(), {
operation: "postUrlencoded",
});
if (results.length === 0) {
return;
}
return {
iri: unversionedIri,
};
}
return {
iri: results[0].iri.value,
};

return results[0]?.iri.value;
};
32 changes: 32 additions & 0 deletions app/rdf/query-latest-cube-iri.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/** Creates SPARQL query to fetch latest cube iri.
* Works for both versioned and unversioned cubes.
*/
export const getLatestCubeIriQuery = (cubeIri: string) => {
return `PREFIX schema: <http://schema.org/>
SELECT ?iri WHERE {
{
SELECT ?iri WHERE {
VALUES ?versionHistory { <${cubeIri}> }
?versionHistory schema:hasPart ?iri .
?iri schema:version ?version .
?iri schema:creativeWorkStatus ?status .
FILTER(NOT EXISTS { ?iri schema:expires ?expires . } && ?status = <https://ld.admin.ch/vocabulary/CreativeWorkStatus/Published>)
}
} UNION {
SELECT ?iri WHERE {
VALUES ?oldIri { <${cubeIri}> }
?versionHistory schema:hasPart ?oldIri .
?versionHistory schema:hasPart ?iri .
?iri schema:version ?version .
?iri schema:creativeWorkStatus ?status .
?oldIri schema:creativeWorkStatus ?oldStatus .
FILTER(NOT EXISTS { ?iri schema:expires ?expires . } && ?status IN (?oldStatus, <https://ld.admin.ch/vocabulary/CreativeWorkStatus/Published>))
}
}
}
ORDER BY DESC(?version)
LIMIT 1`;
};

0 comments on commit a050eff

Please sign in to comment.