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.
Not yet on npm, install direct from GitHub:
$ npm install rhys-vdw/atlas --save
$ npm install knex --save
// 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 });
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', }
]
}
- Atlas
The
atlas
instance is a helper function. It wraps aKnex
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.
- 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 withnew
, 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 incamelCase
. This is okay because theMapper
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.
- Related ⇐
ImmutableBase
Related
is a helper to describe relation trees. Instances are passed toMapper.with
andMapper.load
for eager loading.
- errors :
object
Can be accessed via errors or imported directly.
const { NotFoundError } = Atlas.errors;
import { NotFoundError } from 'atlas/errors';
- 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>')) })
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.`);
});
- Atlas
- new Atlas(knex, [registry])
- instance
- static
- inner
- ~transactionCallback :
function
- ~transactionCallback :
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' . |
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
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. |
Installed version of Atlas.
console.log(Atlas.VERSION);
// 1.0.0
Atlas.errors : errors
Properties
Name | Type |
---|---|
CamelCase | CamelCase |
FormatAttributes | FormatAttributes |
Timestamp | Timestamp |
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. |
Eager loads related records into an existing record.
See: load
- EagerLoader
- new EagerLoader(Self, related)
- .into() ⇒
Promise.<(Object|Array.<Object>)>
Param | Type | Description |
---|---|---|
Self | Mapper |
Mapper of target records. |
related | Related | Array.<Related> |
One or more Related instances describing the relation tree. |
Load relations into one or more records.
Returns: Promise.<(Object|Array.<Object>)>
- One or more records with relations.
Base class for Mapper.
- ImmutableBase
- instance
- inner
- ~callSuper ⇒
mixed
- ~extendCallback ⇒
Object
- ~callSuper ⇒
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. |
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. |
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']
}
});
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);
});
});
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 ⇐
ImmutableBase
- instance
- .all() ⇒
Mapper
- .asImmutable() ⇒
ImmutableBase
- .asMutable() ⇒
ImmutableBase
- .attributes(...attributes) ⇒
Mapper
- .count() ⇒
Promise.<Number>
- .defaultAttribute(attribute, value) ⇒
Mapper
- .defaultAttributes(attributes) ⇒
Mapper
- .destroy(ids) ⇒
Promise.<Number>
- .destroyAll() ⇒
Promise.<Number>
- .extend(...callbackOrMethodsByName) ⇒
ImmutableBase
- .fetch() ⇒
Promise.<(Object|Array.<Object>)>
- .fetchAll() ⇒
Promise.<Array.<Object>>
- .find(...ids) ⇒
Promise.<(Object|Array.<Object>)>
- .findBy() ⇒
Promise.<(Object|Array.<Object>)>
- .first() ⇒
Mapper
- .forge(attributes)
- .getRelation(relationName) ⇒
Relation
- .idAttribute(idAttribute) ⇒
Mapper
- .insert(records) ⇒
Promise.<(Object|Array.<Object>)>
- .isNew(record) ⇒
bool
- .joinMapper(Other, selfAttribute, otherAttribute) ⇒
Mapper
- .joinRelation(relationName) ⇒
Mapper
- .load(...related) ⇒
EagerLoader
- .omitPivot() ⇒
Mapper
- .one() ⇒
Mapper
- .orderBy(attribute, [direction]) ⇒
Mapper
- .pivotAttributes(attributes) ⇒
Mapper
- .query(method, ...args) ⇒
Mapper
- .relations(relationFactoryByName) ⇒
Mapper
- .require() ⇒
Mapper
- .requireState(key) ⇒
mixed
- .save(records) ⇒
Promise.<(Object|Array.<Object>)>
- .setState(nextState) ⇒
ImmutableBase
- .state :
Object
- .strictAttribute(attribute, value) ⇒
Mapper
- .strictAttributes(attributes) ⇒
Mapper
- .table(table) ⇒
Mapper
- .target(ids) ⇒
Mapper
- .targetBy(attribute, ids) ⇒
Mapper
- .toQueryBuilder() ⇒
QueryBuilder
- .update(records) ⇒
Promise.<(Object|Array.<Object>)>
- .updateAll(attributes) ⇒
Promise.<(Array.<Object>|Number)>
- .where(attribute, ...args) ⇒
Mapper
- .whereIn() ⇒
Mapper
- .with(...related) ⇒
Mapper
- .withMutations(...initializer) ⇒
ImmutableBase
- .all() ⇒
- inner
- ~attributeCallback ⇒
mixed
|undefined
- ~createRelation ⇒
Relation
- ~attributeCallback ⇒
- instance
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. |
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. |
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)
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. |
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}`);
);
Retrieve an array of records.
Alias for Mapper.[all](#Mapper+all).[fetch](#Mapper+fetch)
.
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
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
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 |
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='[email protected]';
Accounts.find('[email protected]').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. |
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. |
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: '[email protected]', name: 'Bob', id: 5 };
const jane = { email: '[email protected]', 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!');
});
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. |
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. |
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). |
Return a copy of the underlying QueryBuilder
instance.
Returns: QueryBuilder
- QueryBuilder instance.
See: http://knexjs.org
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. |
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', '[email protected]').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']
}
});
Returns: mixed
| undefined
- Either a value to be assigned to an attribute, or undefined
to mean
none should be set.
Param | Type |
---|---|
attributes | Object |
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. |
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 ⇐
ImmutableBase
- .as(name) ⇒
Related
- .asImmutable() ⇒
ImmutableBase
- .asMutable() ⇒
ImmutableBase
- .extend(...callbackOrMethodsByName) ⇒
ImmutableBase
- .mapper(...initializers) ⇒
Related
- .recursions(recursions) ⇒
Related
- .require() ⇒
Related
- .requireState(key) ⇒
mixed
- .setState(nextState) ⇒
ImmutableBase
- .state :
Object
- .with(...related) ⇒
Related
- .withMutations(...initializer) ⇒
ImmutableBase
- .as(name) ⇒
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.
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. |
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']
}
});
Can be accessed via errors or imported directly.
const { NotFoundError } = Atlas.errors;
import { NotFoundError } from 'atlas/errors';
- errors :
object
No records returned.
Mapper.require().fetch().then(...).catch(error => {
console.log(error);
// ERROR: No rows found!
});
See: require
A specific record was not found.
Users.require().find(999).then(...).catch(error => {
console.log(error);
// ERROR: No row found!
});
See: require
A Mapper was found at this registry key.
atlas.register('Mapper', Mapper.table('users'));
// ERROR: 'Mapper' already registered!
Record could not be identified.
Users.update({ name: 'Bob' })
// ERROR: Expected record to have ID attribute 'id'!
A Mapper was not found.
atlas.register('Users', Mapper.table('users'));
atlas('Useers').fetch()
// ERROR: Unknown registry key 'Useers'!
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. |
Select all relations
Clear all relations