Skip to content

Layer Routes With Layer, Page, and Line Class Refactor#469

Merged
thehabes merged 22 commits into
developmentfrom
460-layer-route-and-class
Mar 10, 2026
Merged

Layer Routes With Layer, Page, and Line Class Refactor#469
thehabes merged 22 commits into
developmentfrom
460-layer-route-and-class

Conversation

@thehabes
Copy link
Copy Markdown
Member

@thehabes thehabes commented Mar 5, 2026

Closes #460
Discovered #468, #470, and #471. Tracked separately, out of scope.

Imagined #465, #466, and #467. Tracked separately, out of scope.

Summary

Refactors the Layer class, layer/page/line route handlers, and shared utilities to consolidate AnnotationCollection formatting logic into the Layer class, improve error handling, and eliminate redundant project fetching patterns.

Changes

classes/Layer/Layer.js

  • Added #hydrated flag to track whether the Layer has been fully loaded from RERUM
  • Added asJSON(isLD) method — returns the Layer as a W3C AnnotationCollection (JSON-LD when isLD=true, simple object otherwise). This centralizes the formatting that was previously inline in route handlers
  • Added #loadAnnotationCollectionDataFromRerum() — fetches and syncs Layer properties from RERUM, called lazily by asJSON() when the Layer hasn't been hydrated yet
  • Added total, first, last constructor params with defaults derived from pages
  • Changed @context from http://www.w3.org/ns/anno.jsonld to http://iiif.io/api/presentation/3/context.json (IIIF Presentation 3.0)
  • Improved error handling in #saveCollectionToRerum() with proper RERUM fetch error wrapping (status 502)
  • Added optional chaining for pages.at(0)?.id and pages.at(-1)?.id to prevent crashes on empty page arrays

layer/index.js

  • GET — simplified from ~20 lines of inline formatting to findLayerById() + layer.asJSON(true)
  • PUT — added handleVersionConflict import and explicit 409 handling in the catch block; added res.headersSent guards to prevent double-response crashes; response now uses layer.asJSON(true) for consistent formatting
  • POST — eliminated redundant Project.getById() call (data is loaded by checkUserAccess)
  • Updated .all() error message to mention both GET and PUT

line/index.js

  • Replaced getProjectById() calls with Project.getById() or relying on checkUserAccess to load project data
  • Removed unused ifNewContent parameter from updatePageAndProject() calls
  • Added if (res.headersSent) return guards after withOptimisticLocking and in catch blocks
  • Renamed shadowed res variable to resp in fetch callbacks
  • Added if (!project?.data) guards with normalized 404 responses in all route handlers

page/index.js

  • Eliminated redundant Project.getById() calls — checkUserAccess already loads project data via #load()
  • Unified projectObjproject variable naming
  • Changed existence check from !project / !project?._id to !project?.data

utilities/shared.js

  • Removed getProjectById() — callers now use Project.getById() directly or rely on checkUserAccess
  • updateLayerAndProject() — added layerIndex < 0 guard (404); wrapped update logic in try-catch with proper error wrapping; refactored page overwrites from .forEach(async ...) + .push() anti-pattern to .filter().map() with Promise.all; added separate partOf update for all pages with optional chaining safety
  • findLayerById() — removed rerum parameter (RERUM fetching is now handled by Layer.asJSON() via #loadAnnotationCollectionDataFromRerum()); removed short numeric ID index-based lookup; now passes creator to the Layer constructor

Cross-cutting: Error message normalization

  • Normalized all "project not found" error messages across layer, line, page routes and shared utilities to use the consistent format `Project ${projectId} was not found`

@thehabes thehabes self-assigned this Mar 5, 2026

This comment was marked as outdated.

@thehabes thehabes removed a link to an issue Mar 9, 2026
@thehabes thehabes marked this pull request as ready for review March 9, 2026 18:57
@thehabes thehabes requested a review from cubap as a code owner March 9, 2026 18:57
Copy link
Copy Markdown
Member

@cubap cubap left a comment

Choose a reason for hiding this comment

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

Generally acceptable, but I am worried only about the change to the constructor.

Comment thread classes/Layer/Layer.js Outdated
* @seeAlso {@link Layer.build}
*/
constructor(projectId, { id, label, pages, creator = null }) {
constructor(projectId, { id, label, pages, creator = null, total, first, last }) {
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.

I think for a constructor, this is information that we don't need, since it is generated from the pages array.

Comment thread classes/Layer/Layer.js
this.#setRerumId()
await this.#saveCollectionToRerum()
}
this.total = this.pages.length
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.

This is generated on .update() so it doesn't matter what it was constructed to. This will break things on accident.

Comment thread classes/Layer/Layer.js Outdated
id: this.id,
type: 'AnnotationCollection',
label: { "none": [this.label] },
total: this.total,
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.

makes sense to generate the first/last/total here instead of requiring it.

Comment thread classes/Layer/Layer.js
total: this.pages.length,
first: this.pages.at(0).id,
last: this.pages.at(-1).id
first: this.pages.at(0)?.id,
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.

This is also not respecting the constructed first/last

Comment thread layer/index.js
return res.status(200).json(layerAsCollection)
const layer = await findLayerById(layerId, projectId)
const layerJson = await layer.asJSON(true)
return res.status(200).json(layerJson)
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.

I like this

Comment thread layer/index.js
if (layer.creator) layerAsCollection.creator = layer.creator
return res.status(200).json(layerAsCollection)
const layer = await findLayerById(layerId, projectId)
const layerJson = await layer.asJSON(true)
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.

This is JSON-LD by default. Is that what we want here?

Comment thread line/index.js
return respondWithError(res, 404, `Line with ID '${req.params.lineId}' not found in page '${req.params.pageId}'`)
}
if (!(oldLine.id && oldLine.target && oldLine.body)) oldLine = await fetch(oldLine.id).then(res => res.json())
if (!(oldLine.id && oldLine.target && oldLine.body)) oldLine = await fetch(oldLine.id).then(resp => resp.json())
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.

weird

Comment thread line/index.js
}
await withOptimisticLocking(
() => updatePageAndProject(page, project, user._id, true),
() => updatePageAndProject(page, project, user._id),
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.

Looking at the blame... I'm not sure what that true was there for before.

@thehabes thehabes merged commit a07b0d4 into development Mar 10, 2026
3 checks passed
@thehabes thehabes deleted the 460-layer-route-and-class branch March 10, 2026 16:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

/layer/:layerid Router & Layer Class Update

3 participants