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:
id | title | content | date_published | status |
---|---|---|---|---|
... | ... | ... | ... | ... |
41 | Connecting to a data source | JSData itself has no... | 2016-03-11T05:39:13.320Z | published |
42 | Reading data | The main purpose of... | NULL | draft |
... | ... | ... | ... | ... |
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:
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? UsefindAll
.
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
id | name |
---|---|
1 | bob |
2 | alice |
posts
id | user_id | title |
---|---|---|
41 | 1 | Connecting to a data source |
42 | 1 | Reading data |
comments
id | post_id | user_id | content |
---|---|---|---|
33 | 41 | 2 | I disagree |
34 | 41 | 1 | -1 |
35 | 41 | 2 | LOL |
36 | 42 | 2 | Okay 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 tofind
andfindAll
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
andafterFindAll
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 underdata
.
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.
Updated over 7 years ago
Continue with part 5 of the Tutorial or explore other documentation.