npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

next-model

v0.4.1

Published

Rails like models using ES6.

Downloads

11

Readme

NextModel

Rails like models using ES6. Build Status

NextModel gives you the ability to:

  • Represent models and their data.
  • Represent associations between these models.
  • Represent inheritance hierarchies through related models.
  • Perform database operations in an object-oriented fashion.
  • Uses Promises for database queries.

Roadmap / Where can i contribute

See GitHub project for current progress/tasks

  • Fix typos

  • Improve documentation

  • createdAt and updatedAt timestamps

  • Predefined and custom validations

  • Improve schema with eg. default values, limits

  • Improve associations eg. cascading deletions

  • Add more packages for eg. versioning and soft deleting

  • There are already some tests, but not every test case is covered.

  • Add more connectors for eg. graphQL and dynamoDB

  • includes prefetches relations with two db queries (fetch records => pluck ids => fetch related records by ids) instead of one query per related model.

    User.includes({address: {}}), Profile.includes({user: {address: {}}})

  • Add a solution to create Migrations

TOC

Naming Conventions

To keep the configuration as short as possible, its recommended to use the following conventions:

  • Everything is in camel case.
    • createdAt
    • someOtherValue
  • Classes should be singular start with an capital letter.
    • Address
    • Chat
    • ChatMessage
  • Foreign keys are the class names starting with an lower case character and end with Id.
    • User - userId
    • Chat - chatId
    • ChatMessage - chatMessageId

Example

// import
const NextModel = require('next-model');

// model definitions
User = class User extends NextModel {
  static get modelName() {
    return 'User';
  }

  static get schema() {
    return {
      id: { type: 'integer' },
      firstName: { type: 'string' },
      lastName: { type: 'string' },
      gender: { type: 'string' },
    };
  }

  static get hasMany() {
    return {
      addresses: { model: Address },
    };
  }

  static get males() {
    return this.scope({ where: { gender: 'male' }});
  }

  static get females() {
    return this.scope({ where: { gender: 'female' }});
  }

  static get withFirstName(firstName) {
    return this.scope({ where: { firstName }});
  }

  get name() {
    return `${this.firstName} ${this.lastName}`;
  }
};

Address = class Address extends NextModel {
  static get modelName() {
    return 'Address';
  }

  static get schema() {
    return {
      id: { type: 'integer' },
      street: { type: 'string' },
    };
  }

  static get belongsTo() {
    return {
      user: { model: User },
    };
  }
};

// Creating
user = User.build({ firstName: 'John', lastName: 'Doe' });
user.gender = 'male';
user.name === 'John Doe';
user.save().then(user => ... );

User.create({
  firstName: 'John',
  lastName: 'Doe',
  gender: 'male',
}).then(user => ... );

user.addresses.create({
  street: 'Bakerstr.'
}).then(address => ...);

// Searching
User.males.all.then(users => ... );
User.withFirstName('John').first(user => ... );
User.addresses.all.then(addresses => ... );
User.where({ lastName: 'Doe' }).all.then(users => ... );
User.males.order({ lastName: 'asc' }).all.then(users => ... );

Model Instances

Build

Initializes new record without saving it to the database.

address = Address.build({ street: '1st street' });
address.isNew === true;

Create

Returns a Promise which returns the created record on success or the initialized if sth. goes wrong.

Address.create({
  street: '1st street'
}).then(address => {
  address.isNew === false;
}).catch(address => {
  address.isNew === true;
});

From Scopes and queries

An record can be build or created from scopes. These records are created with scope values as default.

address = user.addresses.build();
address.userId === user.id;

user = User.males.build();
user.gender === 'male';

user = User.withName('John').build();
user.name === 'John';

user = User.where({ gender: 'male'}).build();
user.gender === 'male';

Relations

Define the Model associations. Describe the relation between models to get predefined scopes and constructors.

Belongs To

Address = class Address extends NextModel {
  static get belongsTo() {
    return {
      user: { model: User },
    }
  }
};

Address.create({
  userId: id
}).then(address => {
  return address.user;
}).then(user => {
  user.id === id;
});

address = Address.build();
address.user = user;
address.userId === user.id;

Has Many

User = class User extends NextModel {
  static get hasMany() {
    return {
      addresses: { model: Address },
    }
  }
};

user.addresses.all.then(addresses => ... );
user.addresses.create({ ... }).then(address => ... );

Has One

User = class User extends NextModel {
  static get hasOne() {
    return {
      address: { model: Address },
    }
  }
};

user.address.then(address => ... );

Searching

Where

Special query syntax is dependent on used connector. But all connectors and the cache supports basic attribute filtering.

User.where({ gender: 'male' });
User.where({ age: 21 });
User.where({ name: 'John', gender: 'male' });
User.males.where({ name: 'John' });

Order

The fetched data can be sorted before fetching then. The order function takes an object with property names as keys and the sort direction as value. Valid values are asc and desc.

User.order({ name: 'asc' });
User.order({ name: 'desc' });
User.order({ name: 'asc', age: 'desc' });
User.males.order({ name: 'asc' });

Scopes

Scopes are predefined search queries on a Model.

User = class User extends NextModel {
  static get males() {
    return this.scope({ where: { gender: 'male' }});
  }

  static get females() {
    return this.scope({ where: { gender: 'female' }});
  }

  static get withName(name) {
    return this.scope({ where: { name }});
  }
};

Now you can use these scopes to search/filter records.

User.males;
User.withName('John');

Scopes can be chained with other scopes or search queries.

User.males.withName('John');
User.withName('John').where({ gender: 'transgender' });

Build from scope

profile = User.males.build();
profile.gender === 'male';

Scope chaining

User.males.young;
User.males.young.where({});

Fetching

If you want to read the data of the samples of the previous section you can fetch if with the following functions. Each fetching function will return a Promise to read the data.

All

Returns all data of the query. Results can be limited by skip and limit.

User.all.then(users => ...);
User.males.all.then(users => ...);
User.where({ name: 'John' }).all.then(users => ...);

First

Returns the first record which matches the query. Use order to sort matching records before fetching the first one.

User.first.then(user => ...);
User.males.first.then(user => ...);
User.where({ name: 'John' }).first.then(user => ...);
User.order({ name: 'asc' }).first.then(user => ...);

Last

Returns last matching record. Needs to have an order set to work properly.

User.last.then(user => ...);
User.males.last.then(user => ...);
User.where({ name: 'John' }).last.then(user => ...);
User.order({ name: 'asc' }).last.then(user => ...);

Count

Returns the count of the matching records. Ignores order, skip and limit and always returns complete count of matching records.

User.count.then(count => ...);
User.males.count.then(count => ...);
User.where({ name: 'John' }).count.then(count => ...);
User.count === User.limit(5).count

Class Properties

Model Name

The model name needs to be defined for every model. The name should be singular camelcase, starting with an uppercase char.

class User extends NextModel {
  static get modelName() {
    return 'User';
  }
}

class UserAddress extends NextModel {
  static get modelName() {
    return 'UserAddress';
  }
}

Schema

A schema describes all (database stored) properties. Foreign keys from relations like belongsTo are automatically added to the schema. The existing types and their names are depending on the used Database connector.

class User extends NextModel {
  static get schema() {
    return {
      id: { type: 'integer' },
      name: { type: 'string' },
    };
  }
}

Connector

A connector is the bridge between models and the database.

Available connectors:

The connector needs to be returned as static getter.

Connector = require('next-model-knex-connector');
connector = new Connector(options);

class User extends NextModel {
  static get connector() {
    return connector;
  }
}

Define an base model with connector to prevent adding connector to all Models.

class BaseModel extends NextModel {
  static get connector() {
    return connector;
  }
}

class User extends BaseModel {
  ...
}

class Address extends BaseModel {
  ...
}

Identifier

Defines the name of the primary key. Default values is id.

class User extends BaseModel {
  static get identifier() {
    return 'key';
  }
}

You also need to define your identifier on the schema.

class User extends BaseModel {
  static get identifier() {
    return 'uid';
  }

  static get schema() {
    return {
      id: { type: 'uuid' },
      ...
    };
  }
}

Table Name

The table name is usually generated from modelName. Default without connector is camelcase plural, starting with lowercase char.

class User extends NextModel {
  static get modelName() {
    return 'User';
  }
}

User.tableName === 'users';

class UserAddress extends NextModel {
  static get modelName() {
    return 'UserAddress';
  }
}

UserAddress.tableName === 'userAddresses';

Table name generation is depending on the used connector. The following examples are for knex connector.

class User extends NextModel {
  static get modelName() {
    return 'User';
  }
}

User.tableName === 'users';

class UserAddress extends NextModel {
  static get modelName() {
    return 'UserAddress';
  }
}

UserAddress.tableName === 'user_addresses';

You can also manually define an custom table name.

class User extends NextModel {
  static get modelName() {
    return 'app_users';
  }
}

The table name could also be a function.

class BaseModel extends NextModel {
  static get modelName() {
    return 'app_' + underscore(this.modelName);
  }
}

class User extends BaseModel { ... }
User.tableName === 'app_users';

class UserAddress extends BaseModel { ... }
UserAddress.tableName === 'app_user_addresses';

Accessors

Accessors define properties which can be passed to build, create functions or assignments, but are not passed to the database. Use them to store temporary data like passing values to model but not to database layer.

class User extends NextModel {
  static get accessors {
    return [
      'checkForConflicts',
    ];
  }
}

user = User.build({ checkForConflicts: true });
user.checkForConflicts === true;

user = User.build({ foo: 'bar' });
user.foo === undefined;

Cache

Caching is currently only working when the cache is manually filled. All searching and fetching are supported.

User.cache = [
  { gender: 'male', name: 'John' },
  { gender: 'male', name: 'Doe' },
];
User.first.then(user => user.name === 'John');
User.order({ name: 'asc' }).first.then(user => user.name === 'Doe');
User.where( ... );

Cache is cleared when you fetch an base model out of an class, or when you unscope or reload the data. But cache is only cleared in this scope, the model itself is not modified.

User.cache = [ ... ];

User.model.cache === undefined;
User.reload.cache === undefined;
User.cache === [ ... ];

Default Scope

Adds an default scope for all queries made on the model. You need to unscope the property to search without the default scope.

class User extends NextModel {
  static get defaultScope {
    return {
      deletedAt: null,
    };
  }
}

User.first.then(user => user.deletedAt === null);
User.unscope('deletedAt').where( ... );

Default Order

Adds an default Order to all queries unless its overwritten.

class User extends NextModel {
  static get defaultOrder {
    return {
      name: 'asc',
    };
  }
}

Instance Attributes

isNew

An record is new unless the record is saved to the database. NextModel checks if the identifier property is set for this attribute.

address = Address.build();
address.isNew === true;
address.save().then(address => {
  address.isNew === false;
});

isPersistent

The opposite of isNew. Returns false unless the record is not saved to the database.

address = Address.build();
address.isPersistent === false;
address.save().then(address => {
  address.isPersistent === true;
});

attributes

Returns an object which contains all properties defined by schema and attrAccessors.

class Address extends NextModel {
  static get schema() {
    return {
      street: { type: 'string' },
      city: { type: 'string' },
    };
  }

  static get attrAccessors() {
    return ['fetchGeoCoord'];
  }
}

address = Address.build({
  street: '1st street',
  city: 'New York',
  fetchGeoCoord: false,
});
address.foo = 'bar';

address.attributes() === {
  street: '1st street',
  city: 'New York',
  fetchGeoCoord: false
};

databaseAttributes

Returns an object which contains all properties defined only by schema.

class Address extends NextModel {
  static get schema() {
    return {
      street: { type: 'string' },
      city: { type: 'string' },
    };
  }

  static get attrAccessors() {
    return ['fetchGeoCoord'];
  }
}

address = Address.build({
  street: '1st street',
  city: 'New York',
  fetchGeoCoord: false,
});
address.foo = 'bar';

address.databaseAttributes() === {
  street: '1st street',
  city: 'New York',
};

isChanged

When you change a fresh build or created Class instance this property changes to true.

address = Address.build({
  street: '1st street',
  city: 'New York',
});
address.isChanged === false;
address.street = '2nd street';
address.isChanged === true;

This property does not change when the value is same after assignment.

address = Address.build({
  street: '1st street',
  city: 'New York',
});
address.isChanged === false;
address.street = '1st street';
address.isChanged === false;

changes

The changes property contains an Array of changes per property which has changed. Each entry contains an from and to property.

address = Address.build({
  street: '1st street',
  city: 'New York',
});
address.changes === {};
address.street = '2nd street';
address.changes === {
  street: [
    { from: '1st street', to: '2nd street' },
  ],
};
address.street = '3rd street';
address.changes === {
  street: [
    { from: '1st street', to: '2nd street' },
    { from: '2nd street', to: '3rd street' },
  ],
};

This property does not change when the value is same after assignment.

address = Address.build({
  street: '1st street',
  city: 'New York',
});
address.changes === {};
address.street = '1st street';
address.changes === {};

Custom Attributes

class User extends NextModel {
  static get schema() {
    return {
      firstname: {type: String},
      lastname: {type: String}
    }
  }

  get name() {
    return `${this.firstName} ${this.lastName}`;
  }
}

user = User.build({
  firstname: 'Foo',
  lastname: 'Bar'
});
user.name === 'Foo Bar';

Instance Callbacks

With callbacks you can run code before or after an action. Actions which currently supports callbacks are save. Callbacks are always named before{Action} and after{Action}. Callbacks can be defined in different ways. Callbacks can be functions, redirects or arrays.

Note: Actions which are Promises also support Promises as callbacks.

Callback can be a function:

class User extends NextModel {
  beforeSave() { ... }
}

Callback can return a function:

class User extends NextModel {
  get beforeSave() {
    return function() { ... }
  }
}

Callback can redirect to a function with a string:

class User extends NextModel {
  get beforeSave() {
    return 'prefillDefaults';
  }

  prefillDefaults() { ... }
}

Callback be an array of Strings:

class User extends NextModel {
  get beforeSave() {
    return ['prefillDefaults', 'setTimestamps'];
  }

  prefillDefaults() { ... }

  setTimestamps() { ... }
}

Callback be an array of mix between functions and redirects:

class User extends NextModel {
  get beforeSave() {
    return ['prefillDefaults', function() { ... }];
  }

  prefillDefaults() { ... }
}

Callback follow also multiple redirects and mixed arrays:

class User extends NextModel {
  get beforeSave() {
    return ['prefillActions', function() { ... }];
  }

  get prefillActions() {
    return ['prefillDefaults', 'setTimestamps']
  }

  prefillDefaults() { ... }

  setTimestamps() { ... }
}

Before Actions are always all executed. If any callback before the action runs on an Error the Action will not be executed. If the Action runs on an Error the after callbacks will not be executed.

Note: The execution order of callbacks can not be guaranteed, they run in parallel if possible.

Platform Specific Callbacks

NextModel can be used with Browser and Node.js. When this package is used on Server and Client side it might be useful to have different callbacks on Server and Client. Each callback can be postfixed with Client or Server to use this callback just on Server or Client.

Use generic callback to run it on both Platforms.

class User extends NextModel {
  get beforeSave() {
    ...
  }
}

Postfix callback with Server to just use this callback when running with Node.js.

class User extends NextModel {
  get beforeSaveServer() {
    ...
  }
}

Postfix callback with Client to just use this callback when running within Browser.

class User extends NextModel {
  get beforeSaveClient() {
    ...
  }
}

Change Callbacks

There is an global afterChange callback and one additional per property named after${propertyName}Change. Even on multiple assignments with one call, afterChange is called for every single property changed.

class User extends NextModel {
  static afterChange() {
    ...
  }

  static afterNameChange() {
    ...
  }
}

Build Callbacks

When promiseBuild is triggered the callback order is: beforeBuild -> promiseBuild -> afterBuild

class User extends NextModel {
  static beforeBuild() {
    ...
  }

  static afterBuild() {
    ...
  }
}

Create Callbacks

When create is triggered the callback order is: beforeCreate -> beforeBuild -> promiseBuild -> afterBuild -> beforeSave -> save -> afterSave -> afterCreate

class User extends NextModel {
  static beforeCreate() {
    ...
  }

  static afterCreate() {
    ...
  }
}

Save Callbacks

When save is triggered the callback order is: beforeSave -> save -> afterSave

class User extends NextModel {
  beforeSave() {
    ...
  }

  afterSave() {
    ...
  }
}

Delete Callbacks

When delete is triggered the callback order is: beforeDelete -> delete -> afterDelete

class User extends NextModel {
  beforeDelete() {
    ...
  }

  afterDelete() {
    ...
  }
}

Instance Actions

assign

You can assign a new value to an schema or accessor defined property. This does not automatically save the data to the database.

address.assignAttribute('street', '1st Street');
address.assign({
  street: '1st Street',
  city: 'New York',
});

save

Saves the record to database. Returns a Promise with the created record including its newly created id. An already existing record gets updated.

address = Address.build({street: '1st street'});
address.save().then(
  (address) => address.isNew === false;
).catch(
  (address) => address.isNew === true;
);
address.street = 'changed';
address.save().then( ... );

delete

Removes the record from database. Returns a Promise with the deleted record.

address.isNew === false;
address.delete().then(
  (address) => address.isNew === true;
).catch(
  (address) => address.isNew === false;
);

reload

Fetches all schema properties new from database. All other values stay untouched. Returns a Promise with the reloaded record.

address.isNew === false;
address.street = 'changed';
address.notAnDatabaseColumn = 'foo';
address.reload().then((address) => {
  address.name === '1st Street';
  address.notAnDatabaseColumn === 'foo';
});

Changelog

See history for more details.

  • 0.0.1 2017-01-23 Initial commit with query and scoping functions
  • 0.0.2 2017-02-05 Published knex connector
  • 0.0.3 2017-02-12 Added CI
  • 0.0.4 2017-02-16 Added callbacks for build, create, save and delete
  • 0.1.0 2017-02-23 Added Browser compatibility
  • 0.2.0 2017-02-25 Improved browser compatibility
  • 0.3.0 2017-02-27 Tracked property changes
  • 0.4.0 2017-02-28 Added platform specific callbacks
  • 0.4.1 2017-04-05 Bugfix: before and after callback