From f954cdfca0008e03d107c1ba0a83e9d9b55a51a3 Mon Sep 17 00:00:00 2001 From: Mat Jordan Date: Wed, 24 Jul 2024 13:33:23 +0000 Subject: [PATCH] Add ?as=iiif param to top level collections endpoint. --- docs/docs/spec/openapi.yaml | 1 + node/src/api/request/models.js | 5 +- node/src/api/response/iiif/collection.js | 167 +++++++++++++----- node/src/api/response/iiif/manifest.js | 32 +--- .../iiif/presentation-api/provider.js | 29 +++ node/src/handlers/get-collections.js | 26 ++- node/test/integration/get-collections.test.js | 26 +++ .../unit/api/response/iiif/collection.test.js | 47 +++++ .../iiif/presentation-api/provider.test.js | 40 +++++ 9 files changed, 293 insertions(+), 80 deletions(-) create mode 100644 node/src/api/response/iiif/presentation-api/provider.js create mode 100644 node/test/unit/api/response/iiif/presentation-api/provider.test.js diff --git a/docs/docs/spec/openapi.yaml b/docs/docs/spec/openapi.yaml index 243ddc06..4dc28c20 100644 --- a/docs/docs/spec/openapi.yaml +++ b/docs/docs/spec/openapi.yaml @@ -35,6 +35,7 @@ paths: tags: - Collection parameters: + - $ref: "./types.yaml#/components/parameters/as" - $ref: "./types.yaml#/components/parameters/page" - $ref: "./types.yaml#/components/parameters/size" - $ref: "./types.yaml#/components/parameters/sort" diff --git a/node/src/api/request/models.js b/node/src/api/request/models.js index 433e5c92..4384162d 100644 --- a/node/src/api/request/models.js +++ b/node/src/api/request/models.js @@ -12,7 +12,10 @@ function extractRequestedModels(requestedModels) { function validModels(models, format) { if (format === "iiif") { - return models.length == 1 && models.every((model) => model === "works"); + return ( + models.length == 1 && + models.every((model) => model === "works" || "collections") + ); } return models.every(isAllowed); } diff --git a/node/src/api/response/iiif/collection.js b/node/src/api/response/iiif/collection.js index 5f4e0723..3c9ed84d 100644 --- a/node/src/api/response/iiif/collection.js +++ b/node/src/api/response/iiif/collection.js @@ -1,5 +1,6 @@ const { dcApiEndpoint, dcUrl } = require("../../../environment"); const { transformError } = require("../error"); +const { provider, nulLogo } = require("./presentation-api/provider"); async function transform(response, pager) { if (response.statusCode === 200) { @@ -27,17 +28,53 @@ async function buildCollection(responseBody, pageInfo) { collectionSummary = "", }, }, + query_url, } = pageInfo; + /** + * if the query_url pathname is `/collections` then we + * know this is a top-level "collection of collections" + */ + const { pathname } = new URL(query_url); + const isTopCollection = pathname.split("/").pop() === "collections"; + const collectionId = parseCollectionId(pageInfo.query_url); + let result = { "@context": ["http://iiif.io/api/presentation/3/context.json"], - id: collectionId(pageInfo), + id: iiifCollectionId(pageInfo), type: "Collection", label: { none: [collectionLabel] }, - summary: { none: [collectionSummary] }, + ...(collectionSummary && { + summary: { + none: [`${collectionSummary}`], + }, + }), + items: getItems(responseBody?.hits?.hits, pageInfo, isTopCollection), + requiredStatement: { + label: { + none: ["Attribution"], + }, + value: { + none: ["Courtesy of Northwestern University Libraries"], + }, + }, + provider: [provider], + logo: [nulLogo], + seeAlso: [ + { + id: isTopCollection + ? `${dcApiEndpoint()}/collections` + : getLinkingPropertyId(pageInfo, dcApiEndpoint(), "query"), + type: "Dataset", + format: "application/json", + label: { + none: ["Northwestern University Libraries Digital Collections API"], + }, + }, + ], homepage: [ { - id: homepageUrl(pageInfo), + id: isTopCollection ? dcUrl() : getLinkingPropertyId(pageInfo, dcUrl()), type: "Text", format: "text/html", label: { @@ -45,14 +82,9 @@ async function buildCollection(responseBody, pageInfo) { }, }, ], - - items: getItems(responseBody?.hits?.hits, pageInfo), }; - if (pageInfo.options?.parameterOverrides) { - const collectionId = new URL(pageInfo.query_url).pathname - .split("/") - .reverse()[0]; + if (!isTopCollection && pageInfo.options?.parameterOverrides) { const thumbnailId = `${dcApiEndpoint()}/collections/${collectionId}/thumbnail`; result.thumbnail = [ { @@ -68,8 +100,9 @@ async function buildCollection(responseBody, pageInfo) { return result; } -function getItems(hits, pageInfo) { - const items = hits.map((item) => loadItem(item["_source"])); +function getItems(hits, pageInfo, isTopCollection) { + const itemType = isTopCollection ? "Collection" : "Manifest"; + const items = hits.map((item) => loadItem(item["_source"], itemType)); if (pageInfo?.next_url) { items.push({ @@ -84,7 +117,7 @@ function getItems(hits, pageInfo) { return items; } -function collectionId(pageInfo) { +function iiifCollectionId(pageInfo) { let collectionId = new URL(pageInfo.query_url); if (pageInfo.current_page > 1) { collectionId.searchParams.set("page", pageInfo.current_page); @@ -92,19 +125,21 @@ function collectionId(pageInfo) { return collectionId; } -function homepageUrl(pageInfo) { +function parseCollectionId(query_url) { + return new URL(query_url).pathname.split("/").reverse()[0]; +} + +function getLinkingPropertyId(pageInfo, baseUrl, queryParam = "q") { let result; if (pageInfo.options?.parameterOverrides) { - const collectionId = new URL(pageInfo.query_url).pathname - .split("/") - .reverse()[0]; - result = new URL(`/collections/${collectionId}`, dcUrl()); + const collectionId = parseCollectionId(pageInfo.query_url); + result = new URL(`/collections/${collectionId}`, baseUrl); } else { - result = new URL("/search", dcUrl()); + result = new URL("/search", baseUrl); if (pageInfo.options?.queryStringParameters?.query) { result.searchParams.set( - "q", + queryParam, pageInfo.options.queryStringParameters.query ); } @@ -120,36 +155,76 @@ function homepageUrl(pageInfo) { return result; } -function loadItem(item) { - return { - id: item.iiif_manifest, - type: "Manifest", - homepage: [ - { - id: new URL(`/items/${item.id}`, dcUrl()), - type: "Text", - format: "text/html", - label: { - none: [`${item.title}`], +function loadItem(item, itemType) { + if (itemType === "Manifest") { + return { + id: item.iiif_manifest, + type: "Manifest", + homepage: [ + { + id: new URL(`/items/${item.id}`, dcUrl()), + type: "Text", + format: "text/html", + label: { + none: [`${item.title}`], + }, }, + ], + label: { + none: [`${item.title}`], }, - ], - label: { - none: [`${item.title}`], - }, - summary: { - none: [`${item.work_type}`], - }, - thumbnail: [ - { - id: item.thumbnail, - format: "image/jpeg", - type: "Image", - width: 400, - height: 400, + summary: { + none: [`${item.work_type}`], }, - ], - }; + thumbnail: [ + { + id: item.thumbnail, + format: "image/jpeg", + type: "Image", + width: 400, + height: 400, + }, + ], + }; + } + + if (itemType === "Collection") { + return { + id: `${item.api_link}?as=iiif`, + type: "Collection", + label: { + none: [`${item.title}`], + }, + ...(item.description && { + summary: { + none: [`${item.description}`], + }, + }), + ...(item.thumbnail && { + thumbnail: [ + { + id: item.thumbnail, + type: "Image", + format: "image/jpeg", + width: 400, + height: 400, + }, + ], + }), + ...(item.canonical_link && { + homepage: [ + { + id: new URL(`/collections/${item.id}`, dcUrl()), + type: "Text", + format: "text/html", + label: { + none: [`${item.title}`], + }, + }, + ], + }), + }; + } } module.exports = { transform }; diff --git a/node/src/api/response/iiif/manifest.js b/node/src/api/response/iiif/manifest.js index 50f4ca6f..51a6e884 100644 --- a/node/src/api/response/iiif/manifest.js +++ b/node/src/api/response/iiif/manifest.js @@ -14,6 +14,7 @@ const { metadataLabelFields } = require("./presentation-api/metadata"); const { buildPlaceholderCanvas, } = require("./presentation-api/placeholder-canvas"); +const { nulLogo, provider } = require("./presentation-api/provider"); function transform(response) { if (response.statusCode === 200) { @@ -260,37 +261,8 @@ function transform(response) { } } - /** Add logo manually (w/o IIIF Builder) */ - const nulLogo = { - id: "https://iiif.dc.library.northwestern.edu/iiif/2/00000000-0000-0000-0000-000000000003/full/pct:50/0/default.webp", - type: "Image", - format: "image/webp", - height: 139, - width: 1190, - }; - jsonManifest.logo = [nulLogo]; - - /** Add provider manually (w/o IIIF Builder) */ - const provider = { - id: "https://www.library.northwestern.edu/", - type: "Agent", - label: { none: ["Northwestern University Libraries"] }, - homepage: [ - { - id: "https://dc.library.northwestern.edu/", - type: "Text", - label: { - none: [ - "Northwestern University Libraries Digital Collections Homepage", - ], - }, - format: "text/html", - language: ["en"], - }, - ], - logo: [nulLogo], - }; jsonManifest.provider = [provider]; + jsonManifest.logo = [nulLogo]; return { statusCode: 200, diff --git a/node/src/api/response/iiif/presentation-api/provider.js b/node/src/api/response/iiif/presentation-api/provider.js new file mode 100644 index 00000000..5bfff0d1 --- /dev/null +++ b/node/src/api/response/iiif/presentation-api/provider.js @@ -0,0 +1,29 @@ +const nulLogo = { + id: "https://iiif.dc.library.northwestern.edu/iiif/2/00000000-0000-0000-0000-000000000003/full/pct:50/0/default.webp", + type: "Image", + format: "image/webp", + height: 139, + width: 1190, +}; + +const provider = { + id: "https://www.library.northwestern.edu/", + type: "Agent", + label: { none: ["Northwestern University Libraries"] }, + homepage: [ + { + id: "https://dc.library.northwestern.edu/", + type: "Text", + label: { + none: [ + "Northwestern University Libraries Digital Collections Homepage", + ], + }, + format: "text/html", + language: ["en"], + }, + ], + logo: [nulLogo], +}; + +module.exports = { nulLogo, provider }; diff --git a/node/src/handlers/get-collections.js b/node/src/handlers/get-collections.js index ceacea0e..4fbb6085 100644 --- a/node/src/handlers/get-collections.js +++ b/node/src/handlers/get-collections.js @@ -1,11 +1,31 @@ const { doSearch } = require("./search-runner"); const { wrap } = require("./middleware"); +const getCollections = async (event) => { + event.pathParameters.models = "collections"; + event.body = { query: { match_all: {} } }; + return doSearch(event, { includeToken: false }); +}; + +const getCollectionsAsIiif = async (event) => { + event.pathParameters.models = "collections"; + event.body = { query: { match_all: {} } }; + event.queryStringParameters.collectionLabel = + "Northwestern University Libraries Digital Collections"; + event.queryStringParameters.collectionSummary = + "Explore digital resources from the Northwestern University Library collections – including letters, photographs, diaries, maps, and audiovisual materials."; + + return doSearch(event, { + includeToken: false, + parameterOverrides: { as: "iiif" }, + }); +}; + /** * A simple function to get Collections */ exports.handler = wrap(async (event) => { - event.pathParameters.models = "collections"; - event.body = { query: { match_all: {} } }; - return doSearch(event, { includeToken: false }); + return event.queryStringParameters?.as === "iiif" + ? getCollectionsAsIiif(event) + : getCollections(event); }); diff --git a/node/test/integration/get-collections.test.js b/node/test/integration/get-collections.test.js index 517d0012..bb673ad5 100644 --- a/node/test/integration/get-collections.test.js +++ b/node/test/integration/get-collections.test.js @@ -71,5 +71,31 @@ describe("Collections route", () => { const url = new URL(query_url); expect(url.pathname).to.eq("/api/v2/search/collections"); }); + + it("returns top level collection as a IIIF collection", async () => { + mock + .post("/dc-v2-collection/_search", makeQuery({ size: 10, from: 0 })) + .reply(200, helpers.testFixture("mocks/collections.json")); + + const event = helpers + .mockEvent("GET", "/collections") + .queryParams({ as: "iiif" }) + .render(); + const result = await handler(event); + expect(result.statusCode).to.eq(200); + expect(result).to.have.header( + "content-type", + /application\/json;.*charset=UTF-8/ + ); + const resultBody = JSON.parse(result.body); + expect(resultBody.type).to.eq("Collection"); + expect(resultBody.label.none[0]).to.eq( + "Northwestern University Libraries Digital Collections" + ); + expect(resultBody.summary.none[0]).to.eq( + "Explore digital resources from the Northwestern University Library collections – including letters, photographs, diaries, maps, and audiovisual materials." + ); + expect(resultBody.items.length).to.eq(69); + }); }); }); diff --git a/node/test/unit/api/response/iiif/collection.test.js b/node/test/unit/api/response/iiif/collection.test.js index 0883408c..44524999 100644 --- a/node/test/unit/api/response/iiif/collection.test.js +++ b/node/test/unit/api/response/iiif/collection.test.js @@ -82,3 +82,50 @@ describe("IIIF Collection response transformer", () => { expect(body.homepage[0].id).to.contain("search?similar=1234"); }); }); + +describe("IIIF Collection response for top level colllections", () => { + helpers.saveEnvironment(); + + let pager = new Paginator( + "http://dcapi.library.northwestern.edu/api/v2/", + "collections", + ["collections"], + { query: { match_all: {} } }, + "iiif", + { + includeToken: false, + parameterOverrides: { as: "iiif" }, + queryStringParameters: { + as: "iiif", + collectionLabel: + "Northwestern University Libraries Digital Collections", + collectionSummary: + "Explore digital resources from the Northwestern University Library collections – including letters, photographs, diaries, maps, and audiovisual materials.", + }, + } + ); + + pager.pageInfo.query_url = + "http://dcapi.library.northwestern.edu/api/v2/collections?as=iiif"; + + it("transform a collection of collections response", async () => { + const response = { + statusCode: 200, + body: helpers.testFixture("mocks/collections.json"), + }; + + const result = await transformer.transform(response, pager); + expect(result.statusCode).to.eq(200); + + const body = JSON.parse(result.body); + expect(body.type).to.eq("Collection"); + expect(body.label.none[0]).to.eq( + "Northwestern University Libraries Digital Collections" + ); + expect(body.summary.none[0]).to.eq( + "Explore digital resources from the Northwestern University Library collections – including letters, photographs, diaries, maps, and audiovisual materials." + ); + expect(body.items.length).to.eq(69); + expect(body.items[0].type).to.eq("Collection"); + }); +}); diff --git a/node/test/unit/api/response/iiif/presentation-api/provider.test.js b/node/test/unit/api/response/iiif/presentation-api/provider.test.js new file mode 100644 index 00000000..c610c85c --- /dev/null +++ b/node/test/unit/api/response/iiif/presentation-api/provider.test.js @@ -0,0 +1,40 @@ +"use strict"; + +const chai = require("chai"); +const expect = chai.expect; + +const { provider, nulLogo } = requireSource( + "api/response/iiif/presentation-api/provider" +); + +describe("IIIF response presentation API provider and logo", () => { + it("outputs a IIIF provider property", async () => { + expect(provider.id).to.contain("https://www.library.northwestern.edu"); + expect(provider.type).to.eq("Agent"); + expect(provider.label.none[0]).to.eq("Northwestern University Libraries"); + expect(provider.homepage[0].id).to.contain( + "https://dc.library.northwestern.edu" + ); + expect(provider.homepage[0].label.none[0]).to.eq( + "Northwestern University Libraries Digital Collections Homepage" + ); + expect(provider.logo).to.be.an("array"); + expect(provider.logo[0].id).to.contain( + "https://iiif.dc.library.northwestern.edu/iiif/2/00000000-0000-0000-0000-000000000003/full/pct:50/0/default.webp" + ); + expect(provider.logo[0].type).to.eq("Image"); + expect(provider.logo[0].format).to.eq("image/webp"); + expect(provider.logo[0].height).to.be.a("number"); + expect(provider.logo[0].width).to.be.a("number"); + }); + + it("outputs a IIIF logo property", async () => { + expect(nulLogo.id).to.contain( + "https://iiif.dc.library.northwestern.edu/iiif/2/00000000-0000-0000-0000-000000000003/full/pct:50/0/default.webp" + ); + expect(nulLogo.type).to.eq("Image"); + expect(nulLogo.format).to.eq("image/webp"); + expect(nulLogo.height).to.be.a("number"); + expect(nulLogo.width).to.be.a("number"); + }); +});