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,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);

View File

@ -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));

View File

@ -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);

View File

@ -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]);
});