// Copyright (c) 2020 Jan Kaßel // Copyright (c) 2022 Gerben // // SPDX-License-Identifier: MIT import escapeString from 'escape-string-regexp'; import etag from 'etag'; import type { Request, Response } from 'express'; import { CollectionInfo } from './handlers/collection.js'; import { renderAnnotation } from './render/renderAnnotation.js'; import { renderCollection } from './render/renderCollection.js'; export function extractAnnotationIdFromUrl( fullAnnotationUrl: string, containerUrl: string, ) { const escapedUrl = escapeString(containerUrl); const pattern = new RegExp(`^${escapedUrl}\/([0-9a-z-]+)$`); const matches = fullAnnotationUrl.match(pattern); return !matches ? null : matches[1]; } export function expandAnnotation(annotation, containerInfo: Container) { return { ...annotation, id: `${containerInfo.url}/${annotation.id}`, }; } export function contractAnnotation(annotation, containerInfo: Container) { return { ...annotation, id: extractAnnotationIdFromUrl(annotation.id, containerInfo.url), }; } export class Container { url: string; name: string; label: string; constructor( private req: Request, public containerPath: string, collectionInfo: CollectionInfo, ) { const host = this.req.headers['x-forwarded-host'] || this.req.headers.host; this.url = `${this.req.protocol}://${host}${this.req.baseUrl}/${this.containerPath}`; this.name = collectionInfo.name; this.label = collectionInfo.label; } } export class PagedContainer extends Container { public total: number; public pageSize: number; private _getPage: (pageNumber: number, pageSize: number) => any[]; constructor( req: Request, containerPath: string, collectionInfo: CollectionInfo, getPage: any[], ); constructor( req: Request, containerPath: string, collectionInfo: CollectionInfo, getPage: (pageNumber: number, pageSize: number) => any[], total: number, pageSize: number, ); constructor( req: Request, containerPath: string, collectionInfo: CollectionInfo, getPage: ((pageNumber: number, pageSize: number) => any[]) | any[], total?: number, pageSize?: number, ) { super(req, containerPath, collectionInfo); if (Array.isArray(getPage)) { const items = getPage; this._getPage = () => items; this.total = items.length; this.pageSize = Infinity; } else { this._getPage = getPage; this.total = total as number; this.pageSize = pageSize as number; } } getPage(pageNumber: number) { return this._getPage(pageNumber, this.pageSize); } get lastPage(): number { return this.pageSize === Infinity ? 0 : Math.floor(this.total / this.pageSize); } } export function sendPage( res: Response, container: PagedContainer, pageNumber: number, iris: boolean, ) { if (pageNumber > container.lastPage) { res.status(404).send('Not found'); return; } const items = container.getPage(pageNumber); const page = { '@context': 'http://www.w3.org/ns/anno.jsonld', id: `${container.url}/?page=${pageNumber}&iris=${iris ? 1 : 0}`, type: 'AnnotationPage', partOf: { id: `${container.url}/?iris=${iris ? 1 : 0}`, total: container.total, }, startIndex: pageNumber === 0 ? 0 : container.pageSize * pageNumber, items: iris ? items.map((item) => item.id) : items, }; res.type('application/ld+json; profile="http://www.w3.org/ns/anno.jsonld"'); res.header('Allow', 'HEAD, GET, OPTIONS'); res.send(page); } export function sendContainer( req: Request, res: Response, container: PagedContainer, iris: boolean, embedFirstPage?: boolean, ) { let firstPage: any; if (embedFirstPage) { const items = container.getPage(0); firstPage = { id: `${container.url}/?page=0&iris=${iris ? 1 : 0}`, type: 'AnnotationPage', startIndex: 0, items: iris ? items.map((item) => item.id) : items, }; } else { firstPage = `${container.url}/?iris=${iris ? 1 : 0}&page=0`; } const collectionJsonldObj = { '@context': [ 'http://www.w3.org/ns/anno.jsonld', 'http://www.w3.org/ns/ldp.jsonld', ], id: `${container.url}/?iris=${iris ? 1 : 0}`, type: ['BasicContainer', 'AnnotationCollection'], total: container.total, label: container.label, first: firstPage, ...(container.lastPage > 0 && { last: `${container.url}/?iris=${iris ? 1 : 0}&page=${container.lastPage}`, }), }; res.header('ETag', etag(JSON.stringify(collectionJsonldObj))); res.format({ html: () => renderCollection(req, res, { collection: collectionJsonldObj, container, user: req.params.user, }), default: () => { res.header('Link', [ '; rel="type"', '; rel="http://www.w3.org/ns/ldp#constrainedBy"', ]); res.header( 'Accept-Post', 'application/ld+json; profile="http://www.w3.org/ns/anno.jsonld"', ); res.type( 'application/ld+json; profile="http://www.w3.org/ns/anno.jsonld"', ); res.header('Allow', 'HEAD, GET, POST, OPTIONS'); res.send(collectionJsonldObj); }, }); } export function sendAnnotation( req: Request, res: Response, annotation, containerInfo: Container, ) { annotation = expandAnnotation(annotation, containerInfo); res.header('ETag', etag(JSON.stringify(annotation))); res.format({ html: () => renderAnnotation(req, res, { annotation, containerInfo }), default: () => { res.header('Allow', 'OPTIONS,HEAD,GET,PUT,DELETE'); res.header('Link', '; rel="type"'); res.type( 'application/ld+json; profile="http://www.w3.org/ns/anno.jsonld"', ); res.send(annotation); }, }); }