/* 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", }); }); }); });