Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Pass items endpoint data into item model
We want to fully remove these manifest fetches, but in order to do so,
I'll need to add more data to the existing items endpoint. In the
meantime, get the ball rolling by using the items endpoint where I can
and adding an interface to make the data types clearer.
  • Loading branch information
sarangj committed Sep 8, 2025
commit da8ea765a7e4659d7f3f0a6f4766ecda211cd6a1
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

## Updated
- Use items endpoint for more item metadata fields (DR-3855)

## [1.1.0] 2025-08-28
## Updated
- updated middleware file to redirect synthetic collections (DR-3750)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"uuid": "1df2df50-c32b-0133-e4e8-60f81dd2b63c",
"buyable": false,
"isRestricted": false,
"permittedLocationText": null,
"contentType": "audio",
"yearStart": null,
"yearEnd": null,
"citationResourceType": "Sound Recording",
"captures": [
{
"uuid": "abcd",
"imageId": null,
"orderInSequence": 1
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"uuid": "d6799d40-c0bf-0139-b046-0242ac110004",
"buyable": false,
"isRestricted": false,
"permittedLocationText": null,
"contentType": "image",
"yearStart": null,
"yearEnd": null,
"citationResourceType": "Three Dimensional Object",
"captures": [
{
"uuid": "abcd",
"imageId": "58317977",
"orderInSequence": 1
},
{
"uuid": "abcd",
"imageId": "58317978",
"orderInSequence": 2
},
{
"uuid": "abcd",
"imageId": "58317979",
"orderInSequence": 3
},
{
"uuid": "abcd",
"imageId": "58317980",
"orderInSequence": 4
},
{
"uuid": "abcd",
"imageId": "58317981",
"orderInSequence": 5
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"uuid": "a9c43f00-c600-012f-59c3-58d385a7bc34",
"buyable": false,
"isRestricted": false,
"permittedLocationText": null,
"contentType": "image",
"yearStart": null,
"yearEnd": null,
"citationResourceType": "Still Image",
"captures": [
{
"uuid": "abcd",
"imageId": "1107651",
"orderInSequence": 1
}
]
}
11 changes: 11 additions & 0 deletions __tests__/__mocks__/data/collectionsApi/items/restricted.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"uuid": "/33e3aab0-16fc-013d-c413-0242ac110002",
"buyable": false,
"isRestricted": true,
"permittedLocationText": null,
"contentType": "video",
"yearStart": null,
"yearEnd": null,
"citationResourceType": "Moving Image",
"captures": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"uuid": "794fcf20-b822-0133-7afc-60f81dd2b63c",
"buyable": false,
"isRestricted": true,
"permittedLocationText": null,
"contentType": "audio",
"yearStart": null,
"yearEnd": null,
"citationResourceType": "Sound Recording",
"captures": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"uuid": "0b850640-e377-0130-8565-3c075448cc4b",
"buyable": false,
"isRestricted": false,
"permittedLocationText": null,
"contentType": "video",
"yearStart": null,
"yearEnd": null,
"citationResourceType": "Moving Image",
"captures": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"uuid": "fd2f5090-0381-0131-a95f-3c075448cc4b",
"buyable": false,
"isRestricted": false,
"permittedLocationText": null,
"contentType": "video",
"yearStart": null,
"yearEnd": null,
"citationResourceType": "Moving Image",
"captures": [
{
"uuid": "abcd",
"imageId": null,
"orderInSequence": 1
}
]
}
39 changes: 33 additions & 6 deletions __tests__/models/ItemModel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,25 @@ import { ItemModel } from "../../app/src/models/item"; // adjust path as needed

//general items
import restrictedAndMissingMediaManifest from "../__mocks__/data/collectionsApi/manifests/item/restricted_and_missing_media.json";
import restrictedAndMissingItemDetail from "../__mocks__/data/collectionsApi/items/restricted_and_missing_media.json";
import restrictedItemDetail from "../__mocks__/data/collectionsApi/items/restricted.json";
import restrictedManifest from "../__mocks__/data/collectionsApi/manifests/item/restricted.json";

//images
import singleImageCaptureItemDetail from "../__mocks__/data/collectionsApi/items/image/single_capture.json";
import singleImageCaptureManifest from "../__mocks__/data/collectionsApi/manifests/item/image/single_capture.json";
import multipleImageCaptureItemDetail from "../__mocks__/data/collectionsApi/items/image/multiple_capture.json";
import multiImageCaptureManifest from "../__mocks__/data/collectionsApi/manifests/item/image/multiple_capture.json";

//audio
import singleAudioCaptureItemDetail from "../__mocks__/data/collectionsApi/items/audio/single_capture.json";
import singleAudioCaptureManifest from "../__mocks__/data/collectionsApi/manifests/item/audio/single_capture.json";

//video
//note: no multi video capture manifest bc we allegedly don't have Items that have multiple video captures
import singleVideoCaptureItemDetail from "../__mocks__/data/collectionsApi/items/video/single_capture.json";
import singleVideoCaptureManifest from "../__mocks__/data/collectionsApi/manifests/item/video/single_capture.json";
import missingMediaVideoCaptureItemDetail from "../__mocks__/data/collectionsApi/items/video/missing_capture.json";
import missingMediaVideoCaptureManifest from "../__mocks__/data/collectionsApi/manifests/item/video/missing_capture.json";

describe("ItemModel - Image - Single Capture", () => {
Expand All @@ -22,7 +29,11 @@ describe("ItemModel - Image - Single Capture", () => {
let item: ItemModel;

beforeEach(() => {
item = new ItemModel(uuid, singleImageCaptureManifest);
item = new ItemModel(
uuid,
singleImageCaptureManifest,
singleImageCaptureItemDetail
);
});

describe("Non-Manifest/Metadata related fields", () => {
Expand Down Expand Up @@ -94,7 +105,11 @@ describe("ItemModel - Image - Multiple Capture", () => {
let item: ItemModel;

beforeEach(() => {
item = new ItemModel(uuid, multiImageCaptureManifest);
item = new ItemModel(
uuid,
multiImageCaptureManifest,
multipleImageCaptureItemDetail
);
});

describe("Non-Manifest/Metadata related fields", () => {
Expand Down Expand Up @@ -142,7 +157,11 @@ describe("ItemModel - Video - Single Capture", () => {
let item: ItemModel;

beforeEach(() => {
item = new ItemModel(uuid, singleVideoCaptureManifest);
item = new ItemModel(
uuid,
singleVideoCaptureManifest,
singleVideoCaptureItemDetail
);
});

describe("sets the correct Manifest-related fields", () => {
Expand Down Expand Up @@ -174,7 +193,11 @@ describe("ItemModel - Video - Missing Captures", () => {
let item: ItemModel;

beforeEach(() => {
item = new ItemModel(uuid, missingMediaVideoCaptureManifest);
item = new ItemModel(
uuid,
missingMediaVideoCaptureManifest,
missingMediaVideoCaptureItemDetail
);
});

describe("sets the correct Manifest-related fields", () => {
Expand Down Expand Up @@ -206,7 +229,11 @@ describe("ItemModel - Audio - Single Capture", () => {
let item: ItemModel;

beforeEach(() => {
item = new ItemModel(uuid, singleAudioCaptureManifest);
item = new ItemModel(
uuid,
singleAudioCaptureManifest,
singleAudioCaptureItemDetail
);
});

describe("sets the correct Manifest-related fields", () => {
Expand Down Expand Up @@ -238,7 +265,7 @@ describe("ItemModel - Restricted", () => {
let item: ItemModel;

beforeEach(() => {
item = new ItemModel(uuid, restrictedManifest);
item = new ItemModel(uuid, restrictedManifest, restrictedItemDetail);
});

describe("sets the correct metadata-related fields", () => {
Expand Down
15 changes: 11 additions & 4 deletions app/items/[uuid]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,15 @@ const getClientIP = async () => {
export async function generateMetadata({
params,
}: ItemProps): Promise<Metadata> {
const manifest = await getItemManifest(params.uuid);
const item = new ItemModel(params.uuid, manifest);
const [itemDetail, manifest] = await Promise.all([
getItemData(params.uuid),
getItemManifest(params.uuid),
]);
// If we get no item data, this ends up being a canvas redirect anyway
if (!itemDetail) {
return {};
}
const item = new ItemModel(params.uuid, manifest, itemDetail);
params.item = item;
const title = item.title;
return {
Expand Down Expand Up @@ -103,7 +110,7 @@ export default async function ItemViewer({ params, searchParams }: ItemProps) {
);
}

const item = new ItemModel(params.uuid, manifest, itemData.captures);
const item = new ItemModel(params.uuid, manifest, itemData);

// only allow canvasIndex to be in the range of 0...item.imageIds.length (number of canvases)
const imageIDs = item.imageIDs || [];
Expand All @@ -130,7 +137,7 @@ export default async function ItemViewer({ params, searchParams }: ItemProps) {
citationsData={citationsData}
manifest={manifest}
uuid={params.uuid}
captures={itemData.captures}
itemDetail={itemData}
canvasIndex={clampedCanvasIndex}
/>
</PageLayout>
Expand Down
4 changes: 2 additions & 2 deletions app/src/components/pages/itemPage/itemPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import { ItemModel } from "@/src/models/item";
export function ItemPage({
manifest,
uuid,
captures,
itemDetail,
canvasIndex,
citationsData,
}) {
const item = new ItemModel(uuid, manifest, captures, citationsData);
const item = new ItemModel(uuid, manifest, itemDetail, citationsData);
return <Item manifest={manifest} item={item} canvasIndex={canvasIndex} />;
}
26 changes: 11 additions & 15 deletions app/src/models/item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
generateCitations,
CitationOutput,
} from "../utils/metadata/generateCitations";
import { APIItem } from "../types/CollectionsAPI";

// https://github.com/jptmoore/maniiifest
// other resources:
Expand Down Expand Up @@ -54,18 +55,17 @@ export class ItemModel {
constructor(
uuid: string,
manifest: any,
captures?: CaptureModel[],
itemDetail: APIItem,
citationData?: any
) {
const parser = new Maniiifest(manifest);
// Non-Manifest/Metadata related fields
this.uuid = uuid;
this.manifestURL = `${process.env.COLLECTIONS_API_URL}/manifests/${uuid}`;
this.captures = captures ?? [];
this.captures = itemDetail.captures;

// Manifest related fields
this.hasItems = manifest.items.length > 0;
const canvases = Array.from(parser.iterateManifestCanvas());
this.hasItems = this.captures.length > 0;
const annotations = Array.from(parser.iterateManifestCanvasAnnotation());
// Metadata assignment
const manifestMetadataArray = Array.from(parser.iterateManifestMetadata());
Expand All @@ -91,13 +91,9 @@ export class ItemModel {
? rawManifestMetadata["Resource Type"].toString()
: "";

this.isRestricted = rawManifestMetadata["Is Restricted"]
? rawManifestMetadata["Is Restricted"].toString().toLowerCase() === "true"
: true;
this.isRestricted = itemDetail.isRestricted;

this.buyable = rawManifestMetadata["Buyable"]
? rawManifestMetadata["Buyable"].toString().toLowerCase() === "true"
: false;
this.buyable = itemDetail.buyable;

//this will break in Prod if we don't deploy API first bc the name of the field is "Library Location"
this.divisionLink =
Expand All @@ -106,8 +102,8 @@ export class ItemModel {
: rawManifestMetadata["Library Locations"]?.[0] || "";

this.permittedLocationText =
this.isRestricted && rawManifestMetadata["Permitted Locations"]
? rawManifestMetadata["Permitted Locations"][0]?.toString()
this.isRestricted && itemDetail.permittedLocationText
? itemDetail.permittedLocationText
: "";

// for viewer configs and order print button
Expand All @@ -126,9 +122,9 @@ export class ItemModel {
// example canvas.id is: "https://iiif.nypl.org/iiif/3/TH-38454/full/!700,700/0/default.jpg"
this.imageIDs =
this.hasItems && this.isImage
? canvases.map((canvas) => {
return canvas.id.split("/")[5];
})
? this.captures.flatMap((capture) =>
capture.imageId ? [capture.imageId] : []
)
: null;

// Special NYPL Identifiers for external links
Expand Down
17 changes: 17 additions & 0 deletions app/src/types/CollectionsAPI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export interface APICapture {
uuid: string;
imageId: string | null;
orderInSequence: number;
}

export interface APIItem {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

uuid: string;
buyable: boolean;
isRestricted: boolean;
permittedLocationText: string | null;
contentType: string | null;
yearStart: string | null;
yearEnd: string | null;
citationResourceType: string | null;
captures: APICapture[];
}
3 changes: 2 additions & 1 deletion app/src/utils/apiClients/apiClients.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from "../../config/constants";
import { fetchApi } from "../fetchApi/fetchApi";
import { Filter } from "../../types/FilterType";
import { APIItem } from "@/src/types/CollectionsAPI";

export class CollectionsApi {
static async getCaptureMetadata(uuid: string) {
Expand Down Expand Up @@ -153,7 +154,7 @@ export class CollectionsApi {
});
}

static async getItemData(uuid: string) {
static async getItemData(uuid: string): Promise<APIItem> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

like it ... just wondering if we're SURE we have ALL the fields we need in the ApiItem interface

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't yet! The page is still getting hydrated with both this and the manifest, this PR was just preliminary to split up some of the work. Once https://github.com/NYPL/collections-api/pull/243, I'll add a followup to this PR to completely cut out the manifest calls.

return await fetchApi({
apiUrl: `${process.env.COLLECTIONS_API_URL}/items/${uuid}`,
options: { isRepoApi: false },
Expand Down