TramwayJS

Add Sub Resource

Now we created a Products API, we will likely want to add Product Accessories. In RESTful design, an Accessory entity is considered to be a sub-resource of the parent Product.

Create API

Once again, we will want to create an API using the CLI tool

tramway create:api Accessory --provider provider.mysql

Remember that if Tramway is installed locally, you will need to reference its path in the node_modules folder: ./node_modules/.bin/tramway

This walkthrough will assume that your provider is ready and working with the necessary schemas.

The command will have created the scaffold for the API leaving just the Accessory entity and AccessoryFactory factory for you to implement. The rest is already implemented and wired with Tramway - including the routing:

VerbRouteController Action
GETaccessoriesget
GETaccessories/:idgetOne
POSTaccessoriescreate
PUTaccessories/:idreplace
DELETEaccessories/:iddelete

There's one problem with this: accessories is at the root level.

Update Route paths

Next you'll want to update the routes to map the desired resource structure.

All the routes of the application are declared in src/config/global/routes.js

You will want to replace all the paths of the accessory entries to require a product.

Your routing configuration for those entries should now look like this:

{
"controller": "controller.accessory",
"action": "get",
+ "path": "products/:productId/accessories",
- "path": "accessories",
"methods": ["get"]
},
{
"controller": "controller.accessory",
"action": "getOne",
+ "path": "products/:productId/accessories",
- "path": "accessories",
"methods": ["get"],
"arguments": ["id"]
},
{
"controller": "controller.accessory",
"action": "create",
+ "path": "products/:productId/accessories",
- "path": "accessories",
"methods": ["post"]
},
{
"controller": "controller.accessory",
"action": "update",
+ "path": "products/:productId/accessories",
- "path": "accessories",
"methods": ["patch"],
"arguments": ["id"]
},
{
"controller": "controller.accessory",
"action": "replace",
+ "path": "products/:productId/accessories",
- "path": "accessories",
"methods": ["put"],
"arguments": ["id"]
},
{
"controller": "controller.accessory",
"action": "delete",
+ "path": "products/:productId/accessories",
- "path": "accessories",
"methods": ["delete"],
"arguments": ["id"]
},

Add Links to Parent Resource

  1. We will then want to add a link entry to the existing Product API so we can discover nested accessories.

Add the link declaration to the Product's getOne action.

{
"controller": "controller.product",
"action": "getOne",
"path": "products",
"methods": ["get"],
"arguments": ["id"],
+ "links": [
+ {"label": "accessories", "link": "accessories"},
+ ]
},

Create Policy to Handle Parent Precondition

  1. Add a Policy to require the Product exist.

Create the src/policies folder and add a ProductPolicy.js file with the following content:

import { policies, errors } from "tramway-core-router";
const { Policy } = policies;
const { HttpNotFoundError, HttpInternalServerError } = errors;
export default class ProductPolicy extends Policy {
constructor(productService, logger) {
this.productService = productService;
this.logger = logger;
}
async check(request) {
let product;
const {productId} = request.params || {};
try {
product = await this.productService.getOne(productId);
} catch(e) {
this.logger.error(e.stack);
throw new HttpInternalServerError();
}
if (!product) {
throw new HttpNotFoundError();
}
return { product };
}
}

Create the policy declaration with dependency injection. Create src/config/services/policies.js with the following contents.

import {
ProductPolicy
} from '../../policies';
export default {
"policy.product": {
"class": ProductPolicy,
"constructor": [
{"type": "service", "key": "service.product"},
{"type": "service", "key": "logger"},
]
}
}

Register the policies services by adding the policies.js file contents to the configuration's root config in src/config/services/index.js:

+import policies from './policies';
export default {
+ ...policies,
}

Apply Policy to Routes

  1. Now that the policy is declared, it can be added to the route config in src/config/parameters/global/routes.js.
{
"controller": "controller.accessory",
"action": "get",
"path": "products/:productId/accessories",
"methods": ["get"],
+ "policy": "policy.product",
},
{
"controller": "controller.accessory",
"action": "getOne",
"path": "products/:productId/accessories",
"methods": ["get"],
"arguments": ["id"],
+ "policy": "policy.product",
},
{
"controller": "controller.accessory",
"action": "create",
"path": "products/:productId/accessories",
"methods": ["post"],
+ "policy": "policy.product",
},
{
"controller": "controller.accessory",
"action": "update",
"path": "products/:productId/accessories",
"methods": ["patch"],
"arguments": ["id"],
+ "policy": "policy.product",
},
{
"controller": "controller.accessory",
"action": "replace",
"path": "products/:productId/accessories",
"methods": ["put"],
"arguments": ["id"],
+ "policy": "policy.product",
},
{
"controller": "controller.accessory",
"action": "delete",
"path": "products/:productId/accessories",
"methods": ["delete"],
"arguments": ["id"],
+ "policy": "policy.product",
},

Use Parent Resource within Child Resource Controller

  1. In the Controller, you can override the method if you need to use the product.
+import {HttpStatus, controllers} from 'tramway-core-router';
-import {controllers} from 'tramway-core-router';
const {RestfulController} = controllers;
export default class AccessoryController extends RestfulController {
constructor(router, service) {
super(router, service);
}
+ async get(req, res) {
+ let items;
+ let {query} = req;
+ const {product} = res.locals || {}
+
+ try {
+ items = await this.service.find({product, ...query});
+ } catch (e) {
+ this.logger && this.logger.error(e.stack);
+ return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({message: e.message, stack: e.stack});
+ }
+
+ if (!items) {
+ this.logger && this.logger.info(`${this.constructor.name} - No items found`);
+ return res.sendStatus(HttpStatus.BAD_REQUEST);
+ }
+
+ return this.sendCollection(res, items, {links: this.getLinks('get')});
+ }
+ async create(req, res) {
+ const {body} = req;
+ const { product } = res.locals;
+ let item;
+
+ try {
+ item = await this.service.create(product, body)
+ } catch (e) {
+ this.logger.error(e.stack);
+ return res.sendStatus(HttpStatus.BAD_REQUEST);
+ }
+
+ let route = this.getRouteByAction('get');
+ let {path} = route;
+
+ let base = this.getHostFromRequest(req);
+ let location = this.router.buildPath(base, path, `${item.getId()}`);
+
+ return res.location(location).sendStatus(HttpStatus.CREATED);
+ }
}