No description, website, or topics provided.
Switch branches/tags
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
dmd-plugin
src
tests
.babelrc
.eslintrc
.gitignore
.travis.yml
README.md
package.json
readme.hbs

README.md

Atlas

Build Status codecov Gitter

Data mapper implementation for JavaScript. Atlas provides a fluent interface for manipulating a relational database.

Atlas uses Knex.js to build and execute queries.

Atlas is not yet released

Atlas is thoroughly tested, but barely used. The API may change (probably not dramatically). Posting feedback and questions to the issue tracker is encouraged.

Currently Atlas is only tested with PostgreSQL, but Knex's portability means that it should (ultimately) work with any Knex supported DBMS.

Installation

Not yet on npm, install direct from GitHub:

$ npm install rhys-vdw/atlas --save
$ npm install knex --save

Configuration

// atlas-instance.js

import Knex from 'knex';
import Atlas from 'atlas';

const knex = Knex({
  client: 'postgres',
  connection: {
    database: 'my_database',
    user: 'user'
  }
});

export default Atlas({ knex });

Usage

Use configured Atlas instance.

import atlas from './atlas-instance';

// Extend from the base mapper.
const Users = atlas('Mapper').table('users');

// Print out a list of all users' names.
Users.fetch().then(users => {
  console.log(users.map(u => u.name).join(', '))
});


// Find two users and then update an attribute.
Users.find(1, 2).then(users =>
  return Users.target(found).updateAll({ has_been_seen: true })
);

// Delete some rows by ID or record.
return Users.destroy(4, { id: 6 });
// delete from users where id in (4, 6)

Records come back as plain objects. Example fetch response:

[
  { id: 1, name: 'Annie', is_admin: true },
  { id: 2, name: 'Beth', is_admin: false },
  { id: 3, name: 'Chris', is_admin: false }
]

Specialize an existing mapper target a subset of rows:

const Admins = Users.where('is_admin', true);

Admins.fetch().then(printAdminList);

Admins.find(3).then(admin => {
  console.log(`${admin.name} is an admin!`);
}).catch(NotFoundError, err => {
  console.error(`No admin with ID 3!`);
});

Works when creating models too:

const heath = Admins.forge({ name: 'Heath' });
// { name: 'Heath', is_admin: true };

return Admins.save({ id: 4, name: 'Nigel' }, { name: 'Heath' });
// update users set name = 'Nigel', is_admin = true where id = 4;
// insert into users (name, is_admin) values ('Heath', true);

Defining a schema with relations:

const Groups = Mapper.table('groups').relations({
  users() { return this.belongsToMany(Users) },
  owner() { return this.hasOne(Users, { selfRef: 'owner_id' }) }
});

// More complex mapper for `users` table.
const Users = Mapper.table('users').relations({
  groups()      { return this.belongsToMany(Groups) }
  ownedGroups() { return this.hasMany(Groups, { otherRef: 'owner_id' }) },
  posts()       { return this.hasMany(Posts, { otherRef: 'author_id' }) },
  lastPost()    { return this.hasOne(Posts.orderBy('created_at', 'desc')) },
});

const Posts = Mapper.table('posts').relations({
  author() { return this.hasOne(Users, { selfRef: 'author_id' }) }
});

Now eager loading those relations:

return Users.with('groups', 'lastLogin', 'posts')
  .findBy('name', 'Annie')
  .then(annie => { console.log(util.inspect(annie)); });

Relations get keyed by name in the returned record, eg:

{
  id: 20,
  name: 'Annie',
  lastLogin: { user_id: 1, created_at: '2015-11-26' },
  groups: [
    { id: 20, name: 'Super Friends', _pivot_user_id: 1 },
  ],
  posts: [
    { id: 120, author_id: 1, created_at: '2015-11-24',
      title: 'Hi', message: 'Hey there!' },
    { id: 110, author_id: 1, created_at: '2015-10-30',
      title: 'Re: Greeting', message: 'Yo', }
  ]
}

Classes

Atlas

The atlas instance is a helper function. It wraps a Knex instance and a mapper registry.

Passing a string to atlas retrieves a registered Mapper by name.

// Retrieve a mapper stored in Atlas's registry.
const Mapper = atlas('Mapper');

A new atlas instance has the default Mapper under the key 'Mapper'. Other mappers can be added via register.

const Movies = atlas('Mapper').table('movies');
atlas.register({ Movies });

Retrieve the previously stored Mapper 'Movies' and perform a query on it.

atlas('Movies').where({ genre: 'horror' }).count().then(count => {
  console.log(`${count || 'No'} horror movies found.`);
});
EagerLoader

Eager loads related records into an existing record.

ImmutableBase

Base class for Mapper.

MapperImmutableBase

Mappers represent a set of data in your database. A mapper can be scoped or specialized by chaining calls to its methods.

Mappers are immutable, so any setter method will return a copy of the Mapper insance with the new settings. Mapper instances need never be instantiated with new, instead each method call that would mutate the instance returns a copy.

// Get the base mapper from `atlas`.
const Mapper = atlas('Mapper');

// Create a new Mapper that represents users. const Users = Mapper.table('users').idAttribute('user_id');

// Create one from Users that represents administrators. const Admins = Users.where('is_admin', true);

// select * from users where is_admin = true; Admins.fetch().then(admins => { // ... });

These docs instead use the convention of naming mappers in PascalCase and records in camelCase. This is okay because the Mapper constructor never appears in your code.

Cars.fetch().then(cars => // ...
Registry

A simple map for storing instances of Mapper. The registry can be helped to break dependency cycles between mappers defined in different scripts.

Each Atlas instance has a registry property.

All manipulation of the registry can be done via the Atlas instance.

RelatedImmutableBase

Related is a helper to describe relation trees. Instances are passed to Mapper.with and Mapper.load for eager loading.

Objects

errors : object

Can be accessed via errors or imported directly.

const { NotFoundError } = Atlas.errors;
import { NotFoundError } from 'atlas/errors';

Functions

related(relationName)Related

Convenience function to help build Related instances. Pass this instance to with or load to describe an eager relation.

These are equivalent:

BooksWithCover = Books.with('coverImage');
BooksWithCover = Books.with(related('coverImage'));

But using the related wrapper allows chaining for more complex eager loading behaviour:

const { NotFoundError } = atlas.errors;

Books.with(related('coverImage').require()).findBy('title', 'Dune') .then(book => renderThumbnail(book)) .catch(NotFoundError, error => { console.error('Could not render thumbnail, no cover image!'); });

Including nested loading:

Authors.where({ surname: 'Herbert' }).with(
  related('books').with('coverImage', 'blurb')
).fetch().then(authors => {
  res.html(authors.map(renderBibliography).join('<br>'))
})

Atlas

Initialize Atlas.

The atlas instance is a helper function. It wraps a Knex instance and a mapper registry.

Passing a string to atlas retrieves a registered Mapper by name.

// Retrieve a mapper stored in Atlas's registry.
const Mapper = atlas('Mapper');

A new atlas instance has the default Mapper under the key 'Mapper'. Other mappers can be added via register.

const Movies = atlas('Mapper').table('movies');
atlas.register({ Movies });

Retrieve the previously stored Mapper 'Movies' and perform a query on it.

atlas('Movies').where({ genre: 'horror' }).count().then(count => {
  console.log(`${count || 'No'} horror movies found.`);
});

new Atlas(knex, [registry])

Creates a new atlas instance using query builder and connection from given knex instance.

If knex is null the instance can still be used to create and register mappers, but no queries can be executed (fetch, save etc will throw).

// mapper-registry.ja

const atlas = Atlas();
const Mapper = atlas('Mapper');

const Purchases = Mapper.table('purchases');

const Customers = Mapper.table('customers').relations({
  purchases: m => m.hasMany('Purchases'),
  purchasedProducts: m => m.belongsToMany('Products', { Pivot: 'Purchases' })
});

const Products = Mapper.table('products').relations({
  sales: m => m.hasMany('Purchases');
  owners: m => m.belongsToMany('Users', { Pivot: 'Purchases' })
});

atlas.register({ Purchases, Customers, Products });

export default atlas.registry;
import Atlas from 'atlas';
import pgKnex from './pg-knex';
import mapperRegistry from './mapper-registry';

const pg = Atlas(pgKnex, mapperRegistry);
const { related } = pg;

pg('Product').with('sales', 'owners').fetch().then(products =>
  // Fetches and related records from PostgreSQL database.
):

Returns: function - atlas function.

Param Type Description
knex Knex A configured instance of [Knex](http://knex.js).
[registry] Registry An existing Mapper registry. If none is passed then one will be created with the base mapper under key 'Mapper'.

atlas.knex : Knex

Knex instance used by this Atlas instance.

Read only: true

atlas.override(nameOrMappersByName, [mapper]) ⇒ Atlas

Like register but allows a registered Mapper to be replaced.

Returns: Atlas - Self, this method is chainable.

Param Type Description
nameOrMappersByName string | Object Either the name of a single Mapper to register, or a hash of Mapper instances keyed by name.
[mapper] Mapper The mapper to be registered if a name is provided as the first argument.

atlas.register(nameOrMappersByName, [mapper]) ⇒ Atlas

Adds a Mapper instance to Atlas's registry under given name. Registered mappers can be retrieved via atlas(mapperName). Using a registry helps to break dependancy cycles between modules.

const Mapper = atlas('Mapper');

const Users = Mapper.table('users').idAttribute('user_id');
atlas.register('Users', Users);

atlas.register({
  NewestUser: Users.orderBy({ created_at: 'desc' }).one()
});

Mapper names can also be used directly in relationship definitions, for example:

// Using registry allows either side of the relation to reference the other
// before it is declared.
const Pet = Mapper.table('pets').relations({ owner: m => m.belongsTo('Owner') });
const Owner = Mapper.table('owners').relations({ pets: m => m.hasMany('Pets') });
atlas.register({ Pet, Owner });

Returns: Atlas - Self, this method is chainable.

Param Type Description
nameOrMappersByName string | Object Either the name of a single Mapper to register, or a hash of Mapper instances by name.
[mapper] Mapper The mapper to be registered if a name is provided as the first argument.

atlas.registry : Registry

Registry used by this Atlas instance.

Read only: true

atlas.related : related

Accessor for related helper function.

Read only: true
See: related

atlas.transaction(callback) ⇒ Promise

Execute queries in a transaction. Provide a callback argument that returns a Promise. If the promise is resolved the transaction will be commited. If it is rejected then the commit will be rolled back.

app.post('groups/', (req, res) => {

  const { ...group, members } = req.body;

  atlas.transaction(t => {

    // Create the new group.
    return t('Groups').save(group).then(group =>

      // Insert each user then reattach them to the `group`. If any of these
      // insertions throws then the entire operation will be rolled back.
      t('Groups').related(group, 'members').insert(members)
        .then(members => { ...group, members })
    );

  }).then(group =>
    res.status(200).send(group)
  )).catch(ValidationError, error =>
    // ValidationError is NYI
    res.status(400).send(error.message)
  ).catch(() =>
    res.status(500).send('Server error')
  );
});

Callback receives argument t, an instance of Atlas connected to the knex transaction. The knex Transaction instance is available as [t.knex](#Atlas+knex):

atlas.transaction(t => {
  return t.knex('users').join('posts', 'posts.author_id', 'users.id')
    .then(usersAndPosts => {
      // ...
    });
}).then(result => // ...

Returns: Promise - A promise resolving to the value returned from the callback.
See: Knex.js transaction documentation for more information.

Param Type Description
callback transactionCallback Callback within which to write transacted queries.

Atlas.VERSION : string

Installed version of Atlas.

console.log(Atlas.VERSION);
// 1.0.0

Atlas.errors : errors

Atlas.plugins : Object

Properties

Name Type
CamelCase CamelCase
FormatAttributes FormatAttributes
Timestamp Timestamp

Atlas~transactionCallback : function

A callback function that runs the transacted queries.

Param Type Description
t Atlas An instance of Atlas connected to the transaction.
t.knex Transaction The Knex.js Transaction instance.

EagerLoader

Eager loads related records into an existing record.

See: load

new EagerLoader(Self, related)

Param Type Description
Self Mapper Mapper of target records.
related Related | Array.<Related> One or more Related instances describing the relation tree.

eagerLoader.into() ⇒ Promise.<(Object|Array.<Object>)>

Load relations into one or more records.

Returns: Promise.<(Object|Array.<Object>)> - One or more records with relations.

ImmutableBase

Base class for Mapper.

immutableBase.asImmutable() ⇒ ImmutableBase

Prevent this instance from being mutated further.

Returns: ImmutableBase - This instance.

immutableBase.asMutable() ⇒ ImmutableBase

Create a mutable copy of this instance.

Calling setState usually returns new instance of ImmutableBase. A mutable ImmutableBase instance can be modified in place.

Typically withMutations is preferable to asMutable.

Returns: ImmutableBase - Mutable copy of this instance.
See

immutableBase.extend(...callbackOrMethodsByName) ⇒ ImmutableBase

Apply one or more mixins.

Create a new ImmutableBase instance with custom methods.

Creates a new class inheriting ImmutableBase class with supplied methods.

Returns an instance of the new class, as it never needs instantiation with new. Copied as instead created via setState.

import { ReadOnlyError } from './errors';

const ReadOnlyMapper = Mapper.extend({
  insert() { throw new ReadOnlyError(); },
  update() { throw new ReadOnlyError(); }
});

If overriding methods in the parent class, a callback argument can be passed instead. It will be invoked with the callSuper function as an argument.

const SPLIT_COMMA = /,\s+/;
const SPLIT_RELATED = /(\w*)(?:\((.*)\))?/;

function compileDsl(string) {
  return string.split(SPLIT_COMMA).map(token => {
    const [_, relationName, nested] = token.match(SPLIT_REGEX);
    const relatedInstance = atlas.related(relationName);
    return nested ? relatedInstance.with(compileDsl(nested)) : relatedInstance;
  });
}

const DslMapper = Mapper.extend(callSuper => {
  return {
    with(related) {
      if (isString(related)) {
        return callSuper(this, 'with', compileDsl(related));
      }
      return callSuper(this, 'with', ...arguments);
    }
  };
});

const Users = DslMapper.table('users').relations({
  account: m => m.hasOne('Account'),
  projects: m => m.hasMany('Projects')
});

Users.with('account, projects(collaborators, documents)').fetch().then(users =>

Returns: ImmutableBase - An instance of the new class inheriting from ImmutableBase.

Param Type Description
...callbackOrMethodsByName Object | extendCallback Object of methods to be mixed into the class. Or a function that returns such an object. The function is invoked with a callSuper helper function.

immutableBase.requireState(key) ⇒ mixed

Get a state value or throw if unset.

Returns: mixed - Value previously assigned to state key. Do not mutate this value.
Throws:

  • UnsetStateError If the option has not been set.
Param Type Description
key string State key to retrieve.

immutableBase.setState(nextState) ⇒ ImmutableBase

Create a new instance with altered state.

Update state. If any provided values differ from those already set then a copy with updated state will be returned. Otherwise the same instance is returned.

Returns: ImmutableBase - A new instance with updated state, or this one if nothing changed.

Param Type Description
nextState Object A hash of values to override those already set.

immutableBase.state : Object

Hash of values that constitute the object state.

Typically accessed from methods when extending ImmutableBase.

state should be considered read-only, and should only ever by modified indirectly via setState.

Read only: true
See: requireState

immutableBase.withMutations(...initializer) ⇒ ImmutableBase

Create a mutated copy of this instance.

Returns: ImmutableBase - Mutated copy of this instance.

Param Type Description
...initializer Array | string | Object | function An initializer callback, taking the ImmutableBase instance as its first argument. Alternatively an object of {[method]: argument} pairs to be invoked.

Example (Using a callback initializer)

AustralianWomen = People.withMutations(People => {
 People
   .where({ country: 'Australia', gender: 'female' });
   .with('spouse', 'children', 'jobs')
});

Example (Using an object initializer)

AustralianWomen = People.withMutations({
  where: { country: 'Australia', gender: 'female' },
  with: ['spouse', 'children', 'jobs']
});

AustralianWomen = People.withMutations(mapper => {
  return {
    where: { country: 'Australia', gender: 'female' },
    with: ['spouse', 'children', 'jobs']
  }
});

ImmutableBase~callSuper ⇒ mixed

Helper method that invokes a super method.

Returns: mixed - The return value of invoked method.

Param Type Description
self ImmutableBase Instance invoking the super method (this in method).
methodName string Name of super method to invoke.

Example

// Invoke super with `callSuper` helper.
const child = parent.extend(callSuper => {
  return {
    method(x, y) {
      return callSuper('method', x, y);
    }
  }
});

// Equivalent manual invocation of super method.
const parentProto = Object.getPrototypeOf(parent);
const child = parent.extend({
  method(x, y) {
    return parentProto.method.call(this, x, y);
  });
});

ImmutableBase~extendCallback ⇒ Object

Returns: Object - A hash of methods.

Param Type Description
callSuper callSuper Helper function that invokes a super method.

Mapper ⇐ ImmutableBase

Mappers represent a set of data in your database. A mapper can be scoped or specialized by chaining calls to its methods.

Mappers are immutable, so any setter method will return a copy of the Mapper insance with the new settings. Mapper instances need never be instantiated with new, instead each method call that would mutate the instance returns a copy.

// Get the base mapper from `atlas`.
const Mapper = atlas('Mapper');

// Create a new Mapper that represents users.
const Users = Mapper.table('users').idAttribute('user_id');

// Create one from `Users` that represents administrators.
const Admins = Users.where('is_admin', true);

// select * from users where is_admin = true;
Admins.fetch().then(admins => {
  // ...
});

These docs instead use the convention of naming mappers in PascalCase and records in camelCase. This is okay because the Mapper constructor never appears in your code.

Cars.fetch().then(cars => // ...

Extends: ImmutableBase

mapper.all() ⇒ Mapper

Query multiple rows. Default behaviour.

Unlimits query. Opposite of one.

Returns: Mapper - Mapper targeting a single row.
Example

const LatestSignUp = Mapper
  .table('users')
  .orderBy('created_at', 'desc')
  .one();

const SignUpsLastWeek = NewestUser
  .where('created_at', '>', moment().subtract(1, 'week'))
  .all();

SignUpsLastWeek.count().then(signUpCount => {
  console.log(`${signUpCount} users signed in the last week`);
});

mapper.asImmutable() ⇒ ImmutableBase

Prevent this instance from being mutated further.

Returns: ImmutableBase - This instance.

mapper.asMutable() ⇒ ImmutableBase

Create a mutable copy of this instance.

Calling setState usually returns new instance of ImmutableBase. A mutable ImmutableBase instance can be modified in place.

Typically withMutations is preferable to asMutable.

Returns: ImmutableBase - Mutable copy of this instance.
See

mapper.attributes(...attributes) ⇒ Mapper

Set attributes to be retrieved by fetch.

// Exclude 'password_hash' and 'salt'.
const userWhitelist = ['name', 'avatar_url', 'created_at', 'last_seen'];

router.get('/user/:userId', (req, res, next) => {
  Users
    .attributes(userWhitelist)
    .find(req.params.userId)
    .then(res.json)
    .catch(next);
});
Param Type Description
...attributes string One or more attributes to fetch.

mapper.count() ⇒ Promise.<Number>

Count records.

Returns: Promise.<Number> - The number of matching records.
Example

const Articles = Mapper.table('articles');

Articles.count().then(count => {
  console.log('Total articles:', count);
});

Articles.where('topic', 'JavaScript').count().then(count => {
  console.log('Total JavaScript articles:', count);
});

mapper.defaultAttribute(attribute, value) ⇒ Mapper

Set a default value for an attribute.

Returns: Mapper - Mapper with a default attribute.
See: defaultAttributes.

Param Type
attribute string
value mixed | attributeCallback

mapper.defaultAttributes(attributes) ⇒ Mapper

Set default values for attributes.

These values will be used by forge and insert when no value is provided.

const Users = Mapper.table('users').defaultAttributes({
  name: 'Anonymous', rank: 0
});

Alternatively values can be callbacks that receive attributes and return a default value. In the below example a new document record is generated with a default name and template.

const HtmlDocuments = Mapper.table('documents').defaultAttributes({
  title: 'New Document',
  content: attributes => (
    `<html>
      <head>
        <title>${ attributes.title || 'New Document'}</title>
      </head>
      <body>
      </body>
    </html>`
  )
});

HtmlDocuments.save({ title: 'Atlas Reference' }).then(doc =>
  console.dir(doc);
  // {
  //   title: 'Atlas Reference',
  //   content: '<html>\n  <head>\n    <title>Atlas Reference</title>...'
  // }
);

Returns: Mapper - Mapper with default attributes.

Param Type Description
attributes Object.<string, (mixed|Mapper~attributeCallback)> An object mapping values (or callbacks) to attribute names.

mapper.destroy(ids) ⇒ Promise.<Number>

Delete specific rows.

Specify rows to be deleted. Rows can be specified by supplying one or more record objects or ID values.

Returns: Promise.<Number> - Promise resolving to the number of rows deleted.

Param Type Description
ids mixed | Array.<mixed> ID(s) or record(s) whose corresponding rows will be destroyed.

Example

const Users = atlas('Mapper').table('users');

Users.destroy(5).then(count =>
// delete from users where id = 5

Users.destroy(1, 2, 3).then(count =>
// delete from users where id in (1, 2, 3)

const sam = { id: 5, name: 'Sam' };
const jane = { id: 16, name: 'Jane' };

Users.destroy(sam, jane).then(count =>
// delete from users where id in (5, 16)

mapper.destroyAll() ⇒ Promise.<Number>

Delete rows matching query.

Delete all rows matching the current query.

Users.where('complaint_count', '>', 10).destroy().then(count =>

Returns: Promise.<Number> - Count or rows deleted.

mapper.extend(...callbackOrMethodsByName) ⇒ ImmutableBase

Apply one or more mixins.

Create a new ImmutableBase instance with custom methods.

Creates a new class inheriting ImmutableBase class with supplied methods.

Returns an instance of the new class, as it never needs instantiation with new. Copied as instead created via setState.

import { ReadOnlyError } from './errors';

const ReadOnlyMapper = Mapper.extend({
  insert() { throw new ReadOnlyError(); },
  update() { throw new ReadOnlyError(); }
});

If overriding methods in the parent class, a callback argument can be passed instead. It will be invoked with the callSuper function as an argument.

const SPLIT_COMMA = /,\s+/;
const SPLIT_RELATED = /(\w*)(?:\((.*)\))?/;

function compileDsl(string) {
  return string.split(SPLIT_COMMA).map(token => {
    const [_, relationName, nested] = token.match(SPLIT_REGEX);
    const relatedInstance = atlas.related(relationName);
    return nested ? relatedInstance.with(compileDsl(nested)) : relatedInstance;
  });
}

const DslMapper = Mapper.extend(callSuper => {
  return {
    with(related) {
      if (isString(related)) {
        return callSuper(this, 'with', compileDsl(related));
      }
      return callSuper(this, 'with', ...arguments);
    }
  };
});

const Users = DslMapper.table('users').relations({
  account: m => m.hasOne('Account'),
  projects: m => m.hasMany('Projects')
});

Users.with('account, projects(collaborators, documents)').fetch().then(users =>

Returns: ImmutableBase - An instance of the new class inheriting from ImmutableBase.

Param Type Description
...callbackOrMethodsByName Object | extendCallback Object of methods to be mixed into the class. Or a function that returns such an object. The function is invoked with a callSuper helper function.

mapper.fetch() ⇒ Promise.<(Object|Array.<Object>)>

Retrieve one or more records.

Returns: Promise.<(Object|Array.<Object>)> - One or more records.
Example

// select * from people;
People.fetch().then(people =>
  const names = people.map(p => p.name).join(', ');
  console.log(`All people: ${names}`);
);

mapper.fetchAll() ⇒ Promise.<Array.<Object>>

Retrieve an array of records.

Alias for Mapper.[all](#Mapper+all).[fetch](#Mapper+fetch).

mapper.find(...ids) ⇒ Promise.<(Object|Array.<Object>)>

Retrieve records by ID.

Fetch one or more records by their idAttribute.

Shorthand for Mapper.target().fetch().

Returns: Promise.<(Object|Array.<Object>)> - One or more records with the given IDs.

Param Type Description
...ids mixed | Array.<mixed> One or more ID values, or arrays of ID values (for composite IDs).

Example (Finding a record with a single key)

const Vehicles = Mapper.table('vehicles');

Vehicles.find(5).then(vehicle =>
// select * from vehicles where id = 5

Vehicles.find({ id: 3, model: 'Commodore' }).then(vehicle =>
// select * from vehicles where id = 3

Vehicles.find(1, 2, 3).then(vehicles =>
// select * from vehicles where id in (1, 2, 3)

Example (Finding a record with a composite key)

const AccessPermissions = Mapper
  .table('permissions')
  .idAttribute(['room_id', 'personnel_id']);

AccessPermissions.find([1, 2]).then(trip =>
// select * from trips where room_id = 1, personnel_id = 2

const personnel = { name: 'Melissa', id: 6 };
const office = { id: 2 };
AccessPermissions.find([office.id, personnel.id]).then(permission =>
// select * from permissions where room_id = 6 and personnel_id = 2

const permission = { room_id: 2, personel_id: 6 };
AccessPermissions.find(permission).then(permission =>
// select * from permissions where room_id = 6 and personnel_id = 2

mapper.findBy() ⇒ Promise.<(Object|Array.<Object>)>

Retrieve record(s) by a specific attribute.

Like find, but allows an attribute other than the primary key as its identity. The provided attribute should be unique within the Mapper's table.

Returns: Promise.<(Object|Array.<Object>)> - One or more records having the supplied attribute.
Example

Users = Mapper.table('users');

function validateCredentials(email, password) {
  return Users.findBy('email', email).then(user => {
    return user != null && verifyPassword(user.password_hash, password);
  });
}

mapper.first() ⇒ Mapper

Fetch the first matching record.

Shorthand for Mapper.one().fetch(). If the query has no ordering, it will be sorted by idAttribute.

Returns: Mapper - Mapper targeting a single row.
Example

Users.first().then(user =>
// select * from users order by id

Users.orderBy('created_at', 'desc').first().then(newestUser =>
// select * from users order by created_at desc

mapper.forge(attributes)

Create a record.

Create a new record object. This doesn't persist any data, it just creates an instance to be manipulated with JavaScript.

const Messages = Mapper.tables('messages').defaultAttributes({
  created_at: () => new Date(),
  urgency: 'low'
});

const greeting = Messages.forge({ message: `Hi there` });
// { message: 'How's it goin?', created_at: '2015-11-28', urgency: 'low' }

By default this is an instance of Object, but it is possible to change the type of the records that Atlas accepts and returns. Override the createRecord, getAttributes, setAttributes, getRelated and setRelated methods.

Param Type
attributes Object

mapper.getRelation(relationName) ⇒ Relation

Get a named Relation instance from a Mapper.

Get a configured Relation instance that was defined previously with relations.

The relation can be converted into a Mapper matching records in that relation. Each Relation type (BelongsTo, BelongsToMany, HasOne and HasMany) provides an of() method that accepts one or more records.

atlas.register({

  Projects: Mapper.table('projects').relations({
    owner: m => m.belongsTo('People', { selfRef: 'owner_id' })
  }),

  People: Mapper.table('people').relations({
    projects: m => m.hasMany('Projects', { otherRef: 'owner_id' })
  })

});

// Assuming `req.user` is added by auth middleware (eg. Passport.js).

// Simple `GET` route, scoped by user.
express.route('/projects').get((req, res) =>
  atlas('People').getRelation('projects').of(req.user).then(projects =>
    res.json(projects)
  )
);
// Alternative to above - share relation `Mapper` between between `GET` and
// `POST`.
express.route('/projects').all((req, res) => {
  req.Projects = atlas('People').getRelation('projects').of(req.user)
  next();
}).get((req, res) =>
  req.Projects.fetch().then(res.json)
).post((req, res) =>
  req.Projects.save(req.body).then(res.json)
);

express.route('/projects/:projectId').all((req, res) => {
  req.Project = atlas('People').getRelation('projects').of(req.user).target(
    req.params.projectId
  ).require();
  next();
}).get((req, res) =>
  req.Project.fetch().then(res.json)
).put((req, res) =>
  // Automatically overrides `owner_id` before insert, regardless of `req.body`.
  req.Projects.save(req.body).then(res.json)
);

This also allows querying on a relation of multiple parents.

const bob = { id: 1, name: 'Bob' };
const sue = { id: 2, name: 'Sue' };

// select * from projects where owner_id in (1, 2)
Users.getRelation('projects').of(bob, sue).then(projects => {
  console.log(
    'Projects belonging to either Bob or Sue:\n' +
    projects.map(p => p.name)
  );
});

See: relations

Param Type Description
relationName string The name of the relation to return

mapper.idAttribute(idAttribute) ⇒ Mapper

Set primary key.

Set the primary key attribute.

const Accounts = Mapper
  .table('accounts')
  .idAttribute('email');

// select * from accounts where email='username@domain.com';
Accounts.find('username@domain.com').then(user =>

Defining a composite key:

const Membeships = Mapper
  .table('memberships')
  .idAttribute(['user_id', 'group_id']);

Returns: Mapper - Mapper with primary key attribute set.

Param Type Description
idAttribute string | Array.<string> Name of primary key attribute.

mapper.insert(records) ⇒ Promise.<(Object|Array.<Object>)>

Insert one or more records.

Insert a record or an array of records into the table assigned to this Mapper. Returns a promise resolving the the record object (or objects) with updated attributes.

This is useful as an alternative to save to force atlas to insert a record that already has an ID value.

Using PostgreSQL every record will be updated to the attributes present in the table after insert. Any other DBMS will only return the primary key of the first record, which is then assigned to the idAttribute.

Returns: Promise.<(Object|Array.<Object>)> - Promise resolving to the record(s) with updated attributes.
Todo

  • Do something better for non-PostgreSQL databases. It could do each insert as an individual query (allowing update of the idAttribute). Or fetch the rows (SELECT *) in range after the insert. For instance, if ten records were inserted, and the first ID is 5, then select rows 5-15 and return them as the response. Need to investigate whether this is safe to do in a transaction (and does not cause performance problems).
Param Type Description
records Object | Array.<Object> One or more records to be inserted.

mapper.isNew(record) ⇒ bool

Check if the record exists in the database.

By default isNew will simply check for the existance of the {@link Mapper#idAttribute idAttribute} on the given record. This method can be overridden for custom behavior.

Returns: bool - true if the model exists in database, otherwise false.

Param Type Description
record Object Record to check.

mapper.joinMapper(Other, selfAttribute, otherAttribute) ⇒ Mapper

Join query with another Mapper.

Performs an inner join between the table of this Mapper and that of Other.

Returns: Mapper - Mapper joined to Other.

Param Type Description
Other Mapper Mapper with query to join.
selfAttribute string | Array.<string> The attribute(s) on this Mapper to join on.
otherAttribute string | Array.<string> The attribute(s) on Other to join on.

mapper.joinRelation(relationName) ⇒ Mapper

Join query with a related Mapper.

Performs an inner join between the table of this Mapper, and that of the named relation.

Returns: Mapper - Mapper joined to given relation.

Param Type Description
relationName String The name of the relation with which to perform an inner join.

mapper.load(...related) ⇒ EagerLoader

Eager load relations into existing records.

Much like with(), but attaches relations to an existing record.

load returns an instance of EagerLoader. EagerLoader exposes a single method, into:

const bob = { email: 'bob@thing.com', name: 'Bob', id: 5 };
const jane = { email: 'jane@thing.com', name: 'Jane', id: 100 };

Users.load('posts').into(bob, jane).then(([bob, jane]) => {
  cosole.log(`Bob's posts: ${bob.posts.map(p => p.title)}`);
  cosole.log(`Jane's posts: ${jane.posts.map(p => p.title)}`);
});
// Load posts.
Posts.fetch(posts => {
  // Now load and attach related authors.
  return Posts.load('author').into(posts);
}).then(postsWithAuthor => {
  // ...
})

// Exactly the same as:
Posts.with('author').fetch().then(postsWithAuthor => {
 // ...
})

See Mapper.relations() for example of how to set up this schema.

Returns: EagerLoader - An EagerLoader instance configured to load the given relations into records.

Param Type Description
...related Related | string | ALL One or more Related instances or relation names. Or ALL to select all registered relations.

mapper.omitPivot() ⇒ Mapper

Exclude columns from a joined table.

Columns from a joined table can be added to a fetch query with pivotAttributes. This chaining this method will prevent them from appearing in the returned record(s).

In a (many-to-many relation), key columns from the join table are included automatically. These are necessary to associate the related records to their parents in an eager load.

Returns: Mapper - Mapper that will not return pivot attributes in a fetch response.
See: pivotAttributes
Example

const bob = { id: 1, name: 'Bob' };
const BobsGroups = Users.getRelation('groups').of(bob).pivotAttributes('is_owner');

BobsGroups.fetch().then(groups => {
  console.log(groups);
  // [{ _pivot_user_id: 1, _pivot_is_owner: false, id: 10, name: 'General' },
  // { _pivot_user_id: 1, _pivot_is_owner: true, id: 11, name: 'Database' }]
});

BobsGroups.omitPivot().fetch().then(groups => {
  console.log(groups);
  // [{ id: 10, name: 'General' }, { id: 11, name: 'Database' }]
});

mapper.one() ⇒ Mapper

Query a single row.

Limit query to a single row. Causes subsequent calls to fetch to resolve to a single record (rather than an array). Opposite of all.

Typically it's simpler to use first.

Returns: Mapper - Mapper targeting a single row.
Example

People.one().where('age', '>', 18).fetch().then(adult => {
 console.log(`${adult.name} is one adult in the database`);
});

mapper.orderBy(attribute, [direction]) ⇒ Mapper

Order the records returned by a query.

Returns: Mapper - Mapper with query ordered by attribute.

Param Type Default Description
attribute string | Array.<string> The attribute(s) by which to order the response.
[direction] string "asc" The direction by which to order the records. Either 'asc' for ascending, or 'desc' for descending.

Example

Messages.orderBy('received_at', 'desc').first().then(message => {
  console.log(`${message.sender} says "${message.text}"`);
});

mapper.pivotAttributes(attributes) ⇒ Mapper

Fetch columns from a joined table.

Include columns from a table that has been joined with joinRelation or joinMapper.

Returns: Mapper - Mapper that will return pivot attributes in a fetch response.
See: omitPivot

Param Type Description
attributes string | Array.<string> Attributes to be included from joined table.

Example

Customers
  .joinMapper(Receipts, 'id', 'customer_id')
  .pivotAttributes({ 'created_at', 'total_cost' })
  .where('receipts.created_at', >, yesterday)
  .then(receipts => {
    console.log('Purchases today: \n' + receipts.map(r =>
      `${r.name} spent $${r._pivot_total_cost} at ${r._pivot_created_at}`
    ));
  });

mapper.query(method, ...args) ⇒ Mapper

Modify the underlying Knex QueryBuilder instance directly.

Returns: Mapper - Mapper with a modified underlying QueryBuilder instance.
See: http://knexjs.org

Param Type Description
method function | string A callback that modifies the underlying QueryBuilder instance, or the name of a QueryBuilder method to invoke.
...args mixed Arguments to be passed to the QueryBuilder method.

mapper.relations(relationFactoryByName) ⇒ Mapper

Define a Mapper's relations.

const Mapper = atlas('Mapper');

const Users = Mapper.table('users').idAttribute('email').relations({
  friends: m => m.belongsToMany(Users),
  sentMessages: m => m.hasMany(Messages, { otherRef: 'from_id' }),
  receivedMessages: m => m.hasMany(Messages, { otherRef: 'to_id' }),
}),

Messages: Mapper.table('messages').relations({
  from: m => m.belongsTo(Users, { selfRef: 'from_id' }),
  to: m => m.belongsTo(Users, { selfRef: 'to_id' })
}).extend({
  unread() { return this.where('is_unread', true); },
  read() { return this.where('is_unread', false); }
}),

Posts: Mapper.table('posts').relations({
  author: m => m.belongsTo(Users, { selfRef: 'author_id' }),
  comments: m => m.hasMany(Comments)
}),

Comments: Mapper.table('comments').relations({
  author: m => m.belongsTo(Users, { selfRef: 'author_id' }),
  post: m => m.belongsTo(Posts)
})

Relation functions are also bound correctly like a method, so you can use this.

const Users = Mapper.table('users').relations({
  friends: function() { return this.belongsToMany(Users) }
})

Returns: Mapper - Mapper with provided relations.
See

Param Type Description
relationFactoryByName Object.<string, Mapper~createRelation> A hash of relations keyed by name.

mapper.require() ⇒ Mapper

Setting require will cause fetch and find to throw when a query returns no records.

const { NoRowsFoundError } = atlas;

User.where('created_at', <, new Date(1500, 0, 1)).fetch()
  .then(user => console.log(user.name))
  .catch(NoRowsFoundError, error => {
    console.log('no rows created before 1500AD!');
  });

mapper.requireState(key) ⇒ mixed

Get a state value or throw if unset.

Returns: mixed - Value previously assigned to state key. Do not mutate this value.
Throws:

  • UnsetStateError If the option has not been set.
Param Type Description
key string State key to retrieve.

mapper.save(records) ⇒ Promise.<(Object|Array.<Object>)>

Persist records.

Insert or update one or more records. The decision of whether to insert or update is based on the result of testing each record with isNew.

Returns: Promise.<(Object|Array.<Object>)> - A promise resolving to the saved record(s) with updated attributes.

Param Type Description
records Object | Array.<Object> One or more records to be saved.

mapper.setState(nextState) ⇒ ImmutableBase

Create a new instance with altered state.

Update state. If any provided values differ from those already set then a copy with updated state will be returned. Otherwise the same instance is returned.

Returns: ImmutableBase - A new instance with updated state, or this one if nothing changed.

Param Type Description
nextState Object A hash of values to override those already set.

mapper.state : Object

Hash of values that constitute the object state.

Typically accessed from methods when extending ImmutableBase.

state should be considered read-only, and should only ever by modified indirectly via setState.

Read only: true
See: requireState

mapper.strictAttribute(attribute, value) ⇒ Mapper

Set an override value for an attribute.

Returns: Mapper - Mapper with strict attributes.
See: strictAttributes

Param Type
attribute string
value mixed | attributeCallback

mapper.strictAttributes(attributes) ⇒ Mapper

Set override values for a attributes.

Set values to override any passed to forge, insert or update.

Alternatively values can be callbacks that receive attributes and return a value.

Users = Mapper.table('users').strictAttributes({
  email: attributes => attributes.email.trim(),
  is_admin: false
});

Returns: Mapper - Mapper with default attributes.

Param Type Description
attributes Object.<string, (mixed|Mapper~attributeCallback)> An object mapping values (or callbacks) to attribute names.

mapper.table(table) ⇒ Mapper

Sets the name of the table targeted by this Mapper.

Returns: Mapper - Mapper instance targeting given table.

Param Type Description
table string The new name of this table.

Example

const Mapper = atlas('Mapper');

const Dogs = Mapper.table('dogs');
const Cats = Mapper.table('cats');

mapper.target(ids) ⇒ Mapper

Limit query to one or more specific rows.

Returns: Mapper - Mapper targeting rows with given ID value(s).

Param Type Description
ids mixed | Array.<mixed> ID values for target rows, or records with ID values.

mapper.targetBy(attribute, ids) ⇒ Mapper

Limit query to one or more rows matching a given attribute.

Returns: Mapper - Mapper targeting rows matching the attribute value(s).

Param Type Description
attribute string | Array.<string> Attribute(s) to identify records by.
ids mixed | Array.<mixed> Values for target rows, or records with values for given attribute(s).

mapper.toQueryBuilder() ⇒ QueryBuilder

Return a copy of the underlying QueryBuilder instance.

Returns: QueryBuilder - QueryBuilder instance.
See: http://knexjs.org

mapper.update(records) ⇒ Promise.<(Object|Array.<Object>)>

Update rows corresponding to one or more records.

Update rows corresponding to one or more records. If the idAttribute is not set on any of the records then the returned promise will be rejected with an UnidentifiableRecordError.

Returns: Promise.<(Object|Array.<Object>)> - A promise resolving to the updated record or records.

Param Type Description
records Object | Array.<Object> Record, or records, to be updated.

mapper.updateAll(attributes) ⇒ Promise.<(Array.<Object>|Number)>

Update all matching rows.

Returns: Promise.<(Array.<Object>|Number)> - Updated records (if returning * is supported), or count of updated rows.

Param Type Description
attributes Object Attributes to be set on all mathed rows.

mapper.where(attribute, ...args) ⇒ Mapper

Select a subset of records.

Passthrough to QueryBuilder#where with some extra features.

const People = Mapper.table('people');
People.where({ name: 'Rhys' }).fetch().then(people => // ...

Mapper respects Mapper#attributeToColumn if overridden.

const { CamelCase }
const Monsters = Mapper.extend(CamelCase()).table('monsters');
const ScaryMonsters = Monsters.where({ eyeColor: 'red', clawSize: 'large' });

ScaryMonsters.count().then(count => {
  if (count > 0) {
    runAway();
  }
});
select count(*) from monsters
where eye_color = 'red' and claw_size = 'large'

Also overrides attributes (by calling strictAttributes internally).

const Deleted = Users.where('is_deleted', true);
const tas = Deleted.forge({ name: 'Tas' });
// tas -> { name: 'Tas', is_deleted: true }

Deleted.save({ id: 5, is_deleted: false }).then(deleted => {
  // deleted -> { id: 5, is_deleted: true }
});

Also allows an operator (see Knex docs for more into):

const MultiHeaded = Monsters.where('headCount', '>', 1);
const Living = Monsters.where('isDead', '!=', 1);

And handles arrays (useful for composite keys):

Mapper.table('monster_kills')
  .where(['monster_id', 'victim_id'], [spider.id, fred.id])
  .count(killCount => {
    console.log(
      `Fred was ${killCount == 0 ? 'not ' : ''} killed by a spider!`
    );
  });
Param Type Description
attribute string | Array.<string> | Object Attribute name(s) or object of values keyed by attribute name.
...args mixed See description.

mapper.whereIn() ⇒ Mapper

Passthrough to QueryBuilder#whereIn that respects Mapper#attributeToColumn if overridden.

mapper.with(...related) ⇒ Mapper

Specify relations to eager load.

Specify relations to eager load with fetch, find etc. These are declared using the Related class.

// Get all posts created today, eager loading author relation for each.
atlas('Posts')
  .where('created_at', '>', moment().startOf('day'))
  .with(author')
  .fetch()
  .then(todaysPosts => {
    // ...
  });

const { related } = atlas;

// Load user with recent posts and unread messages.
atlas('Users').with(

  // Eager load last twent posts.
  related('posts').with(related('comments').with('author')).mapper({
    query: query => query.orderBy('created_at', 'desc').limit(20)
  }),

  // Eager load unread messages.
  related('receivedMessages').mapper('unread').as('unreadMessages')

).findBy('email', 'some.guy@domain.com').then(user => {
  console.log(`${user.name} has ${user.unreadMessages.length} unread messages`);
});

See relations for an example of how to set up this schema.

Returns: Mapper - Mapper configured to eager load related records.
Todo

  • Support saving relations.
Param Type Description
...related Related | string | ALL | NONE One or more Related instances or relation names. Pass ALL or NONE to select all relations or clear any previously specific relations.

mapper.withMutations(...initializer) ⇒ ImmutableBase

Create a mutated copy of this instance.

Returns: ImmutableBase - Mutated copy of this instance.

Param Type Description
...initializer Array | string | Object | function An initializer callback, taking the ImmutableBase instance as its first argument. Alternatively an object of {[method]: argument} pairs to be invoked.

Example (Using a callback initializer)

AustralianWomen = People.withMutations(People => {
 People
   .where({ country: 'Australia', gender: 'female' });
   .with('spouse', 'children', 'jobs')
});

Example (Using an object initializer)

AustralianWomen = People.withMutations({
  where: { country: 'Australia', gender: 'female' },
  with: ['spouse', 'children', 'jobs']
});

AustralianWomen = People.withMutations(mapper => {
  return {
    where: { country: 'Australia', gender: 'female' },
    with: ['spouse', 'children', 'jobs']
  }
});

Mapper~attributeCallback ⇒ mixed | undefined

Returns: mixed | undefined - Either a value to be assigned to an attribute, or undefined to mean none should be set.

Param Type
attributes Object

Mapper~createRelation ⇒ Relation

Callback invoked with the Mapper instance and returning a Relation.

Returns: Relation - A relation instance.
this: Mapper

Param Type Description
Mapper Mapper The Mapper upon which this relation is being invoked.

Registry

A simple map for storing instances of Mapper. The registry can be helped to break dependency cycles between mappers defined in different scripts.

Each Atlas instance has a registry property.

All manipulation of the registry can be done via the Atlas instance.

See

  • Atlas instance for retrieving mappers.

  • registry for an instance of Registry.

  • register to add mappers.

  • override to override previously registered mappers.

Related ⇐ ImmutableBase

Related is a helper to describe relation trees. Instances are passed to Mapper.with and Mapper.load for eager loading.

Extends: ImmutableBase

related.as(name) ⇒ Related

Set the name of the relation. Required if constructed with a Relation instance, or can be used to alias a relation.

Returns: Related - Self, this method is chainable.

Param Type Description
name String The relation name. Used as a key when setting related records.

Example

// Use `name` to alias the `posts` relation.

const lastWeek = moment().subtract(1, 'week');
const recentPosts = related('posts')
  .mapper({ where: ['created_at', '>', lastWeek] })
  .as('recentPosts');

Users.with(recentPosts).findBy('name', 'Joe Bloggs').then(joe => // ...

Example

// Use `name` to provide a relation instance directly.

const posts = hasMany('Post');
Users.with(related(posts).as('posts')).fetch();

related.asImmutable() ⇒ ImmutableBase

Prevent this instance from being mutated further.

Returns: ImmutableBase - This instance.

related.asMutable() ⇒ ImmutableBase

Create a mutable copy of this instance.

Calling setState usually returns new instance of ImmutableBase. A mutable ImmutableBase instance can be modified in place.

Typically withMutations is preferable to asMutable.

Returns: ImmutableBase - Mutable copy of this instance.
See

related.extend(...callbackOrMethodsByName) ⇒ ImmutableBase

Apply one or more mixins.

Create a new ImmutableBase instance with custom methods.

Creates a new class inheriting ImmutableBase class with supplied methods.

Returns an instance of the new class, as it never needs instantiation with new. Copied as instead created via setState.

import { ReadOnlyError } from './errors';

const ReadOnlyMapper = Mapper.extend({
  insert() { throw new ReadOnlyError(); },
  update() { throw new ReadOnlyError(); }
});

If overriding methods in the parent class, a callback argument can be passed instead. It will be invoked with the callSuper function as an argument.

const SPLIT_COMMA = /,\s+/;
const SPLIT_RELATED = /(\w*)(?:\((.*)\))?/;

function compileDsl(string) {
  return string.split(SPLIT_COMMA).map(token => {
    const [_, relationName, nested] = token.match(SPLIT_REGEX);
    const relatedInstance = atlas.related(relationName);
    return nested ? relatedInstance.with(compileDsl(nested)) : relatedInstance;
  });
}

const DslMapper = Mapper.extend(callSuper => {
  return {
    with(related) {
      if (isString(related)) {
        return callSuper(this, 'with', compileDsl(related));
      }
      return callSuper(this, 'with', ...arguments);
    }
  };
});

const Users = DslMapper.table('users').relations({
  account: m => m.hasOne('Account'),
  projects: m => m.hasMany('Projects')
});

Users.with('account, projects(collaborators, documents)').fetch().then(users =>

Returns: ImmutableBase - An instance of the new class inheriting from ImmutableBase.

Param Type Description
...callbackOrMethodsByName Object | extendCallback Object of methods to be mixed into the class. Or a function that returns such an object. The function is invoked with a callSuper helper function.

related.mapper(...initializers) ⇒ Related

Queue up initializers for the Mapper instance used to query the relation. Accepts the same arguments as Mapper.withMutations.

Account.with(related('inboxMessages').mapper(m =>
  m.where({ unread: true })
)).fetch().then(account => // ...

Account.with(
  related('inboxMessages').mapper({ where: { unread: true } })
).fetch().then(account => // ...

Returns: Related - Self, this method is chainable.

Param Type Description
...initializers mixed Accepts the same arguments as withMutations.

related.recursions(recursions) ⇒ Related

Set the number of recursions.

Returns: Related - Self, this method is chainable.

Param Type Description
recursions Number Either an integer or Infinity.

Example

Person.with(
  related('father').recursions(3)
).findBy('name', 'Kim Jong-un').then(person =>
 // person = {
 //   id: 4,
 //   name: 'Kim Jong-un',
 //   father_id: 3,
 //   father: {
 //     id: 3,
 //     name: 'Kim Jong-il',
 //     father_id: 2,
 //     father: {
 //       id: 2,
 //       name: 'Kim Il-sung',
 //       father_id: 1,
 //       father: {
 //         id: 1,
 //         name: 'Kim Hyong-jik',
 //         father_id: null
 //       }
 //     }
 //   }
 // }
)

Example

knex('nodes').insert([
  { id: 1, value: 'a', next_id: 2 },
  { id: 2, value: 't', next_id: 3 },
  { id: 3, value: 'l', next_id: 4 },
  { id: 4, value: 'a', next_id: 5 },
  { id: 5, value: 's', next_id: 6 },
  { id: 6, value: '.', next_id: 7 },
  { id: 7, value: 'j', next_id: 8 },
  { id: 8, value: 's', next_id: null }
])

atlas.register({
  Nodes: Mapper.table('nodes').relations({
    next: m => m.belongsTo('Nodes', { selfRef: 'next_id' })
  })
});

// Fetch nodes recursively.
atlas('Nodes').with(
  related('next').recursions(Infinity)).find(1)
).then(node =>
  const values = [];
  while (node.next != null) {
    letters.push(node.value);
    node = node.next;
  }
  console.log(values.join('')); // "atlas.js"
);

related.require() ⇒ Related

Raise an error if the relation is not found. Currently this is just a passthrough to Mapper.require().

Returns: Related - Self, this method is chainable. Instance that will throw if no records are returned.

related.requireState(key) ⇒ mixed

Get a state value or throw if unset.

Returns: mixed - Value previously assigned to state key. Do not mutate this value.
Throws:

  • UnsetStateError If the option has not been set.
Param Type Description
key string State key to retrieve.

related.setState(nextState) ⇒ ImmutableBase

Create a new instance with altered state.

Update state. If any provided values differ from those already set then a copy with updated state will be returned. Otherwise the same instance is returned.

Returns: ImmutableBase - A new instance with updated state, or this one if nothing changed.

Param Type Description
nextState Object A hash of values to override those already set.

related.state : Object

Hash of values that constitute the object state.

Typically accessed from methods when extending ImmutableBase.

state should be considered read-only, and should only ever by modified indirectly via setState.

Read only: true
See: requireState

related.with(...related) ⇒ Related

Fetch nested relations.

Returns: Related - Self, this method is chainable.

Param Type Description
...related Related | Array.<Related> One or more Related instances describing the nested relation tree.

Example

atlas('Actors').with(
  related('movies').with(related('director', 'cast'))
).findBy('name', 'James Spader').then(actor =>
  assert.deepEqual(
    actor,
    { id: 2, name: 'James Spader', movies: [
      { _pivot_actor_id: 2, id: 3, title: 'Stargate', director_id: 2,
        director: { id: 2, name: 'Roland Emmerich' },
        cast: [
          { id: 2, name: 'James Spader' },
          // ...
        ]
      },
      // ...
    ]},
  )
);

related.withMutations(...initializer) ⇒ ImmutableBase

Create a mutated copy of this instance.

Returns: ImmutableBase - Mutated copy of this instance.

Param Type Description
...initializer Array | string | Object | function An initializer callback, taking the ImmutableBase instance as its first argument. Alternatively an object of {[method]: argument} pairs to be invoked.

Example (Using a callback initializer)

AustralianWomen = People.withMutations(People => {
 People
   .where({ country: 'Australia', gender: 'female' });
   .with('spouse', 'children', 'jobs')
});

Example (Using an object initializer)

AustralianWomen = People.withMutations({
  where: { country: 'Australia', gender: 'female' },
  with: ['spouse', 'children', 'jobs']
});

AustralianWomen = People.withMutations(mapper => {
  return {
    where: { country: 'Australia', gender: 'female' },
    with: ['spouse', 'children', 'jobs']
  }
});

errors : object

Can be accessed via errors or imported directly.

const { NotFoundError } = Atlas.errors;
import { NotFoundError } from 'atlas/errors';

errors.NoRowsFoundError

No records returned.

Mapper.require().fetch().then(...).catch(error => {
  console.log(error);
  // ERROR: No rows found!
});

See: require

errors.NotFoundError

A specific record was not found.

Users.require().find(999).then(...).catch(error => {
  console.log(error);
  // ERROR: No row found!
});

See: require

errors.RegisteredKeyError

A Mapper was found at this registry key.

atlas.register('Mapper', Mapper.table('users'));
// ERROR: 'Mapper' already registered!

errors.UnidentifiableRecordError

Record could not be identified.

Users.update({ name: 'Bob' })
// ERROR: Expected record to have ID attribute 'id'!

errors.UnregisteredKeyError

A Mapper was not found.

atlas.register('Users', Mapper.table('users'));
atlas('Useers').fetch()
// ERROR: Unknown registry key 'Useers'!

errors.UnsetStateError

Unset state was required, but had not been set.

Mapper.save({ name: 'Bob' });
// ERROR: Tried to retrieve unset state 'table'!

See: requireState

related(relationName) ⇒ Related

Convenience function to help build Related instances. Pass this instance to with or load to describe an eager relation.

These are equivalent:

BooksWithCover = Books.with('coverImage');
BooksWithCover = Books.with(related('coverImage'));

But using the related wrapper allows chaining for more complex eager loading behaviour:

const { NotFoundError } = atlas.errors;

Books.with(related('coverImage').require()).findBy('title', 'Dune')
  .then(book => renderThumbnail(book))
  .catch(NotFoundError, error => {
    console.error('Could not render thumbnail, no cover image!');
  });

Including nested loading:

Authors.where({ surname: 'Herbert' }).with(
  related('books').with('coverImage', 'blurb')
).fetch().then(authors => {
  res.html(authors.map(renderBibliography).join('<br>'))
})

Returns: Related - A Related instance.
See

Param Type Description
relationName string | Relation The name of a relation registered with relations or a Relation instance.

related.ALL

Select all relations

related.NONE

Clear all relations