Class-based types for the ORM feel a little more natural.

This commit is contained in:
Timothy Farrell 2017-11-19 22:51:02 -06:00
parent 278fc68831
commit d2c1d3b63c
4 changed files with 272 additions and 270 deletions

View File

@ -1,35 +1,39 @@
import { PouchDB, TYPES as t } from '../services/db.js'; import { PouchDB, TypeSpec } from '../services/db.js';
import { log } from '../services/console.js'; import { log } from '../services/console.js';
import { sha256 } from '../utils/crypto.js'; import { sha256 } from '../utils/crypto.js';
import { blobToArrayBuffer } from '../utils/conversion.js'; import { blobToArrayBuffer } from '../utils/conversion.js';
export const FileType = PouchDB.registerType({ class FileSpec extends TypeSpec {
name: 'File', static getUniqueID(doc) {
getUniqueID: doc => doc.digest.substr(0, 16), return doc.digest.substr(0, 16);
// schema: { }
// name: t.REQUIRED_STRING,
// mimetype: t.REQUIRED_STRING, static getURL(doc, attachmentName = 'data') {
// digest: t.REQUIRED_STRING, const end = attachmentName ? '/' + attachmentName : '';
// size: t.INTEGER, return `/${doc._prefix}/${doc._id}` + end;
// modifiedDate: t.DATE, }
// addDate: t.DATE,
// hasData: t.REQUIRED_BOOLEAN, static async getDocFromURL(path) {
// tags: { if (path.endsWith('/')) {
// type: "object", path = path.substr(0, path.length - 1);
// additionalProperties: t.BOOLEAN }
// } const [_, db, id, attname] = path.split('/');
// }, return await FileType.find(id);
methods: { }
getURL: doc => `/${FileType.prefix}/${doc._id}/data`,
getFromURL: async path => { static async getFromURL(path) {
if (path.endsWith('/')) { if (path.endsWith('/')) {
path = path.substr(0, path.length - 1); path = path.substr(0, path.length - 1);
} }
const [_, db, id, attname] = path.split('/'); const [_, db, id, attname] = path.split('/');
const doc = await FileType.find(id); const doc = await FileType.find(id);
if (attname) {
return await doc.getAttachment(attname); return await doc.getAttachment(attname);
}, }
upload: async function(blob) { return doc;
}
static async upload(blob) {
const digest = await sha256(await blobToArrayBuffer(blob)); const digest = await sha256(await blobToArrayBuffer(blob));
const lastModified = blob.lastModified ? new Date(blob.lastModified) : new Date(); const lastModified = blob.lastModified ? new Date(blob.lastModified) : new Date();
return await FileType.getOrCreate({ return await FileType.getOrCreate({
@ -48,5 +52,24 @@ export const FileType = PouchDB.registerType({
} }
}); });
} }
} //
}); // 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);

View File

@ -1,13 +1,35 @@
import { PouchDB, TYPES as t } from '../services/db.js'; import { PouchDB, TypeSpec } from '../services/db.js';
import { blobToArrayBuffer } from '../utils/conversion.js'; import { blobToArrayBuffer } from '../utils/conversion.js';
import { backgroundTask } from '../utils/event.js'; import { backgroundTask } from '../utils/event.js';
import { FileType } from './file.js'; import { FileType } from './file.js';
export const ImageType = PouchDB.registerType({ class ImageSpec extends TypeSpec {
name: 'Image', static async upload(blob) {
getUniqueID: doc => doc.digest.substr(0, 16), const f = await FileType.upload(blob, false);
getSequence: doc => new Date(doc.originalDate).getTime(), return await ImageType.getOrCreate({
// schema: { digest: f.digest,
originalDate: f.lastModified,
importing: true,
width: 0,
height: 0,
sizes: {
full: FileType.getURL(f)
}
});
}
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, // originalDate: t.REQUIRED_DATE,
// digest: t.REQUIRED_STRING, // digest: t.REQUIRED_STRING,
// width: t.INTEGER, // width: t.INTEGER,
@ -37,22 +59,11 @@ export const ImageType = PouchDB.registerType({
// type: "object", // type: "object",
// additionalProperties: t.BOOLEAN // additionalProperties: t.BOOLEAN
// } // }
// }, // };
methods: { // }
upload: async function(blob) { }
const f = await FileType.upload(blob, false);
return await ImageType.getOrCreate({ const processImportables = backgroundTask(async function _processImportables(importables) {
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) { if (!importables.length) {
return; return;
} }
@ -69,9 +80,7 @@ export const ImageType = PouchDB.registerType({
const { tags, imageSize } = exifData; const { tags, imageSize } = exifData;
const { width, height } = imageSize; const { width, height } = imageSize;
const originalDate = new Date( const originalDate = new Date(
tags.DateTimeOriginal tags.DateTimeOriginal ? new Date(tags.DateTimeOriginal * 1000).toISOString() : image.originalDate
? new Date(tags.DateTimeOriginal * 1000).toISOString()
: image.originalDate
).toISOString(); ).toISOString();
const img = await ImageType.getOrCreate({ const img = await ImageType.getOrCreate({
@ -97,8 +106,8 @@ export const ImageType = PouchDB.registerType({
const module = await import('../context/generateThumbnails'); const module = await import('../context/generateThumbnails');
await module.generateThumbnailForImage(img); await module.generateThumbnailForImage(img);
}, false) }, false);
}
});
ImageType.find({ importing: true }, true).then(fw => fw.subscribe(ImageType.processImportables)); export const ImageType = PouchDB.registerType('Image', ImageSpec);
ImageType.find({ importing: true }, true).then(fw => fw.subscribe(processImportables));

View File

@ -10,6 +10,12 @@ export function error(...args) {
} }
} }
export function warn(...args) {
if (__DEV__) {
console.warn(...args);
}
}
export function group(...args) { export function group(...args) {
if (__DEV__) { if (__DEV__) {
console.group(...args); console.group(...args);

View File

@ -4,7 +4,7 @@ import http from 'pouchdb-adapter-http';
import replication from 'pouchdb-replication'; import replication from 'pouchdb-replication';
import find from 'pouchdb-find'; import find from 'pouchdb-find';
import { log } from './console.js'; import { log, warn } from './console.js';
import { isObject } from '../utils/comparators.js'; import { isObject } from '../utils/comparators.js';
import { LiveArray } from '../utils/livearray.js'; import { LiveArray } from '../utils/livearray.js';
import { deepAssign } from '../utils/conversion.js'; import { deepAssign } from '../utils/conversion.js';
@ -16,128 +16,102 @@ export const PouchDB = core
.plugin(find) .plugin(find)
.plugin(PouchORM); .plugin(PouchORM);
export function generateAttachmentUrl(dbName, docId, attachmentKey) { export class TypeSpec {
return `/_doc_attachments/${dbName}/${docId}/${attachmentKey}`; constructor(props) {
} this._populateId(props);
Object.assign(this, props);
const dbs = new Map();
export function getDatabase(name = 'gallery') {
if (!dbs.has(name)) {
dbs.set(name, new PouchDB(name));
}
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];
}
throw e;
}
}
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 => { static getSequence(doc) {
const { getUniqueID, getSequence, schema, name } = opts; return '';
const prefix = name.toLowerCase(); }
const db = opts.db || new PouchDB(prefix);
function populateId(doc) { static getUniqueID(doc) {
throw 'NotImplemented';
}
static validate(doc) {}
instantiate(doc) {
return new this._cls(docs);
}
_populateId(doc) {
if (!doc._id) { if (!doc._id) {
const sequence = getSequence ? getSequence(doc).toString(36) : ''; doc._id = `${this._prefix}_${this._cls.getSequence(doc)}_${this._cls.getUniqueID(doc)}`;
doc._id = `${prefix}_${sequence}_${getUniqueID(doc)}`;
} }
return doc; return doc;
} }
function validate() { async delete() {
// FIXME return await this.update({ _deleted: true });
return this;
} }
async function save() { async save() {
const { rev } = await db.put(this.validate()); this._cls.validate(this);
const { rev } = await this._db.put(this);
this._rev = rev; this._rev = rev;
return this; return this;
} }
async function addAttachment(attName, dataBlob) { async addAttachment(attName, dataBlob) {
const { rev } = await db.putAttachment(this._id, attName, this._rev, dataBlob, dataBlob.type); const { rev } = await this._db.putAttachment(
this._id,
attName,
this._rev,
dataBlob,
dataBlob.type
);
this._rev = rev; this._rev = rev;
return this; return this;
} }
async function getAttachment(attName) { async getAttachment(attName) {
return await db.getAttachment(this._id, attName); return await this._db.getAttachment(this._id, attName);
} }
async function removeAttachment(attName) { async removeAttachment(attName) {
return await db.removeAttachment(this._id, attName, this._rev); return await this._db.removeAttachment(this._id, attName, this._rev);
} }
function instantiate(docOrResultSet) { async update(props, save = true) {
Object.defineProperties(docOrResultSet, { deepAssign(this, props);
update: { value: update.bind(docOrResultSet) }, if (save) {
save: { value: save.bind(docOrResultSet) }, await this.save();
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;
} }
return this;
}
}
export function PouchORM(PouchDB) {
PouchDB.registerType = (name, cls, db) => {
const prefix = name.toLowerCase();
const _db = db || PouchDB(prefix);
if (!cls.hasOwnProperty('validate')) {
warn(`${cls.name} has no validation.`);
}
const instantiate = doc => new cls(doc);
async function find(idOrQuery, live = false) { async function find(idOrQuery, live = false) {
let results = [];
if (typeof idOrQuery === 'string') { if (typeof idOrQuery === 'string') {
results = await db.get(idOrQuery); return instantiate(await _db.get(idOrQuery));
} else { }
const selector = Object.assign( const selector = Object.assign(
{ _deleted: { exists: false } }, { _deleted: { exists: false } },
isObject(idOrQuery) ? idOrQuery : { _id: { $gt: `${prefix}_0`, $lt: `${prefix}_\ufff0` } } isObject(idOrQuery) ? idOrQuery : { _id: { $gt: `${prefix}_0`, $lt: `${prefix}_\ufff0` } }
); );
if (live) { if (live) {
return LiveArray(db, idOrQuery, instantiate); return LiveArray(_db, idOrQuery, instantiate);
} }
results = await db.find({ selector: idOrQuery }); return (await _db.find({ selector: idOrQuery })).docs.map(instantiate);
}
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();
}
return doc;
} }
async function getOrCreate(props) { async function getOrCreate(props) {
let doc = await _new(props, false); let doc = await new cls(props);
try { try {
await doc.save(); await doc.save();
} catch (e) { } catch (e) {
@ -149,28 +123,18 @@ export function PouchORM(PouchDB) {
return doc; return doc;
} }
return Object.assign( Object.defineProperties(cls.prototype, {
{ _name: { value: name },
new: _new, _prefix: { value: prefix },
getOrCreate, _db: { value: _db },
find, _cls: { value: cls }
prefix, });
db
// delete: // FIXME Object.defineProperties(cls, {
}, getOrCreate: { value: getOrCreate },
opts.methods || {} 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]);
});