diff --git a/app.js b/app.js index ebeb80c..40f449c 100644 --- a/app.js +++ b/app.js @@ -10,6 +10,7 @@ import apiRouter from './routes/api-routes.js' import clientRouter from './routes/client.js' import _gog_fragmentsRouter from './routes/_gog_fragments_from_manuscript.js'; import _gog_glossesRouter from './routes/_gog_glosses_from_manuscript.js'; +import _gog_idRouter from './routes/_gog_id.js'; import rest from './rest.js' import { fileURLToPath } from 'url' const __filename = fileURLToPath(import.meta.url) @@ -83,6 +84,7 @@ app.use('/client', clientRouter) app.use('/gog/fragmentsInManuscript', _gog_fragmentsRouter) app.use('/gog/glossesInManuscript', _gog_glossesRouter) +app.use('/gog/id', _gog_idRouter) /** * Handle API errors and warnings RESTfully. All routes that don't end in res.send() will end up here. diff --git a/controllers/gog.js b/controllers/gog.js index a381c9f..e1d6078 100644 --- a/controllers/gog.js +++ b/controllers/gog.js @@ -327,7 +327,8 @@ const _gog_glosses_from_manuscript = async function (req, res, next) { * */ const expand = async function(primitiveEntity, GENERATOR=undefined, CREATOR=undefined){ - if(!primitiveEntity?.["@id"] || primitiveEntity?.id) return primitiveEntity + // An entity is expandable if it carries a URI under either '@id' or 'id'. + if(!primitiveEntity?.["@id"] && !primitiveEntity?.id) return primitiveEntity const targetId = primitiveEntity["@id"] ?? primitiveEntity.id ?? "unknown" let queryObj = { "__rerum.history.next": { $exists: true, $size: 0 } @@ -380,24 +381,75 @@ const expand = async function(primitiveEntity, GENERATOR=undefined, CREATOR=unde } // Get the Annotations targeting this Entity from the db. Remove _id property. + // Assuming we do not need paged query here let matches = await db.find(queryObj).toArray() matches = matches.map(o => { delete o._id return o }) - // Combine the Annotation bodies with the primitive object + // Combine the Annotation bodies with the primitive object. + // Mirror DEER's client-side expand() (deer-utils.js buildValueObject) + // When more than one current Annotation asserts the same key, collect the values into an Array. let expandedEntity = structuredClone(primitiveEntity) for(const anno of matches){ const body = anno.body - let keys = Object.keys(body) - if(!keys || keys.length !== 1) return - let key = keys[0] - let val = body[key].value ?? body[key] - expandedEntity[key] = val + if(!body || typeof body !== "object") continue + const keys = Object.keys(body) + if(keys.length !== 1) continue + const key = keys[0] + const assertion = body[key] + const valueObject = { + value: assertion?.value ?? assertion, + source: assertion?.source ?? { + citationSource: anno["@id"] ?? anno.id, + citationNote: anno.label ?? anno.name ?? "Composed object from DEER", + comment: "Learn about the assembler for this object at https://github.com/CenterForDigitalHumanities/deer" + }, + evidence: assertion?.evidence ?? anno.evidence ?? "" + } + if(expandedEntity.hasOwnProperty(key)){ + expandedEntity[key] = Array.isArray(expandedEntity[key]) + ? [...expandedEntity[key], valueObject] + : [expandedEntity[key], valueObject] + } + else{ + expandedEntity[key] = valueObject + } } return expandedEntity } -export { _gog_fragments_from_manuscript, _gog_glosses_from_manuscript, expand } +/** + * GET /gog/id/:_id + * Return the RERUM object for :_id with the descriptive Annotations targeting it already merged in. + * Mirrors the caching/negotiation behavior of the GET /v1/id/:_id handler in controllers/crud.js. + * */ +const expandedId = async function (req, res, next) { + res.set("Content-Type", "application/json; charset=utf-8") + const id = req.params["_id"] + try { + let match = await db.findOne({ "$or": [{ "_id": id }, { "__rerum.slug": id }] }) + if (!match) { + const err = { "message": `No RERUM object with id '${id}'`, "status": 404 } + return next(utils.createExpressError(err)) + } + // Same browser-caching policy as GET /v1/id/:_id so this stable URI is cached (24h). + res.set(utils.configureWebAnnoHeadersFor(match)) + res.set("Cache-Control", "max-age=86400, must-revalidate") + res.set(utils.configureLastModifiedHeader(match)) + // Include current version for optimistic locking (parity with GET /v1/id/:_id) + res.set("Current-Overwritten-Version", match.__rerum?.isOverwritten ?? "") + // No GENERATOR/CREATOR filter: merge every current targeting Annotation, matching the + // client's historical expand() behavior (its checkMatch is short-circuited to true). + let expanded = await expand(match) + expanded = idNegotiation(expanded) + res.location(_contextid(expanded["@context"]) ? expanded.id : expanded["@id"]) + res.json(expanded) + } catch (error) { + return next(utils.createExpressError(error)) + } +} + +export { _gog_fragments_from_manuscript, _gog_glosses_from_manuscript, expand, expandedId } diff --git a/db-controller.js b/db-controller.js index 43ee520..44a5f33 100644 --- a/db-controller.js +++ b/db-controller.js @@ -15,7 +15,7 @@ import { putUpdate, patchUpdate, patchSet, patchUnset, overwrite } from './contr import { bulkCreate, bulkUpdate } from './controllers/bulk.js' import { since, history, idHeadRequest, queryHeadRequest, sinceHeadRequest, historyHeadRequest } from './controllers/history.js' import { release } from './controllers/release.js' -import { _gog_fragments_from_manuscript, _gog_glosses_from_manuscript, expand } from './controllers/gog.js' +import { _gog_fragments_from_manuscript, _gog_glosses_from_manuscript, expand, expandedId } from './controllers/gog.js' export default { index, @@ -44,5 +44,6 @@ export default { _gog_glosses_from_manuscript, _gog_fragments_from_manuscript, idNegotiation, - expand + expand, + expandedId } diff --git a/package-lock.json b/package-lock.json index 55b0873..ab176c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rerum_server_nodejs", - "version": "0.0.0", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rerum_server_nodejs", - "version": "0.0.0", + "version": "1.1.0", "license": "UNLICENSED", "dependencies": { "cookie-parser": "~1.4.7", diff --git a/routes/_gog_id.js b/routes/_gog_id.js new file mode 100644 index 0000000..e287ec3 --- /dev/null +++ b/routes/_gog_id.js @@ -0,0 +1,13 @@ +import express from 'express' +const router = express.Router() +import controller from '../db-controller.js' + +// GoG-namespaced, stable, browser-cacheable URL returning the object with its targeting Annotations merged in. +router.route('/:_id') + .get(controller.expandedId) + .all((req, res, next) => { + res.statusMessage = 'Improper request method, please use GET.' + res.status(405).end() + }) + +export default router