Mailspring/app/spec/models/query-spec.es6
2017-09-26 11:33:08 -07:00

257 lines
9 KiB
JavaScript

/* eslint quote-props: 0 */
import ModelQuery from '../../src/flux/models/query';
import Attributes from '../../src/flux/attributes';
import Message from '../../src/flux/models/message';
import Thread from '../../src/flux/models/thread';
import Account from '../../src/flux/models/account';
describe('ModelQuery', function ModelQuerySpecs() {
beforeEach(() => {
this.db = {};
});
describe('where', () => {
beforeEach(() => {
this.q = new ModelQuery(Thread, this.db);
this.m1 = Thread.attributes.id.equal(4);
this.m2 = Thread.attributes.categories.contains('category-id');
});
it('should accept an array of Matcher objects', () => {
this.q.where([this.m1, this.m2]);
expect(this.q._matchers.length).toBe(2);
expect(this.q._matchers[0]).toBe(this.m1);
expect(this.q._matchers[1]).toBe(this.m2);
});
it('should accept a single Matcher object', () => {
this.q.where(this.m1);
expect(this.q._matchers.length).toBe(1);
expect(this.q._matchers[0]).toBe(this.m1);
});
it('should append to any existing where clauses', () => {
this.q.where(this.m1);
this.q.where(this.m2);
expect(this.q._matchers.length).toBe(2);
expect(this.q._matchers[0]).toBe(this.m1);
expect(this.q._matchers[1]).toBe(this.m2);
});
it('should accept a shorthand format', () => {
this.q.where({ id: 4, lastMessageReceivedTimestamp: 1234 });
expect(this.q._matchers.length).toBe(2);
expect(this.q._matchers[0].attr.modelKey).toBe('id');
expect(this.q._matchers[0].comparator).toBe('=');
expect(this.q._matchers[0].val).toBe(4);
});
it('should return the query so it can be chained', () => {
expect(this.q.where({ id: 4 })).toBe(this.q);
});
it('should immediately raise an exception if an un-queryable attribute is specified', () =>
expect(() => {
this.q.where({ snippet: 'My Snippet' });
}).toThrow());
it('should immediately raise an exception if a non-existent attribute is specified', () =>
expect(() => {
this.q.where({ looksLikeADuck: 'of course' });
}).toThrow());
});
describe('order', () => {
beforeEach(() => {
this.q = new ModelQuery(Thread, this.db);
this.o1 = Thread.attributes.lastMessageReceivedTimestamp.descending();
this.o2 = Thread.attributes.subject.descending();
});
it('should accept an array of SortOrders', () => {
this.q.order([this.o1, this.o2]);
expect(this.q._orders.length).toBe(2);
});
it('should accept a single SortOrder object', () => {
this.q.order(this.o2);
expect(this.q._orders.length).toBe(1);
});
it('should extend any existing ordering', () => {
this.q.order(this.o1);
this.q.order(this.o2);
expect(this.q._orders.length).toBe(2);
expect(this.q._orders[0]).toBe(this.o1);
expect(this.q._orders[1]).toBe(this.o2);
});
it('should return the query so it can be chained', () => {
expect(this.q.order(this.o2)).toBe(this.q);
});
});
describe('include', () => {
beforeEach(() => {
this.q = new ModelQuery(Message, this.db);
});
it('should throw an exception if the attribute is not a joined data attribute', () =>
expect(() => {
this.q.include(Message.attributes.unread);
}).toThrow());
it('should add the provided property to the list of joined properties', () => {
expect(this.q._includeJoinedData).toEqual([]);
this.q.include(Message.attributes.body);
expect(this.q._includeJoinedData).toEqual([Message.attributes.body]);
});
});
describe('includeAll', () => {
beforeEach(() => {
this.q = new ModelQuery(Message, this.db);
});
it('should add all the JoinedData attributes of the class', () => {
expect(this.q._includeJoinedData).toEqual([]);
this.q.includeAll();
expect(this.q._includeJoinedData).toEqual([Message.attributes.body]);
});
});
describe('response formatting', () =>
it('should always return a Number for counts', () => {
const q = new ModelQuery(Message, this.db);
q.where({ accountId: 'abcd' }).count();
const raw = [{ count: '12' }];
expect(q.formatResult(q.inflateResult(raw))).toBe(12);
}));
describe('sql', () => {
beforeEach(() => {
this.runScenario = (klass, scenario) => {
const q = new ModelQuery(klass, this.db);
Attributes.Matcher.muid = 1;
scenario.builder(q);
expect(
q
.sql()
.replace(/ /g, '')
.trim()
).toBe(scenario.sql.replace(/ /g, '').trim());
};
});
it('should finalize the query so no further changes can be made', () => {
const q = new ModelQuery(Account, this.db);
spyOn(q, 'finalize');
q.sql();
expect(q.finalize).toHaveBeenCalled();
});
it('should correctly generate queries with multiple where clauses', () => {
this.runScenario(Account, {
builder: q => q.where({ emailAddress: 'ben@nylas.com' }).where({ id: 2 }),
sql:
'SELECT `Account`.`data` FROM `Account` ' +
"WHERE `Account`.`emailAddress` = 'ben@nylas.com' AND `Account`.`id` = 2",
});
});
it('should correctly escape single quotes with more double single quotes (LIKE)', () => {
this.runScenario(Account, {
builder: q => q.where(Account.attributes.emailAddress.like("you're")),
sql:
"SELECT `Account`.`data` FROM `Account` WHERE `Account`.`emailAddress` like '%you''re%'",
});
});
it('should correctly escape single quotes with more double single quotes (equal)', () => {
this.runScenario(Account, {
builder: q => q.where(Account.attributes.emailAddress.equal("you're")),
sql: "SELECT `Account`.`data` FROM `Account` WHERE `Account`.`emailAddress` = 'you''re'",
});
});
it('should correctly generate COUNT queries', () => {
this.runScenario(Thread, {
builder: q => q.where({ accountId: 'abcd' }).count(),
sql: 'SELECT COUNT(*) as count FROM `Thread` ' + "WHERE `Thread`.`accountId` = 'abcd' ",
});
});
it('should correctly generate LIMIT 1 queries for single items', () => {
this.runScenario(Thread, {
builder: q => q.where({ accountId: 'abcd' }).one(),
sql:
'SELECT `Thread`.`data` FROM `Thread` ' +
"WHERE `Thread`.`accountId` = 'abcd' " +
'ORDER BY `Thread`.`lastMessageReceivedTimestamp` DESC LIMIT 1',
});
});
it('should correctly generate `contains` queries using JOINS', () => {
this.runScenario(Thread, {
builder: q =>
q.where(Thread.attributes.categories.contains('category-id')).where({ id: '1234' }),
sql:
'SELECT `Thread`.`data` FROM `Thread` ' +
'INNER JOIN `ThreadCategory` AS `M1` ON `M1`.`id` = `Thread`.`id` ' +
"WHERE `M1`.`value` = 'category-id' AND `Thread`.`id` = '1234' " +
'ORDER BY `Thread`.`lastMessageReceivedTimestamp` DESC',
});
this.runScenario(Thread, {
builder: q =>
q.where([
Thread.attributes.categories.contains('l-1'),
Thread.attributes.categories.contains('l-2'),
]),
sql:
'SELECT `Thread`.`data` FROM `Thread` ' +
'INNER JOIN `ThreadCategory` AS `M1` ON `M1`.`id` = `Thread`.`id` ' +
'INNER JOIN `ThreadCategory` AS `M2` ON `M2`.`id` = `Thread`.`id` ' +
"WHERE `M1`.`value` = 'l-1' AND `M2`.`value` = 'l-2' " +
'ORDER BY `Thread`.`lastMessageReceivedTimestamp` DESC',
});
});
it("should correctly generate queries with the class's naturalSortOrder when one is available and no other orders are provided", () => {
this.runScenario(Thread, {
builder: q => q.where({ accountId: 'abcd' }),
sql:
'SELECT `Thread`.`data` FROM `Thread` ' +
"WHERE `Thread`.`accountId` = 'abcd' " +
'ORDER BY `Thread`.`lastMessageReceivedTimestamp` DESC',
});
this.runScenario(Thread, {
builder: q =>
q
.where({ accountId: 'abcd' })
.order(Thread.attributes.lastMessageReceivedTimestamp.ascending()),
sql:
'SELECT `Thread`.`data` FROM `Thread` ' +
"WHERE `Thread`.`accountId` = 'abcd' " +
'ORDER BY `Thread`.`lastMessageReceivedTimestamp` ASC',
});
this.runScenario(Account, {
builder: q => q.where({ id: 'abcd' }),
sql: 'SELECT `Account`.`data` FROM `Account` ' + "WHERE `Account`.`id` = 'abcd' ",
});
});
it('should correctly generate queries requesting joined data attributes', () => {
this.runScenario(Message, {
builder: q => q.where({ id: '1234' }).include(Message.attributes.body),
sql:
"SELECT `Message`.`data`, IFNULL(`MessageBody`.`value`, '!NULLVALUE!') AS `body` " +
'FROM `Message` LEFT OUTER JOIN `MessageBody` ON `MessageBody`.`id` = `Message`.`id` ' +
"WHERE `Message`.`id` = '1234' ORDER BY `Message`.`date` ASC",
});
});
});
});