FoalTS: Merge hooks/decorators in a reusable way
TLDR
error in FoalTS using mergeHooks
to combine multiple hooks into one, resulted in error message
t.push is not iterable (cannot read property Symbol(Symbol.iterator))
underlying problem was that some of the decorators I was trying to combine with mergeHooks
were actually not hooks (ValidatePathParam
) but rather decorators (ApiOperationId
). So instead of mergeHooks
, the proper approach was a custom mergeDecorators
method.
Longer version
We are using FoalTS as framework for our (nodejs/typescript-based) application server at pagerista. One nice feature are decorators and hooks for annotation of controllers to describe stuff like validation rules for incoming requests.
A controller might look like this:
class ProductController {
@Get('/:uuid')
@ApiOperationId(`find$ProductByUuid`),
@ApiOperationSummary(`Find a product by ID.`),
@ApiResponse(404, { description: product not found.` }),
@ApiResponse(200, { description: `Returns the product` }),
@ValidatePathParam('uuid', { type: 'string', format: 'uuid' })
public async getProduct(ctx: Context<User>) {
const products = await Product.findOne({
uuid: ctx.request.params.uuid
});
return new HttpResponseOK(products);
}
}
As our appserver controllers for typical crud-operations (GET/POST/PUT/DELETE) follow a similar pattern, some sort code-reuse seemed prudent. The FoalTS docs describe merging multiple hooks as
function ValidateAll() {
return MergeHooks(
ValidateHeaders({...}),
ValidateCookie({...})
)
}
class ProductController {
@Get('/:uuid')
@ValidateAll()
getProduct() {
// ...
}
}
so doing the above with following custom hook to combine all commonly used decorators for a getByUuid
method
import { ApiOperationId, ApiOperationSummary, ApiResponse, ValidatePathParam, MergeHooks } from '@foal/core';
export function ApidocItemGet(entityName: string) {
return MergeHooks(
ApiOperationId(`find${entityName}ByUuid`),
ApiOperationSummary(`Find a ${entityName} by ID.`),
ApiResponse(404, { description: `${entityName} not found.` }),
ApiResponse(200, { description: `Returns the ${entityName}.` }),
ValidatePathParam('uuid', { type: 'string', format: 'uuid' })
);
}
resulted in error:
t.push is not iterable (cannot read property Symbol(Symbol.iterator))
Solution
The underlying problem was that some of the decorators I was trying to combine with mergeHooks
(in particular the Api* decorators) were actually not hooks (ValidatePathParam
) but rather decorators (ApiOperationId
). So instead of mergeHooks
, the proper approach was a custom mergeDecorators
method:
mergeDecorators helper:
export function mergeDecorators(...decorators: (ClassDecorator | MethodDecorator | PropertyDecorator)[]) {
return <TFunction extends Function, Y>(target: TFunction | object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<Y>) => {
for (const decorator of decorators) {
if (target instanceof Function && !descriptor) {
(decorator as ClassDecorator)(target);
continue;
}
(decorator as MethodDecorator | PropertyDecorator)(target, propertyKey, descriptor);
}
}; }
re-usable dynamic hook:
import { ApiOperationId, ApiOperationSummary, ApiResponse, ValidatePathParam } from '@foal/core';
import { mergeDecorators } from './mergeDecorators.helper';
export function ApidocItemGet(entityName: string) {
return mergeDecorators(
ApiOperationId(`find${entityName}ByUuid`),
ApiOperationSummary(`Find a ${entityName} by ID.`),
ApiResponse(404, { description: `${entityName} not found.` }),
ApiResponse(200, { description: `Returns the ${entityName}.` }),
ValidatePathParam('uuid', { type: 'string', format: 'uuid' })
);
}
controller:
class ProductController {
@Get('/:uuid')
@ApidocItemGet('Product')
public async getProduct(ctx: Context<User>) {
const products = await Product.findOne({
uuid: ctx.request.params.uuid
});
return new HttpResponseOK(products);
}
}