import { db } from './db'; import { AnnotationSource, IAnnotationSource } from './AnnotationSource'; import { targetsUrl } from 'web-annotation-utils'; import type { WebAnnotation } from 'web-annotation-utils'; // Annotation model in database export class IAnnotation { constructor( public _id: number, public annotation: WebAnnotation, public source: IAnnotationSource['_id'], public dirty?: boolean, public toDelete?: boolean, public lastModified?: Date, ) {} } // Expanded form, i.e. with source nested. export type IAnnotationWithSource = Omit & { source: IAnnotationSource; }; export class Annotation { constructor(public data: IAnnotation) {} async save() { await db.annotations.put({ ...this.data, lastModified: new Date(), }); } async setDirty(value: boolean, data?: Partial) { this.data.dirty = value; Object.assign(this.data, data); await this.save(); } async update(webAnnotation: WebAnnotation) { await this.setDirty(true, { annotation: webAnnotation }); const source = await this.source(); await source.uploadAnnotation(this.data.annotation); await this.setDirty(false); } async delete() { // Could not delete it upstream. Mark it as waiting for deletion. await this.setDirty(true, { toDelete: true }); const source = await this.source(); try { await source.deleteAnnotationRemotely(this.data.annotation); await db.annotations.delete(this.data._id); } catch (error: any) { throw new Error( `Failed to delete: ${error.message} (The annotation should be deleted on a subsequent refresh)`, ); } } async source() { return await AnnotationSource.get(this.data.source); } async expand(): Promise { return { ...this.data, source: (await this.source()).data, }; } static async new(data: Omit) { // @ts-ignore: _id is not needed in put() const annotation: IAnnotation = { ...data, lastModified: new Date(), } const key = (await db.annotations.put(annotation)) as IAnnotation['_id']; return new this({ ...data, _id: key }); } static async count() { return await db.annotations.count(); } static async get(id: IAnnotation['_id']): Promise { const data = await db.annotations.get(id); if (!data) throw new Error(`No annotation exists with id ${id}.`); return new Annotation(data); } static async getAll(): Promise { const annotations = await db.annotations.toArray(); return annotations.map((data) => new this(data)); } static async getAnnotationsForUrls(urls: string[]) { // TODO Use the index again, somehow. May need a separate field with a multiEntry index. const matches = await db.annotations // .where('annotation.target') // .startsWith(pageUrl) // .or('annotation.target.source') // .startsWith(pageUrl) // .or('annotation.target.id') // .startsWith(pageUrl) .filter((item) => urls.some((url) => targetsUrl(item.annotation.target, url)), ) .toArray(); return matches.map((data) => new this(data)); } static async getAnnotationsFromSource(source: number): Promise { const annotations = await db.annotations .where('source') .equals(source) .toArray(); return annotations.map((data) => new this(data)); } }