jbrumwell / mock-knex

A mock knex adapter for simulating a database during testing
MIT License
239 stars 71 forks source link

Related Models w/ Bookshelf #51

Closed robinduckett closed 7 years ago

robinduckett commented 7 years ago

Hi Guys,

I have a more detailed example of the problem I'm getting with related models with Bookshelf

let driver = require('knex');

let mockDb = require('mock-knex');

let knex = driver({
  dialect: 'sqlite3',
  connection: {
    filename: ':memory:'
  },
  createTableIfNotExists: true,
  useNullAsDefault: true
});

let contentForge, layerForge, slotContentForge, slotForge;

let Slot, slotModel;

let tracker;

knex.schema.createTableIfNotExists('layer', function (table) {
  table.increments();
  table.integer('channel_id');
  table.integer('sequence');
  table.string('name');
  table.integer('pixel_width');
  table.integer('pixel_height');
  table.string('diagonal_size');
}).createTableIfNotExists('content', function (table) {
  table.increments('id').primary();
  table.integer('content_type_id');
  table.string('filename');
  table.string('size');
  table.integer('pixel_width');
  table.integer('pixel_height');
}).createTableIfNotExists('slot', function (table) {
  table.increments('id').primary();
  table.integer('slot_content_id').references('slot_content');
  table.integer('layer_id').references('layer');
  table.integer('schedule_id');
  table.integer('day');
  table.time('start_time');
  table.time('end_time');
  table.integer('priority');
  table.integer('play_through');
  table.integer('locked');
  table.string('source');
}).createTableIfNotExists('slot_content', function (table) {
  table.increments('id').primary();
  table.integer('content_id').references('content');
  table.integer('slot_id').references('slot');
  table.integer('stamp');
})
.then(() => {
  mockDb.mock(knex, 'knex@0.11');

  tracker = mockDb.getTracker();

  tracker.on('query', function(q) {
    console.log(q);
  });

  let bookshelf = require('bookshelf')(knex);

  let Layer = bookshelf.Model.extend({
    tableName: 'layer'
  });

  let Content = bookshelf.Model.extend({
    tableName: 'content'
  });

  let SlotContent = bookshelf.Model.extend({
    tableName: 'slot_content',
    defaults: function() {
      return {
        stamp: '0'
      };
    }
  });

  Slot = bookshelf.Model.extend({
    tableName: 'slot',
    layer: function() {
      return this.belongsTo(Layer, 'layer_id');
    },

    available_contents: function() {
      let contents = this.belongsToMany(Content).through(SlotContent);
      return contents;
    },

    default_content: function() {
      let defaultContent = this.belongsTo(Content).through(SlotContent, 'slot_content_id');
      return defaultContent;
    }
  });

  contentForge = Content.forge({
    id: 1,
    content_type_id: 1,
    filename: 'test.zip',
    size: '12mb',
    pixel_width: 1920,
    pixel_height: 1080
  });

  slotContentForge = SlotContent.forge({
    id: 1,
    content_id: 1,
    slot_id: 1,
    stamp: 8
  });

  layerForge = Layer.forge({
    id: 1,
    channel_id: 1,
    sequence: 1,
    name: 'Test Layer',
    pixel_width: 1920,
    pixel_height: 1080,
    diagonal_size: '32"'
  });

  slotForge = Slot.forge({
    id: 1,
    slot_content_id: 1,
    layer_id: 1,
    schedule_id: 1,
    day: 1,
    start_time: '00:00:00',
    end_time: '00:01:00',
    priority: 1,
    play_through: 1,
    locked: 0,
    source: 'hq'
  });

  return contentForge.save();
})
.then(() => slotContentForge.save())
.then(() => layerForge.save())
.then(() => slotForge.save())
.then(() => {
  return slotForge.fetch({ withRelated: ['layer', 'available_contents', 'default_content'] })
})
.then(model => {
  console.log(model);
  slotModel = model;
  console.log(model.related('available_contents'));
  let availableContentsModel = slotModel.relations.available_contents;
  console.log(slotModel.relations);
  return availableContentsModel.attach([1]);
})
.then(relatedAvailableContentsCollection => {
  return relatedAvailableContentsCollection
    .fetch()
    .then(relatedAvailableContents => {
      let defaultRelatedContentRecord = relatedAvailableContents.find(relatedAvailableContent => {
        return relatedAvailableContent.attributes.id === 1;
      });

      return slotModel
        .save({ slot_content_id: defaultRelatedContentRecord.pivot.id });
    });
})
.then(() => {
  Slot.fetchAll().then(result => {
    console.log(result);
  }).catch(err => {
    console.error(err);
  });
}).catch(err => {
  console.error(err);
});

I'm really sorry if I'm just misunderstanding how I should be using mock-knex, but I'm wondering if the relationships are being mapped properly.

jbrumwell commented 7 years ago

Your tracker isn't returning any responses?

tracker.on('query', function(q, step) {
  console.log(q);
});

You can inspect the query object and use step parameter to return the data it requires. If your not sure you can knex's events using a live unmocked db to get the information you need.

http://knexjs.org/#Interfaces-Events

Likely either query and query-response or you may be able to get all the information from query-response.

Cheers,

Jason

robinduckett commented 7 years ago

Hi Jason,

I have a more succinct example:


let driver = require('knex');
let mockDb = require('mock-knex');

let knex = driver({
  dialect: 'sqlite3',
  connection: {
    filename: ':memory:'
  },
  createTable: true,
  useNullAsDefault: true
});

Promise.all([
  knex.schema.createTable('users', function (table) {
    table.increments('id');
    table.string('username');
  }),

  knex.schema.createTable('messages', function (table) {
    table.increments('id').primary();
    table.integer('user_id').references('users');
    table.string('body');
  }),

  knex.schema.createTable('messages_tags', function (table) {
    table.increments('id').primary();
    table.integer('message_id').references('messages');
    table.integer('tag_id').references('tags');
  }),

  knex.schema.createTable('tags', function (table) {
    table.increments('id').primary();
    table.string('name');
  }),

  knex.insert({
    username: 'robin'
  }).into('users'),

  knex.insert({
    user_id: 1,
    body: 'Message Test'
  }).into('messages'),

  knex.insert({
    name: 'uncategorized'
  }).into('tags'),

  knex.insert({
    tag_id: 1,
    message_id: 1
  }).into('messages_tags')
]).then(function() {
  console.log('mocking');

  mockDb.mock(knex, 'knex@0.11');

  let tracker = mockDb.getTracker();

  tracker.on('query', function(q) {
    console.log(q);
  });

  var bookshelf = require('bookshelf')(knex);

  var User = bookshelf.Model.extend({
    tableName: 'users',
    posts: function() {
      return this.hasMany(Posts);
    }
  });

  var Posts = bookshelf.Model.extend({
    tableName: 'messages',
    tags: function() {
      return this.belongsToMany(Tag);
    }
  });

  var Tag = bookshelf.Model.extend({
    tableName: 'tags'
  });

  User.where('id', 1).fetch({withRelated: ['posts.tags']}).then(function(user) {
    console.log(JSON.stringify(user.related('posts').toJSON(), null, 2));
  }).catch(function(err) {
    console.error(err);
  });
});

When running this code without the mock-knex code, it runs fine, and returns the correct data.

When running this code with the mock-knex code, it fails to function, and returns an error.

TypeError: Cannot read property 'related' of null
    at .<anonymous> (/home/robin/devtesting/test.js:86:36)
    at tryCatcher (/home/robin/devtesting/node_modules/bookshelf/node_modules/bluebird/js/main/util.js:26:23)
    at Promise._settlePromiseFromHandler (/home/robin/devtesting/node_modules/bookshelf/node_modules/bluebird/js/main/promise.js:510:31)
    at Promise._settlePromiseAt (/home/robin/devtesting/node_modules/bookshelf/node_modules/bluebird/js/main/promise.js:584:18)
    at Async._drainQueue (/home/robin/devtesting/node_modules/bookshelf/node_modules/bluebird/js/main/async.js:128:12)
    at Async._drainQueues (/home/robin/devtesting/node_modules/bookshelf/node_modules/bluebird/js/main/async.js:133:10)
    at Immediate.Async.drainQueues [as _onImmediate] (/home/robin/devtesting/node_modules/bookshelf/node_modules/bluebird/js/main/async.js:15:14)
    at tryOnImmediate (timers.js:543:15)
    at processImmediate [as _immediateCallback] (timers.js:523:5)
robinduckett commented 7 years ago

I have checked this code with the latest develop branch of mock-knex and it seems to work, I will continue testing.

jbrumwell commented 7 years ago

@robinduckett for your example above you will change your tracker to look like

tracker.on('query', (query, step) => {
          let result;

          switch (step) {
            case 1:
              result = [{
                id : 1,
              }];
              break;

            case 2:
              result = [
                {
                  id : 1,
                  user_id : 1,
                  message : 'this is a message',
                },
                {
                  id : 2,
                  user_id : 1,
                  message : 'this is another message',
                },
                {
                  id : 3,
                  user_id : 1,
                  message : 'this is a third message',
                },
              ]
              break;

            case 3:
                result = [];
                break;
          }

          query.response(result);
        });

The reason you were getting Cannot read property 'related' of null is because you are not returning a response so it becomes null instead of a model.