UNPKG

sutando

Version:

A modern Node.js ORM. Makes it enjoyable to interact with your database. Support Mysql, MSSql, MariaDB, Sqlite.

1,404 lines (1,227 loc) 85 kB
const unset = require('lodash/unset'); const filter = require('lodash/filter'); const kebabCase = require('lodash/kebabCase'); const { sutando, Model, Collection, Builder, Paginator, compose, SoftDeletes, Attribute, HasUniqueIds, CastsAttributes, ModelNotFoundError, make, makeCollection, makePaginator, } = require('../src'); const config = require(process.env.SUTANDO_CONFIG || './config'); const dayjs = require('dayjs'); const crypto = require('crypto'); const collect = require('collect.js'); const QueryBuilder = require('../src/query-builder'); Promise.delay = function (duration) { return new Promise((resolve, reject) => { setTimeout(() => { resolve(true); }, duration) }); } describe('node environment test', () => { test('should load the node version of the module', () => { // Test automatically loads the node version const module = require('sutando'); expect(module.isBrowser).toBeUndefined(); }); }); describe('Sutando', () => { it('should fail if passing a wrong connection info', () => { sutando.addConnection({ client: 'abc', connection: { host : '127.0.0.1', port : 1234, } }); expect(() => { sutando.connection(); }).toThrow(); sutando.connections = {}; }); }); describe('Model', () => { const SomePlugin = (Model) => { return class extends Model { pluginAttribtue = 'plugin'; pluginMethod() { return this.pluginAttribtue; } } } class User extends compose( Model, SomePlugin, ) { relationPost() { return this.hasMany(Post); } } class Post extends Model { relationAuthor() { return this.belongsTo(User); } relationTags() { return this.belongsToMany(Tag, 'post_tag'); } relationThumbnail() { return this.belongsTo(Thumbnail, 'thumbnail_id'); } } class Tag extends Model { relationPosts() { return this.belongsToMany(Post, 'post_tag'); } } class Thumbnail extends Model {} class Media extends Model {} const manager = new sutando; manager.createModel('User', { plugins: [SomePlugin], relations: { post: (model) => model.hasMany(manager.models.Post), } }); manager.createModel('Post', { relations: { author: (model) => model.belongsTo(manager.models.User), tags: (model) => model.belongsToMany(Tag, 'post_tag'), thumbnail: (model) => model.belongsTo(Thumbnail, 'thumbnail_id'), } }); it('return the table name of the plural model name', () => { const user = new User; const media = new Media; expect(user.getTable()).toBe('users'); expect(media.getTable()).toBe('media'); const anotherUser = new manager.models.User; expect(anotherUser.getTable()).toBe('users'); }); describe('#compose', () => { it('should return a Model instance', () => { const user = new User; expect(user).toBeInstanceOf(Model); const anotherUser = new manager.models.User; expect(anotherUser).toBeInstanceOf(Model); }); it('has mixin\'s attributes and methods', () => { const user = new User; expect(user.pluginAttribtue).toBe('plugin'); expect(user.pluginMethod()).toBe('plugin'); const anotherUser = new manager.models.User; expect(anotherUser.pluginAttribtue).toBe('plugin'); expect(anotherUser.pluginMethod()).toBe('plugin'); }) }) describe('#toData & #toJson', () => { class User extends Model { attributeFullName() { return Attribute.make({ get: (value, attributes) => `${attributes.firstName} ${attributes.lastName}`, set: (value, attributes) => ({ firstName: value.split(' ')[0], lastName: value.split(' ')[1] }) }) } get another_full_name() { return `${this.attributes.firstName} ${this.attributes.lastName}`; } set another_full_name(value) { const names = value.split(' '); this.attributes.firstName = names[0]; this.attributes.lastName = names[1]; } } class Post extends Model {} let testModel; beforeEach(() => { testModel = new User({ id: 1, firstName: 'Joe', lastName: 'Shmoe', address: '123 Main St.' }); }); it('includes the relations loaded on the model', () => { testModel.setRelation('posts', new Collection([ new Post({id: 1}), new Post({id: 2}) ])); const data = testModel.toData(); expect(Object.keys(data)).toEqual(['id', 'firstName', 'lastName', 'address', 'posts']); expect(data.posts.length).toBe(2); }); it('serializes correctly', () => { testModel.setVisible(['firstName']); expect(testModel.toJson()).toBe('{"firstName":"Joe"}'); expect(testModel.toString()).toBe('{"firstName":"Joe"}'); }); describe('#visible & #hidden', () => { it('only shows the fields specified in the model\'s "visible" property', () => { testModel.visible = ['firstName']; expect(testModel.toData()).toEqual({firstName: 'Joe'}); }); it('hides the fields specified in the model\'s "hidden" property', () => { expect(testModel.setHidden(['firstName']).toData()).toEqual({id: 1, lastName: 'Shmoe', address: '123 Main St.'}); testModel.setHidden(['firstName', 'lastName']).makeVisible('firstName'); expect(testModel.getHidden()).toEqual(['lastName']); expect(testModel.toData()).toEqual({id: 1, firstName: 'Joe', address: '123 Main St.'}); }); it('hides the fields specified in the "options.hidden" property', () => { testModel.setHidden(['firstName', 'id']); expect(testModel.toData()).toEqual({lastName: 'Shmoe', address: '123 Main St.'}); }); it('prioritizes "hidden" if there are conflicts when using both "hidden" and "visible"', () => { testModel.setVisible(['firstName', 'lastName']); testModel.setHidden(['lastName']); expect(testModel.toData()).toEqual({ firstName: 'Joe' }); }); it('allows overriding the model\'s "hidden" property with a "setHidden" argument', () => { testModel.hidden = ['lastName']; testModel.setHidden(['firstName', 'id']); const data = testModel.toData(); expect(data).toEqual({ lastName: 'Shmoe', address: '123 Main St.' }); }); it('allows overriding the model\'s "hidden" property with a "makeHidden" argument', () => { testModel.hidden = ['lastName']; testModel.makeHidden(['firstName', 'id']); const data = testModel.toData(); expect(data).toEqual({ address: '123 Main St.' }); }); it('prioritizes "setHidden" when overriding both the model\'s "hidden" and "visible" properties with "setHidden" and "setVisible" arguments', () => { testModel.visible = ['lastName', 'address']; testModel.hidden = ['address']; const data = testModel.setVisible(['firstName', 'lastName']).setHidden(['lastName']).toData(); expect(data).toEqual({firstName: 'Joe'}); }); it('append virtual attribute', () => { const data = testModel.append(['another_full_name', 'full_name']).toData(); expect(data).toEqual({ address: '123 Main St.', firstName: 'Joe', another_full_name: 'Joe Shmoe', full_name: 'Joe Shmoe', id: 1, lastName: 'Shmoe' }); testModel.another_full_name = 'Bill Gates'; expect(testModel.toData()).toEqual({ address: '123 Main St.', firstName: 'Bill', another_full_name: 'Bill Gates', full_name: 'Bill Gates', id: 1, lastName: 'Gates' }); expect(testModel.isDirty('firstName')).toBeTruthy(); expect(testModel.isDirty('lastName')).toBeTruthy(); expect(testModel.isDirty()).toBeTruthy(); }); }); it('model getter settings', () => { expect(testModel.full_name).toBe('Joe Shmoe'); }); it('model setter settings', () => { testModel.full_name = 'Bill Gates'; expect(testModel.firstName).toBe('Bill'); expect(testModel.lastName).toBe('Gates'); }); }) describe('#isDirty', () => { it('returns true if an attribute was set on a new model instance', () => { const model = new Model({test: 'something'}); expect(model.isDirty('test')).toBeTruthy(); }); it("returns false if the attribute isn't set on a new model instance", () => { const model = new Model({test_test: 'something'}); // expect(model.getDirty()).toEqual({ a: 1}) expect(model.isDirty('id')).toBeFalsy(); expect(model.isDirty()).toBeTruthy(); }); it('returns true if an existing attribute is updated', () => { const model = new Model; model.test = 'something else'; expect(model.isDirty('test')).toBeTruthy(); }); }); }) describe('Collection', () => { let collection; class User extends Model { primaryKey = 'some_id'; } class Post extends Model {} beforeEach(() => { collection = new Collection([ new User({some_id: 1, name: 'Test'}), new User({name: 'Test2'}), new Post({id: 2, name: 'Test3'}) ]); }); it('should initialize the items passed to the constructor', () => { expect(collection.count()).toBe(3); expect(collection.modelKeys()).toEqual([1, undefined, 2]); // expect(collection.get(1).getKey()).toBeUndefined(); }); it('should ', () => { expect(collection.toData()).toEqual([ {some_id: 1, name: 'Test'}, {name: 'Test2'}, {id: 2, name: 'Test3'}, ]); }) }) describe('Builder', () => { }) describe('Paginator', () => { }) describe('Integration test', () => { const databases = [ // { // client: 'postgres', // connection: config.postgres // } ]; if (process.env.DB === 'mysql') { databases.push(config.mysql); } else if (process.env.DB === 'sqlite') { databases.push(config.sqlite); } else if (process.env.DB === 'postgres') { databases.push(config.postgres); } databases.map(config => { describe('Client: ' + config.client, () => { sutando.addConnection(config, config.client); const connection = sutando.connection(config.client); class Base extends Model { connection = config.client; } class Admin extends Base { table = 'administrators'; } class User extends Base { hidden = ['password', 'remember_token']; attributeFullName() { return Attribute.make({ get: (value, attributes) => `${attributes.firstName} ${attributes.name}` }) } relationPosts() { return this.hasMany(Post); } } class UuidUser extends compose(Base, HasUniqueIds) { newUniqueId() { return crypto.randomUUID(); } attributeName() { return Attribute.make({ get: (value, attributes) => attributes.name || attributes.id, }) } } class Post extends Base { scopeIdOf(query, id) { return query.where('id', id); } scopePublish(query) { return query.where('status', 1); } relationAuthor() { return this.belongsTo(User); } relationDefaultAuthor() { return this.belongsTo(User).withDefault({ name: 'Default Author' }); } relationDefaultPostAuthor() { return this.belongsTo(User).withDefault((user, post) => { user.name = post.name + ' - Default Author'; }); } relationThumbnail() { return this.belongsTo(Media, 'thumbnail_id'); } relationMedia() { return this.belongsToMany(Media); } relationTags() { return this.belongsToMany(Tag); } relationComments() { return this.hasMany(Comment); } } sutando.createModel('Post', { connection: config.client, attributes: { slug: Attribute.make({ get: (value, attributes) => kebabCase(attributes.name) }) }, scopes: { idOf: (query, id) => query.where('id', id), publish: (query) => query.where('status', 1) }, relations: { author: (model) => model.belongsTo(User), default_author: (model) => model.belongsTo(User).withDefault({ name: 'Default Author' }), default_post_author: (model) => model.belongsTo(User).withDefault((user, post) => { user.name = post.name + ' - Default Author'; }), thumbnail: (model) => model.belongsTo(Media, 'thumbnail_id'), media: (model) => model.belongsToMany(Media), tags: (model) => model.belongsToMany(Tag), } }); class Tag extends Base { relationPosts() { return this.belongsToMany(Post); } } class Comment extends Base {} class Media extends Base {} class SoftDeletePost extends compose(Base, SoftDeletes) {} class Json extends CastsAttributes { static get(model, key, value, attributes) { try { return JSON.parse(value); } catch (e) { return null; } } static set(model, key, value, attributes) { return JSON.stringify(value); } } class CastPost extends Base { casts = { text_to_json: 'json', text_to_collection: 'collection', custom_cast: Json, some_string: 'string', some_int: 'int', some_date: 'date', some_datetime: 'datetime', is_published: 'boolean', } } beforeAll(() => { return Promise.all(['users', 'tags', 'posts', 'post_tag', 'administrators', 'comments', 'media'].map(table => { return connection.schema.dropTableIfExists(table); })).then(() => { return connection.schema .createTable('users', (table) => { table.increments('id'); table.string('name'); table.string('first_name'); table.timestamps(); }) .createTable('uuid_users', (table) => { table.string('id').primary(); table.string('name'); table.timestamps(); }) .createTable('media', (table) => { table.increments('id'); table.integer('mediaable_id').defaultTo(0); table.string('mediaable_type').defaultTo(''); table.string('uuid').defaultTo(''); table.timestamps(); }) .createTable('tags', (table) => { table.increments('id'); table.string('name'); table.timestamps(); }) .createTable('administrators', (table) => { table.increments('id'); table.string('username'); table.string('password'); table.timestamps(); }) .createTable('posts', (table) => { table.increments('id'); table.integer('user_id').defaultTo(0); table.string('name'); table.text('content'); table.timestamps(); }) .createTable('post_tag', (table) => { table.increments('id'); table.integer('post_id').defaultTo(0); table.integer('tag_id').defaultTo(0); table.timestamps(); }) .createTable('comments', function(table) { table.increments('id'); table.integer('post_id').defaultTo(0); table.string('name'); table.string('email'); table.text('comment'); table.timestamps(); }) .createTable('soft_delete_posts', function(table) { table.increments('id'); table.string('name'); table.text('content'); table.datetime('deleted_at').defaultTo(null); table.timestamps(); }) .createTable('cast_posts', function(table) { table.increments('id'); table.text('text_to_json'); table.text('text_to_collection'); table.text('custom_cast'); table.integer('some_string'); table.string('some_int'); table.datetime('some_date').defaultTo(null); table.datetime('some_datetime').defaultTo(null); table.tinyint('is_published').defaultTo(0); table.timestamps(); }) }).then(() => { const date = dayjs().format('YYYY-MM-DD HH:mm:ss'); return Promise.all([ connection.table('users').insert([ { first_name: 'Tim', name: 'Shuri', created_at: date, updated_at: date, }, { first_name: 'X', name: 'Alice', created_at: date, updated_at: date, } ]), // connection.table('uuid_users').insert([]), connection.table('administrators').insert([ { username: 'test1', password: 'testpwd1', created_at: date, updated_at: date }, { username: 'test2', password: 'testpwd2', created_at: date, updated_at: date } ]), connection.table('post_tag').insert([ { post_id: 1, tag_id: 1, created_at: date, updated_at: date }, { post_id: 1, tag_id: 2, created_at: date, updated_at: date }, { post_id: 1, tag_id: 3, created_at: date, updated_at: date }, { post_id: 4, tag_id: 1, created_at: date, updated_at: date } ]), connection.table('comments').insert([ { post_id: 3, name: '(blank)', email: 'test@example.com', comment: 'this is neat.', created_at: date, updated_at: date } ]), connection.table('tags').insert([ { name: 'cool', created_at: date, updated_at: date }, { name: 'boring', created_at: date, updated_at: date }, { name: 'exciting', created_at: date, updated_at: date }, { name: 'amazing', created_at: date, updated_at: date } ]), connection.table('posts').insert([ { user_id: 1, name: 'This is a new Title!', content: 'Lorem ipsum Labore eu sed sed Excepteur enim laboris deserunt adipisicing dolore culpa aliqua cupidatat proident ea et commodo labore est adipisicing ex amet exercitation est.', created_at: date, updated_at: date }, { user_id: 2, name: 'This is a new Title 2!', content: 'Lorem ipsum Veniam ex amet occaecat dolore in pariatur minim est exercitation deserunt Excepteur enim officia occaecat in exercitation aute et ad esse ex in in dolore amet consequat quis sed mollit et id incididunt sint dolore velit officia dolor dolore laboris dolor Duis ea ex quis deserunt anim nisi qui culpa laboris nostrud Duis anim deserunt esse laboris nulla qui in dolor voluptate aute reprehenderit amet ut et non voluptate elit irure mollit dolor consectetur nisi adipisicing commodo et mollit dolore incididunt cupidatat nulla ut irure deserunt non officia laboris fugiat ut pariatur ut non aliqua eiusmod dolor et nostrud minim elit occaecat commodo consectetur cillum elit laboris mollit dolore amet id qui eiusmod nulla elit eiusmod est ad aliqua aute enim ut aliquip ex in Ut nisi sint exercitation est mollit veniam cupidatat adipisicing occaecat dolor irure in aute aliqua ullamco.', created_at: date, updated_at: date }, { user_id: 2, name: 'This is a new Title 3!', content: 'Lorem ipsum Reprehenderit esse esse consectetur aliquip magna.', created_at: date, updated_at: date }, { user_id: 30, name: 'This is a new Title 4!', content: 'Lorem ipsum Anim sed eu sint aute.', created_at: date, updated_at: date }, { user_id: 4, name: 'This is a new Title 5!', content: 'Lorem ipsum Commodo consectetur eu ea amet laborum nulla eiusmod minim veniam ullamco nostrud sed mollit consectetur veniam mollit Excepteur quis cupidatat.', created_at: date, updated_at: date } ]), connection.table('soft_delete_posts').insert([ { name: 'This is a new Title!', content: 'Lorem ipsum Labore eu sed sed Excepteur enim laboris deserunt adipisicing dolore culpa aliqua cupidatat proident ea et commodo labore est adipisicing ex amet exercitation est.', created_at: date, updated_at: date, deleted_at: null, }, { name: 'This is a new Title 2!', content: 'Lorem ipsum Veniam ex amet occaecat dolore in pariatur minim est exercitation deserunt Excepteur enim officia occaecat in exercitation aute et ad esse ex in in dolore amet consequat quis sed mollit et id incididunt sint dolore velit officia dolor dolore laboris dolor Duis ea ex quis deserunt anim nisi qui culpa laboris nostrud Duis anim deserunt esse laboris nulla qui in dolor voluptate aute reprehenderit amet ut et non voluptate elit irure mollit dolor consectetur nisi adipisicing commodo et mollit dolore incididunt cupidatat nulla ut irure deserunt non officia laboris fugiat ut pariatur ut non aliqua eiusmod dolor et nostrud minim elit occaecat commodo consectetur cillum elit laboris mollit dolore amet id qui eiusmod nulla elit eiusmod est ad aliqua aute enim ut aliquip ex in Ut nisi sint exercitation est mollit veniam cupidatat adipisicing occaecat dolor irure in aute aliqua ullamco.', created_at: date, updated_at: date, deleted_at: date }, { name: 'This is a new Title 3!', content: 'Lorem ipsum Labore eu sed sed Excepteur enim laboris deserunt adipisicing dolore culpa aliqua cupidatat proident ea et commodo labore est adipisicing ex amet exercitation est.', created_at: date, updated_at: date, deleted_at: null, }, { name: 'This is a new Title 4!', content: 'Lorem ipsum Veniam ex amet occaecat dolore in pariatur minim est exercitation deserunt Excepteur enim officia occaecat in exercitation aute et ad esse ex in in dolore amet consequat quis sed mollit et id incididunt sint dolore velit officia dolor dolore laboris dolor Duis ea ex quis deserunt anim nisi qui culpa laboris nostrud Duis anim deserunt esse laboris nulla qui in dolor voluptate aute reprehenderit amet ut et non voluptate elit irure mollit dolor consectetur nisi adipisicing commodo et mollit dolore incididunt cupidatat nulla ut irure deserunt non officia laboris fugiat ut pariatur ut non aliqua eiusmod dolor et nostrud minim elit occaecat commodo consectetur cillum elit laboris mollit dolore amet id qui eiusmod nulla elit eiusmod est ad aliqua aute enim ut aliquip ex in Ut nisi sint exercitation est mollit veniam cupidatat adipisicing occaecat dolor irure in aute aliqua ullamco.', created_at: date, updated_at: date, deleted_at: date }, ]), connection.table('cast_posts').insert([ { text_to_json: '{"a": "foo", "b": "bar"}', text_to_collection: '[{"name":"foo1"}, {"name":"bar2"}]', custom_cast: '{"a": "foo", "b": "bar"}', some_string: 1, some_int: '1', some_date: date, some_datetime: date, is_published: 1, created_at: date, updated_at: date, }, ]), ]) }); }); afterAll(() => { connection.destroy(); }); describe('QueryBuilder', () => { it('should return a QueryBuilder instance', () => { expect(connection).toBeInstanceOf(QueryBuilder); }); describe('raw', () => { it('should execute raw SQL query and return correct result', async () => { if (process.env.DB === 'sqlite') { const user = await connection.raw('SELECT id FROM users WHERE id > ? LIMIT 1', [1]); expect(user).toEqual([{ id: 2 }]); } else if (process.env.DB === 'mysql') { const res = await connection.raw('SELECT id FROM users WHERE id > ? LIMIT 1', [1]); expect(res[0]).toEqual([{ id: 2 }]); } else if (process.env.DB === 'postgres') { const res = await connection.raw('SELECT id FROM users WHERE id > ? LIMIT 1', [1]); expect(res.rows).toEqual([{ id: 2 }]); } }); }); describe('query', () => { it('calls query-builder method with the first argument, returning the query builder', () => { const query = connection.table('users'); const q = query.where('id', 1); expect(q).toStrictEqual(query); }); it('allows passing an object to query', () => { const query = connection.table('users'); expect(filter(query._statements, {grouping: 'where'}).length).toBe(0); const q = query.where('id', 1).orWhere('id', '>', 10); expect(q).toStrictEqual(query); expect(filter(query._statements, {grouping: 'where'}).length).toBe(2); }); it('allows passing a function to query', () => { const query = connection.table('users'); expect(filter(query._statements, {grouping: 'where'}).length).toBe(0); const q = query.where((q) => { q.where('id', 1).orWhere('id', '>', '10'); }); expect(q).toEqual(query); expect(filter(query._statements, {grouping: 'where'}).length).toBe(1); }); describe('#first() & #find()', () => { it('issues a first (get one), triggering a fetched event, returning a promise', () => { const query = connection.table('users').where('id', 1); return query.first().then((user) => { expect(user.id).toBe(1); expect(user.name).toBe('Shuri'); }); }); it('allows specification of select columns in query', () => { return connection.table('users').where('id', 1).select(['id', 'first_name']).first().then((user) => { expect(user).toEqual({id: 1, first_name: 'Tim'}); }); }); }); describe('#get()', () => { it('should merge models with duplicate ids by default', async () => { const users = await connection.table('users').get(); expect(users.length).toBe(2); expect(users.map(user => user.name)).toEqual(['Shuri', 'Alice']); }); it('returns an empty collection if there are no results', async () => { const users = await connection.table('users') .where('name', 'hal9000') .get(); expect(users.length).toBe(0); }); }); describe('#chunk()', () => { it('fetches a single page of results with defaults', async () => { const names = []; await connection.table('tags').orderBy('id', 'asc').chunk(2, (tags) => { tags.map(tag => { names.push(tag.name); }); }); expect(names).toEqual(['cool', 'boring', 'exciting', 'amazing']); await connection.table('tags').orderBy('id', 'desc').chunk(2, (tags) => { tags.map(tag => { names.push(tag.name); }); }); expect(names).toEqual(['cool', 'boring', 'exciting', 'amazing', 'amazing', 'exciting', 'boring', 'cool']); }); }); describe('take/skip/limit/offset/forPage', () => { it('should allow specifying limit and offset', () => { return connection.table('users').limit(1).offset(1).get().then((users) => { expect(users.length).toBe(1); expect(users[0].id).toBe(2); }); }); it('should allow specifying take and skip', () => { return connection.table('users').take(1).skip(1).get().then((users) => { expect(users.length).toBe(1); expect(users[0].id).toBe(2); }); }); it('should allow specifying forPage', () => { return connection.table('users').forPage(2, 1).get().then((users) => { expect(users.length).toBe(1); expect(users[0].id).toBe(2); }); }); }); describe('#paginate()', () => { it('fetches a single page of results with defaults', () => { return connection.table('users').paginate() .then((users) => { expect(users).toBeInstanceOf(Paginator); }); }); it('fetches a page of results with specified page size', () => { return connection.table('users').paginate(1, 2) .then((results) => { expect(results).toBeInstanceOf(Paginator); expect(results.count()).toBe(2); expect(results.total()).toBe(2); expect(results.currentPage()).toBe(1); }); }); it('fetches a page by page number', () => { return connection.table('users').orderBy('id', 'asc').paginate(1, 2) .then((results) => { expect(results.get(0).id).toBe(1); expect(results.get(1).id).toBe(2); }); }); describe('with groupBy', () => { it('counts grouped rows instead of total rows', () => { let total; return connection.table('posts').count().then(count => { total = parseInt(count); return connection.table('posts') .select('user_id') .groupBy('user_id') .whereNotNull('user_id') .paginate(); }).then(posts => { expect(posts.count()).toBeLessThanOrEqual(total); }); }); it('counts grouped rows when using table name qualifier', () => { let total; connection.table('posts').count() .then(count => { total = parseInt(count, 10); return connection.table('posts') .select('user_id') .groupBy('posts.user_id') .whereNotNull('user_id') .paginate(); }) .then(posts => { expect(posts.count()).toBeLessThanOrEqual(total); }); }); }); describe('with distinct', () => { it('counts distinct occurences of a column instead of total rows', () => { let total; return connection.table('posts').count() .then(count => { total = count; return connection.table('posts').distinct('user_id').get(); }) .then(distinctPostUsers => { expect(distinctPostUsers.length).toBeLessThanOrEqual(total); }); }); }); }); describe('orderBy', () => { it('returns results in the correct order', () => { const asc = connection.table('users') .orderBy('id', 'asc') .get() .then(result => { return result.map(user => user.id); }); const desc = connection.table('users') .orderBy('id', 'desc') .get() .then(result => { return result.map(user => user.id); }); return Promise.all([asc, desc]).then((results) => { expect(results[0].reverse()).toEqual(results[1]); }); }); }); }); }); describe('Model', () => { it('should return a same instance', () => { expect(connection).toBe(sutando.connection(config.client)); }); it('should return a Builder instance', () => { expect(User.query()).toBeInstanceOf(Builder); }); describe('#make', () => { it('should return a Model instance', () => { const user = make(User, { id: 1 }); expect(user).toBeInstanceOf(User); const anotherUser = make(User, { id: 1, posts: [ { id: 1, title: 'Test' }, { id: 2, title: 'Test 2' } ] }); expect(anotherUser).toBeInstanceOf(User); expect(anotherUser.posts).toBeInstanceOf(Collection); expect(anotherUser.posts.count()).toBe(2); expect(anotherUser.posts.get(1).title).toBe('Test 2'); }); it('should return a Collection instance', () => { const data = [ { id: 1, name: 'Test' }, { id: 2, name: 'Test 2' } ]; const users = make(User, data); expect(users).toBeInstanceOf(Collection); expect(users.count()).toBe(2); expect(users.get(1).name).toBe('Test 2'); const users2 = makeCollection(User, data); expect(users2).toBeInstanceOf(Collection); expect(users2.count()).toBe(2); expect(users2.get(1).name).toBe('Test 2'); }); it('should return a Paginator instance', () => { const data = { total: 2, data: [ { id: 1, name: 'Test' }, { id: 2, name: 'Test 2' } ], current_page: 1, per_page: 10, }; const users = make(User, data, { paginated: true }); expect(users).toBeInstanceOf(Paginator); expect(users.total()).toBe(2); expect(users.perPage()).toBe(10); expect(users.currentPage()).toBe(1); expect(users.items().count()).toBe(2); expect(users.items().get(1)).toBeInstanceOf(User); const users2 = makePaginator(User, data, { paginated: true }); expect(users2).toBeInstanceOf(Paginator); expect(users2.total()).toBe(2); expect(users2.perPage()).toBe(10); expect(users2.currentPage()).toBe(1); expect(users2.items().count()).toBe(2); expect(users2.items().get(1)).toBeInstanceOf(User); }); it('should return a Model instance', () => { const user = User.make({ id: 1 }); expect(user).toBeInstanceOf(User); const anotherUser = User.make({ id: 1, posts: [ { id: 1, title: 'Test' }, { id: 2, title: 'Test 2' } ] }); expect(anotherUser).toBeInstanceOf(User); expect(anotherUser.posts).toBeInstanceOf(Collection); expect(anotherUser.posts.count()).toBe(2); expect(anotherUser.posts.get(1).title).toBe('Test 2'); }); }) describe('first', () => { it('should create a new model instance', async () => { const user = await User.query().first(); expect(user.getTable()).toBe('users'); expect(user).toBeInstanceOf(User); expect(user).toBeInstanceOf(Model); }); }); describe('query', () => { let model; beforeEach(() => { model = new User; }); it('returns the Builder when no arguments are passed', () => { expect(User.query()).toBeInstanceOf(Builder); }); it('calls builder method with the first argument, returning the model', () => { const query = User.query(); const q = query.where('id', 1); expect(q).toStrictEqual(query); }); it('allows passing an object to query', () => { const query = User.query(); expect(filter(query.query._statements, {grouping: 'where'}).length).toBe(0); const q = query.where('id', 1).orWhere('id', '>', 10); expect(q).toStrictEqual(query); expect(filter(query.query._statements, {grouping: 'where'}).length).toBe(2); }); it('allows passing a function to query', () => { const query = User.query(); expect(filter(query.query._statements, {grouping: 'where'}).length).toBe(0); const q = query.where((q) => { q.where('id', 1).orWhere('id', '>', '10'); }); expect(q).toEqual(query); expect(filter(query.query._statements, {grouping: 'where'}).length).toBe(1); }); }); describe('#first() & #find()', () => { it('issues a first (get one), triggering a fetched event, returning a promise', () => { const query = User.query().where('id', 1); return query.first().then((user) => { expect(user).toBeInstanceOf(User); expect(user.id).toBe(1); expect(user.name).toBe('Shuri'); }); }); it('allows specification of select columns in query', () => { return User.query().where('id', 1).select(['id', 'first_name']).first().then((user) => { expect(user.toData()).toEqual({id: 1, first_name: 'Tim'}); }); }); it('resolves to null if no record exists and the {require: false} option is passed', () => { return User.query().where('id', 200).first().then(user => { expect(user).toBeNull(); }) }); it('rejects with an error if no record exists', () => { return User.query().where('id', 200).firstOrFail().then(user => { // expect(user).toBeNull(); }).catch(e => { expect(e).toBeInstanceOf(ModelNotFoundError); }); }); it('locks the table when called with the forUpdate option during a transaction', async () => { let userId; const user = new User; user.first_name = 'foo'; await user.save(); userId = user.id; await Promise.all([ connection.transaction(trx => { return User.query(trx).forUpdate().find(user.id) .then(() => { return Promise.delay(100); }) .then(() => { return User.query(trx).find(user.id); }) .then(user => { expect(user.first_name).toBe('foo'); }); }), Promise.delay(25).then(() => { return User.query().where('id', user.id).update({ first_name: 'changed', }); }) ]); await User.query().where('id', userId).delete(); }); it('locks the table when called with the forShare option during a transaction', () => { let userId; const user = new User({ first_name: 'foo'}); return user.save() .then(() => { userId = user.id; return Promise.all([ connection.transaction(trx => { return User.query(trx).forShare().find(user.id) .then(() => Promise.delay(100)) .then(() => User.query(trx).find(user.id)) .then(user => { expect(user.first_name).toBe('foo'); }) }), Promise.delay(60).then(() => { return User.query().where('id', user.id).update({ first_name: 'changed', }); }) ]) }) .then(() => { return User.query().where('id', userId).delete() }); }); }); describe('#get()', () => { it('should merge models with duplicate ids by default', async () => { const users = await User.query().get(); expect(users).toBeInstanceOf(Collection); expect(users.count()).toBe(2); expect(users.pluck('name').all()).toEqual(['Shuri', 'Alice']); }); it('returns an empty collection if there are no results', async () => { const users = await User.query() .where('name', 'hal9000') .get(); expect(users).toBeInstanceOf(Collection); expect(users.count()).toBe(0); }); it('returns results filtered using scope', async () => { let posts = await Post.query().idOf(3).get(); expect(posts.modelKeys()).toEqual([3]); posts = await Post.query().idOf(3).orWhere(q => { q.idOf(4); }).get(); expect(posts.modelKeys()).toEqual([3, 4]); }); }); describe('#chunk()', () => { it('fetches a single page of results with defaults', async () => { const names = []; await Tag.query().chunk(2, (tags) => { tags.map(tag => { names.push(tag.name); }); }); expect(names).toEqual(['cool', 'boring', 'exciting', 'amazing']); await Tag.query().orderBy('id', 'desc').chunk(2, (tags) => { tags.map(tag => { names.push(tag.name); }); }); expect(names).toEqual(['cool', 'boring', 'exciting', 'amazing', 'amazing', 'exciting', 'boring', 'cool']); }); }); describe('#paginate()', () => { it('fetches a single page of results with defaults', () => { return User.query().paginate() .then((users) => { expect(users).toBeInstanceOf(Paginator); }); }); it('returns an empty collection if there are no results', () => { return Comment.query().delete() .then(() => Comment.query().paginate()) .then(results => { expect(results).toBeInstanceOf(Paginator); expect(results.count()).toBe(0); }); }); it('fetches a page of results with specified page size', () => { return User.query().paginate(1, 2) .then((results) => { expect(results).toBeInstanceOf(Paginator); expect(results.count()).toBe(2); expect(results.total()).toBe(2); expect(results.currentPage()).toBe(1); }); }); it('fetches a page by page number', () => { return User.query().orderBy('id', 'asc').paginate(1, 2) .then((results) => { expect(results.get(0).id).toBe(1); expect(results.get(1).id).toBe(2); }); }); describe('inside a transaction', () => { it('returns consistent results for rowCount and number of models', async () => { // await Post.query().insert({ // user_id: 0, // name: 'a new post' // }); return connection.transaction(async trx => { await Post.query(trx).insert({ user_id: 0, name: 'a new post' }); const posts = await Post.query(trx).paginate(1, 25); expect(posts.total()).toBe(posts.count()); }); }); }); describe('with groupBy', () => { it('counts grouped rows instead of total rows', () => { let total; return Post.query().count().then(count => { total = parseInt(count); return Post.query() // .max('id') .select('user_id') .groupBy('user_id') .whereNotNull('user_id') .paginate(); }).then(posts => { expect(posts.count()).toBeLessThanOrEqual(total); }); }); it('counts grouped rows when using table name qualifier', () => { let total; Post.query().count() .then(count => { total = parseInt(count, 10); return Post.query() // .max('id') .select('user_id') .groupBy('posts.user_id') .whereNotNull('user_id') .paginate(); }) .then(posts => { expect(posts.count()).toBeLe