364 lines
9.1 KiB
JavaScript
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);
|
|
}
|
|
});
|
|
});
|