Relationships

What is a relationship?

A relationship is a connection between two collections.

Lumber
Rails
Express/Sequelize
Express/Mongoose

Forest supports natively all the relationships defined in your Sequelize models (belongsTo, hasMany, …).

However, as databases don’t have the notion of inverse relationships, Lumber does not generate hasMany or hasOne relationships, so you will have to do it manually. Check out How to add relationships manually.

Forest supports natively all the relationships defined in your ActiveRecord models (belongsTo, hasMany, …). Check the Rails documentation to create new ones.

Forest supports natively all the relationships defined in your Sequelize models (belongsTo, hasMany, …). Check the Sequelize documentation to create new ones.

Forest supports natively all the relationships defined in your Mongoose models (ref option, embed documents, …). Check the Mongoose documentation to create new ones.

What is a Smart Relationship?

Sometimes, you want to create a virtual relationship between two set of data that does not exist in your database. A concrete example could be creating a relationship between two collections available in two different databases. Creating a Smart Relationship allows you to customize with code how your collections are linked together.

Create a BelongsTo Smart Relationship

On the Live Demo example, we have an order which belongsTo a customer which belongsTo a delivery address. We’ve created here a BelongsTo Smart Relationship that acts like a shortcut between the order and the delivery address.

A BelongsTo Smart Relationship is created like a Smart Field with the reference option to indicates on which collection the Smart Relationship points to.

Lumber
Rails
Express/Sequelize
Express/Mongoose
/forest/orders.js
const Liana = require('forest-express-sequelize');
const models = require('../models');
Liana.collection('orders', {
fields: [{
field: 'delivery_address',
type: 'String',
reference: 'addresses.id',
get: function (order) {
return models.addresses
.findAll({
include: [{
model: models.customers,
include: [{
model: models.orders,
where: { ref: order.ref }
}]
}],
})
.then((addresses) => {
if (addresses) { return addresses[0]; }
});
}
}]
});
/lib/forest_liana/collections/order.rb
class Forest::Order
include ForestLiana::Collection
collection :Order
belongs_to :delivery_address, reference: 'Address.id' do
object.customer.address
end
end
/forest/orders.js
const Liana = require('forest-express-sequelize');
const models = require('../models');
Liana.collection('orders', {
fields: [{
field: 'delivery_address',
type: 'String',
reference: 'addresses.id',
get: function (order) {
return models.addresses
.findAll({
include: [{
model: models.customers,
include: [{
model: models.orders,
where: { ref: order.ref }
}]
}],
})
.then((addresses) => {
if (addresses) { return addresses[0]; }
});
}
}]
});
/forest/orders.js
const Liana = require('forest-express-mongoose');
const Address = require('../models/addresses');
Liana.collection('Order', {
fields: [{
field: 'delivery_address',
type: 'String',
reference: 'Address',
get: function (order) {
return Address
.aggregate([
{
$lookup:
{
from: 'orders',
localField: 'customer_id',
foreignField: 'customer_id',
as: 'orders_docs'
}
},
{
$match:
{
'orders_docs._id': order._id
}
}
])
.then((addresses) => {
if (addresses) { return addresses[0]._id; }
});
}
}]
});

Create a HasMany Smart Relationship

On the Live Demo example, we have a product hasMany orders and an order belongsTo customer. We’ve created a Smart Relationship that acts like a shortcut: product hasMany customers.

A HasMany Smart Relationship is created like a Smart Field with the reference option to indicates on which collection the Smart Relationship points to.

Lumber
Rails
Express/Sequelize
Express/Mongoose

Upon browsing, an API call is triggered when accessing the data of the HasMany relationships in order to fetch them asynchronously. In the following example, the API call is a GET on /products/:product_id/relationships/buyers.

We’ve built the right SQL query using Sequelize to count and find all customers who bought the current product.

Then, you should handle pagination in order to avoid performance issue. The API call has a querystring available which gives you all the necessary parameters you need to enable pagination.

Finally, you don’t have to serialize the data yourself. The Forest Liana already knows how to serialize your collection (customers in this example). You can access to the serializer through the Liana.ResourceSerializer object.

/forest/products.js
const Liana = require('forest-express-sequelize');
Liana.collection('products', {
fields: [{
field: 'buyers',
type: ['String'],
reference: 'customers.id'
}]
});
/routes/products.js
const P = require('bluebird');
const express = require('express');
const router = express.Router();
const Liana = require('forest-express-sequelize');
const models = require('../models');
router.get('/products/:product_id/relationships/buyers',
Liana.ensureAuthenticated, (req, res, next) => {
let limit = parseInt(req.query.page.size) || 10;
let offset = (parseInt(req.query.page.number) - 1) * limit;
let queryType = models.sequelize.QueryTypes.SELECT;
let countQuery = `
SELECT COUNT(*)
FROM customers
JOIN orders ON orders.customer_id = customers.id
JOIN products ON orders.product_id = products.id
WHERE product_id = ${req.params.product_id};
`;
let dataQuery = `
SELECT customers.*
FROM customers
JOIN orders ON orders.customer_id = customers.id
JOIN products ON orders.product_id = products.id
WHERE product_id = ${req.params.product_id}
LIMIT ${limit}
OFFSET ${offset}
`;
return P
.all([
models.sequelize.query(countQuery, { type: queryType }),
models.sequelize.query(dataQuery, { type: queryType })
])
.spread((count, customers) => {
return new Liana.ResourceSerializer(Liana, models.customers, customers, null, {}, {
count: count[0].count
}).perform();
})
.then((products) => {
res.send(products);
})
.catch((err) => next(err));
});
module.exports = router;

Upon browsing, an API call is triggered when accessing the data of the HasMany relationships in order to fetch them asynchronously. In the following example, the API call is a GET on /Product/:product_id/relationships/buyers.

We’ve built the right SQL query using Active Record to count and find all customers who bought the current product.

Then, you should handle pagination in order to avoid performance issue. The API call has a querystring available which gives you all the necessary parameters you need to enable pagination.

Finally, you don’t have to serialize the data yourself. The Forest Liana already knows how to serialize your collection (Customer in this example). You can access to the serializer through the serialize_models() function.

/lib/forest_liana/collections/product.rb
class Forest::Product
include ForestLiana::Collection
collection :Product
has_many :buyers, type: ['String'], reference: 'Customer.id'
end
/config/routes.rb
Rails.application.routes.draw do
# MUST be declared before the mount ForestLiana::Engine.
namespace :forest do
get '/Product/:product_id/relationships/buyers' => 'orders#buyers'
end
mount ForestLiana::Engine => '/forest'
end
/app/controllers/forest/products_controller.rb
class Forest::ProductsController < ForestLiana::ApplicationController
def buyers
limit = params['page']['size'].to_i
offset = (params['page']['number'].to_i - 1) * limit
orders = Product.find(params['product_id']).orders
customers = orders.limit(limit).offset(offset).map(&:customer)
count = orders.count
render json: serialize_models(customers, include: ['address'], meta {count: count})
end
end

Upon browsing, an API call is triggered when accessing the data of the HasMany relationships in order to fetch them asynchronously. In the following example, the API call is a GET on /products/:product_id/relationships/buyers.

We’ve built the right SQL query using Sequelize to count and find all customers who bought the current product.

Then, you should handle pagination in order to avoid performance issue. The API call has a querystring available which gives you all the necessary parameters you need to enable pagination.

Finally, you don’t have to serialize the data yourself. The Forest Liana already knows how to serialize your collection (customers in this example). You can access to the serializer through the Liana.ResourceSerializer object.

/forest/products.js
const Liana = require('forest-express-sequelize');
Liana.collection('products', {
fields: [{
field: 'buyers',
type: ['String'],
reference: 'customers.id'
}]
});
/routes/products.js
const P = require('bluebird');
const express = require('express');
const router = express.Router();
const Liana = require('forest-express-sequelize');
const models = require('../models');
router.get('/products/:product_id/relationships/buyers',
Liana.ensureAuthenticated, (req, res, next) => {
let limit = parseInt(req.query.page.size) || 10;
let offset = (parseInt(req.query.page.number) - 1) * limit;
let queryType = models.sequelize.QueryTypes.SELECT;
let countQuery = `
SELECT COUNT(*)
FROM customers
JOIN orders ON orders.customer_id = customers.id
JOIN products ON orders.product_id = products.id
WHERE product_id = ${req.params.product_id};
`;
let dataQuery = `
SELECT customers.*
FROM customers
JOIN orders ON orders.customer_id = customers.id
JOIN products ON orders.product_id = products.id
WHERE product_id = ${req.params.product_id}
LIMIT ${limit}
OFFSET ${offset}
`;
return P
.all([
models.sequelize.query(countQuery, { type: queryType }),
models.sequelize.query(dataQuery, { type: queryType })
])
.spread((count, customers) => {
return new Liana.ResourceSerializer(Liana, models.customers, customers, null, {}, {
count: count[0].count
}).perform();
})
.then((products) => {
res.send(products);
})
.catch((err) => next(err));
});
module.exports = router;

Upon browsing, an API call is triggered when accessing the data of the HasMany relationships in order to fetch them asynchronously. In the following example, the API call is a GET on /Product/:product_id/relationships/buyers.

We use the $lookup operator of the aggregate pipeline. Since there's a many-to-many relationship between Product and Customer, the $lookup operator needs to look into orders which is an array we have to flatten first using $unwind.

Finally, you don’t have to serialize the data yourself. The Forest Liana already knows how to serialize your collection (Customer in this example). You can access to the serializer through the Liana.ResourceSerializer object.

/forest/products.js
const Liana = require('forest-express-mongoose');
Liana.collection('products', {
fields: [{
field: 'buyers',
type: ['String'],
reference: 'Customer'
}]
});
/routes/products.js
const P = require('bluebird');
const express = require('express');
const router = express.Router();
const Liana = require('forest-express-mongoose');
const Customers = require('../models/customers');
const mongoose = require('mongoose');
router.get('/Product/:product_id/relationships/buyers',
Liana.ensureAuthenticated, (req, res, next) => {
let limit = parseInt(req.query.page.size) || 10;
let offset = (parseInt(req.query.page.number) - 1) * limit;
let countQuery = Customers.aggregate([
{
$lookup:
{
from: 'orders',
localField: 'orders',
foreignField: '_id',
as: 'orders_docs'
}
},
{
$unwind: "$orders_docs"
},
{
$lookup:
{
from: 'products',
localField: 'orders_docs._id',
foreignField: 'orders',
as: 'products_docs'
}
},
{
$match:
{
'products_docs._id': mongoose.Types.ObjectId(req.params.product_id)
}
},
{
$count: "products_docs"
}
]);
let dataQuery = Customers.aggregate([
{
$lookup:
{
from: 'orders',
localField: 'orders',
foreignField: '_id',
as: 'orders_docs'
}
},
{
$unwind: "$orders_docs"
},
{
$lookup:
{
from: 'products',
localField: 'orders_docs._id',
foreignField: 'orders',
as: 'products_docs'
}
},
{
$match:
{
'products_docs._id': mongoose.Types.ObjectId(req.params.product_id)
}
}
]);
return P
.all([
countQuery,
dataQuery
])
.spread((count, customers) => {
return new Liana.ResourceSerializer(Liana, Customers, customers, null, {}, {
count: count.orders_count
}).perform();
})
.then((products) => {
res.send(products);
})
.catch((err) => next(err));
});
module.exports = router;

Include/Exclude models

By default, all models are analyzed by the Forest Liana and displayed as collections in your admin panel. To specifically include or exclude models, follow this tutorial of the How-to's section.