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.
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.
/forest/orders.jsconst { collection } = require('forest-express-sequelize');const models = require('../models');collection('orders', {fields: [{field: 'delivery_address',type: 'String',reference: 'addresses.id',get: function (order) {return models.addresses.findAll({include: [{model: models.customers,where: { id: order.customer_id },include: [{model: models.orders,where: { ref: order.ref }}]}],}).then((addresses) => {if (addresses) { return addresses[0]; }});}}]});
/forest/orders.jsconst { collection } = require('forest-express-mongoose');const Address = require('../models/addresses');collection('Order', {fields: [{field: 'delivery_address',type: 'String',reference: 'Address._id',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; }});}}]});
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.
/forest/products.jsconst { collection } = require('forest-express-sequelize');collection('products', {fields: [{field: 'buyers',type: ['String'],reference: 'customers.id'}]});
/forest/products.jsconst { collection } = require('forest-express-mongoose');collection('products', {fields: [{field: 'buyers',type: ['String'],reference: 'Customer._id'}]});
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’ll use the findAll and count methods provided by Sequelize to find and count all customers who bought the current product (buyers).
Then, you should handle pagination in order to avoid performance issue. The API call has a query string 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 recordsGetter.serialize
function.
/routes/products.jsconst express = require('express');const { PermissionMiddlewareCreator, RecordsGetter } = require('forest-express-sequelize');const { products, customers, orders} = require('../models');const router = express.Router();const permissionMiddlewareCreator = new PermissionMiddlewareCreator('products');router.get('/products/:product_id/relationships/buyers', (request, response, next) => {const productId = request.params.product_id;const limit = parseInt(request.query.page.size, 10) || 20;const offset = (parseInt(request.query.page.number, 10) - 1) * limit;const recordsGetter = new RecordsGetter(customers);const include = [{model: orders,as: 'orders',where: { product_id: productId },}];// find the customers for the requested page and page sizeconst findAll = customers.findAll({include,offset,limit,});// count all customers for paginationconst count = customers.count({ include });// resolve the two promises and serialize the responsePromise.all([findAll, count]).then(([customersFound, customersCount]) =>recordsGetter.serialize(customersFound, { count: customersCount })).then((recordsSerialized) => response.send(recordsSerialized)).catch(next);});
We’ll use raw SQL query and Sequelize to count and find all customers who bought the current product (buyers).
Then, you should handle pagination in order to avoid performance issue. The API call has a query string 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 recordsGetter.serialize
function.
/routes/products.jsconst express = require('express');const router = express.Router();const models = require('../models');router.get('/products/:product_id/relationships/buyers', (req, res, next) => {let limit = parseInt(req.query.page.size) || 10;let offset = (parseInt(req.query.page.number) - 1) * limit;const recordsGetter = new RecordsGetter(customers);let queryType = models.sequelize.QueryTypes.SELECT;let countQuery = `SELECT COUNT(*)FROM customersJOIN orders ON orders.customer_id = customers.idJOIN products ON orders.product_id = products.idWHERE product_id = ${req.params.product_id};`;let dataQuery = `SELECT customers.*FROM customersJOIN orders ON orders.customer_id = customers.idJOIN products ON orders.product_id = products.idWHERE product_id = ${req.params.product_id}LIMIT ${limit}OFFSET ${offset}`;return Promise.all([models.sequelize.query(countQuery, { type: queryType }),models.sequelize.query(dataQuery, { type: queryType })]).then(([count, customers]) => recordsGetter.serialize(customers, { count: count[0].count })).then((customers) => res.send(customers)).catch((err) => next(err));});module.exports = router;
If your primary key column name (customer_id
) is different than the model field name (customerId
), you must alias the primary key column with the name of the model field in the dataQuery.
Ex: SELECT customers.*, customers.customer_id AS “customerId”
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.jsconst { collection } = require('forest-express-mongoose');collection('products', {fields: [{field: 'buyers',type: ['String'],reference: 'Customer._id'}]});
/routes/products.jsconst 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', (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;