2. Modeling your data
Mapping your persisted data into in-memory objects
Getting started
How you organize your data will have a significant impact on the design, complexity, and performance of your application. Many approaches have been theorized, but there is only one right way to do it. Just kidding. There are, however, tradeoffs that make different approaches better or worse for certain use cases.
It is likely JSData has not completely foreseen every nuance of how you organize your data, how you structure your API, or how you move data into and out of your database. JSData instead attempts to satisfy 95% of CRUD scenarios with a balance of convention and configuration, leaving the remaining highly optimized, ultra performant, super custom, or unorthodox bits to you.
JSData is the "Twitter Bootstrap of data layers", so to speak.
Tip
Before continuing with tutorial, make sure you understand the concepts behind JSData.
At the end of this tutorial you should understand:
- The basics of how to model your data with JSData
- The purpose of the
Mapper
component - The basics of defining schemata and relationships
This tutorial uses a simple blog example to walk through a basic approach to modeling your data using JSData.
Identifying your Resources
What is a Resource? What is a Model? These are heavily overloaded terms, but in this documentation, a Resource or Model is defined as a data abstraction. A Resource is "CRUD-able"—instances of a Resource can be created, read, updated, and deleted. Here's an example: we're building a blog and we decide that our blog's data consists of "users", "posts", and "comments". These data abstractions are our Resources—a User Resource, a Post Resource, and a Comment Resource. Identifying your Resources may be as simple as looking at what tables you have in your database or what Resources are exposed by your API.
JSData performs CRUD operations in that it creates, reads, updates, and deletes Records. What is a Record? A Record is a single, uniquely identifiable instance of some Resource. A Record might be a single row in a MySql table, a single document in a MongoDB collection, or an object (that has a unique identifier) generated or aggregated from other data, like a report.
Now for the main purpose of JSData: Mappers. A Mapper (the core component of JSData) maps Records of a Resource from their persisted state to objects in memory and vice versa.
Tip
A Resource is a "CRUD-able" data abstraction. A Record is a unique instance of a Resource. A Mapper maps Records of a Resource to objects in memory.
Defining Mappers
For each of your CRUD Resources, JSData needs a Mapper. A Mapper knows how to perform CRUD operations for a Resource. Using the blog example, we need a User Mapper, Post Mapper, and a Comment Mapper, each performing CRUD operations for their respective Resource.
Tip
Read more about the Data Mapper Pattern and JSData's
Mapper
component.
Let's define the Mappers needed for the server part of our blog application:
// It is recommended that you use JSData's Container,
// SimpleStore, or DataStore classes to manage your Mappers.
// Import the necessary components
import { Mapper } from 'js-data';
import { SqlAdapter } from 'js-data-sql';
// Instantiate the SqlAdapter
const adapter = new SqlAdapter({
knexOpts: {
// This example server application uses a MySQL database
client: 'mysql'
}
});
// Instantiate a Mapper
const userService = new Mapper({ name: 'user' });
// Register the adapter with the Mapper
userService.registerAdapter('sql', adapter, { 'default': true });
// For this Mapper, specify a specific table name, instead of
// the Mapper inferring the table name from the "name" option
const postService = new Mapper({ name: 'post', table: 'posts' });
postService.registerAdapter('sql', adapter, { 'default': true });
const commentService = new Mapper({ name: 'comment' });
commentService.registerAdapter('sql', adapter, { 'default': true });
Let's review the above example:
We defined three Mappers and saved them to userService
, postService
, and commentService
variables. As each Mapper was instantiated it was passed a name
option. This metadata allows each Mapper to know which resource it's dealing with. For example, userService
infers that it will be performing operations against a user
table. The three Mappers can perform CRUD operations against MySQL tables because they're using the SQL adapter, for example:
postService.findAll([query][, opts])
- Retrieve a selection of post records from theposts
tablepostService.find(id[, opts])
- Retrieve a post record from theposts
table by its primary keypostService.create(record[, opts])
- Create one new post record in theposts
tablepostService.createMany(records[, opts])
- Create many new post records in theposts
tablepostService.update(id, props[, opts])
- Update a single post record in theposts
tablepostService.updateAll(props[, query][, opts])
- Perform a single update against multiple post records in theposts
tablepostService.updateMany(records[, opts])
- Perform multiple individual updates against post records in theposts
tablepostService.destroy(id[, opts])
- Delete a post record from theposts
table by its primary keypostService.destroyAll([query][, opts])
- Delete a selection of post records from theposts
table
Now let's define the Mappers needed for the browser part of our blog application:
// It is recommended that you use JSData's Container,
// SimpleStore, or DataStore classes to manage your Mappers.
// Import the necessary components
import { Mapper } from 'js-data';
import { HttpAdapter } from 'js-data-http';
// Instantiate the HttpAdapter
const adapter = new HttpAdapter();
// Instantiate a Mapper
const userService = new Mapper({ name: 'user' });
// Register the adapter with the Mapper
userService.registerAdapter('http', adapter, { 'default': true });
// For this Mapper, specify a specific REST endpoint, instead of
// the Mapper inferring the REST endpoint from the "name" option
const postService = new Mapper({ name: 'post', endpoint: 'posts' });
postService.registerAdapter('http', adapter, { 'default': true });
const commentService = new Mapper({ name: 'comment' });
commentService.registerAdapter('http', adapter, { 'default': true });
We defined three Mappers and saved them to userService
, postService
, and commentService
variables. As each Mapper was instantiated it was passed a name
option. This metadata allows each Mapper to know which resource it's dealing with. For example, userService
infers that it will be performing operations against a user
table. The three Mappers can perform CRUD operations against REST API endpoints because they're using the HTTP adapter, for example:
postService.findAll([query][, opts])
-GET /posts
- Retrieve a selection of post records from the/posts
REST endpointpostService.find(id[, opts])
-GET /posts/:id
- Retrieve a post record from the/posts
REST endpoint by its primary keypostService.create(record[, opts])
-POST /posts
- Create one new post record via the/posts
REST endpointpostService.createMany(records[, opts])
-POST /posts
- Create many new post records via the/posts
REST endpointpostService.update(id, props[, opts])
-PUT /posts/:id
- Update a single post record via the/posts
REST endpointpostService.updateAll(props[, query][, opts])
-PUT /posts
- Perform a single update against multiple post records via the/posts
REST endpointpostService.updateMany(records[, opts])
-PUT /posts
- Perform multiple individual updates against post records via the/posts
REST endpointpostService.destroy(id[, opts])
-DELETE /posts:id
- Delete a post record via the/posts
REST endpoint by its primary keypostService.destroyAll([query][, opts])
-DELETE /posts
- Delete a selection of post records via the/posts
REST endpoint
Short example
Let's run through a scenario and talk about where JSData fits in: creating a new post.
- Client: User clicks "new post"
- Client: The "new post" view is displayed
- Client: The view collects the user's input
- Client: The User clicks "save"
- Client: At this point, the client calls
postService.create(newPostData)
- Client: A request is made:
POST /posts
with body:{"title":"my new post","content":"once upon a time...","user_id":1234}
- Server: Server receives request at the
app.route('/posts').post(...)
- Server: Server calls
postService.create(req.body)
- Server: A row is inserted into the MySQL
posts
table - Server: The promise returned by
postService.create
resolves with the createdpost
record - Server: Server responds to request with created
post
record - Client: The promise returned by the client's
postService.create
call resolves with the createdpost
record - Client: Client redirects to
/posts/:id
- Client: The Post view displays the appropriate
post
record.
This scenario is a bit simplified, as there is probably be some data validation and permissions checking that needs to happen, and probably the server would assign the user_id
to the post, but it shows JSData's role in handling the CRUD aspects of our blog application.
Tip
View the live demo of the "Define Mappers" section.
Adding a Schema
ORMs typically allow you to define a schema for your Resources in order to enable application-level validation. Let's explore schemas in JSData.
First, schemas in JSData follow the JSON-Schema specification. JSData also defines some new keywords for some additional features like the following:
- The
DataStore
component looks forindexed
andtrack
keywords to enable secondary indexes and active change detection in its in-memory store. - If its
applySchema
option istrue
, aMapper
component enables real-time validation of Records during property assignment, which makes it impossible to put an in-memory Record instance into a bad state.
Tip
Read more about validation and secondary indexes. Read
Schema
class API Reference Documentation.
Second, you can either define a standalone schema or provide the schema configuration when defining a Mapper. Here is an example:
import { Mapper, Schema } from 'js-data';
const userService = new Mapper({
name: 'user',
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' }
}
}
});
console.log(userService.schema); // Schema {...}
import { Mapper, Schema } from 'js-data';
const userSchema = 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' }
}
});
const userService = new Mapper({
name: 'user',
schema: userSchema
});
console.log(userService.schema === postSchema); // true
Let's update our blog example:
import { Mapper, Schema } from 'js-data';
import { HttpAdapter } from 'js-data-http';
const adapter = new HttpAdapter();
const userSchema = 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' }
}
});
const postSchema = new Schema({
type: 'object',
properties: {
id: { type: 'number' },
user_id: { type: 'number', indexed: true },
title: { type: 'string' },
content: { type: 'string' },
date_published: { type: ['string', 'null'] }
}
});
const commentSchema = new Schema({
type: 'object',
properties: {
id: { type: 'number' },
post_id: { type: 'number', indexed: true },
user_id: { type: 'number', indexed: true },
content: { type: 'string' }
}
});
const userService = new Mapper({ name: 'user', endpoint: 'users' });
userService.registerAdapter('http', adapter, { 'default': true });
const postService = new Mapper({ name: 'post', endpoint: 'posts' });
postService.registerAdapter('http', adapter, { 'default': true });
const commentService = new Mapper({ name: 'comment', endpoint: 'comments' });
commentService.registerAdapter('http', adapter, { 'default': true });
import { Mapper, Schema } from 'js-data';
import { SqlAdapter } from 'js-data-sql';
const adapter = new SqlAdapter({
knexOpts: {
client: 'mysql'
}
});
const userSchema = 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' }
}
});
const postSchema = new Schema({
type: 'object',
properties: {
id: { type: 'number' },
user_id: { type: 'number', indexed: true },
title: { type: 'string' },
content: { type: 'string' }
}
});
const commentSchema = new Schema({
type: 'object',
properties: {
id: { type: 'number' },
post_id: { type: 'number', indexed: true },
user_id: { type: 'number', indexed: true },
content: { type: 'string' }
}
});
const userService = new Mapper({ name: 'user', table: 'users' });
userService.registerAdapter('sql', adapter, { 'default': true });
const postService = new Mapper({ name: 'post', table: 'posts' });
postService.registerAdapter('sql', adapter, { 'default': true });
const commentService = new Mapper({ name: 'comment', table: 'comments' });
commentService.registerAdapter('sql', adapter, { 'default': true });
Notice how there is now a somewhat uncomfortable amount of duplicated code between the client and server code. Depending on your situation and preference you could refactor common bits into files that are shared by the client and the server (see Universal JavaScript).
Here's an example of shared JavaScript:
import { Mapper } from 'js-data';
import { HttpAdapter } from 'js-data-http';
import * as schemas from '../shared/schemas';
const adapter = new HttpAdapter();
const userService = new Mapper({
name: 'user',
endpoint: 'users',
schema: schemas.user
});
userService.registerAdapter('http', adapter, { 'default': true });
const postService = new Mapper({
name: 'post',
endpoint: 'posts',
schema: schemas.post
});
postService.registerAdapter('http', adapter, { 'default': true });
const commentService = new Mapper({
name: 'comment',
endpoint: 'comments',
schema: schemas.comment
});
commentService.registerAdapter('http', adapter, { 'default': true });
import { Mapper } from 'js-data';
import { SqlAdapter } from 'js-data-sql';
import * as schemas from '../shared/schemas';
const adapter = new SqlAdapter({
knexOpts: {
client: 'mysql'
}
});
const userService = new Mapper({
name: 'user',
table: 'users',
schema: schemas.user
});
userService.registerAdapter('sql', adapter, { 'default': true });
const postService = new Mapper({
name: 'post',
table: 'posts',
schema: schemas.post
});
postService.registerAdapter('sql', adapter, { 'default': true });
const commentService = new Mapper({
name: 'comment',
table: 'comments',
schema: schemas.comment
});
commentService.registerAdapter('sql', adapter, { 'default': true });
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 and SimpleStore components care 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 and SimpleStore components care about the "indexed" attribute
post_id: { type: 'number', indexed: true },
// Only the DataStore and SimpleStore components care about the "indexed" attribute
user_id: { type: 'number', indexed: true },
content: { type: 'string' }
}
});
Tip
View the live demo of the "Add a Schema" section.
Choosing a Store
At this point it may have occurred to you that the examples call registerAdapter
an awful a lot. In addition, your application may have tens or hundreds of Resources, leading to tens or hundreds of Mappers. Managing so many Mapper instances is a hassle that doesn't need to be solved twice, so JSData provides the Container
component.
A Container
is nothing more than a glorified bucket of Mappers. With a Container
you don't have to keep track of your Mapper instances. Just use the Container
to instantiate the Mappers, then pass the Container
around in your application.
Tip
With a
Container
or its derivatives (SimpleStore
andDataStore
) it's much easier to manage all of your Mappers.
Container
proxies Mapper methods, so just pass the name of the Mapper as the first argument to the method, for example: store.find('post', 1234)
.
Let's update the blog example:
import { Container } from 'js-data';
import { SqlAdapter } from 'js-data-sql';
import * as schemas from '../shared/schemas';
const adapter = new SqlAdapter({
knexOpts: {
client: 'mysql'
}
});
const store = new Container();
// Adapters registered with the Container are shared
// by all Mappers in the Container.
store.registerAdapter('sql', adapter, { 'default': true });
store.defineMapper('user', {
table: 'users',
schema: schemas.user
});
store.defineMapper('post', {
table: 'posts',
schema: schemas.post
});
store.defineMapper('comment', {
table: 'comments',
schema: schemas.comment
});
import { DataStore } from 'js-data';
import { HttpAdapter } from 'js-data-http';
import * as schemas from '../shared/schemas';
const adapter = new HttpAdapter();
const store = new DataStore();
// Adapters registered with the Container are shared
// by all Mappers in the Container.
store.registerAdapter('http', adapter, { 'default': true });
store.defineMapper('user', {
endpoint: 'users',
schema: schemas.user
});
store.defineMapper('post', {
endpoint: 'posts',
schema: schemas.post
});
store.defineMapper('comment', {
endpoint: 'comments',
schema: schemas.comment
});
The client example uses DataStore
instead of Container
. DataStore
extends Container
and adds a caching layer using the Collection
to store records in memory for instance access. This use case is much more relevant in the browser than it is on the server, hence the server example just uses `Container.
Tip
View the live demo of the "Choose a Store" section.
Defining relationships
JSData helps you keep track of the relationships among your Resources. In our blog example, how are the User, Post, and Comment Resources related? Let's break it down:
- User — one-to-many — Post
- User — one-to-many — Comment
- Post — one-to-many — Comment
Here is how JSData looks at it:
- User
hasMany
Post and PostbelongsTo
User - User
hasMany
Comment and CommentbelongsTo
User - Post
hasMany
Comment and CommentbelongsTo
Post
"User hasMany
Post and Post belongsTo
User" represents both directions of the relationship. For a given User record, what are its related Post records? For a given Post record, what is its related User record? JSData provides facilities to drill into relationships in both directions.
Tip
Read more about relations here.
So how do we tell JSData about the relationships? Let's look at the "User hasMany
Post" relation first:
User hasMany
Post means that for every User record there are zero or more related Post records. How do we know which Post records are related to any given User record? Typically one would use a foreign key to relate one record to another. In SQL databases, a foreign key is a field in one table that uniquely identifies a row of another table. So, the posts
table might have a user_id
column that holds the primary key (e.g. id
) of a row in the users
table. Let's try it:
store.defineMapper('user', {
table: 'users',
relations: {
hasMany: {
// "post" is the name of the Post Mapper
post: {
// The "posts" table has a "user_id" column
foreignKey: 'user_id',
// In-memory User records will be linked to their
// related in-memory Post records via "user.post"
localField: 'post'
}
}
}
})
In the above example, we define a Mapper for the User Resource and introduced the relations
keyword. Nested within the relations
configuration object are the hasMany
keyword and another configuration object. The keys of the hasMany
configuration object are the names of related Mappers. When we previously defined a Mapper for the Post resource we used the name "post"
, so in our User hasMany
Post relationship, we use the key post
so JSData can find the appropriate Mapper.
Let's see how to define the inverse relationship:
store.defineMapper('post', {
table: 'posts',
relations: {
belongsTo: {
// "user" is the name of the User Mapper
user: {
// The "posts" table has a "user_id" column
foreignKey: 'user_id',
// In-memory Post records will be linked to their
// related in-memory User records via "post.user"
localField: 'user'
}
}
}
})
The foreignKey
and localField
options actually allow JSData to connect the dots. foreignKey
specifies the field that holds the primary key of the related record. localField
specifies the property that JSData will use to join related objects in memory. Let's visualize some data:
{
"id": 1,
"name": 'John'
}
[
{
"id": 41,
"title": "Connecting to a data source",
"content": "See http://js-data.io/docs/connecting-to-a-data-source",
"user_id": 1
},
{
"id": 42,
"title": "Reading data",
"content": "See http://js-data.io/docs/reading-data",
"user_id": 1
}
]
Okay, so what is the benefit of telling JSData about the relationships? Here are some examples:
// When using a DataStore component, nested data is automatically
// added to the right parts of the in-memory store.
const user = store.add('user', {
id: 1,
name: 'John',
posts: [
{
id: 41,
title: 'Connecting to a data source',
user_id: 1,
comments: [
{
id: 756,
post_id: 41,
user_id: 4,
content: 'Nice post!'
}
]
}
]
});
// These assertions show that the user object and its embedded
// relations were correctly parsed and inserted into the store
assert.isTrue(user === store.get('user', 1));
assert.deepEqual(
user.posts,
// fast lookup in the in-memory store using a secondary index
store.getAll('post', 1, { index: 'user_id' })
);
assert.deepEqual(
user.posts,
// filtering is slower than using "getAll" and the secondary index
store.filter('post', { user_id: user.id })
);
assert.deepEqual(
user.posts[0].comments,
// fast lookup in the in-memory store using a secondary index
store.getAll('comment', 41, { index: 'post_id' })
);
assert.deepEqual(
user.posts[0].comments,
// filtering is slower than using "getAll" and the secondary index
store.filter('comment', { post_id: user.posts[0].id })
);
assert.isTrue(user.post === store.get('post', 41));
assert.isTrue(user.comments[0].post === store.get('post', 41));
assert.isTrue(store.get('post', 41).user === user);
// When reading data, eager loading relations is convenient and sometimes
// more efficient.
// Note: All adapters except the Http adapter support eager loading, because
// the Http adapter can't force your server to return embedded relations, but
// you can if you want, as the "DataStore insertion" example shows.
// Retrieve a user with its posts and the posts' comments embedded
const user = await store.find('user', 1, { with: ['post', 'post.comment'] })
user // { id: 1, name: 'John', posts: [...] }
user.posts // [...]
user.posts[0] // { id: 42, title: 'Reading data', comments: [...], user_id: 1 }
users.posts[0].comments // [...]
// When using a DataStore component, data that has been inserted
// into the store is automatically linked to its relations that
// reside elsewhere in the store
const user = store.add('user', {
id: 1,
name: 'John'
})
const posts = store.add('post', [
{
id: 41,
title: 'Connecting to a data source',
user_id: 1
}
])
const comments = store.add('comment', [
{
id: 756,
post_id: 41,
user_id: 4,
content: 'Nice post!'
}
])
// These assertions show that the user, posts, and comments have been
// automatically linked together. These are the exact same assertions
// from the "DataStore insertion" example.
assert.isTrue(user === store.get('user', 1))
assert.deepEqual(
user.posts,
// fast lookup in the in-memory store using a secondary index
store.getAll('post', 1, { index: 'user_id' })
)
assert.deepEqual(
user.posts,
// filtering is slower than using "getAll" and the secondary index
store.filter('post', { user_id: user.id })
)
assert.deepEqual(
user.posts[0].comments,
// fast lookup in the in-memory store using a secondary index
store.getAll('comment', 41, { index: 'post_id' })
)
assert.deepEqual(
user.posts[0].comments,
// filtering is slower than using "getAll" and the secondary index
store.filter('comment', { post_id: user.posts[0].id })
)
assert.isTrue(user.post === store.get('post', 41))
assert.isTrue(user.comments[0].post === store.get('post', 41))
assert.isTrue(store.get('post', 41).user === user)
// Want a plain object representation of some data? Try this:
const user = store.createRecord('user', {
id: 1,
name: 'John',
posts: [
{
id: 41,
title: 'Connecting to a data source',
user_id: 1,
comments: [
{
id: 756,
post_id: 41,
user_id: 4,
content: 'Nice post!'
}
]
}
]
})
// "user" is an instance of Record
store.is('user', user) // true
// "user.posts[0]" is an instance of Record
store.is('post', user.posts[0]) // true
// "user.posts[0].comments[0]" is an instance of Record
store.is('comment', user.posts[0].comments[0]) // true
let json = user.toJSON({ with: ['post', 'post.comment'] })
json // { id: 1, name: 'John', posts: [...] }
json.posts // [...]
json.posts[0] // { id: 42, title: 'Reading data', comments: [...], user_id: 1 }
json.posts[0].comments // [...]
// "json" is a plain object
store.is('user', json) // false
json.toJSON // undefined
// "json.posts[0]" is a plain object
store.is('post', json.posts[0]) // false
// "json.posts[0].comments[0]" is a plain object
store.is('comment', json.posts[0].comments[0]) // false
// Don't include relations
json = user.toJSON()
json // { id: 1, name: 'John' }
json.posts // undefined
Here is our blog example updated with relationships:
export const user = {
hasMany: {
post: {
// database column, e.g. console.log(post.user_id) // 2
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) // 16
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) // 2
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) // 5
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) // 16
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) // 5
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
});
Tip
View the live demo of the "Define relationships" section.
Full example
The completed example blog application is available on GitHub.
See an issue with this tutorial?
You can open an issue or better yet, suggest edits right on this page.
Updated about 5 years ago
Continue with part 3 of the Tutorial or explore other documentation.