Browser extension that demonstrates the Web Annotation Discovery mechanism: subscribe to people’s annotation collections/‘feeds’, to see their notes on the web; and create & publish annotations yourself.

web-annotation-discovery-we.../src/storage/ Annotation.ts
120 lines
3.4 KiB

  1. import { db } from './db';
  2. import { AnnotationSource, IAnnotationSource } from './AnnotationSource';
  3. import { targetsUrl } from 'web-annotation-utils';
  4. import type { WebAnnotation } from 'web-annotation-utils';
  5. // Annotation model in database
  6. export class IAnnotation {
  7. constructor(
  8. public _id: number,
  9. public annotation: WebAnnotation,
  10. public source: IAnnotationSource['_id'],
  11. public dirty?: boolean,
  12. public toDelete?: boolean,
  13. public lastModified?: Date,
  14. ) {}
  15. }
  16. // Expanded form, i.e. with source nested.
  17. export type IAnnotationWithSource = Omit<IAnnotation, 'source'> & {
  18. source: IAnnotationSource;
  19. };
  20. export class Annotation {
  21. constructor(public data: IAnnotation) {}
  22. async save() {
  23. await db.annotations.put({
  24. ...this.data,
  25. lastModified: new Date(),
  26. });
  27. }
  28. async setDirty(value: boolean, data?: Partial<IAnnotation>) {
  29. this.data.dirty = value;
  30. Object.assign(this.data, data);
  31. await this.save();
  32. }
  33. async update(webAnnotation: WebAnnotation) {
  34. await this.setDirty(true, { annotation: webAnnotation });
  35. const source = await this.source();
  36. await source.uploadAnnotation(this.data.annotation);
  37. await this.setDirty(false);
  38. }
  39. async delete() {
  40. // Could not delete it upstream. Mark it as waiting for deletion.
  41. await this.setDirty(true, { toDelete: true });
  42. const source = await this.source();
  43. try {
  44. await source.deleteAnnotationRemotely(this.data.annotation);
  45. await db.annotations.delete(this.data._id);
  46. } catch (error: any) {
  47. throw new Error(
  48. `Failed to delete: ${error.message} (The annotation should be deleted on a subsequent refresh)`,
  49. );
  50. }
  51. }
  52. async source() {
  53. return await AnnotationSource.get(this.data.source);
  54. }
  55. async expand(): Promise<IAnnotationWithSource> {
  56. return {
  57. ...this.data,
  58. source: (await this.source()).data,
  59. };
  60. }
  61. static async new(data: Omit<IAnnotation, '_id'>) {
  62. // @ts-ignore: _id is not needed in put()
  63. const annotation: IAnnotation = {
  64. ...data,
  65. lastModified: new Date(),
  66. }
  67. const key = (await db.annotations.put(annotation)) as IAnnotation['_id'];
  68. return new this({ ...data, _id: key });
  69. }
  70. static async count() {
  71. return await db.annotations.count();
  72. }
  73. static async get(id: IAnnotation['_id']): Promise<Annotation> {
  74. const data = await db.annotations.get(id);
  75. if (!data) throw new Error(`No annotation exists with id ${id}.`);
  76. return new Annotation(data);
  77. }
  78. static async getAll(): Promise<Annotation[]> {
  79. const annotations = await db.annotations.toArray();
  80. return annotations.map((data) => new this(data));
  81. }
  82. static async getAnnotationsForUrls(urls: string[]) {
  83. // TODO Use the index again, somehow. May need a separate field with a multiEntry index.
  84. const matches = await db.annotations
  85. // .where('annotation.target')
  86. // .startsWith(pageUrl)
  87. // .or('annotation.target.source')
  88. // .startsWith(pageUrl)
  89. // .or('annotation.target.id')
  90. // .startsWith(pageUrl)
  91. .filter((item) =>
  92. urls.some((url) => targetsUrl(item.annotation.target, url)),
  93. )
  94. .toArray();
  95. return matches.map((data) => new this(data));
  96. }
  97. static async getAnnotationsFromSource(source: number): Promise<Annotation[]> {
  98. const annotations = await db.annotations
  99. .where('source')
  100. .equals(source)
  101. .toArray();
  102. return annotations.map((data) => new this(data));
  103. }
  104. }