PouchORM becomes PouchType
API changed and full test coverage.
This commit is contained in:
parent
10dd03a48c
commit
07d3c77451
@ -17,6 +17,11 @@ A browser-based app for viewing photos. (WIP)
|
|||||||
|
|
||||||
A utility to expose an asynchronous API between a web worker and its parent.
|
A utility to expose an asynchronous API between a web worker and its parent.
|
||||||
|
|
||||||
|
## [PouchType](./packages/pouchtype/README.md)
|
||||||
|
|
||||||
|
An type-based abstraction layer over PouchDB inspired by [Hood.ie](https://hood.ie/) and
|
||||||
|
[Django](https://djangoproject.com)
|
||||||
|
|
||||||
## [Router](./packages/router/README.md)
|
## [Router](./packages/router/README.md)
|
||||||
|
|
||||||
A slim and unopinionated hash router.
|
A slim and unopinionated hash router.
|
||||||
|
|||||||
@ -1,140 +0,0 @@
|
|||||||
# PouchORM
|
|
||||||
|
|
||||||
An ORM for PouchDB inspired by [Hood.ie](https://hood.ie/) and [Django](https://djangoproject.com)
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Type Definition
|
|
||||||
|
|
||||||
In a type definition class, specify a static `getUniqueID` method and optionally a static `validate`
|
|
||||||
method.
|
|
||||||
|
|
||||||
```js
|
|
||||||
import { PouchDB } from 'pouchdb';
|
|
||||||
import { TypeSpec } from 'PouchORM';
|
|
||||||
|
|
||||||
class FileSpec extends TypeSpec {
|
|
||||||
static getUniqueID(doc) {
|
|
||||||
return doc.digest.substr(0, 16);
|
|
||||||
}
|
|
||||||
|
|
||||||
static validate(doc) {
|
|
||||||
const REQUIRED_PROPS = ['filename', 'size', 'digest'];
|
|
||||||
const missing_props = REQUIRED_PROPS.filter(prop => !doc.hasOwnProperty(prop));
|
|
||||||
if (missing_props) {
|
|
||||||
throw new ValidationError('FileType missing required props: ', missing_props);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FileType = PouchDB.registerType('File', FileSpec);
|
|
||||||
```
|
|
||||||
|
|
||||||
Document properties of a type are not directly defined but the type definition. Specific properties
|
|
||||||
can be enforced by the `validate` method. The following properties are illegal:
|
|
||||||
|
|
||||||
* starting with an underscore (\_) - these are used by PouchDB internally
|
|
||||||
* starting with a double dollar-sign ($$) - these are reserved for future use by PouchORM. (Single
|
|
||||||
dollar-sign properties may be used for your implementation’s meta-data.)
|
|
||||||
|
|
||||||
### Type Functions
|
|
||||||
|
|
||||||
* `FileType.getOrCreate(obj)` - Try to save the passed object as a document. If it fails with a /409
|
|
||||||
Conflict/, look up the existing one and return it (ignoring differences between passed object and
|
|
||||||
the found object)
|
|
||||||
|
|
||||||
* /obj/ - an object containing a partial document including an `id` property
|
|
||||||
* _Returns_ a FileType instance
|
|
||||||
|
|
||||||
* `FileType.find(idOrSelector, options)` - Find one or many records. Optionally return a
|
|
||||||
subscribeable that will fire events when the selection changes.
|
|
||||||
|
|
||||||
* /idOrSelector/
|
|
||||||
* if a string or number is passed, `find` will attempt to lookup the document with this as the
|
|
||||||
id.
|
|
||||||
* if an object is passed, it will be treated as a selector for a find query
|
|
||||||
* /options/ can have the following optional properties
|
|
||||||
* /index/ - specify a specific index to use with the passed selector
|
|
||||||
* /live/ - boolean (default /false/) return a subscribeable that will fire when the results of
|
|
||||||
the selector are updated.
|
|
||||||
* /…/ - other options will be passed to `db.changes()` when `live: true`
|
|
||||||
* _Returns_
|
|
||||||
* if `options.live` is /false/, an array of FileType instances will be returned
|
|
||||||
* if `options.live` is /true/, a subscribeable will be returned. Subscription callbacks will be
|
|
||||||
called when the results of /idOrSelector/ changes
|
|
||||||
|
|
||||||
* `FileType.index(index, fields)` - Create a PouchDB
|
|
||||||
[Index](https://pouchdb.com/api.html#create_index) that can be used by `FileType.find` to sort on
|
|
||||||
non-ID fields
|
|
||||||
|
|
||||||
* /index/ - a string value that can optionally be provided to the options object of
|
|
||||||
`FileType.find`
|
|
||||||
* /fields/ - an array of field names for PouchDB to index on.
|
|
||||||
* _Returns_ void
|
|
||||||
|
|
||||||
* `FileType.delete(id)` - Remove a document from the db
|
|
||||||
|
|
||||||
* /id/ - the id string of the document to be removed
|
|
||||||
* _Returns_ void
|
|
||||||
|
|
||||||
* `FileType.subscribe(callback)` - subscribe to all FileType changes
|
|
||||||
* _Returns_ - a function that unsubscribes the passed callback when called.
|
|
||||||
|
|
||||||
### Type Properties
|
|
||||||
|
|
||||||
* `FileType.db` - the PouchDB database used by `FileType`
|
|
||||||
* `FileType.name` - the type name passed to the `registerType` function
|
|
||||||
* `FileType.prefix` - the prefix used on all document ids stored in PouchDB
|
|
||||||
* `FileType.selector` - the PouchDB selector definition used by ‘subscribe’ and ‘find’
|
|
||||||
|
|
||||||
### Instance Methods
|
|
||||||
|
|
||||||
Creating a new instance is standard Javascript for any new instance:
|
|
||||||
|
|
||||||
```js
|
|
||||||
let note = new FileType({
|
|
||||||
filename: 'note.txt',
|
|
||||||
type: 'text/plain'
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
* _static_ `note.getUniqueID(doc)` - You _must override_ this function in your type definition to
|
|
||||||
provide the id to be used for this document.
|
|
||||||
|
|
||||||
* /doc/ - an object representing the document. This could be an instance of the type or a bare
|
|
||||||
object with properties for this type.
|
|
||||||
* _Returns_ - a unique string value to be used as a document id
|
|
||||||
|
|
||||||
* _static_ `note.validate()` - By default this does nothing. Optionally override it with a method
|
|
||||||
that throws exceptions when a document is invalid to block writing invalid data to the database.
|
|
||||||
|
|
||||||
* `note.save()` - Calls `validate()` and then writes the document to the database.
|
|
||||||
|
|
||||||
* _Returns_ - a self-reference to the instance
|
|
||||||
|
|
||||||
* `note.update(props, save=true)` - Update this instance
|
|
||||||
|
|
||||||
* /props/ - a deeply nested object that will be applied to this instance
|
|
||||||
* /save/ - a boolean to save this document after updating.
|
|
||||||
* _Returns_ - a self-reference to the instance
|
|
||||||
|
|
||||||
* `note.addAttachment(name, blob)` - Add a named attachment to this instance.
|
|
||||||
|
|
||||||
* /name/ - the name used to reference the attachment
|
|
||||||
* /blob/ - a blob instance of the actual data
|
|
||||||
* _Returns_ - a self-reference to the instance
|
|
||||||
|
|
||||||
* `note.getAttachment(name)` - Retrieve a named attachment from this instance.
|
|
||||||
|
|
||||||
* /name/ - the name of the attachment specified in `addAttachment`
|
|
||||||
* _Returns_ - a Blob instance of the attachment data
|
|
||||||
|
|
||||||
* `note.removeAttachment(name)` - Remove a named attachment from this instance.
|
|
||||||
|
|
||||||
* /name/ - the name of the attachment specified in `addAttachment`
|
|
||||||
* _Returns_ -
|
|
||||||
|
|
||||||
* `note.delete()` - Flag this document as `_deleted` == `true`. This will cause it to not show up in
|
|
||||||
`find` results. (_Note_: documents are left in the database in this state to allow for deletion to
|
|
||||||
be synced to other nodes. To truly remove documents,
|
|
||||||
[compact your database](https://pouchdb.com/api.html#compaction).)
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
import json from 'rollup-plugin-json';
|
|
||||||
import babel from 'rollup-plugin-babel';
|
|
||||||
|
|
||||||
const babelConfig = {
|
|
||||||
env: {
|
|
||||||
es6: true,
|
|
||||||
browser: true
|
|
||||||
},
|
|
||||||
plugins: [],
|
|
||||||
presets: ['es2015-rollup']
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
entry: 'src/index.js',
|
|
||||||
moduleName: 'pouchorm',
|
|
||||||
plugins: [json(), babel(babelConfig)],
|
|
||||||
output: {
|
|
||||||
format: 'umd',
|
|
||||||
file: 'dist/pouchorm.js'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { PouchORM } from '../src/index.js';
|
|
||||||
|
|
||||||
// describe('A PouchORM Document', () => {
|
|
||||||
// it('.', () => {
|
|
||||||
// expect().nothing();
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"spec_dir": "spec",
|
|
||||||
"spec_files": ["**/*[sS]pec.js"],
|
|
||||||
"helpers": ["helpers/**/*.js"],
|
|
||||||
"stopSpecOnExpectationFailure": false,
|
|
||||||
"random": false
|
|
||||||
}
|
|
||||||
@ -1,113 +0,0 @@
|
|||||||
import { LiveArray } from './livearray.js';
|
|
||||||
import { Watcher } from './watcher.js';
|
|
||||||
import { isObject } from './utils.js';
|
|
||||||
|
|
||||||
export function PouchORM(PouchDB) {
|
|
||||||
PouchDB.registerType = (name, cls, db) => {
|
|
||||||
const prefix = name.toLowerCase();
|
|
||||||
const _db = db || PouchDB(prefix);
|
|
||||||
_db.setMaxListeners(1000);
|
|
||||||
const _baseSelector = Object.freeze({
|
|
||||||
_id: { $gt: `${prefix}_0`, $lt: `${prefix}_\ufff0` }
|
|
||||||
});
|
|
||||||
const watch = Watcher(_db, _baseSelector, { include_docs: true });
|
|
||||||
|
|
||||||
if (!cls.hasOwnProperty('validate')) {
|
|
||||||
// warn(`${cls.name} has no validation.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const instantiate = doc => new cls(doc);
|
|
||||||
|
|
||||||
async function find(idOrSelector, opts = {}) {
|
|
||||||
if (typeof idOrSelector === 'string') {
|
|
||||||
return instantiate(await _db.get(idOrSelector));
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSelector = isObject(idOrSelector);
|
|
||||||
|
|
||||||
const selector = Object.assign(
|
|
||||||
isSelector && idOrSelector._deleted ? { _deleted: true } : { _deleted: { exists: false } },
|
|
||||||
isSelector ? idOrSelector : _baseSelector
|
|
||||||
);
|
|
||||||
if (opts.index) {
|
|
||||||
opts.use_index = [prefix, opts.index];
|
|
||||||
delete opts.index;
|
|
||||||
}
|
|
||||||
if (opts.live) {
|
|
||||||
opts.mapper = instantiate;
|
|
||||||
return LiveArray(_db, idOrSelector, opts);
|
|
||||||
}
|
|
||||||
return (await _db.find(Object.assign({ selector: idOrSelector }, opts))).docs.map(instantiate);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getOrCreate(props) {
|
|
||||||
let doc = await new cls(props);
|
|
||||||
try {
|
|
||||||
await doc.save();
|
|
||||||
} catch (e) {
|
|
||||||
if (e.status !== 409) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
doc = await find(doc._id);
|
|
||||||
}
|
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function _delete(id) {
|
|
||||||
try {
|
|
||||||
const doc = await find(id);
|
|
||||||
await doc.delete();
|
|
||||||
} catch (e) {
|
|
||||||
if (e.status !== 404) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function _index(name, fields) {
|
|
||||||
return _db.createIndex({
|
|
||||||
index: {
|
|
||||||
ddoc: prefix,
|
|
||||||
fields,
|
|
||||||
name
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.defineProperties(cls.prototype, {
|
|
||||||
_name: { value: name },
|
|
||||||
_prefix: { value: prefix },
|
|
||||||
_db: { value: _db },
|
|
||||||
_cls: { value: cls },
|
|
||||||
_baseSelector: { value: _baseSelector }
|
|
||||||
});
|
|
||||||
|
|
||||||
const methods = {
|
|
||||||
getOrCreate: { value: getOrCreate },
|
|
||||||
find: { value: find },
|
|
||||||
index: { value: _index },
|
|
||||||
delete: { value: _delete },
|
|
||||||
subscribe: { value: watch }
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.defineProperties(
|
|
||||||
cls,
|
|
||||||
Object.assign(
|
|
||||||
{
|
|
||||||
db: { value: _db },
|
|
||||||
name: { value: name },
|
|
||||||
prefix: { value: prefix },
|
|
||||||
selector: { value: _baseSelector }
|
|
||||||
},
|
|
||||||
Object.entries(methods)
|
|
||||||
.filter(([name, obj]) => cls[name] === undefined)
|
|
||||||
.reduce((acc, [name, obj]) => {
|
|
||||||
acc[name] = obj;
|
|
||||||
return acc;
|
|
||||||
}, {})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
return cls;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,73 +0,0 @@
|
|||||||
import { pouchDocHash, deepAssign } from './utils.js';
|
|
||||||
|
|
||||||
export class TypeSpec {
|
|
||||||
constructor(props) {
|
|
||||||
this._populateId(props);
|
|
||||||
Object.assign(this, { $$links: {} }, props, { $$type: this._prefix });
|
|
||||||
}
|
|
||||||
|
|
||||||
static getUniqueID(doc) {
|
|
||||||
throw 'NotImplemented';
|
|
||||||
}
|
|
||||||
|
|
||||||
static isType(doc) {
|
|
||||||
return doc && doc.$$type === TypeSpec.prefix;
|
|
||||||
}
|
|
||||||
|
|
||||||
static validate(doc) {}
|
|
||||||
|
|
||||||
instantiate(doc) {
|
|
||||||
return new this._cls(docs);
|
|
||||||
}
|
|
||||||
|
|
||||||
_populateId(doc) {
|
|
||||||
if (!doc._id) {
|
|
||||||
doc._id = `${this._prefix}_${this._cls.getUniqueID(doc)}`;
|
|
||||||
}
|
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
|
|
||||||
_hash() {
|
|
||||||
return pouchDocHash(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
137
packages/pouchtype/README.md
Normal file
137
packages/pouchtype/README.md
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
# PouchType
|
||||||
|
|
||||||
|
An type-based abstraction layer over PouchDB inspired by [Hood.ie](https://hood.ie/) and
|
||||||
|
[Django](https://djangoproject.com)
|
||||||
|
|
||||||
|
## Extending the TypeHandler class
|
||||||
|
|
||||||
|
PouchType works by extending the _TypeHandler_ class with methods to define how a document type
|
||||||
|
should be handled. The resulting class is instantiated with a PouchDB instance and used to interact
|
||||||
|
with documents of that type.
|
||||||
|
|
||||||
|
All subclasses of _TypeHandler_ must override the `getUniqueID` method. This method should return a
|
||||||
|
unique string for documents of this type. (NOTE: The document id will be prefixed with the
|
||||||
|
type_string as well to avoid collisions across types.)
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { PouchDB } from 'pouchdb';
|
||||||
|
import { TypeHandler } from 'PouchType';
|
||||||
|
|
||||||
|
const PDB = PouchDB.plugin(find); // PouchType requires the find plugin.
|
||||||
|
|
||||||
|
class ContactHandler extends TypeHandler {
|
||||||
|
getUniqueID(doc) {
|
||||||
|
return doc.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
validate(doc) {
|
||||||
|
super.validate(doc);
|
||||||
|
|
||||||
|
if (typeof doc.email != 'string') {
|
||||||
|
throw new Error('email property is required');
|
||||||
|
} else if (doc.email.length <= 2) {
|
||||||
|
throw new Error('email must be longer than 2 characters');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## TypeHandler management methods
|
||||||
|
|
||||||
|
### getUniqueID(doc)
|
||||||
|
|
||||||
|
_This method must be overridden in subclasses._
|
||||||
|
|
||||||
|
Return a unique string that will be used to populate `doc._id` if it isn't already populated.
|
||||||
|
|
||||||
|
### hash(doc)
|
||||||
|
|
||||||
|
Returns a hash string of the current document for comparison. By default this is
|
||||||
|
"`doc._id`:`doc._rev`" and will work as long as the hash is only taken after any changes are saved.
|
||||||
|
You may wish to override this to provide content-specific hashing.
|
||||||
|
|
||||||
|
### index(name, fields)
|
||||||
|
|
||||||
|
Create a index to be used in a `filter` selector and sort options. Specify the `name` of the index
|
||||||
|
and the `fields` as an array of strings of the document properties to include in the index.
|
||||||
|
|
||||||
|
### isType(doc)
|
||||||
|
|
||||||
|
Check if the passed `doc` belongs to this handler.
|
||||||
|
|
||||||
|
### validate(doc)
|
||||||
|
|
||||||
|
Check if the passed `doc` has valid data before it is written. Invalidation happens by raising an
|
||||||
|
exception. For more fine-grained validation, refer to the
|
||||||
|
[pouchdb-validation](https://github.com/pouchdb/pouchdb-validation) plugin or the
|
||||||
|
[validate_doc_update](http://guide.couchdb.org/draft/validation.html) function.
|
||||||
|
|
||||||
|
## TypeHandler query methods
|
||||||
|
|
||||||
|
### get(id)
|
||||||
|
|
||||||
|
Return the document referenced by `id` or `null` if the document does not exist.
|
||||||
|
|
||||||
|
### getOrCreate(doc, defaults={})
|
||||||
|
|
||||||
|
If the passed document doesn't have a `_id` property, populate it. Try to lookup a document with the
|
||||||
|
`id`. If it exists, update it with the properties in `doc`. If it does not exist, add the properties
|
||||||
|
in `defaults` to `props` and save the new document.
|
||||||
|
|
||||||
|
### filter(selector, options={})
|
||||||
|
|
||||||
|
Return an array of documents that match the criteria in `selector`. `options.index` can contain the
|
||||||
|
`name` passed to `index()` if needed. All other option properties are passed through to the
|
||||||
|
[PouchDB.find()](https://pouchdb.com/api.html#query_index)
|
||||||
|
|
||||||
|
### watch(selector, options={})
|
||||||
|
|
||||||
|
The parameters for `watch()` are identical to `filter()` but `watch()` returns a
|
||||||
|
[computed](../frptools/README.md#computed) instance that will call subscribers whenever any data
|
||||||
|
matching the selector changes.
|
||||||
|
|
||||||
|
## TypeHandler change methods
|
||||||
|
|
||||||
|
### remove(docOrId)
|
||||||
|
|
||||||
|
`remove` accepts a document or an id string.
|
||||||
|
|
||||||
|
Flag this document as `doc._deleted == true`. This will cause it to not show up in `get`, `filter`
|
||||||
|
or `watch` results. (_Note_: documents are left in the database in this state to allow for deletion
|
||||||
|
to be synced to other nodes. To truly remove documents,
|
||||||
|
[compact your database](https://pouchdb.com/api.html#compaction).)
|
||||||
|
|
||||||
|
### save(doc)
|
||||||
|
|
||||||
|
If the document does not have properties `_id` or `type`, populate them appropriately. Run
|
||||||
|
`TypeHandler.validate` on the document. If validate doesn't throw any errors, then save the document
|
||||||
|
to the database. It's `_rev` property will be updated to reflect the revision in the database.
|
||||||
|
|
||||||
|
### update(doc, props)
|
||||||
|
|
||||||
|
Deeply assign `props` to the passed `doc` object and `save()` it.
|
||||||
|
|
||||||
|
### addAttachment(doc, key, blob)
|
||||||
|
|
||||||
|
Attach the passed `blob` to the document referenced with the `key` string.
|
||||||
|
|
||||||
|
### removeAttachment(doc, key)
|
||||||
|
|
||||||
|
Remove a previously attached blob at `key`.
|
||||||
|
|
||||||
|
### getAttachment(doc, key)
|
||||||
|
|
||||||
|
Return a previously attached blob at `key` or `null` if none exists.
|
||||||
|
|
||||||
|
## Using Types
|
||||||
|
|
||||||
|
```js
|
||||||
|
const db = PDB('type_example');
|
||||||
|
|
||||||
|
export const Contact = new ContactHandler(db, 'contact');
|
||||||
|
|
||||||
|
const doc = await Contact.getOrCreate({
|
||||||
|
name: "John Doe",
|
||||||
|
email: "jd@example.com"
|
||||||
|
});
|
||||||
|
```
|
||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "pouchorm",
|
"name": "pouchtype",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Document Abstraction Layer for PouchDB",
|
"description": "Document Management Layer for PouchDB",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"files": ["dist", "lib", "src"],
|
"files": ["dist", "lib", "src"],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -10,7 +10,7 @@
|
|||||||
"author": "Timothy Farrell <tim@thecookiejar.me> (https://github.com/explorigin)",
|
"author": "Timothy Farrell <tim@thecookiejar.me> (https://github.com/explorigin)",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"frptools": "^3.1.0"
|
"frptools": "~3.2.1",
|
||||||
},
|
"pouchdb": "~7.0.0"
|
||||||
"devDependencies": {}
|
}
|
||||||
}
|
}
|
||||||
@ -14,8 +14,8 @@ describe('A LiveArray', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns a computed (subscribable).', () => {
|
it('returns a computed (subscribable).', async () => {
|
||||||
const la = LiveArray(fakePouch, selector, opts);
|
const la = await LiveArray(fakePouch, selector, opts);
|
||||||
|
|
||||||
expect(typeof la).toEqual('function');
|
expect(typeof la).toEqual('function');
|
||||||
expect(typeof la.subscribe).toEqual('function');
|
expect(typeof la.subscribe).toEqual('function');
|
||||||
@ -23,37 +23,7 @@ describe('A LiveArray', () => {
|
|||||||
expect(JSON.stringify(la())).toEqual('[]');
|
expect(JSON.stringify(la())).toEqual('[]');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has a "ready" subscribable that is fired once and subscribers cleared when the main subscribable has data for the first time.', () => {
|
it('fires when data changes.', async () => {
|
||||||
let sub = null;
|
|
||||||
|
|
||||||
const db = {
|
|
||||||
changes: options => {
|
|
||||||
return db;
|
|
||||||
},
|
|
||||||
on: (eventName, callback) => {
|
|
||||||
if (eventName == 'change') {
|
|
||||||
sub = callback;
|
|
||||||
}
|
|
||||||
expect(['change', 'error'].indexOf(eventName) !== -1).toBeTruthy();
|
|
||||||
return db;
|
|
||||||
},
|
|
||||||
cancel: () => (sub = null),
|
|
||||||
find: async selector => {
|
|
||||||
return Promise.resolve({ docs: [{ _id: 234 }] });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const la = LiveArray(db, selector, opts);
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const unsub = la.ready.subscribe(() => {
|
|
||||||
unsub();
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('fires when data changes.', () => {
|
|
||||||
let state = 0;
|
let state = 0;
|
||||||
const changes = {
|
const changes = {
|
||||||
234: { id: 234, deleted: false, doc: { _id: 234 } },
|
234: { id: 234, deleted: false, doc: { _id: 234 } },
|
||||||
@ -89,29 +59,21 @@ describe('A LiveArray', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const la = LiveArray(db, selector, opts);
|
const la = await LiveArray(db, selector, opts);
|
||||||
state = 1;
|
state = 1;
|
||||||
|
|
||||||
let innerState = 0;
|
let innerState = 0;
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const unsub = la.ready.subscribe(() => {
|
|
||||||
la.subscribe(data => {
|
la.subscribe(data => {
|
||||||
if (data.length) {
|
if (data.length) {
|
||||||
expect(data[0]._id).toEqual(parseInt(changeKeys[innerState]));
|
expect(data[0]._id).toEqual(parseInt(changeKeys[innerState]));
|
||||||
}
|
}
|
||||||
innerState += 1;
|
innerState += 1;
|
||||||
|
|
||||||
resolve();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
changeKeys.forEach(id => {
|
changeKeys.forEach(id => {
|
||||||
const doc = changes[parseInt(id)];
|
const doc = changes[parseInt(id)];
|
||||||
sub(doc);
|
sub(doc);
|
||||||
});
|
});
|
||||||
|
|
||||||
unsub();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
360
packages/pouchtype/spec/typehandler.spec.js
Normal file
360
packages/pouchtype/spec/typehandler.spec.js
Normal file
@ -0,0 +1,360 @@
|
|||||||
|
import { TypeHandler } from '../src/index.js';
|
||||||
|
|
||||||
|
const PDB = PouchDB.plugin(find);
|
||||||
|
|
||||||
|
class ContactHandler extends TypeHandler {
|
||||||
|
getUniqueID(doc) {
|
||||||
|
return doc.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
validate(doc) {
|
||||||
|
if (typeof doc.email != 'string') {
|
||||||
|
throw new Error('email property is required');
|
||||||
|
} else if (doc.email.length <= 2) {
|
||||||
|
throw new Error('email must be more than 2 characters');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LocationHandler extends TypeHandler {
|
||||||
|
getUniqueID(doc) {
|
||||||
|
return doc.latlon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const notDesignDocs = d => !d.id.startsWith('_design');
|
||||||
|
|
||||||
|
function compareDataToDoc(dataObj, doc) {
|
||||||
|
Object.keys(dataObj).forEach(k => {
|
||||||
|
expect(doc[k]).toEqual(dataObj[k]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('PouchType Handler', () => {
|
||||||
|
const db = new PDB('pouchtype-test', { adapter: 'memory' });
|
||||||
|
const Contact = new ContactHandler(db, 'contact');
|
||||||
|
const Location = new LocationHandler(db, 'location');
|
||||||
|
|
||||||
|
Contact.index('lastIndex', ['last']);
|
||||||
|
|
||||||
|
const dataset = [
|
||||||
|
{
|
||||||
|
first: 'Jane',
|
||||||
|
last: 'Doe',
|
||||||
|
email: 'jd@example.com',
|
||||||
|
type: 'contact',
|
||||||
|
_id: 'contact_jd@example.com'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
first: 'Bob',
|
||||||
|
last: 'Smith',
|
||||||
|
email: 'bs@example.com',
|
||||||
|
type: 'contact',
|
||||||
|
_id: 'contact_bs@example.com'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'Home',
|
||||||
|
latlon: '12345',
|
||||||
|
type: 'location',
|
||||||
|
_id: 'location_12345'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
first: 'Joe',
|
||||||
|
last: 'Smithers',
|
||||||
|
email: 'js@example.com',
|
||||||
|
type: 'contact',
|
||||||
|
_id: 'contact_js@example.com'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
async function flushDb() {
|
||||||
|
const res = await db.allDocs();
|
||||||
|
await Promise.all(res.rows.filter(notDesignDocs).map(d => db.remove(d.id, d.value.rev))).catch(
|
||||||
|
() => {}
|
||||||
|
);
|
||||||
|
await db.compact();
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(flushDb);
|
||||||
|
|
||||||
|
it('.getOrCreate() gets existing records.', async () => {
|
||||||
|
const doc = {
|
||||||
|
first: 'Jane',
|
||||||
|
last: 'Doe',
|
||||||
|
email: 'jd@example.com'
|
||||||
|
};
|
||||||
|
await db.bulkDocs(dataset);
|
||||||
|
const existing = await db.get(dataset[0]._id);
|
||||||
|
const instance = await Contact.getOrCreate(doc);
|
||||||
|
expect(instance._id).toEqual(existing._id);
|
||||||
|
expect(instance._rev).toEqual(existing._rev);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('.getOrCreate() saves non-existing records and populates _id and _rev.', async () => {
|
||||||
|
const doc = {
|
||||||
|
first: 'Jill',
|
||||||
|
last: 'Doener',
|
||||||
|
email: 'jd2@example.com'
|
||||||
|
};
|
||||||
|
const expectedId = `${Contact.type}_${doc.email}`;
|
||||||
|
try {
|
||||||
|
await db.get(expectedId);
|
||||||
|
fail('db.get() should throw when passed an id of a removed document.');
|
||||||
|
return;
|
||||||
|
} catch (e) {
|
||||||
|
expect(e.status).toBe(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = await Contact.getOrCreate(doc);
|
||||||
|
expect(instance._id).toEqual(expectedId);
|
||||||
|
expect(instance._rev).toBeTruthy();
|
||||||
|
compareDataToDoc(doc, instance);
|
||||||
|
const savedDoc = await db.get(expectedId);
|
||||||
|
expect(savedDoc._id).toEqual(expectedId);
|
||||||
|
expect(savedDoc._rev).toBeTruthy();
|
||||||
|
compareDataToDoc(doc, savedDoc);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('.get() returns a document when it exists.', async () => {
|
||||||
|
await db.bulkDocs(dataset);
|
||||||
|
const instance = await Contact.get(dataset[1]._id);
|
||||||
|
compareDataToDoc(dataset[1], instance);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(".get() returns null when it doesn't exist or has been removed.", async () => {
|
||||||
|
await db.bulkDocs(dataset);
|
||||||
|
const empty = await Contact.get('does_not_exist');
|
||||||
|
expect(empty).toBeNull();
|
||||||
|
const doc = await db.get(dataset[1]._id);
|
||||||
|
doc._deleted = true;
|
||||||
|
await db.put(doc);
|
||||||
|
const empty2 = await Contact.get('does_not_exist');
|
||||||
|
expect(empty).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('.get() only returns records for its type.', async () => {
|
||||||
|
await db.bulkDocs(dataset);
|
||||||
|
const instance = await Contact.get(dataset.filter(d => d.type !== Contact.type)[0]._id);
|
||||||
|
expect(instance).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('.filter() returns an array of matching instances.', async () => {
|
||||||
|
await db.bulkDocs(dataset);
|
||||||
|
const res = await Contact.filter({ last: { $regex: /^Smith.*/ } });
|
||||||
|
expect(res.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('.filter() only returns records for its type.', async () => {
|
||||||
|
await db.bulkDocs(dataset);
|
||||||
|
const res = await Contact.filter({});
|
||||||
|
expect(res.length).toBe(3);
|
||||||
|
res.forEach(c => expect(c.type).toBe(Contact.type));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('.isType(instance) identifies instances of the type.', async () => {
|
||||||
|
const instance = await Contact.getOrCreate({
|
||||||
|
first: 'Bob',
|
||||||
|
last: 'Smith',
|
||||||
|
email: 'bs@example.com'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(Contact.isType(instance)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('.isType(doc) detects documents of the type.', async () => {
|
||||||
|
const data = await Contact.getOrCreate({
|
||||||
|
first: 'Bob',
|
||||||
|
last: 'Smith',
|
||||||
|
email: 'bs@example.com'
|
||||||
|
});
|
||||||
|
const doc = await db.get(data._id);
|
||||||
|
|
||||||
|
expect(Contact.isType(doc)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('.remove() sets the ._deleted property and documents are no longer gettable.', async () => {
|
||||||
|
const id = dataset[1]._id;
|
||||||
|
await db.bulkDocs(dataset);
|
||||||
|
const doc = await Contact.get(id);
|
||||||
|
await Contact.remove(doc);
|
||||||
|
expect(doc._deleted).toBe(true);
|
||||||
|
try {
|
||||||
|
await db.get(id);
|
||||||
|
fail('db.get() should throw when passed an id of a removed document.');
|
||||||
|
return;
|
||||||
|
} catch (e) {
|
||||||
|
expect(e.status).toBe(404);
|
||||||
|
}
|
||||||
|
const doc2 = await Contact.get(id);
|
||||||
|
expect(doc2).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('.save() populates ._id and .type properties and writes to the db.', async () => {
|
||||||
|
const doc = {
|
||||||
|
first: 'Bob',
|
||||||
|
last: 'Smith',
|
||||||
|
email: 'bs@example.com'
|
||||||
|
};
|
||||||
|
await Contact.save(doc);
|
||||||
|
const d = await db.get(doc._id);
|
||||||
|
expect(Contact.isType(d)).toBe(true);
|
||||||
|
expect(d._id).toBeTruthy();
|
||||||
|
expect(d.type).toEqual(Contact.type);
|
||||||
|
compareDataToDoc(doc, d);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('.validate() can interrupt save() by throwing an exception.', async () => {
|
||||||
|
const doc = {
|
||||||
|
first: 'Bob',
|
||||||
|
last: 'Smith',
|
||||||
|
email: 'bs'
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await Contact.save(doc);
|
||||||
|
} catch (e) {
|
||||||
|
expect(e.message).toBe('email must be more than 2 characters');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fail('TypeHandler.save() should call validate on save()');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('.update() will deeply apply object properties to a document.', async () => {
|
||||||
|
const data = {
|
||||||
|
first: 'Bob',
|
||||||
|
last: 'Smitherines',
|
||||||
|
email: 'bsmitherines@example.com',
|
||||||
|
addresses: {
|
||||||
|
home: '123 Privet Drive'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const doc = await Contact.save(data);
|
||||||
|
await Contact.update(doc, { addresses: { home: '221B Baker Street' } });
|
||||||
|
const doc2 = await Contact.get(doc._id);
|
||||||
|
expect(doc2.addresses.home).toEqual(doc.addresses.home);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('.update() only updates the database when the underlying document changes and save is not precluded.', async () => {
|
||||||
|
const data = {
|
||||||
|
first: 'Bob',
|
||||||
|
last: 'Smitherines',
|
||||||
|
email: 'bsmitherines@example.com'
|
||||||
|
};
|
||||||
|
const doc = await Contact.save(data);
|
||||||
|
spyOn(Contact, 'save');
|
||||||
|
await Contact.update(doc, { last: 'Shell', email: 'bshell@example.com' });
|
||||||
|
expect(Contact.save).toHaveBeenCalledTimes(1);
|
||||||
|
await Contact.update(doc, { last: 'Shell' });
|
||||||
|
expect(Contact.save).toHaveBeenCalledTimes(1);
|
||||||
|
await Contact.update(doc, { last: 'Tootsie' }, false);
|
||||||
|
expect(Contact.save).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// TODO: Potentially undesirable/non-intuitive behavior here. Update calls save even though the data hasn't truly changed from the last saved state.
|
||||||
|
await Contact.update(doc, { last: 'Shell' });
|
||||||
|
expect(Contact.save).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('.watch() returns a subscribable LiveArray of matching instances.', done => {
|
||||||
|
let expectedLength = 0;
|
||||||
|
let checkCount = 0;
|
||||||
|
let sub;
|
||||||
|
let livedata;
|
||||||
|
|
||||||
|
function poll(fn, val) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const wrap = () => {
|
||||||
|
if (fn()) {
|
||||||
|
resolve(val);
|
||||||
|
} else {
|
||||||
|
setTimeout(wrap, 5);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
wrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Contact.watch({ watchTest: { $eq: true } })
|
||||||
|
.then(_ld => {
|
||||||
|
livedata = _ld;
|
||||||
|
sub = livedata.subscribe(data => {
|
||||||
|
checkCount += 1;
|
||||||
|
expect(data.length).toBe(expectedLength);
|
||||||
|
});
|
||||||
|
return db.bulkDocs(dataset);
|
||||||
|
})
|
||||||
|
.then(_ =>
|
||||||
|
poll(() => {
|
||||||
|
return livedata().length === 0;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.then(_ => {
|
||||||
|
expectedLength = 1;
|
||||||
|
return db.put({
|
||||||
|
first: 'Bill',
|
||||||
|
last: 'Smitherts',
|
||||||
|
email: 'bsmithers@example.com',
|
||||||
|
_id: 'contact_bsmithers@example.com',
|
||||||
|
type: 'contact',
|
||||||
|
watchTest: true
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(_ =>
|
||||||
|
poll(() => {
|
||||||
|
return livedata().length === 1;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.then(_ => {
|
||||||
|
return db.put({
|
||||||
|
first: 'Bart',
|
||||||
|
last: 'Smitty',
|
||||||
|
email: 'bsmitty@example.com',
|
||||||
|
type: 'contact',
|
||||||
|
_id: 'contact_bsmitty@example.com'
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
return poll(() => {
|
||||||
|
return livedata().length === 1;
|
||||||
|
}, res);
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
expectedLength = 2;
|
||||||
|
db.put({
|
||||||
|
first: 'Bart',
|
||||||
|
last: 'Smitty',
|
||||||
|
email: 'bsmitty@example.com',
|
||||||
|
type: 'contact',
|
||||||
|
_id: 'contact_bsmitty@example.com',
|
||||||
|
_rev: res.rev,
|
||||||
|
watchTest: true
|
||||||
|
});
|
||||||
|
return poll(() => {
|
||||||
|
return livedata().length === 2;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(_ => {
|
||||||
|
expect(livedata().length).toBe(expectedLength);
|
||||||
|
})
|
||||||
|
.then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('.index() creates an index for non-id sorting.', async () => {
|
||||||
|
await db.bulkDocs(dataset);
|
||||||
|
try {
|
||||||
|
const res = await Contact.filter(
|
||||||
|
{
|
||||||
|
last: { $gte: '' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sort: ['last'],
|
||||||
|
index: 'lastIndex'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
expect(res.length).toEqual(3);
|
||||||
|
expect(res[0].last).toEqual('Doe');
|
||||||
|
expect(res[1].last).toEqual('Smith');
|
||||||
|
expect(res[2].last).toEqual('Smithers');
|
||||||
|
} catch (e) {
|
||||||
|
fail(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,3 +1,2 @@
|
|||||||
import { isObject, deepAssign, pouchDocHash, pouchDocArrayHash } from './utils.js';
|
import { isObject, deepAssign, pouchDocHash, pouchDocArrayHash } from './utils.js';
|
||||||
export { TypeSpec } from './type.js';
|
export { TypeHandler } from './type.js';
|
||||||
export { PouchORM } from './plugin.js';
|
|
||||||
@ -5,20 +5,17 @@ import { pouchDocArrayHash } from './utils.js';
|
|||||||
|
|
||||||
// LiveArray is a subscribable property function that always returns the db results that match
|
// LiveArray is a subscribable property function that always returns the db results that match
|
||||||
// the provided selector and calls subscribers when the results change.
|
// the provided selector and calls subscribers when the results change.
|
||||||
export function LiveArray(db, selector, opts = {}) {
|
export async function LiveArray(db, selector, opts = {}) {
|
||||||
const mapper = opts.mapper || id;
|
|
||||||
opts.mapper && delete opts.mapper;
|
|
||||||
opts.include_docs = true;
|
opts.include_docs = true;
|
||||||
const _watcher = Watcher(db, selector, opts);
|
const _watcher = Watcher(db, selector, opts);
|
||||||
let changeSub = null;
|
let changeSub = null;
|
||||||
|
|
||||||
const ready = prop(false);
|
const paginator = opts.paginator;
|
||||||
const data = prop({ docs: [] });
|
const data = prop({ docs: [] });
|
||||||
const docs = computed(r => r.docs.map(mapper), [data], pouchDocArrayHash);
|
const docs = computed(r => r.docs, [data], pouchDocArrayHash);
|
||||||
|
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
docs.unsubscribeAll();
|
docs.unsubscribeAll();
|
||||||
ready.unsubscribeAll();
|
|
||||||
if (changeSub) {
|
if (changeSub) {
|
||||||
changeSub();
|
changeSub();
|
||||||
changeSub = null;
|
changeSub = null;
|
||||||
@ -27,21 +24,22 @@ export function LiveArray(db, selector, opts = {}) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const refresh = async function refresh(...args) {
|
const refresh = async function refresh(...args) {
|
||||||
data(await db.find({ selector }));
|
const queryOpts = { selector };
|
||||||
|
if (paginator) {
|
||||||
|
Object.assign(queryOpts, paginator.queryOptions());
|
||||||
|
}
|
||||||
|
data(await db.find(queryOpts));
|
||||||
};
|
};
|
||||||
|
|
||||||
docs.ready = ready;
|
|
||||||
docs.cleanup = cleanup;
|
docs.cleanup = cleanup;
|
||||||
docs.selector = selector;
|
docs.selector = selector;
|
||||||
docs.db = db;
|
docs.db = db;
|
||||||
|
|
||||||
refresh()
|
await refresh();
|
||||||
.then(() => {
|
|
||||||
changeSub = _watcher(refresh);
|
changeSub = _watcher(refresh);
|
||||||
ready(true);
|
if (paginator) {
|
||||||
})
|
paginator.queryOptions.subscribe(refresh);
|
||||||
.then(() => {
|
}
|
||||||
ready.unsubscribeAll();
|
|
||||||
});
|
|
||||||
return docs;
|
return docs;
|
||||||
}
|
}
|
||||||
132
packages/pouchtype/src/type.js
Normal file
132
packages/pouchtype/src/type.js
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import { pouchDocHash, deepAssign } from './utils.js';
|
||||||
|
import { LiveArray } from './livearray.js';
|
||||||
|
|
||||||
|
export class TypeHandler {
|
||||||
|
constructor(db, typeKey) {
|
||||||
|
this.db = db;
|
||||||
|
this.type = typeKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
getUniqueID(doc) {
|
||||||
|
return undefined; // To be overridden if you want deterministic IDs.
|
||||||
|
}
|
||||||
|
|
||||||
|
_makeID(doc) {
|
||||||
|
return `${this.type}_${this.getUniqueID(doc)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
isType(doc) {
|
||||||
|
return doc && doc.type === this.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
hash(doc) {
|
||||||
|
return pouchDocHash(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(doc) {
|
||||||
|
if (!this.isType(doc)) {
|
||||||
|
throw TypeError(`Document "${doc.type}:${doc._id}" does not match type "${this.type}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(docOrId) {
|
||||||
|
const doc = typeof docOrId === 'string' ? await this.get(docOrId) : docOrId;
|
||||||
|
return await this.update(doc, { _deleted: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async save(doc) {
|
||||||
|
if (!doc.type) {
|
||||||
|
doc.type = this.type;
|
||||||
|
}
|
||||||
|
this.validate(doc);
|
||||||
|
if (!doc._id) {
|
||||||
|
doc._id = this._makeID(doc);
|
||||||
|
}
|
||||||
|
if (!doc.$$links) {
|
||||||
|
doc.$$links = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { rev } = await this.db.put(doc);
|
||||||
|
doc._rev = rev;
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
async addAttachment(doc, attName, dataBlob) {
|
||||||
|
const { rev } = await this.db.putAttachment(doc._id, attName, doc._rev, dataBlob, dataBlob.type);
|
||||||
|
|
||||||
|
doc._rev = rev;
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAttachment(doc, attName) {
|
||||||
|
return await this.db.getAttachment(doc._id, attName);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeAttachment(doc, attName) {
|
||||||
|
return await this.db.removeAttachment(doc._id, attName, doc._rev);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(doc, props, save = true) {
|
||||||
|
if (deepAssign(doc, props) && save) {
|
||||||
|
await this.save(doc);
|
||||||
|
}
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
_filterOpts(selector, opts) {
|
||||||
|
selector.type = { $eq: this.type };
|
||||||
|
opts.selector = selector;
|
||||||
|
if (opts.index) {
|
||||||
|
opts.use_index = `${this.type}_${opts.index}`;
|
||||||
|
delete opts.index;
|
||||||
|
}
|
||||||
|
return opts;
|
||||||
|
}
|
||||||
|
|
||||||
|
async filter(selector, opts = {}) {
|
||||||
|
const res = await this.db.find(this._filterOpts(selector, opts));
|
||||||
|
return res.docs;
|
||||||
|
}
|
||||||
|
|
||||||
|
async watch(selector, opts = {}) {
|
||||||
|
opts.live = true;
|
||||||
|
this._filterOpts(selector, opts);
|
||||||
|
return await LiveArray(this.db, opts.selector, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(id) {
|
||||||
|
try {
|
||||||
|
const doc = await this.db.get(id);
|
||||||
|
return this.isType(doc) ? doc : null;
|
||||||
|
} catch (e) {
|
||||||
|
if (e.status === 404) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrCreate(props, defaults = {}) {
|
||||||
|
const doc = Object.assign({}, defaults, props);
|
||||||
|
if (!doc._id) {
|
||||||
|
doc._id = this._makeID(doc);
|
||||||
|
}
|
||||||
|
const existing_doc = doc._id && (await this.get(doc._id));
|
||||||
|
|
||||||
|
if (existing_doc) {
|
||||||
|
return this.update(existing_doc, props);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.save(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
async index(name, fields) {
|
||||||
|
return this.db.createIndex({
|
||||||
|
index: {
|
||||||
|
ddoc: `${this.type}_${name}`,
|
||||||
|
fields: fields,
|
||||||
|
name
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,17 +1,24 @@
|
|||||||
export function deepAssign(to, ...rest) {
|
export function deepAssign(to, ...rest) {
|
||||||
|
let updated = false;
|
||||||
for (let src of rest) {
|
for (let src of rest) {
|
||||||
for (let prop in src) {
|
for (let prop in src) {
|
||||||
const value = src[prop];
|
const value = src[prop];
|
||||||
|
const oldValue = to[prop];
|
||||||
if (typeof value === 'object' && !Array.isArray(value)) {
|
if (typeof value === 'object' && !Array.isArray(value)) {
|
||||||
to[prop] = deepAssign(to[prop] || {}, value);
|
if (typeof oldValue !== 'object') {
|
||||||
|
to[prop] = {};
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
updated = deepAssign(to[prop], value) || updated;
|
||||||
} else if (value === undefined && to[prop] !== undefined) {
|
} else if (value === undefined && to[prop] !== undefined) {
|
||||||
delete to[prop];
|
delete to[prop];
|
||||||
} else {
|
} else if (value !== oldValue) {
|
||||||
|
updated = true;
|
||||||
to[prop] = value;
|
to[prop] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return to;
|
return updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const pouchDocHash = d => (isObject(d) ? `${d._id}:${d._rev}` : d);
|
export const pouchDocHash = d => (isObject(d) ? `${d._id}:${d._rev}` : d);
|
||||||
9
packages/pouchtype/test.json
Normal file
9
packages/pouchtype/test.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"spec_dir": "spec",
|
||||||
|
"spec_files": ["**/*[sS]pec.js"],
|
||||||
|
"lib_files": [
|
||||||
|
"node_modules/pouchdb/dist/pouchdb.js",
|
||||||
|
"node_modules/pouchdb/dist/pouchdb.memory.js",
|
||||||
|
"node_modules/pouchdb/dist/pouchdb.find.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user