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 { 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 => {
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);
},
upload: async function(blob) {
}
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({
@ -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 { 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: {
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)
}
});
}
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,
@ -37,22 +59,11 @@ export const ImageType = PouchDB.registerType({
// 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) {
const processImportables = backgroundTask(async function _processImportables(importables) {
if (!importables.length) {
return;
}
@ -69,9 +80,7 @@ export const ImageType = PouchDB.registerType({
const { tags, imageSize } = exifData;
const { width, height } = imageSize;
const originalDate = new Date(
tags.DateTimeOriginal
? new Date(tags.DateTimeOriginal * 1000).toISOString()
: image.originalDate
tags.DateTimeOriginal ? new Date(tags.DateTimeOriginal * 1000).toISOString() : image.originalDate
).toISOString();
const img = await ImageType.getOrCreate({
@ -97,8 +106,8 @@ export const ImageType = PouchDB.registerType({
const module = await import('../context/generateThumbnails');
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) {
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}`;
export class TypeSpec {
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);
static getSequence(doc) {
return '';
}
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;
}
static getUniqueID(doc) {
throw 'NotImplemented';
}
export function PouchORM(PouchDB) {
async function update(props, save = true) {
deepAssign(this, props);
if (save) {
await this.save();
} else {
this.validate();
}
return this;
static validate(doc) {}
instantiate(doc) {
return new this._cls(docs);
}
PouchDB.registerType = opts => {
const { getUniqueID, getSequence, schema, name } = opts;
const prefix = name.toLowerCase();
const db = opts.db || new PouchDB(prefix);
function populateId(doc) {
_populateId(doc) {
if (!doc._id) {
const sequence = getSequence ? getSequence(doc).toString(36) : '';
doc._id = `${prefix}_${sequence}_${getUniqueID(doc)}`;
doc._id = `${this._prefix}_${this._cls.getSequence(doc)}_${this._cls.getUniqueID(doc)}`;
}
return doc;
}
function validate() {
// FIXME
return this;
async delete() {
return await this.update({ _deleted: true });
}
async function save() {
const { rev } = await db.put(this.validate());
async save() {
this._cls.validate(this);
const { rev } = await this._db.put(this);
this._rev = rev;
return this;
}
async function addAttachment(attName, dataBlob) {
const { rev } = await db.putAttachment(this._id, attName, this._rev, dataBlob, dataBlob.type);
async addAttachment(attName, dataBlob) {
const { rev } = await this._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 getAttachment(attName) {
return await this._db.getAttachment(this._id, attName);
}
async function removeAttachment(attName) {
return await db.removeAttachment(this._id, attName, this._rev);
async removeAttachment(attName) {
return await this._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;
async update(props, save = true) {
deepAssign(this, props);
if (save) {
await this.save();
}
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) {
let results = [];
if (typeof idOrQuery === 'string') {
results = await db.get(idOrQuery);
} else {
return instantiate(await _db.get(idOrQuery));
}
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 LiveArray(_db, idOrQuery, instantiate);
}
results = await db.find({ selector: 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();
}
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]);
});