From ad6666bd5050d747512208a75364669610ef6992 Mon Sep 17 00:00:00 2001 From: Timothy Farrell Date: Sun, 19 Nov 2017 22:51:02 -0600 Subject: [PATCH] Class-based types for the ORM feel a little more natural. --- packages/gallery/src/data/file.js | 117 +++++++----- packages/gallery/src/data/image.js | 203 +++++++++++---------- packages/gallery/src/services/console.js | 6 + packages/gallery/src/services/db.js | 216 ++++++++++------------- 4 files changed, 272 insertions(+), 270 deletions(-) diff --git a/packages/gallery/src/data/file.js b/packages/gallery/src/data/file.js index 27f0f2c..6ce518a 100644 --- a/packages/gallery/src/data/file.js +++ b/packages/gallery/src/data/file.js @@ -1,52 +1,75 @@ -import { PouchDB, TYPES as t } from '../services/db.js'; +import { PouchDB, TypeSpec } from '../services/db.js'; import { log } from '../services/console.js'; import { sha256 } from '../utils/crypto.js'; import { blobToArrayBuffer } from '../utils/conversion.js'; -export const FileType = PouchDB.registerType({ - name: 'File', - getUniqueID: doc => doc.digest.substr(0, 16), - // schema: { - // name: t.REQUIRED_STRING, - // mimetype: t.REQUIRED_STRING, - // digest: t.REQUIRED_STRING, - // size: t.INTEGER, - // modifiedDate: t.DATE, - // addDate: t.DATE, - // hasData: t.REQUIRED_BOOLEAN, - // tags: { - // type: "object", - // additionalProperties: t.BOOLEAN - // } - // }, - methods: { - getURL: doc => `/${FileType.prefix}/${doc._id}/data`, - getFromURL: async path => { - if (path.endsWith('/')) { - path = path.substr(0, path.length - 1); - } - const [_, db, id, attname] = path.split('/'); - const doc = await FileType.find(id); - return await doc.getAttachment(attname); - }, - upload: async function(blob) { - const digest = await sha256(await blobToArrayBuffer(blob)); - const lastModified = blob.lastModified ? new Date(blob.lastModified) : new Date(); - return await FileType.getOrCreate({ - name: blob.name, - mimetype: blob.type, - size: blob.size, - lastModified: lastModified.toISOString(), - addDate: new Date().toISOString(), - digest, - tags: {}, - _attachments: { - data: { - content_type: blob.type, - data: blob - } - } - }); - } +class FileSpec extends TypeSpec { + static getUniqueID(doc) { + return doc.digest.substr(0, 16); } -}); + + static getURL(doc, attachmentName = 'data') { + const end = attachmentName ? '/' + attachmentName : ''; + return `/${doc._prefix}/${doc._id}` + end; + } + + static async getDocFromURL(path) { + if (path.endsWith('/')) { + path = path.substr(0, path.length - 1); + } + const [_, db, id, attname] = path.split('/'); + return await FileType.find(id); + } + + static async getFromURL(path) { + if (path.endsWith('/')) { + path = path.substr(0, path.length - 1); + } + const [_, db, id, attname] = path.split('/'); + const doc = await FileType.find(id); + if (attname) { + return await doc.getAttachment(attname); + } + return doc; + } + + static async upload(blob) { + const digest = await sha256(await blobToArrayBuffer(blob)); + const lastModified = blob.lastModified ? new Date(blob.lastModified) : new Date(); + return await FileType.getOrCreate({ + name: blob.name, + mimetype: blob.type, + size: blob.size, + lastModified: lastModified.toISOString(), + addDate: new Date().toISOString(), + digest, + tags: {}, + _attachments: { + data: { + content_type: blob.type, + data: blob + } + } + }); + } + // + // static validate(doc) { + // // TODO actually validate perhaps against a JSON schema + // + // const schema = { + // name: t.REQUIRED_STRING, + // mimetype: t.REQUIRED_STRING, + // digest: t.REQUIRED_STRING, + // size: t.INTEGER, + // modifiedDate: t.DATE, + // addDate: t.DATE, + // hasData: t.REQUIRED_BOOLEAN, + // tags: { + // type: "object", + // additionalProperties: t.BOOLEAN + // } + // }; + // } +} + +export const FileType = PouchDB.registerType('File', FileSpec); diff --git a/packages/gallery/src/data/image.js b/packages/gallery/src/data/image.js index dfda9b5..8e4789c 100644 --- a/packages/gallery/src/data/image.js +++ b/packages/gallery/src/data/image.js @@ -1,104 +1,113 @@ -import { PouchDB, TYPES as t } from '../services/db.js'; +import { PouchDB, TypeSpec } from '../services/db.js'; import { blobToArrayBuffer } from '../utils/conversion.js'; import { backgroundTask } from '../utils/event.js'; import { FileType } from './file.js'; -export const ImageType = PouchDB.registerType({ - name: 'Image', - getUniqueID: doc => doc.digest.substr(0, 16), - getSequence: doc => new Date(doc.originalDate).getTime(), - // schema: { - // originalDate: t.REQUIRED_DATE, - // digest: t.REQUIRED_STRING, - // width: t.INTEGER, - // height: t.INTEGER, - // sizes: { - // type: 'object', - // properties: { - // full: t.REQUIRED_STRING, - // thumbnail: t.STRING, - // } - // }, - // orientation: t.INTEGER, - // make: t.STRING, - // model: t.STRING, - // flash: t.BOOLEAN, - // iso: t.INTEGER, - // gps: { - // type: 'object', - // properties: { - // latitude: t.NUMBER, - // longitude: t.NUMBER, - // altitude: t.NUMBER, - // heading: t.NUMBER, - // } - // }, - // tags: { - // type: "object", - // additionalProperties: t.BOOLEAN - // } - // }, - methods: { - upload: async function(blob) { - const f = await FileType.upload(blob, false); - return await ImageType.getOrCreate({ - digest: f.digest, - originalDate: f.lastModified, - importing: true, - width: 0, - height: 0, - sizes: { - full: FileType.getURL(f) - } - }); - }, - processImportables: backgroundTask(async function _processImportables(importables) { - if (!importables.length) { - return; +class ImageSpec extends TypeSpec { + static async upload(blob) { + const f = await FileType.upload(blob, false); + return await ImageType.getOrCreate({ + digest: f.digest, + originalDate: f.lastModified, + importing: true, + width: 0, + height: 0, + sizes: { + full: FileType.getURL(f) } - - const image = importables[0]; - const { _id, _rev } = image; - const imageData = await FileType.getFromURL(image.sizes.full); - - const ExifParser = await import('exif-parser'); - - const buffer = await blobToArrayBuffer(imageData); - - const exifData = ExifParser.create(buffer).parse(); - const { tags, imageSize } = exifData; - const { width, height } = imageSize; - const originalDate = new Date( - tags.DateTimeOriginal - ? new Date(tags.DateTimeOriginal * 1000).toISOString() - : image.originalDate - ).toISOString(); - - const img = await ImageType.getOrCreate({ - originalDate, - width, - height, - orientation: tags.Orientation, - digest: image.digest, - make: tags.Make, - model: tags.Model, - flash: !!tags.Flash, - iso: tags.ISO, - sizes: image.sizes, - gps: { - latitude: tags.GPSLatitude, - longitude: tags.GPSLongitude, - altitude: tags.GPSAltitude, - heading: tags.GPSImgDirection - } - }); - - image.delete(); - - const module = await import('../context/generateThumbnails'); - await module.generateThumbnailForImage(img); - }, false) + }); } -}); -ImageType.find({ importing: true }, true).then(fw => fw.subscribe(ImageType.processImportables)); + static getUniqueID(doc) { + return doc.digest.substr(0, 16); + } + + static getSequence(doc) { + return new Date(doc.originalDate).getTime(); + } + // + // static validate(doc) { + // // TODO actually validate perhaps against a JSON schema + // + // const schema = { + // originalDate: t.REQUIRED_DATE, + // digest: t.REQUIRED_STRING, + // width: t.INTEGER, + // height: t.INTEGER, + // sizes: { + // type: 'object', + // properties: { + // full: t.REQUIRED_STRING, + // thumbnail: t.STRING, + // } + // }, + // orientation: t.INTEGER, + // make: t.STRING, + // model: t.STRING, + // flash: t.BOOLEAN, + // iso: t.INTEGER, + // gps: { + // type: 'object', + // properties: { + // latitude: t.NUMBER, + // longitude: t.NUMBER, + // altitude: t.NUMBER, + // heading: t.NUMBER, + // } + // }, + // tags: { + // type: "object", + // additionalProperties: t.BOOLEAN + // } + // }; + // } +} + +const processImportables = backgroundTask(async function _processImportables(importables) { + if (!importables.length) { + return; + } + + const image = importables[0]; + const { _id, _rev } = image; + const imageData = await FileType.getFromURL(image.sizes.full); + + const ExifParser = await import('exif-parser'); + + const buffer = await blobToArrayBuffer(imageData); + + const exifData = ExifParser.create(buffer).parse(); + const { tags, imageSize } = exifData; + const { width, height } = imageSize; + const originalDate = new Date( + tags.DateTimeOriginal ? new Date(tags.DateTimeOriginal * 1000).toISOString() : image.originalDate + ).toISOString(); + + const img = await ImageType.getOrCreate({ + originalDate, + width, + height, + orientation: tags.Orientation, + digest: image.digest, + make: tags.Make, + model: tags.Model, + flash: !!tags.Flash, + iso: tags.ISO, + sizes: image.sizes, + gps: { + latitude: tags.GPSLatitude, + longitude: tags.GPSLongitude, + altitude: tags.GPSAltitude, + heading: tags.GPSImgDirection + } + }); + + image.delete(); + + const module = await import('../context/generateThumbnails'); + await module.generateThumbnailForImage(img); +}, false); + +export const ImageType = PouchDB.registerType('Image', ImageSpec); + +ImageType.find({ importing: true }, true).then(fw => fw.subscribe(processImportables)); diff --git a/packages/gallery/src/services/console.js b/packages/gallery/src/services/console.js index c0b9a70..85a291c 100644 --- a/packages/gallery/src/services/console.js +++ b/packages/gallery/src/services/console.js @@ -10,6 +10,12 @@ export function error(...args) { } } +export function warn(...args) { + if (__DEV__) { + console.warn(...args); + } +} + export function group(...args) { if (__DEV__) { console.group(...args); diff --git a/packages/gallery/src/services/db.js b/packages/gallery/src/services/db.js index a2ce4a7..23493a6 100644 --- a/packages/gallery/src/services/db.js +++ b/packages/gallery/src/services/db.js @@ -4,7 +4,7 @@ import http from 'pouchdb-adapter-http'; import replication from 'pouchdb-replication'; import find from 'pouchdb-find'; -import { log } from './console.js'; +import { log, warn } from './console.js'; import { isObject } from '../utils/comparators.js'; import { LiveArray } from '../utils/livearray.js'; import { deepAssign } from '../utils/conversion.js'; @@ -16,128 +16,102 @@ export const PouchDB = core .plugin(find) .plugin(PouchORM); -export function generateAttachmentUrl(dbName, docId, attachmentKey) { - return `/_doc_attachments/${dbName}/${docId}/${attachmentKey}`; -} - -const dbs = new Map(); -export function getDatabase(name = 'gallery') { - if (!dbs.has(name)) { - dbs.set(name, new PouchDB(name)); +export class TypeSpec { + constructor(props) { + this._populateId(props); + Object.assign(this, props); } - return dbs.get(name); -} -export async function getOrCreate(doc) { - const db = getDatabase(); - try { - const results = await db.get(doc._id); - return [results, false]; - } catch (e) { - if (e.status === 404) { - const results = db.put(doc); - return [results, true]; + static getSequence(doc) { + return ''; + } + + static getUniqueID(doc) { + throw 'NotImplemented'; + } + + static validate(doc) {} + + instantiate(doc) { + return new this._cls(docs); + } + + _populateId(doc) { + if (!doc._id) { + doc._id = `${this._prefix}_${this._cls.getSequence(doc)}_${this._cls.getUniqueID(doc)}`; } - throw e; + return doc; + } + + async delete() { + return await this.update({ _deleted: true }); + } + + async save() { + this._cls.validate(this); + const { rev } = await this._db.put(this); + this._rev = rev; + return this; + } + + async addAttachment(attName, dataBlob) { + const { rev } = await this._db.putAttachment( + this._id, + attName, + this._rev, + dataBlob, + dataBlob.type + ); + + this._rev = rev; + return this; + } + + async getAttachment(attName) { + return await this._db.getAttachment(this._id, attName); + } + + async removeAttachment(attName) { + return await this._db.removeAttachment(this._id, attName, this._rev); + } + + async update(props, save = true) { + deepAssign(this, props); + if (save) { + await this.save(); + } + return this; } } export function PouchORM(PouchDB) { - async function update(props, save = true) { - deepAssign(this, props); - if (save) { - await this.save(); - } else { - this.validate(); - } - return this; - } - - PouchDB.registerType = opts => { - const { getUniqueID, getSequence, schema, name } = opts; + PouchDB.registerType = (name, cls, db) => { const prefix = name.toLowerCase(); - const db = opts.db || new PouchDB(prefix); + const _db = db || PouchDB(prefix); - function populateId(doc) { - if (!doc._id) { - const sequence = getSequence ? getSequence(doc).toString(36) : ''; - doc._id = `${prefix}_${sequence}_${getUniqueID(doc)}`; - } - return doc; + if (!cls.hasOwnProperty('validate')) { + warn(`${cls.name} has no validation.`); } - function validate() { - // FIXME - return this; - } - - async function save() { - const { rev } = await db.put(this.validate()); - this._rev = rev; - return this; - } - - async function addAttachment(attName, dataBlob) { - const { rev } = await db.putAttachment(this._id, attName, this._rev, dataBlob, dataBlob.type); - - this._rev = rev; - return this; - } - - async function getAttachment(attName) { - return await db.getAttachment(this._id, attName); - } - - async function removeAttachment(attName) { - return await db.removeAttachment(this._id, attName, this._rev); - } - - function instantiate(docOrResultSet) { - Object.defineProperties(docOrResultSet, { - update: { value: update.bind(docOrResultSet) }, - save: { value: save.bind(docOrResultSet) }, - delete: { value: _delete.bind(docOrResultSet) }, - addAttachment: { value: addAttachment.bind(docOrResultSet) }, - getAttachment: { value: getAttachment.bind(docOrResultSet) }, - removeAttachment: { value: removeAttachment.bind(docOrResultSet) }, - validate: { value: validate.bind(docOrResultSet) } - }); - return docOrResultSet; - } + const instantiate = doc => new cls(doc); async function find(idOrQuery, live = false) { - let results = []; - if (typeof idOrQuery === 'string') { - results = await db.get(idOrQuery); - } else { - const selector = Object.assign( - { _deleted: { exists: false } }, - isObject(idOrQuery) ? idOrQuery : { _id: { $gt: `${prefix}_0`, $lt: `${prefix}_\ufff0` } } - ); - if (live) { - return LiveArray(db, idOrQuery, instantiate); - } - results = await db.find({ selector: idOrQuery }); + return instantiate(await _db.get(idOrQuery)); } - return instantiate(results); - } - - async function _delete() { - return await this.update({ _deleted: true }); - } - - async function _new(props, save = true) { - const doc = instantiate(populateId(props)); - if (save) { - await doc.save(); + const selector = Object.assign( + { _deleted: { exists: false } }, + isObject(idOrQuery) ? idOrQuery : { _id: { $gt: `${prefix}_0`, $lt: `${prefix}_\ufff0` } } + ); + if (live) { + return LiveArray(_db, idOrQuery, instantiate); } - return doc; + return (await _db.find({ selector: idOrQuery })).docs.map(instantiate); } async function getOrCreate(props) { - let doc = await _new(props, false); + let doc = await new cls(props); try { await doc.save(); } catch (e) { @@ -149,28 +123,18 @@ export function PouchORM(PouchDB) { return doc; } - return Object.assign( - { - new: _new, - getOrCreate, - find, - prefix, - db - // delete: // FIXME - }, - opts.methods || {} - ); + Object.defineProperties(cls.prototype, { + _name: { value: name }, + _prefix: { value: prefix }, + _db: { value: _db }, + _cls: { value: cls } + }); + + Object.defineProperties(cls, { + getOrCreate: { value: getOrCreate }, + find: { value: find } + }); + + return cls; }; } - -export const TYPES = { - STRING: { type: 'string' }, - INTEGER: { type: 'integer' }, - BOOLEAN: { type: 'boolean' }, - DATE: { type: 'date' } -}; - -// Add required types -Object.keys(TYPES).forEach(k => { - TYPES['REQUIRED_' + k] = Object.assign({ required: true }, TYPES[k]); -});