From 15dd7c203ba17514a75ecbcd5324eed84b585908 Mon Sep 17 00:00:00 2001 From: Mat Jordan Date: Fri, 12 May 2023 20:10:32 +0000 Subject: [PATCH 1/6] Update format for .m3u8 A/V IIIF canvas annotation body. --- src/api/response/iiif/presentation-api/items.js | 7 +++++-- test/unit/api/response/iiif/manifest.test.js | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/api/response/iiif/presentation-api/items.js b/src/api/response/iiif/presentation-api/items.js index 5c0b803c..36bb0ea5 100644 --- a/src/api/response/iiif/presentation-api/items.js +++ b/src/api/response/iiif/presentation-api/items.js @@ -5,10 +5,13 @@ function annotationType(workType) { } function buildAnnotationBody(fileSet, workType) { + const bodyType = annotationType(workType); const body = { id: buildAnnotationBodyId(fileSet, workType), - type: annotationType(workType), - format: fileSet.mime_type, + type: bodyType, + format: isAudioVideo(bodyType) + ? "application/x-mpegurl" + : fileSet.mime_type, height: fileSet.height || 100, width: fileSet.width || 100, }; diff --git a/test/unit/api/response/iiif/manifest.test.js b/test/unit/api/response/iiif/manifest.test.js index 6c298b4b..ca825e44 100644 --- a/test/unit/api/response/iiif/manifest.test.js +++ b/test/unit/api/response/iiif/manifest.test.js @@ -190,6 +190,7 @@ describe("A/V Work as IIIF Manifest response transformer", () => { expect(annotation.body.duration).to.eq(5.599); expect(annotation.body.type).to.eq("Video"); + expect(annotation.body.format).to.eq("application/x-mpegurl"); expect(annotation.body.id).to.eq(source.file_sets[0].streaming_url); }); }); From e6868a708f176ff6fcf42372f4e8f61e5247a6fb Mon Sep 17 00:00:00 2001 From: Mat Jordan Date: Tue, 16 May 2023 14:26:45 +0000 Subject: [PATCH 2/6] Add placeholderCanvas for Image canvases. Co-authored-by: Michael B. Klein --- src/api/response/iiif/manifest.js | 21 ++++++ .../presentation-api/placeholder-canvas.js | 54 +++++++++++++++ test/unit/api/response/iiif/manifest.test.js | 7 ++ .../placeholder-canvas.test.js | 66 +++++++++++++++++++ 4 files changed, 148 insertions(+) create mode 100644 src/api/response/iiif/presentation-api/placeholder-canvas.js create mode 100644 test/unit/api/response/iiif/presentation-api/placeholder-canvas.test.js diff --git a/src/api/response/iiif/manifest.js b/src/api/response/iiif/manifest.js index a3b37c29..6699f21f 100644 --- a/src/api/response/iiif/manifest.js +++ b/src/api/response/iiif/manifest.js @@ -8,6 +8,9 @@ const { isAudioVideo, } = require("./presentation-api/items"); const { metadataLabelFields } = require("./presentation-api/metadata"); +const { + buildPlaceholderCanvas, +} = require("./presentation-api/placeholder-canvas"); function transform(response) { if (response.statusCode === 200) { @@ -234,6 +237,24 @@ function transform(response) { } }); + /** + * Add a placeholderCanvas property to a Canvas if the annotation body is of type "Image" + * (iiif-builder package currently doesn't support adding this property) + */ + for (let i = 0; i < jsonManifest.items.length; i++) { + if (jsonManifest.items[i].items[0].items[0].body.type === "Image") { + const { id, thumbnail } = jsonManifest.items[i]; + const placeholderFileSet = source.file_sets.find( + (fileSet) => + fileSet.representative_image_url === thumbnail[0].service[0]["@id"] + ); + jsonManifest.items[i].placeholderCanvas = buildPlaceholderCanvas( + id, + placeholderFileSet + ); + } + } + return { statusCode: 200, headers: { diff --git a/src/api/response/iiif/presentation-api/placeholder-canvas.js b/src/api/response/iiif/presentation-api/placeholder-canvas.js new file mode 100644 index 00000000..ecbf94a6 --- /dev/null +++ b/src/api/response/iiif/presentation-api/placeholder-canvas.js @@ -0,0 +1,54 @@ +function buildPlaceholderCanvas(id, fileSet, size = 640) { + const { representative_image_url } = fileSet; + const { placeholderWidth, placeholderHeight } = getPlaceholderSizes( + fileSet, + size + ); + + return { + id: `${id}/placeholder`, + type: "Canvas", + width: placeholderWidth, + height: placeholderHeight, + items: [ + { + id: `${id}/placeholder/annotation-page/0`, + type: "AnnotationPage", + items: [ + { + id: `${id}/placeholder/annotation/0`, + type: "Annotation", + motivation: "painting", + body: { + id: `${representative_image_url}/full/!${placeholderWidth},${placeholderHeight}/0/default.jpg`, + type: "Image", + format: fileSet.mime_type, + width: placeholderWidth, + height: placeholderHeight, + service: [ + { + ["@id"]: representative_image_url, + ["@type"]: "ImageService2", + profile: "http://iiif.io/api/image/2/level2.json", + }, + ], + }, + target: `${id}/placeholder`, + }, + ], + }, + ], + }; +} + +function getPlaceholderSizes(fileset, size) { + const { width, height } = fileset; + const placeholderWidth = width > size ? size : width; + const placeholderHeight = Math.floor((placeholderWidth / width) * height); + return { placeholderWidth, placeholderHeight }; +} + +module.exports = { + buildPlaceholderCanvas, + getPlaceholderSizes, +}; diff --git a/test/unit/api/response/iiif/manifest.test.js b/test/unit/api/response/iiif/manifest.test.js index ca825e44..580dfb0f 100644 --- a/test/unit/api/response/iiif/manifest.test.js +++ b/test/unit/api/response/iiif/manifest.test.js @@ -114,6 +114,13 @@ describe("Image Work as IIIF Manifest response transformer", () => { ); }); + it("adds a placeholderCanvas property to Image canvases", async () => { + const { manifest } = await setup(); + const { placeholderCanvas } = manifest.items[0]; + expect(placeholderCanvas.id).to.eq(`${manifest.items[0].id}/placeholder`); + expect(placeholderCanvas.type).to.eq("Canvas"); + }); + it("excludes Preservation and Supplemental filesets", async () => { const { manifest } = await setup(); manifest.items.forEach((canvas) => { diff --git a/test/unit/api/response/iiif/presentation-api/placeholder-canvas.test.js b/test/unit/api/response/iiif/presentation-api/placeholder-canvas.test.js new file mode 100644 index 00000000..467d3ea5 --- /dev/null +++ b/test/unit/api/response/iiif/presentation-api/placeholder-canvas.test.js @@ -0,0 +1,66 @@ +"use strict"; + +const chai = require("chai"); +const expect = chai.expect; +const transformer = requireSource("api/response/iiif/manifest"); +const { buildPlaceholderCanvas, getPlaceholderSizes } = requireSource( + "api/response/iiif/presentation-api/placeholder-canvas" +); + +describe("IIIF response presentation API placeholderCanvas helpers", () => { + async function setup() { + const response = { + statusCode: 200, + body: helpers.testFixture("mocks/work-1234.json"), + }; + const source = JSON.parse(response.body)._source; + + const result = await transformer.transform(response); + expect(result.statusCode).to.eq(200); + + return { source, manifest: JSON.parse(result.body) }; + } + + it("buildPlaceholderCanvas(value)", async () => { + const { source, manifest } = await setup(); + const id = manifest.items[0].id; + const fileSet = source.file_sets[0]; + const placeholder = buildPlaceholderCanvas(id, fileSet, 640); + + expect(placeholder.id).to.eq(`${id}/placeholder`); + expect(placeholder.type).to.eq("Canvas"); + expect(placeholder.width).to.eq(640); + expect(placeholder.height).to.eq(480); + expect(placeholder.items[0].id).to.eq( + `${id}/placeholder/annotation-page/0` + ); + expect(placeholder.items[0].type).to.eq("AnnotationPage"); + expect(placeholder.items[0].items[0].type).to.eq("Annotation"); + expect(placeholder.items[0].items[0].motivation).to.eq("painting"); + expect(placeholder.items[0].items[0].body.id).to.eq( + `${fileSet.representative_image_url}/full/!640,480/0/default.jpg` + ); + expect(placeholder.items[0].items[0].body.type).to.eq("Image"); + expect(placeholder.items[0].items[0].body.format).to.eq(fileSet.mime_type); + expect(placeholder.items[0].items[0].body.width).to.eq(640); + expect(placeholder.items[0].items[0].body.height).to.eq(480); + expect(placeholder.items[0].items[0].body.service[0]["@id"]).to.eq( + fileSet.representative_image_url + ); + }); + + it("getPlaceholderSizes(fileSet, size)", () => { + const fileSet = { + width: 3125, + height: 2240, + }; + + const { placeholderHeight, placeholderWidth } = getPlaceholderSizes( + fileSet, + 1000 + ); + + expect(placeholderWidth).to.eq(1000); + expect(placeholderHeight).to.eq(716); + }); +}); From aa8539529aeb4eacd25643136670209c922c9fb1 Mon Sep 17 00:00:00 2001 From: Karen Shaw Date: Mon, 22 May 2023 14:47:28 +0000 Subject: [PATCH 3/6] Fix typos in production-types.yml workflow --- .github/workflows/production-types.yml | 3 +-- .github/workflows/staging-types.yml | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/production-types.yml b/.github/workflows/production-types.yml index 5659add5..acf92463 100644 --- a/.github/workflows/production-types.yml +++ b/.github/workflows/production-types.yml @@ -8,7 +8,6 @@ on: paths: - "docs/docs/spec/data-types.yaml" - ".github/flags/deploy-data-types" - - ".github/workflows/production-types.yml" workflow_dispatch: jobs: Push-To-Types-Repo-Production: @@ -26,7 +25,7 @@ jobs: GH_TOKEN: ${{secrets.GH_TOKEN}} steps: - run: | - echo PR with changes to datatypes.yml was merged into staging + echo PR with changes to datatypes.yml was merged into main - name: Checkout dc-api-v2 uses: actions/checkout@v3 with: diff --git a/.github/workflows/staging-types.yml b/.github/workflows/staging-types.yml index b017954c..8fed542a 100644 --- a/.github/workflows/staging-types.yml +++ b/.github/workflows/staging-types.yml @@ -8,7 +8,6 @@ on: paths: - "docs/docs/spec/data-types.yaml" - ".github/flags/deploy-data-types" - - ".github/workflows/staging-types.yml" workflow_dispatch: jobs: Push-To-Types-Repo-Staging: From 36bba39f0806743bcfe1d4c7748da5a8a2f194f0 Mon Sep 17 00:00:00 2001 From: Karen Shaw Date: Tue, 23 May 2023 18:29:17 +0000 Subject: [PATCH 4/6] Add shared link expiration to work response --- docs/docs/spec/data-types.yaml | 6 +++++- package.json | 4 ++-- src/api/response/opensearch/index.js | 14 +++++++------- src/api/response/transformer.js | 2 +- src/environment.js | 4 +++- src/handlers/get-shared-link-by-id.js | 6 +++--- src/package.json | 2 +- test/unit/api/response/opensearch.test.js | 6 +++--- 8 files changed, 25 insertions(+), 19 deletions(-) diff --git a/docs/docs/spec/data-types.yaml b/docs/docs/spec/data-types.yaml index c38b8a98..8fd141d7 100644 --- a/docs/docs/spec/data-types.yaml +++ b/docs/docs/spec/data-types.yaml @@ -251,11 +251,15 @@ components: - id - label Info: - description: Global DC API properties + description: Additional Information type: object properties: description: type: string + link_expiration: + type: string + format: date-time + nullable: true name: type: string version: diff --git a/package.json b/package.json index 1a1a75c9..4a0e80e1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dc-api-build", - "version": "2.0.1", + "version": "2.1.0", "description": "NUL Digital Collections API Build Environment", "repository": "https://github.com/nulib/dc-api-v2", "author": "nulib", @@ -51,4 +51,4 @@ ] } } -} +} \ No newline at end of file diff --git a/src/api/response/opensearch/index.js b/src/api/response/opensearch/index.js index d965682d..917d3397 100644 --- a/src/api/response/opensearch/index.js +++ b/src/api/response/opensearch/index.js @@ -1,17 +1,17 @@ const { appInfo } = require("../../../environment"); const { transformError } = require("../error"); -async function transform(response, pager) { +async function transform(response, options = {}) { if (response.statusCode === 200) { const responseBody = JSON.parse(response.body); return await (responseBody?.hits?.hits - ? transformMany(responseBody, pager) - : transformOne(responseBody)); + ? transformMany(responseBody, options) + : transformOne(responseBody, options)); } return transformError(response); } -async function transformOne(responseBody) { +async function transformOne(responseBody, options = {}) { return { statusCode: 200, headers: { @@ -19,12 +19,12 @@ async function transformOne(responseBody) { }, body: JSON.stringify({ data: responseBody._source, - info: appInfo(), + info: appInfo(options), }), }; } -async function transformMany(responseBody, pager) { +async function transformMany(responseBody, options) { return { statusCode: 200, headers: { @@ -32,7 +32,7 @@ async function transformMany(responseBody, pager) { }, body: JSON.stringify({ data: extractSource(responseBody.hits.hits), - pagination: await paginationInfo(responseBody, pager), + pagination: await paginationInfo(responseBody, options?.pager), info: appInfo(), aggregations: responseBody.aggregations, }), diff --git a/src/api/response/transformer.js b/src/api/response/transformer.js index e02b37b7..0a4b1a3f 100644 --- a/src/api/response/transformer.js +++ b/src/api/response/transformer.js @@ -11,7 +11,7 @@ async function transformSearchResult(response, pager) { return await iiifCollectionResponse.transform(response, pager); } - return await opensearchResponse.transform(response, pager); + return await opensearchResponse.transform(response, { pager: pager }); } return transformError(response); } diff --git a/src/environment.js b/src/environment.js index 8a2815d6..3443075c 100644 --- a/src/environment.js +++ b/src/environment.js @@ -1,3 +1,4 @@ +const exp = require("constants"); const fs = require("fs"); const jwt = require("jsonwebtoken"); const path = require("path"); @@ -22,11 +23,12 @@ function apiTokenSecret() { return process.env.API_TOKEN_SECRET; } -function appInfo() { +function appInfo(options = {}) { return { name: PackageInfo.name, description: PackageInfo.description, version: PackageInfo.version, + link_expiration: options.expires || null, }; } diff --git a/src/handlers/get-shared-link-by-id.js b/src/handlers/get-shared-link-by-id.js index 6b9ed8ef..10a9a4ae 100644 --- a/src/handlers/get-shared-link-by-id.js +++ b/src/handlers/get-shared-link-by-id.js @@ -21,10 +21,10 @@ exports.handler = wrap(async (event) => { }); if (workResponse.statusCode !== 200) return invalidRequest("Not Found"); - // add entitlement for the work id - // TODO make part of request/response processing event.userToken.addEntitlement(workId); - return await opensearchResponse.transform(workResponse); + return await opensearchResponse.transform(workResponse, { + expires: expirationDate, + }); }); const invalidRequest = (message) => { diff --git a/src/package.json b/src/package.json index ccf0156f..862de0a4 100644 --- a/src/package.json +++ b/src/package.json @@ -1,6 +1,6 @@ { "name": "dc-api", - "version": "2.0.1", + "version": "2.1.0", "description": "NUL Digital Collections API", "repository": "https://github.com/nulib/dc-api-v2", "author": "nulib", diff --git a/test/unit/api/response/opensearch.test.js b/test/unit/api/response/opensearch.test.js index c126d413..c91c7856 100644 --- a/test/unit/api/response/opensearch.test.js +++ b/test/unit/api/response/opensearch.test.js @@ -22,7 +22,7 @@ describe("OpenSearch response transformer", () => { statusCode: 200, body: helpers.testFixture("mocks/work-1234.json"), }; - const result = await transformer.transform(response, pager); + const result = await transformer.transform(response, { pager: pager }); expect(result.statusCode).to.eq(200); const body = JSON.parse(result.body); @@ -36,7 +36,7 @@ describe("OpenSearch response transformer", () => { statusCode: 200, body: helpers.testFixture("mocks/search.json"), }; - const result = await transformer.transform(response, pager); + const result = await transformer.transform(response, { pager: pager }); expect(result.statusCode).to.eq(200); const body = JSON.parse(result.body); @@ -60,7 +60,7 @@ describe("OpenSearch response transformer", () => { body: helpers.testFixture("mocks/missing-index.json"), }; - const result = await transformer.transform(response, pager); + const result = await transformer.transform(response, { pager: pager }); expect(result.statusCode).to.eq(404); const body = JSON.parse(result.body); From e35d44310d2770395c6d86f7f54cc45de2249d50 Mon Sep 17 00:00:00 2001 From: "Michael B. Klein" Date: Tue, 23 May 2023 21:13:51 +0000 Subject: [PATCH 5/6] Update poetry to v1.4.2 --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c783e6ee..0b0ad70d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -88,7 +88,7 @@ jobs: python-version: 3.9 - uses: abatilo/actions-poetry@v2 with: - poetry-version: 1.1.14 + poetry-version: 1.4.2 - name: Install dependencies run: poetry install working-directory: ./docs From aa6e547153db32f13958c67e0b6582d06a7466d4 Mon Sep 17 00:00:00 2001 From: "Michael B. Klein" Date: Tue, 23 May 2023 21:20:59 +0000 Subject: [PATCH 6/6] Allow manual dispatch of documentation update --- .github/workflows/deploy.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0b0ad70d..2fe0758f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -11,6 +11,11 @@ on: - "src/**" - "template.yaml" workflow_dispatch: + inputs: + force_deploy_docs: + description: Deploy documentation even if no changes detected + type: boolean + default: false concurrency: group: ${{ github.workflow }}-${{ github.ref }} env: @@ -70,7 +75,7 @@ jobs: docs/* publish-docs: needs: docs-changed - if: ${{ needs.docs-changed.outputs.result == 'true' }} + if: ${{ needs.docs-changed.outputs.result == 'true' || inputs.force_deploy_docs }} runs-on: ubuntu-latest permissions: id-token: write