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 the posts table
  • postService.find(id[, opts]) - Retrieve a post record from the posts table by its primary key
  • postService.create(record[, opts]) - Create one new post record in the posts table
  • postService.createMany(records[, opts]) - Create many new post records in the posts table
  • postService.update(id, props[, opts]) - Update a single post record in the posts table
  • postService.updateAll(props[, query][, opts]) - Perform a single update against multiple post records in the posts table
  • postService.updateMany(records[, opts]) - Perform multiple individual updates against post records in the posts table
  • postService.destroy(id[, opts]) - Delete a post record from the posts table by its primary key
  • postService.destroyAll([query][, opts]) - Delete a selection of post records from the posts 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 endpoint
  • postService.find(id[, opts]) - GET /posts/:id - Retrieve a post record from the /posts REST endpoint by its primary key
  • postService.create(record[, opts]) - POST /posts - Create one new post record via the /posts REST endpoint
  • postService.createMany(records[, opts]) - POST /posts - Create many new post records via the /posts REST endpoint
  • postService.update(id, props[, opts]) - PUT /posts/:id - Update a single post record via the /posts REST endpoint
  • postService.updateAll(props[, query][, opts]) - PUT /posts - Perform a single update against multiple post records via the /posts REST endpoint
  • postService.updateMany(records[, opts]) - PUT /posts - Perform multiple individual updates against post records via the /posts REST endpoint
  • postService.destroy(id[, opts]) - DELETE /posts:id - Delete a post record via the /posts REST endpoint by its primary key
  • postService.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.

  1. Client: User clicks "new post"
  2. Client: The "new post" view is displayed
  3. Client: The view collects the user's input
  4. Client: The User clicks "save"
  5. Client: At this point, the client calls postService.create(newPostData)
  6. Client: A request is made: POST /posts with body: {"title":"my new post","content":"once upon a time...","user_id":1234}
  7. Server: Server receives request at the app.route('/posts').post(...)
  8. Server: Server calls postService.create(req.body)
  9. Server: A row is inserted into the MySQL posts table
  10. Server: The promise returned by postService.create resolves with the created post record
  11. Server: Server responds to request with created post record
  12. Client: The promise returned by the client's postService.create call resolves with the created post record
  13. Client: Client redirects to /posts/:id
  14. 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 for indexed and track keywords to enable secondary indexes and active change detection in its in-memory store.
  • If its applySchema option is true, a Mapper 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 and DataStore) 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 Post belongsTo User
  • User hasMany Comment and Comment belongsTo User
  • Post hasMany Comment and Comment belongsTo 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.


What’s Next

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