Initial PouchORM commit.
This commit is contained in:
parent
e8764c3072
commit
4923844527
140
packages/pouchorm/README.md
Normal file
140
packages/pouchorm/README.md
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
# 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).)
|
||||||
17
packages/pouchorm/package.json
Normal file
17
packages/pouchorm/package.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "pouchorm",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Document Abstraction Layer for PouchDB",
|
||||||
|
"main": "lib/index.js",
|
||||||
|
"jsnext:main": "src/index.js",
|
||||||
|
"files": ["dist", "lib", "src"],
|
||||||
|
"scripts": {
|
||||||
|
"test": "node ../../bin/runTests.js ./"
|
||||||
|
},
|
||||||
|
"author": "Timothy Farrell <tim@thecookiejar.me> (https://github.com/explorigin)",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"frptools": "^3.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {}
|
||||||
|
}
|
||||||
21
packages/pouchorm/rollup.config.js
Normal file
21
packages/pouchorm/rollup.config.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
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'
|
||||||
|
}
|
||||||
|
};
|
||||||
117
packages/pouchorm/spec/livearray.spec.js
Normal file
117
packages/pouchorm/spec/livearray.spec.js
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import { LiveArray } from '../src/livearray.js';
|
||||||
|
import { pouchDocArrayHash } from '../src/utils.js';
|
||||||
|
|
||||||
|
describe('A LiveArray', () => {
|
||||||
|
let fakePouch;
|
||||||
|
const selector = {};
|
||||||
|
const opts = {};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fakePouch = {
|
||||||
|
find: () => {},
|
||||||
|
changes: () => fakePouch,
|
||||||
|
on: () => fakePouch
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a computed (subscribable).', () => {
|
||||||
|
const la = LiveArray(fakePouch, selector, opts);
|
||||||
|
|
||||||
|
expect(typeof la).toEqual('function');
|
||||||
|
expect(typeof la.subscribe).toEqual('function');
|
||||||
|
|
||||||
|
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.', () => {
|
||||||
|
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;
|
||||||
|
const changes = {
|
||||||
|
234: { id: 234, deleted: false, doc: { _id: 234 } },
|
||||||
|
34: { id: 34, deleted: true, doc: { _id: 34 } },
|
||||||
|
4564565: { id: 4564565, deleted: false, doc: { _id: 4564565 } }
|
||||||
|
};
|
||||||
|
const changeKeys = Object.keys(changes);
|
||||||
|
let sub = null;
|
||||||
|
|
||||||
|
const db = {
|
||||||
|
changes: options => {
|
||||||
|
expect(state).toEqual(1);
|
||||||
|
expect(options.live).toEqual(true);
|
||||||
|
expect(options.since).toEqual('now');
|
||||||
|
expect(options.selector).toBe(selector);
|
||||||
|
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 => {
|
||||||
|
const doc = changes[parseInt(changeKeys[state - 1])];
|
||||||
|
state += 1;
|
||||||
|
if (doc === undefined || doc.deleted) {
|
||||||
|
return Promise.resolve({ docs: [] });
|
||||||
|
}
|
||||||
|
return Promise.resolve({ docs: [doc.doc] });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const la = LiveArray(db, selector, opts);
|
||||||
|
state = 1;
|
||||||
|
|
||||||
|
let innerState = 0;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const unsub = la.ready.subscribe(() => {
|
||||||
|
la.subscribe(data => {
|
||||||
|
if (data.length) {
|
||||||
|
expect(data[0]._id).toEqual(parseInt(changeKeys[innerState]));
|
||||||
|
}
|
||||||
|
innerState += 1;
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
changeKeys.forEach(id => {
|
||||||
|
const doc = changes[parseInt(id)];
|
||||||
|
sub(doc);
|
||||||
|
});
|
||||||
|
|
||||||
|
unsub();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
7
packages/pouchorm/spec/pouchorm.spec.js
Normal file
7
packages/pouchorm/spec/pouchorm.spec.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { PouchORM } from '../src/index.js';
|
||||||
|
|
||||||
|
// describe('A PouchORM Document', () => {
|
||||||
|
// it('.', () => {
|
||||||
|
// expect().nothing();
|
||||||
|
// });
|
||||||
|
// });
|
||||||
7
packages/pouchorm/spec/support/jasmine.json
Normal file
7
packages/pouchorm/spec/support/jasmine.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"spec_dir": "spec",
|
||||||
|
"spec_files": ["**/*[sS]pec.js"],
|
||||||
|
"helpers": ["helpers/**/*.js"],
|
||||||
|
"stopSpecOnExpectationFailure": false,
|
||||||
|
"random": false
|
||||||
|
}
|
||||||
148
packages/pouchorm/spec/watcher.spec.js
Normal file
148
packages/pouchorm/spec/watcher.spec.js
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
import { Watcher } from '../src/watcher.js';
|
||||||
|
|
||||||
|
describe('A watcher', () => {
|
||||||
|
const selector = {};
|
||||||
|
const opts = {};
|
||||||
|
|
||||||
|
it('initially does nothing.', () => {
|
||||||
|
const db = {
|
||||||
|
changes: () => fail('Watcher should not call changes until the first subscription.')
|
||||||
|
};
|
||||||
|
|
||||||
|
const w = Watcher(db, selector, opts);
|
||||||
|
expect().nothing();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls PouchDB.change API on the first subscription.', () => {
|
||||||
|
let state = 0;
|
||||||
|
const db = {
|
||||||
|
changes: options => {
|
||||||
|
expect(state).toEqual(1);
|
||||||
|
expect(options.live).toEqual(true);
|
||||||
|
expect(options.since).toEqual('now');
|
||||||
|
expect(options.selector).toBe(selector);
|
||||||
|
return db;
|
||||||
|
},
|
||||||
|
on: (eventName, callback) => {
|
||||||
|
expect(['change', 'error'].indexOf(eventName) !== -1).toBeTruthy();
|
||||||
|
return db;
|
||||||
|
},
|
||||||
|
cancel: () => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const w = Watcher(db, selector, opts);
|
||||||
|
state = 1;
|
||||||
|
w(() => fail('Subscription callback should not be called until data changes.'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cancels change subscription when the last subscriber unsubscribes', () => {
|
||||||
|
let state = 0;
|
||||||
|
const db = {
|
||||||
|
changes: options => {
|
||||||
|
expect(state).toEqual(1);
|
||||||
|
expect(options.live).toEqual(true);
|
||||||
|
expect(options.since).toEqual('now');
|
||||||
|
expect(options.selector).toBe(selector);
|
||||||
|
return db;
|
||||||
|
},
|
||||||
|
on: (eventName, callback) => {
|
||||||
|
expect(['change', 'error'].indexOf(eventName) !== -1).toBeTruthy();
|
||||||
|
return db;
|
||||||
|
},
|
||||||
|
cancel: () => {
|
||||||
|
expect(state).toEqual(2);
|
||||||
|
state = 3;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const w = Watcher(db, selector, opts);
|
||||||
|
state = 1;
|
||||||
|
const unsub = w(() => fail('Subscription callback should not be called until data changes.'));
|
||||||
|
state = 2;
|
||||||
|
unsub();
|
||||||
|
expect(state).toEqual(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes change events to subscribers.', () => {
|
||||||
|
let state = 0;
|
||||||
|
const changes = {
|
||||||
|
234: { id: 234, deleted: false, doc: { _id: 234 } },
|
||||||
|
34: { id: 34, deleted: true, doc: { _id: 34 } },
|
||||||
|
4564565: { id: 4564565, deleted: false, doc: { _id: 4564565 } }
|
||||||
|
};
|
||||||
|
let sub = null;
|
||||||
|
const db = {
|
||||||
|
changes: options => {
|
||||||
|
expect(state).toEqual(1);
|
||||||
|
expect(options.live).toEqual(true);
|
||||||
|
expect(options.since).toEqual('now');
|
||||||
|
expect(options.selector).toBe(selector);
|
||||||
|
return db;
|
||||||
|
},
|
||||||
|
on: (eventName, callback) => {
|
||||||
|
if (eventName == 'change') {
|
||||||
|
sub = callback;
|
||||||
|
}
|
||||||
|
expect(['change', 'error'].indexOf(eventName) !== -1).toBeTruthy();
|
||||||
|
return db;
|
||||||
|
},
|
||||||
|
cancel: () => (sub = null)
|
||||||
|
};
|
||||||
|
|
||||||
|
const w = Watcher(db, selector, opts);
|
||||||
|
state = 1;
|
||||||
|
w((id, deleted, doc) => {
|
||||||
|
expect(changes.hasOwnProperty(id)).toBeTruthy();
|
||||||
|
expect(changes[id].doc).toBe(doc);
|
||||||
|
expect(changes[id].deleted).toEqual(deleted);
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.values(changes).forEach(sub);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dumps subscribers when an error event happens.', () => {
|
||||||
|
let state = 0;
|
||||||
|
const changes = {
|
||||||
|
234: { id: 234, deleted: false, doc: { _id: 234 } },
|
||||||
|
34: { id: 34, deleted: true, doc: { _id: 34 } },
|
||||||
|
4564565: { id: 4564565, deleted: false, doc: { _id: 4564565 } }
|
||||||
|
};
|
||||||
|
let sub = null;
|
||||||
|
let errorSub = null;
|
||||||
|
const db = {
|
||||||
|
changes: options => {
|
||||||
|
expect(state).toEqual(1);
|
||||||
|
expect(options.live).toEqual(true);
|
||||||
|
expect(options.since).toEqual('now');
|
||||||
|
expect(options.selector).toBe(selector);
|
||||||
|
return db;
|
||||||
|
},
|
||||||
|
on: (eventName, callback) => {
|
||||||
|
if (eventName == 'change') {
|
||||||
|
sub = callback;
|
||||||
|
}
|
||||||
|
if (eventName == 'error') {
|
||||||
|
errorSub = callback;
|
||||||
|
}
|
||||||
|
expect(['change', 'error'].indexOf(eventName) !== -1).toBeTruthy();
|
||||||
|
return db;
|
||||||
|
},
|
||||||
|
cancel: () => {
|
||||||
|
sub = null;
|
||||||
|
errorSub = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const w = Watcher(db, selector, opts);
|
||||||
|
state = 1;
|
||||||
|
w(() => fail('Subscription callback should not be called until data changes.'));
|
||||||
|
|
||||||
|
const error = new Error('TestError');
|
||||||
|
try {
|
||||||
|
errorSub(error);
|
||||||
|
} catch (e) {
|
||||||
|
expect(e).toBe(error);
|
||||||
|
}
|
||||||
|
Object.values(changes).forEach(sub);
|
||||||
|
});
|
||||||
|
});
|
||||||
3
packages/pouchorm/src/index.js
Normal file
3
packages/pouchorm/src/index.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { isObject, deepAssign, pouchDocHash, pouchDocArrayHash } from './utils.js';
|
||||||
|
export { TypeSpec } from './type.js';
|
||||||
|
export { PouchORM } from './plugin.js';
|
||||||
47
packages/pouchorm/src/livearray.js
Normal file
47
packages/pouchorm/src/livearray.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { prop, computed, id } from '../node_modules/frptools/src/index.js';
|
||||||
|
|
||||||
|
import { Watcher } from './watcher.js';
|
||||||
|
import { pouchDocArrayHash } from './utils.js';
|
||||||
|
|
||||||
|
// LiveArray is a subscribable property function that always returns the db results that match
|
||||||
|
// the provided selector and calls subscribers when the results change.
|
||||||
|
export function LiveArray(db, selector, opts = {}) {
|
||||||
|
const mapper = opts.mapper || id;
|
||||||
|
opts.mapper && delete opts.mapper;
|
||||||
|
opts.include_docs = true;
|
||||||
|
const _watcher = Watcher(db, selector, opts);
|
||||||
|
let changeSub = null;
|
||||||
|
|
||||||
|
const ready = prop(false);
|
||||||
|
const data = prop({ docs: [] });
|
||||||
|
const docs = computed(r => r.docs.map(mapper), [data], pouchDocArrayHash);
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
docs.unsubscribeAll();
|
||||||
|
ready.unsubscribeAll();
|
||||||
|
if (changeSub) {
|
||||||
|
changeSub();
|
||||||
|
changeSub = null;
|
||||||
|
}
|
||||||
|
data({ docs: [] });
|
||||||
|
};
|
||||||
|
|
||||||
|
const refresh = async function refresh(...args) {
|
||||||
|
data(await db.find({ selector }));
|
||||||
|
};
|
||||||
|
|
||||||
|
docs.ready = ready;
|
||||||
|
docs.cleanup = cleanup;
|
||||||
|
docs.selector = selector;
|
||||||
|
docs.db = db;
|
||||||
|
|
||||||
|
refresh()
|
||||||
|
.then(() => {
|
||||||
|
changeSub = _watcher(refresh);
|
||||||
|
ready(true);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
ready.unsubscribeAll();
|
||||||
|
});
|
||||||
|
return docs;
|
||||||
|
}
|
||||||
99
packages/pouchorm/src/plugin.js
Normal file
99
packages/pouchorm/src/plugin.js
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import { LiveArray } from './livearray.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);
|
||||||
|
doc._deleted = true;
|
||||||
|
await _db.put(doc);
|
||||||
|
} 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 }
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.defineProperties(cls, {
|
||||||
|
getOrCreate: { value: getOrCreate },
|
||||||
|
find: { value: find },
|
||||||
|
index: { value: _index },
|
||||||
|
delete: { value: _delete },
|
||||||
|
subscribe: { value: watch },
|
||||||
|
db: { value: _db },
|
||||||
|
name: { value: name },
|
||||||
|
prefix: { value: prefix },
|
||||||
|
selector: { value: _baseSelector }
|
||||||
|
});
|
||||||
|
|
||||||
|
return cls;
|
||||||
|
};
|
||||||
|
}
|
||||||
69
packages/pouchorm/src/type.js
Normal file
69
packages/pouchorm/src/type.js
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { pouchDocHash } 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 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
23
packages/pouchorm/src/utils.js
Normal file
23
packages/pouchorm/src/utils.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
export function deepAssign(to, ...rest) {
|
||||||
|
for (let src of rest) {
|
||||||
|
for (let prop in src) {
|
||||||
|
const value = src[prop];
|
||||||
|
if (typeof value === 'object' && !Array.isArray(value)) {
|
||||||
|
to[prop] = deepAssign(to[prop] || {}, value);
|
||||||
|
} else if (value === undefined && to[prop] !== undefined) {
|
||||||
|
delete to[prop];
|
||||||
|
} else {
|
||||||
|
to[prop] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pouchDocHash = d => (isObject(d) ? `${d._id}:${d._rev}` : d);
|
||||||
|
export const pouchDocArrayHash = arr =>
|
||||||
|
Array.isArray(arr) ? arr.map(pouchDocHash).join('?') : arr;
|
||||||
|
|
||||||
|
export function isObject(obj) {
|
||||||
|
return typeof obj === 'object' && !Array.isArray(obj) && obj !== null;
|
||||||
|
}
|
||||||
37
packages/pouchorm/src/watcher.js
Normal file
37
packages/pouchorm/src/watcher.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
export function Watcher(db, selector, opts) {
|
||||||
|
const subscribers = new Set();
|
||||||
|
let changes = null;
|
||||||
|
|
||||||
|
return function subscribe(fn) {
|
||||||
|
subscribers.add(fn);
|
||||||
|
|
||||||
|
if (subscribers.size === 1 && !changes) {
|
||||||
|
changes = db
|
||||||
|
.changes(
|
||||||
|
Object.assign(
|
||||||
|
{
|
||||||
|
since: 'now',
|
||||||
|
live: true,
|
||||||
|
selector
|
||||||
|
},
|
||||||
|
opts
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.on('change', change => {
|
||||||
|
const { id, deleted, doc } = change;
|
||||||
|
subscribers.forEach(s => s(id, !!deleted, doc));
|
||||||
|
})
|
||||||
|
.on('error', err => {
|
||||||
|
subscribers.clear();
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
subscribers.delete(fn);
|
||||||
|
if (subscribers.size === 0 && changes) {
|
||||||
|
changes.cancel();
|
||||||
|
changes = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user