This repository has been archived on 2020-08-24. You can view files and clone it, but cannot push or open issues or pull requests.
pouchtype/spec/typehandler.spec.js

364 lines
9.1 KiB
JavaScript

import { TypeHandler } from '../src/index.js';
import PouchDB from 'pouchdb';
import PouchDBFind from 'pouchdb-find';
import memory from 'pouchdb-adapter-memory';
const PDB = PouchDB.plugin(PouchDBFind).plugin(memory);
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: 'Smith' });
expect(res.length).toBe(1);
});
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);
}
});
});