Store and publish annotations on the web, as described in the Web Annotation Discovery proposal.

collection.ts 3.8 KiB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. // Copyright (c) 2020 Jan Kaßel
  2. // Copyright (c) 2022 Gerben
  3. //
  4. // SPDX-License-Identifier: MIT
  5. import db from '../db.js';
  6. import {
  7. Container,
  8. PagedContainer,
  9. sendPage,
  10. sendContainer,
  11. expandAnnotation,
  12. } from '../ldp.js';
  13. import { getPrefixedEntries } from '../util.js';
  14. import type { Request, Response } from 'express';
  15. export interface CollectionInfo {
  16. name: string,
  17. label: string,
  18. }
  19. export async function createCollection(req: Request, res: Response) {
  20. if (!req.body?.name) {
  21. res.status(400).send('Bad request');
  22. return;
  23. }
  24. const { name, label } = req.body;
  25. const collectionKey = `${req.params.user}/${name}`;
  26. try {
  27. await db.get(collectionKey);
  28. res.status(409).send('Conflict');
  29. return;
  30. } catch (err: any) {
  31. if (!err.notFound) {
  32. console.error(req.method, req.path, err);
  33. res.status(500).send('Internal server error');
  34. return;
  35. }
  36. const collection: CollectionInfo = {
  37. name,
  38. label,
  39. };
  40. await db.put(collectionKey, collection);
  41. res.format({
  42. html: () => {
  43. res.redirect(303, `${req.baseUrl}/${collectionKey}/`);
  44. },
  45. default: () => {
  46. res
  47. .status(201)
  48. .header('Location', `${req.baseUrl}/${collectionKey}/`)
  49. .header('Content-Location', `${req.baseUrl}/${collectionKey}/`);
  50. req.params.collection = name;
  51. getCollection(req, res);
  52. }
  53. });
  54. }
  55. }
  56. export async function getCollection(req: Request, res: Response) {
  57. const collectionKey = `${req.params.user}/${req.params.collection}`;
  58. let collectionInfo: CollectionInfo;
  59. try {
  60. collectionInfo = await db.get(collectionKey);
  61. } catch (err: any) {
  62. if (err.notFound) {
  63. res.status(404).send('Not found');
  64. } else {
  65. console.error(req.method, req.path, err);
  66. res.status(500).send('Internal server error');
  67. }
  68. return;
  69. }
  70. const pageNumber = req.query.page
  71. ? Number.parseInt(req.query.page as string)
  72. : null;
  73. const iris = req.query.iris === '1';
  74. const containerInfo = new Container(req, collectionKey, collectionInfo);
  75. try {
  76. const annotations = (
  77. await getPrefixedEntries(db, `${collectionKey}/`, true)
  78. ).map(([_, value]) => value);
  79. // Sort most recent first (based on annotations’ self-reported modified/created date).
  80. annotations.sort((a, b) =>
  81. (a.modified ?? a.created ?? '') < (b.modified ?? b.created ?? '')
  82. ? 1
  83. : -1,
  84. );
  85. const collection = new PagedContainer(
  86. req,
  87. collectionKey,
  88. collectionInfo,
  89. annotations.map((annotation) =>
  90. expandAnnotation(annotation, containerInfo),
  91. ),
  92. );
  93. if (pageNumber !== null) {
  94. sendPage(res, collection, pageNumber, iris);
  95. } else {
  96. // Embed the first page unless the client prefers otherwise.
  97. const embedFirstPage = req.headers.prefer?.includes(
  98. 'http://www.w3.org/ns/ldp#PreferMinimalContainer',
  99. )
  100. ? false
  101. : true;
  102. sendContainer(req, res, collection, iris, embedFirstPage);
  103. }
  104. } catch (err: any) {
  105. if (err.notFound) {
  106. res.status(404).send('Not found');
  107. } else {
  108. console.error(req.method, req.path, err);
  109. res.status(500).send('Internal server error');
  110. }
  111. }
  112. }
  113. export async function deleteCollection(req: Request, res: Response) {
  114. const collectionKey = `${req.params.user}/${req.params.collection}`;
  115. try {
  116. await db.get(collectionKey);
  117. const entries = await getPrefixedEntries(db, `${collectionKey}/`);
  118. for (const [key, _] of entries) {
  119. await db.del(key);
  120. }
  121. await db.del(collectionKey);
  122. res.status(204).send();
  123. } catch (err: any) {
  124. if (err.notFound) {
  125. res.status(404).send('Not found');
  126. } else {
  127. console.error(req.method, req.path, err);
  128. res.status(500).send('Internal server error');
  129. }
  130. return;
  131. }
  132. }