w33ble / simple-knex-model

A simple model for knex queries
MIT License
7 stars 1 forks source link

simple-knex-model

A simple model for knex queries.

GitHub license Build Status npm Project Status

Features

Inspired a bit by objection, but with way less features. On the plus side, it works with es6 modules.

Usage

yarn add simple-knex-model knex
# or npm install --save simple-knex-model knex

Configure your knex instance and provide it to the base model. This can be done any time before you use a model instance, and subsequent calls are safe.

import BaseModel from 'simple-knex-model';
import knex from 'knex';

const knexConfig = {};
const db = knex(knexConfig);

BaseModel.knex(db);

Then define your models.

import BaseModel from 'simple-knex-model';

export default class User extends BaseModel {
  static get tableName() { // required
    return 'users';
  }

  static get primaryKey() { // optional
    return 'id'; // default value
  }

  static get jsonSchema() { // optional
    return {
      type: 'object',
      required: ['name', 'email'],
      properties: {
        id: { type: 'string' },
        name: { type: 'string' },
        email: { type: 'string', format: 'email' },
        status: { type: 'string', enum: ['pending', 'approved', 'denied'] },
      },
    };
  }
}

If you provide a jsonSchema, documents will be validated with it before they are saved.

API

Model here is your model, which extends BaseModel (simple-knex-model);

new Model(document)

Creates a new instance of the model, ready to be saved. Document is an object.

modelInstance.save()

Inserts the document into the database. Returns a promise. If jsonSchema is defined on the model, it will be used to validate the document before saving, and the Promise will reject if the validation fails.

Model.query()

Returns an instance of knex, with the table name already applied. Use this to execute any of the normal knex operations.

Model.queryById(id)

Returns an instance of knex, with the table name already applied and a where on the primary key for the provided id. Use this to execute any of the normal knex operations on documents with a given id. Most useful when used with .first().

Model.queryWith(relations)

relations is either a key from the relationships object or an array of said keys. Returns an instance of knex, with the table name already applied and the related join queries applied. Use this to execute any of the normal knex operations with your defined joins.

Relationships

Basic relationships are supported, using BaseModel.hasMany, BaseModel.belongsTo, and BaseModel.belongsToMany. The naming should be pretty self-explanatory, but there is plenty of information around that explains how it works. Here's a quick example:

import BaseModel from 'simple-knex-model';

class Chef extends BaseModel {
  static get tableName() {
    return 'chefs';
  }

  static get relationships() {
    return {
      recipes: {
        model: 'Recipe',
        relation: BaseModel.hasMany,
        remote: 'chef_id',
      }
    };
  }
}

class Recipe extends BaseModel {
  static get tableName() {
    return 'recipes';
  }

  static get relationships() {
    return {
      chef: {
        model: 'Chef',
        relation: BaseModel.belongsTo,
        local: 'chef_id',
      }
    };
  }
}

// get all recipes and include the chef's name in the joined results
await results = Recipe.queryWith('recipes').select('chefs.name as chef', 'recipes.*');

Here's the breakdown of the options for each. If you need something more complex, you'll have to craft if by hand directly from the knex instance.

BaseModel.hasMany

Owner model has many child models

property default description
model string - Model name to join with
relation BaseModel.hasMany - The model relationship type
joinType inner string - The join type to use (inner, left, right, leftOuter, rightOuter, fullOuter, cross)
local primaryKey string - The local field to join with
remote string - The remote field to join with

BaseModel.belongsTo

Owner model belongs to child model

property default description
model string - Model name to join with
relation BaseModel.belongsTo - The model relationship type
joinType inner string - The join type to use (inner, left, right, leftOuter, rightOuter, fullOuter, cross)
local string - The local field to join with
remote primaryKey string - The remote field to join with

BaseModel.belongsToMany

Owner model has and belongs to many child models, coordinated through a join table

property default description
model string - Model name to join with
relation BaseModel.belongsToMany - The model relationship type
joinType inner string - The join type to use (inner, left, right, leftOuter, rightOuter, fullOuter, cross)
joinTable string - Table join table to use
local primaryKey string - The local field to join with
joinLocal string - The field in the join table to match on the local field
remote primaryKey string - The remote field to join with
joinRemote string - The field in the join table to match on the remote field

Adding Functionality

simple-knex-model provides 3 hooks which can be used to modify data on the fly. You can use that to create your own custom base model, or even to write plugins. For example, let's say you want to apply a custom id to documents, via uuid or some other method, you can use the onCreate hook to do just that.

import BaseModel from 'simple-knex-model';
import crypto from 'crypto';

const rand = len => crypto.randomBytes(Math.ceil(len / 2)).toString('hex').slice(0, len);

class User extends BaseModel {
  static get tableName() {
    return 'users';
  }

  static onCreate(doc) {
    doc.id = rand(12);
  }
}

const user = new User({ email: 'user@email.co' }); 
console.log(user); // { id: 'daa92600af5d', email: 'user@email.co' }

Now every User document you create will have a random id. You could plug any GUID/UUID functionality you want in here. You can also take this idea further and create your own custom base class that adds this functionality to every model that extends it.

import BaseModel from 'simple-knex-model';
import crypto from 'crypto';

const rand = len => crypto.randomBytes(Math.ceil(len / 2)).toString('hex').slice(0, len);

class CustomModel extends BaseModel {
  static onCreate(doc) {
    doc.id = rand(12);
  }
}

class User extends CustomModel {
  static get tableName() {
    return 'users';
  }
}

class Post extends CustomModel {
  static get tableName() {
    return 'posts';
  }
}

const user = new User({ email: 'user@email.co' }); 
const post = new Post({ title: 'Hello World' });
console.log(user); // { id: 'b8919bf858ab', email: 'user@email.co' }
console.log(post): // { id: 'f624c9ad373c', title: 'Hello World' }

Hooks

onCreate(doc)

Called when a new instance of the model is created. The doc object is passed by reference and can be mutated directly.

beforeValidate(jsonSchema, doc)

Called before the doc is validated against the defined schema. jsonSchema is a shallow clone of the schema defined on the model, and anything returned from this function will be used as the new schema. The doc object is passed by reference and can be mutated directly.

beforeCreate(doc, [returning])

Called after the validation but before the document is written to the database. The doc object is passed by reference and can be mutated directly. All arguments from .insert() are passed directly. This method can return a Promise, and the query won't execute until the Promise resolves.

beforeUpdate(doc, [returning]) / beforeUpdate(key, value, [returning])

Called after the validation but before the document update query is executed. If doc object is provided, is it passed by reference and can be mutated directly. All arguments from .update() are passed directly, so you should check what you get. This method can return a Promise, and the query won't execute until the Promise resolves.

License

MIT © w33ble