Giter Site home page Giter Site logo

bookshelf-modelbase's Introduction

bookshelf-modelbase

Build Status Code Climate Test Coverage Version Downloads

Why

Bookshelf.js is awesome. However, we found ourselves extending bookshelf.Model for the same reasons over and over - parsing and formatting (to and from DB) niceties, adding timestamps, and validating data on save, for example. Since these are problems you'll likely have to solve for most use cases of Bookshelf, it made sense to provide a convenient set of core model features.

Please note

  • bookshelf-modelbase will not force you to use it for all your models. If you want to use it for some and not others, nothing bad will happen.

  • bookshelf-modelbase requires you to pass in an initialized instance of bookshelf, meaning that you can configure bookshelf however you please. Outside of overriding bookshelf.Model, there is nothing you can do to your bookshelf instance that will break bookshelf-modelbase.

Features

  • Adds timestamps (created_at and updated_at)

  • Validate own attributes on save using Joi. You can pass in a validation object as a class attribute when you extend bookshelf-modelbase - see below for usage.

  • Naive CRUD methods - findAll, findOne, findOrCreate, create, update, and destroy

Usage

var db        = require(knex)(require('./knexfile'));
var bookshelf = require('bookshelf')(db);
var Joi = require('joi');
// Pass an initialized bookshelf instance
var ModelBase = require('bookshelf-modelbase')(bookshelf);
// Or initialize as a bookshelf plugin
bookshelf.plugin(require('bookshelf-modelbase').pluggable);

var User = ModelBase.extend({
  tableName: 'users',

  // validation is passed to Joi.object(), so use a raw object
  validate: {
    firstName: Joi.string()
  }
});

User.create({ firstName: 'Grayson' })
.then(function () {
  return User.findOne({ firstName: 'Grayson' }, { require: true });
})
.then(function (grayson) {
  // passes patch: true to .save() by default
  return User.update({ firstName: 'Basil' }, { id: grayson.id });
})
.then(function (basil) {
  return User.destroy({ id: basil.id });
})
.then(function () {
  return User.findAll();
})
.then(function (collection) {
  console.log(collection.models.length); // => 0
})

API

model.create

/**
 * Insert a model based on data
 * @param {Object} data
 * @param {Object} [options] Options for model.save
 * @return {Promise(bookshelf.Model)}
 */
create: function (data, options) {
  return this.forge(data).save(null, options);
}

model.destroy

/**
 * Destroy a model by id
 * @param {Object} options
 * @param {String|Integer} options.id The id of the model to destroy
 * @param {Boolean} [options.require=false]
 * @return {Promise(bookshelf.Model)} empty model
 */
destroy: function (options) {
  options = extend({ require: true }, options);
  return this.forge({ [this.prototype.idAttribute]: options.id })
    .destroy(options);
}

model.findAll

/**
 * Select a collection based on a query
 * @param {Object} [query]
 * @param {Object} [options] Options used of model.fetchAll
 * @return {Promise(bookshelf.Collection)} Bookshelf Collection of Models
 */
findAll: function (filter, options) {
  return this.forge().where(extend({}, filter)).fetchAll(options);
}

model.findById

/**
 * Find a model based on it's ID
 * @param {String} id The model's ID
 * @param {Object} [options] Options used of model.fetch
 * @return {Promise(bookshelf.Model)}
 */
findById: function (id, options) {
  return this.findOne({ [this.prototype.idAttribute]: id }, options);
}

model.findOne

/**
 * Select a model based on a query
 * @param {Object} [query]
 * @param {Object} [options] Options for model.fetch
 * @param {Boolean} [options.require=false]
 * @return {Promise(bookshelf.Model)}
 */
findOne: function (query, options) {
  options = extend({ require: true }, options);
  return this.forge(query).fetch(options);
}

model.findOrCreate

/**
  * Select a model based on data and insert if not found
  * @param {Object} data
  * @param {Object} [options] Options for model.fetch and model.save
  * @param {Object} [options.defaults] Defaults to apply to a create
  * @return {Promise(bookshelf.Model)} single Model
  */
findOrCreate: function (data, options) {
  return this.findOne(data, extend(options, { require: false }))
    .bind(this)
    .then(function (model) {
      var defaults = options && options.defaults;
      return model || this.create(extend(defaults, data), options);
    });
}

model.update

/**
 * Update a model based on data
 * @param {Object} data
 * @param {Object} options Options for model.fetch and model.save
 * @param {String|Integer} options.id The id of the model to update
 * @param {Boolean} [options.patch=true]
 * @param {Boolean} [options.require=true]
 * @return {Promise(bookshelf.Model)}
 */
update: function (data, options) {
  options = extend({ patch: true, require: true }, options);
  return this.forge({ [this.prototype.idAttribute]: options.id }).fetch(options)
    .then(function (model) {
      return model ? model.save(data, options) : undefined;
    });
}

model.upsert

/**
 * Select a model based on data and update if found, insert if not found
 * @param {Object} selectData Data for select
 * @param {Object} updateData Data for update
 * @param {Object} [options] Options for model.save
 */
upsert: function (selectData, updateData, options) {
  return this.findOne(selectData, extend(options, { require: false }))
    .bind(this)
    .then(function (model) {
      return model
        ? model.save(
          updateData,
          extend({ patch: true, method: 'update' }, options)
        )
        : this.create(
          extend(selectData, updateData),
          extend(options, { method: 'insert' })
        )
    });
}

bookshelf-modelbase's People

Contributors

ajbraus avatar alanhoff avatar bsiddiqui avatar chentsulin avatar dependabot[bot] avatar dexfs avatar garbin avatar graysonchao avatar jhamill34 avatar kamronbatman avatar rahulbir avatar thebergamo avatar vankhoawin avatar vellotis avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

bookshelf-modelbase's Issues

Use the models id attribute on CRUD methods

My models use UUIDs as the primary idAttribute and I don't use id at all on any of my tables. I'm finding myself overriding a bunch of the CRUD (create, update, delete) methods to use:

...
tableName: 'user',
idAttribute: 'uuid',
...
create(data, options) {
   ...
   return this.forge({ [this.prototype.idAttribute]: options.id })
   ...
}

I noticed findById already uses { [this.prototype.idAttribute]: options.id }.

Suggestion, always use { [this.prototype.idAttribute]: options.id } instead of id: options.id

Possible to use `findAll` for a WHERE IN query?

The following query works in bookshelf:

Models.Store.where("id", "in", affectedIds)
  .fetchAll()
  ....

I had thought that I could just sub this where clause into findAll and get the same result:

Models.Store.findAll("id", "in", affectedIds)

but looking at the source I noticed that the arguments to findAll get translated into an object.

Is there another way that this query can be performed using findAll?

nice project

...wish I would have found this before I did basically the exact same thing for our project :) This is great and will definitely be useful for others!

static method Model.create issure

I have to specify the options as following to specify the INSERT action
=> MyModel.create( model, { method: "insert" })
or I it will failed with msg [Unhandled rejection CustomError: No Rows Updated]

the packages what I used.
"bookshelf": "^0.10.3",
"bookshelf-modelbase": "^2.10.3",
"knex": "^0.12.6",
"mysql": "^2.13.0",

Thanks in advanced!

Methods to simplify query writing while joining tables

I am a fan of this library. 👍 And just may have a neat suggestion for additional functionality.

I am using some neat methods that have easened where and join queries.

Add to Model static methods...

// Helper
  var formatFieldName = function( fieldName ) {
    if ( ( fieldName != null ? fieldName.trim() : undefined ) !== '*' ) {
      fieldName = "`" + fieldName + "`";
    }
    return fieldName;
  };

// Static methods

  field: function( fieldName ) {
    return bookshelf.knex.raw( "`" + this.prototype.tableName + "`." + formatFieldName( fieldName ) );
  },

  /**
   * Method to 
   */
  alias: function(alias) {
    var result;
    // Raw table alias declaration
    result = Models.Bookshelf.knex.raw("`" + this.prototype.tableName + "` `" + alias + "`");
    // Add "field" method to declared alias
    result.field = function(fieldName) {
      return bookshelf.knex.raw("`" + alias + "`." + (formatFieldName(fieldName)));
    };
    return result;
  }

Example of usage (in CoffeeScript):

  Company.forge().query (_) ->
    _.where Company.field('reg_no'), req.params.reg_no
  .fetch
    require: true
  .then (company) ->

      company
      .transactions()
      .query (_) ->
          employee_person = Person.alias('employee')
          terminals_alias = Terminal.alias('terminals_alias')
          _.whereNotNull(Person.field('personal_id'))
          # Joining
          _.leftOuterJoin Client::tableName, Transaction.field('client_id'), Client.field('id')
          _.leftOuterJoin Person::tableName, Client.field('person_id'), Person.field('id')
          _.leftOuterJoin EmployeeTerminal::tableName, Transaction.field('employee_terminal_id'), EmployeeTerminal.field('id')
          _.leftOuterJoin CompanyEmployee::tableName, EmployeeTerminal.field('company_employee_id'), CompanyEmployee.field('id')
          _.leftOuterJoin employee_person, CompanyEmployee.field('person_id'), employee_person.field('id')
          _.leftOuterJoin terminals_alias, Transaction.field('terminal_id'), terminals_alias.field('id')
          _.leftOuterJoin Company::tableName, terminals_alias.field('company_id'), Company.field('id')
          # ngAdmin pagination
          if req.query._perPage?
            _.limit(req.query._perPage)
            _.offset((req.query._page - 1) * req.query._perPage) if req.query._page?
          # ngAdmin sorting
          _.orderBy(req.query._sortField, req.query._sortDir || 'DESC') if req.query._sortField?
          # Returns
          _.select(
            Transaction.field('*')
            Bookshelf.knex.raw("#{ terminals_alias.field('name') } as terminal_name")
            Bookshelf.knex.raw("#{ Person.field('personal_id') } as client_personal_id")
            Bookshelf.knex.raw("#{ Person.field('first_name') } as client_first_name")
            Bookshelf.knex.raw("#{ Person.field('last_name') } as client_last_name")
            Bookshelf.knex.raw("#{ Company.field('reg_no') } as company_reg_no")
            Bookshelf.knex.raw("#{ employee_person.field('personal_id') } as employee_personal_id")
            Bookshelf.knex.raw("#{ employee_person.field('first_name') } as employee_first_name")
            Bookshelf.knex.raw("#{ employee_person.field('last_name') } as employee_last_name")
            )
      .fetch()

In case this is not a case of discussion be free to close the issue. I would contribute for PR.

Validation of salted password

I am using bookshelf-modelBase to model the user class. As per best-practises I am salting the password before saving it to the DB.
The elegant way of doing it is overriding the .create(), like:

create: function (data, options) {
  data.password = mySalt(data.password);
  return this.forge(data).save(null, options);
}

This works fine but it presents a problem if you also want to perform some validation of the password like:

  validate: {
    password: Joi.string().min(8)
  }

The validation will be performed just before the saving and, by that time, the password has already been salted and it will always be longer that 8 chars.

So, how can I perform the validation on the user input and still save a salted password?

Error on save (regarding validation)

Getting an error when saving:

Unhandled rejection TypeError: Cannot read property 'map' of undefined
    at bookshelf.Model.extend.validateSave (/var/www/dev-app/node_modules/bookshelf-modelbase/lib/index.js:42:55)

Installed versions/code:

├─┬ [email protected]
│ ├─┬ [email protected]

var ApiKey = ModelBase.extend({
    tableName: 'api_keys',
    hasTimestamps: false

    // instance model functions here
}, {
    generate: function(userId, sessionId, minutes) {
        var key = crypto.randomBytes(64).toString('hex').substr(0, 32);
        var expiredAt = moment().add(120, 'minutes').format('YYYY-MM-DD HH:mm:ss');
        return this.findOrCreate({
            'user_id': userId,
            'session_id': sessionId
        }).then(function(model) {
            return model.save({
                'key': key,
                'expired_at': expiredAt
            });
        });
    }
});

unable to use ModelBase::findOrCreate, get 'bind' not a function error

so I've been using ModelBase a little bit in my project, but i'm having this issue, and i can't figure out what's causing it.
it seems to be related to the use of findOne(..).bind()

//  ------ usage.js

    return Users.findOrCreate({ email: _organizer.email }, {
    defaults: {
      email: _organizer.email,
      ...parseNameString(_organizer),
      password: 'default',
      account_status: 'created'
    }})
    .then(organizer => {
      ....
    })
    .catch(err => {
      throw err
    })
}
// --- users.js

const bookshelf = require('../index'),
  ModelBase = require('bookshelf-modelbase')(bookshelf),
;
const Users = ModelBase.extend({
  tableName: 'users',
}, {
  findByEmail: function (email) {
    return this.forge().query({ where: { email: email }}).fetch({ require: true })
  },
})

module.exports = bookshelf.model('Users', Users)

autocomplete issue when working on Intellij IDEA

unfortunatly, it seems that the autocomplete utility is not supported yet.
the autocomplete works just fine with bookshelf methods, but not with bookshelf-modelbase methods like orWhere, findOne ....
Is there any solutions?
Thanks

update method

@bsiddiqui had a question for you regarding the update method:

Is there a reason to fetch the model first before making the update? In the past when I've done raw update queries, I haven't fetched the model beforehand.

I was actually trying to find a way to do a direct update in which there are multiple where clauses, but save() only uses the id of the record. See the issue I posted over on the Bookshelf repo. I realize it could be done with Knex, but it seems like there should be a way to do this without jumping into knex.

Sorry to open this up here, but am curious to the approach.

Best, James

upsert fails with application-assigned IDs

Upsert fails with application-assigned IDs. It's kind of a weird one. For example, if I have a model with two fields, "id" and "something", and I specify the ID to upsert:

MyModel.upsert({id:id},{something:something})

If the row isn't in the DB yet this will fail with "no rows inserted". The reason it fails is a Bookshelf quirk:

MyModel.forge({id:id,something:something}).save()

The quirk is that will always generate an update, never an insert, because save() only inserts if !isNew() but isNew() == true when the id is passed to forge(). It's sort of a design flaw-ish kind of thing.

Now, this can normally be worked around by passing {method:"insert"} to save when you want to force an insert. The problem as it relates to modelbase, though, is that you can't pass {method:"insert"} to upsert.

The reason you can't pass it is that upsert passes the same option set to both save and create in its little ternary expression, but in the case that the model exists, patch:true is also passed, and {patch:true,method:"insert"} is not a valid option set, and so the upsert fails if the model exists. Without method:"insert", upsert fails if the model doesn't exist.

Therefore, upsert always fails with user-assigned primary keys.

I'm not really sure what a good approach is. Things I can think of:

  • Nag bookshelf to ignore "method" if "patch" is set. Seems unlikely.
  • Redesign bookshelf. Seems unlikely.
  • Force inclusion of "method:insert" in the upsert implementation when the model doesn't exist. I am pretty sure this will solve the problem without breaking anything, and it's probably the best solution.
  • Something else?

It's a weirdly roundabout problem...

Manage multtiple models

Hi Basil,

I was just wondering and trying to use bookshelf-modelbase in hapi.js application.

But cant figure out the proper way to use it.

I am putting it in a plugin like:
var Knex = require('knex');
const databaseConfig = JSON.parse(JSON.stringify(config.get('database')));

exports.register = function(server, options, next) {
var knex = Knex(databaseConfig);
var bookshelf = require('bookshelf')(knex);
bookshelf.plugin(require('bookshelf-modelbase').pluggable);
server.expose('bookshelf', bookshelf);

next();

};

exports.register.attributes = {
name: 'database'
};

I wanna use it for multiple models,
I dont want to write first two lines in every model :

var ModelBase = require('bookshelf-modelbase')(bookshelf);
// Or initialize as a bookshelf plugin
bookshelf.plugin(require('bookshelf-modelbase').pluggable);

var User = ModelBase.extend({
tableName: 'users',

// validation is passed to Joi.object(), so use a raw object
validate: {
firstName: Joi.string()
}
});

Basically I am not able to figure out how to use it in multiple models
And how to use those models in a function?
Thanks

es6 implementation done

Hi !

I've re-implemented modelbase in es6
I wanted to make a PR but :

  • it changes a lot of the original code
  • I've added a few new methods that are useful to me

So I created my own repo, but if you like it, tell me and I'll gladly PR you !
the repo is here
Please note that I can remove the new methods if you want to merge without them.

Upsert not working when querying for multiple fields?

So I've been able to successfully use the upsert function to update data when querying for a single field, but when passing a has of multiple fields I get the following error:

Error: A model cannot be updated without a "where" clause or an idAttribute.

For example, this works:

return Models.Artist
      .upsert({ name: name }, { website: website })
      .then(model => model.toJSON())

Where Artist has 2 columns: name and website and I want to search existing artists by name, update their website field if the artist already exists, create if not found.

But this does not work:

return Models.Printing
        .upsert({ card: card, set: set, number: number }, { ...fields })
        .then(model =>  model.toJSON())

Where Printing has about a dozen fields (which I am using spread syntax here as the fields are irrelevant), and I want to search existing printings by card, set, and number, ie:

SELECT * FROM printing WHERE card = '3' AND set = '2' AND number = '5'

Of course this will work when a row does not already exist, because it will be created instead. The problem only arises when trying to update an existing row.

For reference, Printing does have an ID attribute, so if a selection is successfully made the result should have an ID attribute, so I don't know why that would cause the error.

I've tried re-writing upsert in different ways, such as:

upsert: function (selectData, updateData, options) {
  return this.findOne(selectData, extend(options, { require: false }))
    .bind(this)
    .then(function (model) {
      return model
        ? model.where(selectData).save(
          updateData,
          extend({ patch: true, method: 'update' }, options)
        )
        : this.create(
          extend(selectData, updateData),
          extend(options, { method: 'insert' })
        )
    });
}

To ensure that if there was not an ID attribute, the where condition was met as per the error message. But doing so results in a model being returned with only the selectData attributes.

I'm not sure what to do from here short of dropping down into Knex and trying to be more explicit in constructing my queries than the methods provided by Bookshelf. That seems unnecessarily complicated for what should be a simple problem.

update return Undefined binding(s) detected when compiling SELECT query

my use case is this
I have products that belongs to a category, I want that if that category is deleted for the products in it to go to another category "uncategorized"

my code

import Product from "./Product"

const Category = bookshelf.Model.extend({
  tableName: "Categories",
  products: function() {
    return this.hasMany(Product)
  },
  initialize: function() {
    this.on("destroying", (model, options) => {
      Category.findOrCreate({name: "Uncategorized"}).then(uncategorized => {
        Product.update({category_id: model.id}, {category_id: uncategorized.id})
      }).catch(err => {
        throw err
      })
    })
  }
})

it keeps return this error

Unhandled rejection Error: Undefined binding(s)
detected when compiling SELECT query:
select `Products`.* from `Products` where `Products`.`id` = ? limit ?

console debug

{ method: 'del',
  options: {},
  timeout: false,
  cancelOnTimeout: false,
  bindings: [ 1 ],
  __knexQueryUid: 'c62d4111-1d54-4faf-926e-0aee92ec6f3e',
  sql: 'delete from `Categories` where `id` = ?' }
{ method: 'select',
  options: {},
  timeout: false,
  cancelOnTimeout: false,
  bindings: [ 'Uncategorized', 1 ],
  __knexQueryUid: '19e02681-b4a4-493e-a6a2-fe5bdeacdc6a',
  sql: 'select `Categories`.* from `Categories` where `Categories`.`name` = ? limit ?' }

Create Method to findOrCreate based on id or query object.

the method findOrCreate() is great. I need to find or create in the database based off of a single property as apposed to the entire object. Ideally we will have a findOrCreateById() implemented in the same fashion as findById(), and a custom findOrCreateByProperty() which can accept a query object.

Additionally, new tests should be created for both new methods. Tests must pass.

As an example, we will check for a single property, username, and find or create a record based on this data point. From there, return the data from the promise object returned as a means of demonstration.

// bring in user table
var User = ModelBase.extend({
    tableName: 'users'
});

// find or create query with custom property object.
User.findOrCreateByProperty( {
  username: 'yourUserNameHere',
  age: 25,
  company: 'yourCompanyHere',
  email: 'yourEmailHere',
  location: 'yourLocationHere',
  name: 'yourNameHere'
}, null, { username: 'yourUserNameHere' }).then((collection) => {
  if(collection){
    return collection.attributes;
  }
});

PR inbound!!
--thexande

Canceling timestamp field

Whenever I create new model it requires table to have created_at and updated_at fields. Most of tables does not have these fields and I do not need them.
Is it possible to cancel created_at and updated_at fields.

Support typescript

The bookshelf supports typescript so it is quite uncomfortable to use the plugin with typescript since it is not supported. I wonder if you have any prediction for typescript support?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.