dandi

🌻A modular DI, MVC, and Model binding/validation framework for NodeJS and TypeScript or ES6

View the Project on GitHub just-dandi/dandi

@dandi/mvc-hal

@dandi/mvc-hal provides services for emitting HAL JSON (application/hal+json) from @dandi/mvc controllers.

Usage

To use the HAL services from @dandi/mvc-hal, add the MvcHalModule to your server container.

Decorators

Relations between resources, as well as resource identifiers, are defined using decorators.

Defining a Resource

Use @ResourceId() to define the identifier property on a resource:

export class TaskList {
  @ResourceId()
  @Property(Uuid)
  @Required()
  public listId: Uuid;
}

The @ResourceId() decorator on the resource model must correspond with a @ResourceAccessor() decorator, which is applied to the controller method used to get that resource, and a @AccessorResourceId() decorator on the parameter specifying the source for model’s ID:

@Controller('/list')
export class TaskListController {

  @HttpGet(':listId')
  @ResourceAccessor(TaskList)
  public getList(@PathParam(Uuid) @AccessorResourceId() listId: Uuid): Promise<TaskList> {
    ...
  }

}

The @AccessorResourceId() decorator on the listId parameter will be linked to the listId property on the TaskList model since the @ResourceAccessor() decorator specifies TaskList as its type, and listId is defined as its ID property by its own @ResourceId() decorator.

Controller methods that list a resource can be identified using the @ResourceListAccessor() decorator:

@Controller('/list')
export class TaskListController {

  @HttpGet(':listId')
  @ResourceAccessor(TaskList)
  public getList(@PathParam(Uuid) @AccessorResourceId() listId: Uuid): Promise<TaskList> {
    ...
  }

  @HttpGet()
  @ResourceListAccessor(TaskList)
  public getAllLists(): Promise<TaskList[]> {
    ...
  }

}

The combination of these decorators enables the resource composer to correctly and automatically generate the self relation link.

Defining Resource Relations

The @ResourceId() decorator can also be used in correlation with @Relation to define relations of a resource.

export class Task {
  @Property(Uuid)
  @Required()
  @ResourceId()
  public taskId: Uuid;

  @Property(Uuid)
  @Required()
  @ResourceId(List, 'list')
  public listId: Uuid;

  @Relation(List)
  public list?: List;
}

The @Relation() decorator on the list property marks that property as a relation. The @ResourceId() decorator on the listId property describes that property as the identifier for the aforementioned list relation. Assuming a TaskController implementation with a corresponding @ResourceAccessor for the Task resource, these decorators will allow the resource composer to automatically generate links or embed resources for the list relation of a task.

Avoiding Circular References with Circular Relations

Using the example of a task list, we will probably want the following relations:

Attempting to do this with one model per resource will result in unresolvable circular dependency issues. One way to work around this is to define the relations in separate models:

export class TaskList {
  @ResourceId()
  @Property(Uuid)
  @Required()
  public listId: Uuid;
}

export class Task {
  @Property(Uuid)
  @Required()
  @ResourceId()
  public taskId: Uuid;

  @Property(Uuid)
  @Required()
  @ResourceId(List, 'list')
  public listId: Uuid;
}

export class TaskListResource extends TaskList {
  @ListRelation(Task)
  public tasks: Task[];
}

export class TaskResource extends Task {
  @Relation(List)
  public list?: List;
}

@Controller('/list')
export class TaskListController {

  @HttpGet(':listId')
  @ResourceAccessor(TaskListResource)
  public getList(@PathParam(Uuid) @AccessorResourceId() listId: Uuid): Promise<TaskList> {
    ...
  }

  @HttpGet(':listId/task')
  @ResourceListAccessor(Task)
  public listTasks(@PathParam(Uuid) @AccessorResourceId(List) listId: Uuid): Promise<Task[]> {
    ...
  }

}

@Controller('/task')
export class TaskListController {

  @HttpGet(':taskId')
  @ResourceAccessor(TaskResource)
  public getTask(@PathParam(Uuid) @AccessorResourceId() listId: Uuid): Promise<Task> {
    ...
  }

}