4. Reading data

How to use JSData to read data into your application.

Getting started

This page assumes you've read part 3 of the tutorial, Connecting to a data source.

The main purpose of an ORM/ODM is to map rows/documents in a table/collection to objects in memory—to take care of your CRUD. This tutorial covers the "R" in CRUD: read.

At the end of this tutorial you should understand:

  • How to use JSData to read records from a data source into memory
    • How to retrieve a record by primary key vs how to query for a collection of records
  • Basics of JSData's Query Syntax
  • How to eager load relations
  • How to customize the read process

Let's start with a sample posts table from our blog example:

idtitlecontentdate_publishedstatus
...............
41Connecting to a data sourceJSData itself has no...2016-03-11T05:39:13.320Zpublished
42Reading dataThe main purpose of...NULLdraft
...............

A single row represents a single blog post. Each row has a unique identifier that sets it apart from all other rows, but each row also has other fields whose values may or may not be the same as in other rows. In your application you want to consume and manipulate this data using a powerful high-level programming language, as opposed to restricting yourself to the DSL of your data storage engine. Hence the need to map stored data to objects in memory.

This example could very well represent a table in say, a MySQL database, but that's an implementation detail that can be hidden away by an adapter.

👍

Tip

It doesn't matter where or how your data is stored, or what your data even is, as long as it can be expressed in terms of uniquely identifiable records that can be represented as JavaScript Objects.

Here is the same posts data stored in Firebase:

1580

The magic of an adapter-based ORM/ODM is the ability to map data from disparate sources, such as MySQL and Firebase, to objects in memory. The posts shown in the two examples would map to the following JavaScript objects:

[
  {
    "id": 41,
    "title": "Connecting to a data source",
    "content": "JSData itself has no...",
    "date_published": "2016-03-11T05:39:13.320Z",
    "status": "published"
  },
  {
    "id": 42,
    "title": "Reading data",
    "content": "The main purpose of...",
    "date_published": null,
    "status": "draft"
  }
]

So how would JSData read those posts into memory? Continuing with our blog example, here are some simple examples:

// Retrieve post #41
store.find('post', 41)
  .then((post) => {
    console.log(post.id); // 41
    console.log(post.title); // "Connecting to a data source"

    // Retrieve all published posts
    return store.findAll('post', { status: 'published' });
	})
  .then((posts) => {
    // Show that post #41 is in the array of published posts
    posts = posts.filter((post) => post.id === 41);
    console.log(post.id); // 41
    console.log(post.title); // "Connecting to a data source"
  });
// Retrieve post #41
postService.find(41)
  .then((post) => {
    console.log(post.id); // 41
    console.log(post.title); // "Connecting to a data source"

    // Retrieve all published posts
    return postService.findAll({ status: 'published' });
	})
  .then((posts) => {
    const post = posts.filter((post) => post.id === 41)
    console.log(post.id); // 41
    console.log(post.title); // "Connecting to a data source"
  });

👍

Tip

View the live demo of the "Introduction" section.

Understanding "find" vs "findAll"

JSData has two methods for reading data:

  • find
    • Retrieve a single record based on the provided primary key
    • Returned Promise resolves with either the record or undefined if the record doesn't exist
  • findAll
    • Retrieve a collection of records based on an optional selection query
    • Returned Promise resolves with an array of zero or more records

Referencing the sample posts table from the Introduction section, find would be used to retrieve a single row from the posts table, such as post #41, and findAll would be used to retrieve all rows that match an optional selection query.

👍

Tip

Need to retrieve a record by its primary key? Use find. Need to retrieve a collection of records according to some selection criteria? Use findAll.

By themselves find and findAll don't know how to retrieve your data, so they delegate to the find and findAll methods of your configured adapter, and expect to receive back from the adapter a single record or an array of records, respectively.

To use find you must provide the primary key of the record you want to retrieve. This is typically a string or a number. A second options argument can be provided, which is also passed to the adapter's find method.

To use findAll, you can optionally provide a selection query. If you don't provide one, then all records will be retrieved. Otherwise, the result will be filtered, sorted, offset, and limited according to the query. A second options argument can be provided, which is also passed to the adapter's findAll method.

👍

Tip

When using the DataStore component and relying on query caching, DataStore#find and DataStore#findAll should be used to asynchronously load data into the store, but when it comes time to use that data in your View, etc., DataStore#get, DataStore#getAll, and DataStore#filter should be used to synchronously select the needed Records, though there are exceptions.

In most cases when using the DataStore component, your single-page app should be calling DataStore#get, DataStore#getAll, and DataStore#filter more frequently than DataStore#find and DataStore#findAll.

Understanding the JSData Query Syntax

It has been mentioned several times now, so it's time to talk about the "selection query". Here we give an overview with some samples.

In some form or another, you've probably queried a collection. You've run an SQL query, sliced an array, passed a parameter to an HTTP request, or done something to acquire just a subset of the total possible results. Maybe you wanted a single "page" of results, maybe you only wanted results where value > 30, maybe you just wanted your results sorted, or maybe you wanted all of these at once.

Unfortunately, storage engines have different ways to filter, sort, offset, and limit results, so the JSData adapters need a common language for expressing these kinds of queries. Enter JSData's Query Syntax.

Using a static structure, JSData's Query Syntax describes queries against collections—how to filter, sort, offset, and limit. Due to the different capabilities of various storage engines, JSData's Query Syntax is limited. It can't express every kind of query. It's simple, static, and an easy way to hide the differences between storage engines.

👍

Tip

Read the full Query Syntax specification

If you need to run a query that is more complex than the Query Syntax can handle, consider running a more general query and then manipulating the result in your application code, or depending on your adapter, bypassing JSData and sending a custom query directly to the storage engine.

🚧

Caution

JSData's Query Syntax is limited, as it's meant to be simple and database-agnostic. Complex queries may need to be written manually.

Which methods accept a selection query?

  • findAll([query[, opts]])
  • updateAll(props[, query[, opts]])
  • destroyAll([query[, opts]])
  • filter([query])

Where query is a selection query built according to JSData's Query Syntax. Continuing with the posts table example from above, let's run some queries:

// Retrieve all posts
store.findAll('post')
  .then((posts) => {
    // ...

    // same as above
    return store.findAll('post', {});
  })
  .then((posts) => {
    // ...
  });
// Retrieve all published posts
store.findAll('post', {
  status: 'published'
})
  .then((posts) => {
    // ...

    // Here is another way to run the same query:
    return store.findAll('post', {
      where: {
        status: {
          '==': 'published'
        }
      }
    });
  })
  .then((posts) => {
    // ...
  })
// Retrieve all published posts sorted by date_published
store.findAll('post', {
  where: {
    status: {
      '==': 'published'
    }
  },
  orderBy: 'date_published'
})
  .then((posts) => {
    // ...

    // Here is a more complex example:
    return store.findAll('post', {
      where: {
        status: {
          '==': 'published'
        }
      },
      orderBy: [
        // sort by date_published descending
        ['date_published', 'DESC'],
        // sub-sort by title ascending
        ['title', 'ASC']
      ]
    });
  })
  .then((posts) => {
    // ...
  });
const PAGE_SIZE = 10
// 0 -> page 1
// 1 -> page 2
// 2 -> page 3
// etc.
let currentPage = 2

// Retrieve page 3
store.findAll('post', {
  limit: PAGE_SIZE, // 10 posts per page
  offset: currentPage * PAGE_SIZE, // skip the first 2 pages
  status: 'published'
})
  .then((posts) => {
    // ...

    // Here is a more complex example:
    return store.findAll('post', {
      limit: PAGE_SIZE, // 10 posts per page
      offset: currentPage * PAGE_SIZE, // skip the first 2 pages
      where: {
        status: {
          '==': 'published'
        }
      },
      orderBy: [
        // sort by date_published descending
        ['date_published', 'DESC'],
        // sub-sort by title ascending
        ['title', 'ASC']
      ]
    });
  })
  .then((posts) => {
  	// ...
	});
// A more complex example.
//
// Equivalent SQL:
//
// SELECT *
// FROM `posts`
// WHERE
//   `posts`.`status` = "published"
//   AND `posts`.`author` IN ("bob", "alice")
//   OR `posts`.`author` IN ("karen")
// ORDER BY
//   `posts`.`date_published DESC,
//   `posts`.`title`
store.findAll('post', {
  where: {
    status: {
      // WHERE status = 'published'
      '==': 'published'
    },
    author: {
      // AND author IN ('bob', 'alice')
      'in': ['bob', 'alice'],
      // OR author IN ('karen')
      '|in': ['karen']
    }
  },
  orderBy: [
    // sort by date_published descending
    ['date_published', 'DESC'],
    // sub-sort by title ascending
    ['title', 'ASC']
  ]
}).then((posts) => {
  // ...
})
// This shows grouping clauses.
//
// Equivalent SQL:
//
// SELECT *
// FROM `posts`
// WHERE
//   (
//     (`posts`.`content` = "foo" AND `posts`.`status` = "draft")
//     OR
//     (`posts`.`status` = "published"
//   )
//   OR
//   (
//     `posts`.`content` = "test" AND `posts`.`status` = "flagged"
//   )
// ORDER BY `posts`.`status
store.findAll('post', {
  where: [
    [
      {
        content: {
          '=': 'foo'
        },
        status: {
          '=': 'draft'
        }
      },
      'or',
      {
        status: {
          '=': 'published'
        }
      }
    ],
    'or',
    {
      content: {
        '=': 'test'
      },
      status: {
        '=': 'flagged'
      }
    }
  ],
  orderBy: 'status'
}).then((posts) => {
  // ...
});

👍

Tip

View the live demo of the "Query Syntax" section.

Eagerly loading relations

Eager loading relations is when you want to retrieve records and their relations in one step. From your application's point of view it's a single read, but in reality, it is multiple reads (with the exception of the HTTP adapter, if your server can respond to a single request with relations nested in the payload). Convenience is the main benefit of eager loading relations.

🚧

Caution

Eager loading relations can be more efficient, but it's not guaranteed.

Let's start with some sample data:

users

idname
1bob
2alice

posts

iduser_idtitle
411Connecting to a data source
421Reading data

comments

idpost_iduser_idcontent
33412I disagree
34411-1
35412LOL
36422Okay this one makes sense

We've got a users table, a posts table, and a comments table. The relations are:

  • User hasMany Post
  • User hasMany Comment
  • Post belongsTo User
  • Post hasMany Comment
  • Comment belongsTo Post
  • Comment belongsTo User

Let's review the relationships in the code:

export const user = {
  hasMany: {
    post: {
      // database column, e.g. console.log(post.user_id);
      foreignKey: 'user_id',
      // reference to related objects in memory, e.g. user.posts
      localField: 'posts'
    },
    comment: {
      // database column, e.g. console.log(comment.user_id);
      foreignKey: 'user_id',
      // reference to related objects in memory, e.g. user.comments
      localField: 'comments'
    }
  }
};

export const post = {
  belongsTo: {
    // comment belongsTo user
    user: {
      // database column, e.g. console.log(comment.user_id);
      foreignKey: 'user_id',
      // reference to related object in memory, e.g. post.user
      localField: 'user'
    }
  },
  hasMany: {
  	comment: {
      // database column, e.g. console.log(comment.post_id);
      foreignKey: 'post_id',
      // reference to related objects in memory, e.g. post.comments
      localField: 'comments'
    }
  }
};

export const comment = {
  belongsTo: {
    // comment belongsTo user
    user: {
      // database column, e.g. console.log(comment.user_id);
      foreignKey: 'user_id',
      // reference to related object in memory, e.g. comment.user
      localField: 'user'
    },
    // comment belongsTo post
    post: {
      // database column, e.g. console.log(comment.post_id);
      foreignKey: 'post_id',
      // reference to related object in memory, e.g. comment.post
      localField: 'post'
    }
  }
};
import { Schema } from 'js-data';

export const user = new Schema({
  $schema: 'http://json-schema.org/draft-04/schema#', // optional
  title: 'User', // optional
  description: 'Schema for User records', // optional
  type: 'object',
  properties: {
    id: { type: 'number' },
    name: { type: 'string' }
  }
});

export const post = new Schema({
  type: 'object',
  properties: {
    id: { type: 'number' },
    // Only the DataStore component cares about the "indexed" attribute
    user_id: { type: 'number', indexed: true },
    title: { type: 'string' },
    content: { type: 'string' },
    date_published: { type: ['string', 'null'] }
  }
});

export const comment = new Schema({
  type: 'object',
  properties: {
    id: { type: 'number' },
    // Only the DataStore component cares about the "indexed" attribute
    post_id: { type: 'number', indexed: true },
    // Only the DataStore component cares about the "indexed" attribute
    user_id: { type: 'number', indexed: true },
    content: { type: 'string' }
  }
});
// DataStore is mostly recommended for use in the browser
import { DataStore } from 'js-data';
import { HttpAdapter } from 'js-data-http';
import * as schemas from '../shared/schemas';
import * as relations from '../shared/relations';
  
export const adapter = new HttpAdapter();
export const store = new DataStore();
store.registerAdapter('http', adapter, { 'default': true });

store.defineMapper('user', {
  endpoint: 'users',
  schema: schemas.user,
  relations: relations.user
});
store.defineMapper('post', {
  endpoint: 'posts',
  schema: schemas.post,
  relations: relations.post
});
store.defineMapper('comment', {
  endpoint: 'comments',
  schema: schemas.comment,
  relations: relations.comment
});
// Container is mostly recommended for use in Node.js
import { Container } from 'js-data';
import { SqlAdapter } from 'js-data-sql';
import * as schemas from '../shared/schemas';
import * as relations from '../shared/relations';

export const adapter = new SqlAdapter({
  knexOpts: {
    // This adapter will connect to MySQL
    client: 'mysql'
  }
});
export const store = new Container();
store.registerAdapter('sql', adapter, { 'default': true });

store.defineMapper('user', {
  table: 'users',
  schema: schemas.user,
  relations: relations.user
});
store.defineMapper('post', {
  table: 'posts',
  schema: schemas.post,
  relations: relations.post
});
store.defineMapper('comment', {
  table: 'comments',
  schema: schemas.comment,
  relations: relations.comment
});

Now let's run some queries, loading relations in different ways:

// retrieve user #1
store.find('user', 1)
  .then((user) => {
    console.log(user.id); // 1
    console.log(user.name); // "bob"

    // retrieve all comments that belong to user #1
    const query = {
      user_id: 1
    };
    return store.findAll('comment', query);
  })
  .then((comments) => {
    console.log(comments[0].user_id); // 1

    // retrieve user #1 with comments eager loaded
    const opts = {
      with: ['comment']
    }
    return store.find('user', 1, opts);
  })
  .then((user) => {
    console.log(user.id); // 1
    console.log(user.name); // "bob"
    console.log(user.comments); // [{...}, {...}, ...]
    console.log(user.comments[0].user_id); // 1
  });
// retrieve user #1 with posts eager loaded
let opts = {
  with: ['post']
};
store.find('user', 1, opts)
  .then((user) => {
    console.log(user.id); // 1
    console.log(user.name); // "bob"
    console.log(user.posts); // [{...}, {...}, ...]
    console.log(user.posts[0].user_id); // 1

    // retrieve user #1 with posts eager loaded and
    // each post's comments eager loaded
    opts = {
      with: ['post', 'post.comment']
    }
    return store.find('user', 1, opts);
  })
  .then((user) => {
    console.log(user.id); // 1
    console.log(user.name); // "bob"
    console.log(user.posts); // [{...}, {...}, ...]
    console.log(user.posts[0].user_id); // 1
    console.log(user.posts[0].comments); // [{...}, {...}, ...]
    console.log(user.posts[0].id === user.posts[0].comments[0].post_id); // true
  })
// retrieve all users with posts eager loaded
let query = null;
let opts = {
  with: ['post']
};
store.findAll('user', query, opts)
  .then((users) => {
    console.log(users[0].posts); // [{...}, {...}, ...]
    console.log(users[0].id === users[0].posts[0].user_id); // true

    // retrieve all users with posts eager loaded and
    // each post's comments eager loaded
    opts = {
      with: ['post', 'post.comment']
    };
    return store.findAll('user', query, opts)
  })
  .then((users) => {
    console.log(users[0].posts); // [{...}, {...}, ...]
    console.log(users[0].id === users[0].posts[0].user_id); // true

    console.log(users[0].posts); // [{...}, {...}, ...]
    console.log(users[0].posts[0].id === users[0].posts[0].comments[0].post_id); // true
  });
// retrieve all users with only draft posts eager loaded
let query = null;
let opts = {
  with: [
    {
      relation: 'post',
      // This query will be mixed into the query
      // used to load the posts
      query: {
        status: 'draft'
      }
    }
  ]
};
store.findAll('user', query, opts)
  .then((users) => {
    console.log(users[0].posts); // [{...}, {...}, ...]
    console.log(users[0].id === users[0].posts[0].user_id); // true

    // retrieve all users with only published posts eager loaded and
    // each post's comments eager loaded
    opts = {
      with: [
        {
          relation: 'post',
          // This query will be mixed into the query
          // used to load the posts
          query: {
            status: 'published'
          }
        },
        'post.comment'
      ]
    };
    return store.findAll('user', query, opts)
  })
  .then((users) => {
    console.log(users[0].posts); // [{...}, {...}, ...]
    console.log(users[0].id === users[0].posts[0].user_id); // true

    console.log(users[0].posts); // [{...}, {...}, ...]
    console.log(users[0].posts[0].id === users[0].posts[0].comments[0].post_id); // true
  });

👍

Tip

View the live demo of the "Eager loading relations" section.

Sending options to the adapter

find takes a required id argument and an optional options argument. findAll takes optional query and options arguments.

The options argument allows the behavior of find and findAll to be configured when they are executed. find and findAll both delegate to an adapter, which in turn may call several of its own methods in order to accomplish the task.

👍

Tip

The options argument passed to find and findAll is forwarded to subsequent method calls, so you can also set options for methods that are called farther down the operation chain, e.g. adapter and client driver methods.

Here are some examples:

import { DataStore } from 'js-data';
import { HttpAdapter } from 'js-data-http';

const httpAdapter = new HttpAdapter();
const store = new DataStore();

store.registerAdapter('http', httpAdapter, { 'default': true });

store.defineMapper('user', {
  endpoint: 'users'
});

// GET /users/1?debug=true
store.find('user', 1, { params: { debug: true } }); // "params" is an axios option

// GET /users.json
store.findAll('user', null, { suffix: '.json' }); // "suffix" is an http adapter option

// GET /users/1
store.find('user', 1, { debug: true }); // "debug" is an http adapter option

// GET https://otherapi.com/users
store.findAll('user', null, { basePath: 'https://otherapi.com' }); // "basePath" is an http adapter option

// GET /user-detail/1
store.find('user', 1, { url: '/user-detail/1' }); // "url" is an axios option

// GET /appusers/1
store.find('user', 1, { endpoint: 'appusers' }); // "endpoint" is an adapter option
import { Container } from 'js-data';
import { RethinkDBAdapter } from 'js-data-rethinkdb';

const adapter = new RethinkDBAdapter();
const store = new Container();

store.registerAdapter('rethinkdb', adapter, { 'default': true });

store.defineMapper('user', {
  table: 'users'
});

store.find('user', 1, {
  // "readMode" is an r#run option
  runOpts: { readMode: 'outdated' }
});

const query = {};
store.findAll('user', query, {
  // "profile" is an r#run option
  runOpts: { profile: true },
  raw: true
});

store.find('user', 1, { debug: true }); // "debug" is a rethinkdb adapter option

Hooking into the read process

JSData has many lifecycle hooks that you can override to add your custom behavior to different parts of the CRUD process.

beforeFind, afterFind, beforeFindAll, and afterFindAll are available and can be overridden to hook into the read process. Some adapters may have additional hooks that are called during a find or findAll operation, e.g. the deserialize hook of the HTTP adapter.

Here are some examples:

import { DataStore, Mapper } from 'js-data';
import { HttpAdapter } from 'js-data-http';

const hooks = [];
  
const httpAdapter = new HttpAdapter({
	beforeFind: function (mapper, id, opts) {
    // preserve default behavior
    HttpAdapter.prototype.beforeFind.call(this, mapper, id, opts);
    hooks.push('adapter beforeFind');
  },
  afterFind: function (mapper, id, opts, result) {
    // preserve default behavior
    HttpAdapter.prototype.afterFind.call(this, mapper, id, opts, result);
    hooks.push('adapter afterFind');
  }
});
const store = new DataStore();

store.registerAdapter('http', httpAdapter, { 'default': true });

store.defineMapper('user', {
  beforeFind: function (id, opts) {
    // preserve default behavior
    Mapper.prototype.beforeFind.call(this, id, opts);
    hooks.push('user beforeFind');
  },
  afterFind: function (id, opts, result) {
    // preserve default behavior
    Mapper.prototype.beforeFind.call(this, id, opts, result);
    hooks.push('user afterFind');
  }
});

store.find('user', 1).then((user) => {
	console.log(hooks); // [
                      //   'user beforeFind',
                      //   'adapter beforeFind',
                      //   'adapter afterFind',
                      //   'user afterFind'
                      // ]
});
// this example shows how hooks can return
// a promise to make them async

import { DataStore, Mapper } from 'js-data';
import { HttpAdapter } from 'js-data-http';

const hooks = [];
  
const httpAdapter = new HttpAdapter({
	beforeFind: function (mapper, id, opts) {
    return new Promise((resolve) => {
      // preserve default behavior
      HttpAdapter.prototype.beforeFind.call(this, mapper, id, opts);
			hooks.push('adapter beforeFind');
      resolve();
    });
  },
  afterFind: function (mapper, id, opts, result) {
    return new Promise((resolve) => {
      // preserve default behavior
      HttpAdapter.prototype.afterFind.call(this, mapper, id, opts, result);
    	hooks.push('adapter afterFind');
      resolve();
    });
  }
});
const store = new DataStore();

store.registerAdapter('http', httpAdapter, { 'default': true });

store.defineMapper('user', {
  beforeFind: function (id, opts) {
    return new Promise((resolve) => {
      // preserve default behavior
      Mapper.prototype.beforeFind.call(this, id, opts);
    	hooks.push('user beforeFind');
      resolve();
    });
  },
  afterFind: function (id, opts, result) {
    return new Promise((resolve) => {
      // preserve default behavior
      Mapper.prototype.afterFind.call(this, id, opts, result);
    	hooks.push('user afterFind');
      resolve();
    });
  }
});

store.find('user', 1).then((user) => {
	console.log(hooks); // [
                      //   'user beforeFind',
                      //   'adapter beforeFind',
                      //   'adapter afterFind',
                      //   'user afterFind'
                      // ]
})
// this example shows modifying arguments
// as they pass by

import { DataStore, Mapper } from 'js-data';
import { HttpAdapter } from 'js-data-http';

const hooks = [];
  
const httpAdapter = new HttpAdapter({
	beforeFind: function (mapper, id, opts) {
    // preserve default behavior
    HttpAdapter.prototype.beforeFind.call(this, mapper, id, opts);
    options.debug = false;
    hooks.push('adapter beforeFind');
  },
  afterFind: function (mapper, id, opts, result) {
    // preserve default behavior
    HttpAdapter.prototype.afterFind.call(this, mapper, id, opts, result);
    hooks.push('adapter afterFind');
    result._timestamp = new Date();
  }
});
const store = new DataStore();

store.registerAdapter('http', httpAdapter, { 'default': true });

store.defineMapper('user', {
  beforeFind: function (id, opts) {
    // preserve default behavior
    Mapper.prototype.beforeFind.call(this, id, opts);
    options.debug = true;
    hooks.push('user beforeFind');
  },
  afterFind: function (id, opts, result) {
    // preserve default behavior
    Mapper.prototype.afterFind.call(this, id, opts, result);
    hooks.push('user afterFind');
    result._timestamp = result._timestamp.getTime();
  }
});

store.find('user', 1).then((user) => {
	console.log(hooks); // [
                      //   'user beforeFind',
                      //   'adapter beforeFind',
                      //   'adapter afterFind',
                      //   'user afterFind'
                      // ]
});
// this example shows hooks that can return a
// value which will replace the last argument

import { DataStore } from 'js-data';
import { HttpAdapter } from 'js-data-http';

const hooks = [];
  
const httpAdapter = new HttpAdapter({
	afterFind: function (mapper, id, opts, result) {
    // preserve default behavior
    HttpAdapter.prototype.afterFind.call(this, mapper, id, opts, result);
    hooks.push('adapter afterFind');
    return result[mapper.name];
  }
});
const store = new DataStore();

store.registerAdapter('http', httpAdapter, { 'default': true });

store.defineMapper('user', {
  afterFind: function (id, opts, result) {
    // preserve default behavior
    Mapper.prototype.afterFind.call(this, id, opts, result);
    hooks.push('user afterFind');
    
    // can replace result AND be async
    return new Promise((resolve) => {
    	resolve({
        id: result.id,
        profile: result
      });
    });
  }
});

store.find('user', 1).then((user) => {
	console.log(hooks); // [
                      //   'adapter afterFind',
                      //   'user afterFind'
                      // ]
})

The findAll lifecycle hooks work similarly. You can read more about them at http://api.js-data.io.

👍

Tip

View a live demo of beforeFindAll and afterFindAll hooks.

Making reads with metadata

Sometimes you want to retrieve more than just the resulting record(s)—you want the metadata too. This is especially common with the HTTP adapter. Use the raw option to receive a result object that contains the data and any metadata.

👍

Tip

Use the raw option to receive a more detailed response, with the normally returned response nested in the detailed response under data.

Here are some examples:

import { DataStore } from 'js-data';
import { HttpAdapter } from 'js-data-http';

const httpAdapter = new HttpAdapter();
const store = new DataStore();

store.registerAdapter('http', httpAdapter, { 'default': true });

store.defineMapper('user');

store.find('user', 1)
  .then((user) => {
    console.log(user); // { id: 1, ... }

    store.find('user', 2, { raw: true });
  })
  .then((result) => {
    console.log(result.data); // { id: 2, ... }
    console.log(result.found); // 1
    console.log(result.headers); // { ... }
    console.log(result.statusCode); // 200
    console.log(result.adapter); // "http"
    // etc.
  });
import { Container } from 'js-data';
import { SqlAdapter } from 'js-data-sql';

const sqlAdapter = new SqlAdapter();
const store = new Container();

store.registerAdapter('sql', sqlAdapter, { 'default': true });

store.defineMapper('user');

store.findAll('user')
  .then((users) => {
    console.log(users.length); // 4

    return store.findAll('user', null, { raw: true });
  })
  .then((result) => {
    console.log(result.data.length); // 4
    console.log(result.found); // 4
    console.log(result.adapter); // "sql"
    // etc.
  });

👍

Tip

View the live demo of the "Reads with metadata" section.

Making custom queries

Sometimes you want to run a query or retrieve data in such a way that can't be satisfied by find or findAll. You may have to talk directly to your storage engine.

👍

Tip

Adapters typically expose the client driver that they use so you can execute custom queries.

Here are some examples:

import { DataStore } from 'js-data';
import { HttpAdapter } from 'js-data-http';

const httpAdapter = new HttpAdapter();
const store = new DataStore();

store.registerAdapter('http', httpAdapter, { 'default': true });

store.defineMapper('user');

// Make a GET request
// GET /user/1/report
httpAdapter.GET('/user/1/report');

// More low-level
// GET /user/1/report
httpAdapter.HTTP({
  url: '/user/1/report',
  method: 'GET'
});

// Even more low-level, use axios directly
// GET /user/1/report
httpAdapter.http({
  url: '/user/1/report',
  method: 'GET'
});

httpAdapter.addAction('userAction', {
  getEndpoint (userService, options) {
    let url = `/user-actions/${options.action}`;
    if (options.id !== undefined) {
      url = `${url}/${options.id}`;
    }
    return url;
  },
  method: 'POST',
  response (res) {
    return res.data;
  }
})(store.getMapper('user'));

// POST /userActions/makeAdmin/1
store.getMapper('user').userAction({
  id: 1,
  action: 'makeAdmin'
}).then((result) => { /*...*/ });

// GET /userActions/getActive?limit=10
store.getMapper('user').userAction({
  action: 'getActive',
  method: 'GET',
  params: {
    limit: 10
  }
}).then((activeUsers) => { /*...*/ });

// POST /userActions/login
//   body: {"username":"bob","password":"welcome1234"}
store.getMapper('user').userAction({
  action: 'login',
  data: {
    username: 'bob',
    password: 'welcome1234'
  }
}).then((result) => { /*...*/ });
import { Container } from 'js-data';
import { MongoDBAdapter } from 'js-data-mongodb';

const mongoAdapter = new MongoDBAdapter();
const store = new Container();

store.registerAdapter('mongodb', mongoAdapter, { 'default': true });

store.defineMapper('user')

mongoAdapter.getClient()
  .then((client) => {
    return new Promise((resolve, reject) => {
      client
        .collection('user')
        .find({ active: true })
        .count((err, count) => err ? reject(err) : resolve(count));
	})
  .then((numActiveUsers) => {
    // ...  
  });
import { Container } from 'js-data';
import { RethinkDBAdapter } from 'js-data-rethinkdb';

const rethinkAdapter = new RethinkDBAdapter();
const store = new Container();

store.registerAdapter('rethinkdb', rethinkAdapter, { 'default': true });

store.defineMapper('user');

rethinkAdapter.r
  .table('user')
  .filter({ active: true })
  .count()
  .run()
  .then((numActiveUsers) => {
    // ...  
  });
import { Container } from 'js-data';
import { SqlAdapter } from 'js-data-sql';

const sqlAdapter = new SqlAdapter({
  knexOpts: {
    client: 'mysql'
  }
});
const store = new Container();

store.registerAdapter('sql', sqlAdapter, { 'default': true });

store.defineMapper('user');

sqlAdapter.query
  .table('user')
  .where({ active: true })
  .count('*')
  .then((numActiveUsers) => {
    // ...  
  });

👍

Tip

View the live demo of the "Custom Queries" section.

🚧

See an issue with this tutorial?

You can open an issue or better yet, suggest edits right on this page.


What’s Next

Continue with part 5 of the Tutorial or explore other documentation.