4. Working with Use Cases
Introduction to the Use Case concept
Previous when we run herbs update
command, herbs-cli
generate inside src/domain/usecases
a folder for each entity we have created.
src
└── domain
└── usecases
├── item
└── list
Inside these folders named after each entity we will find two types of files, the use cases and the files that have .spec
in the name.
Now we will talk about the use cases, and in the next step of the tutorial we will talk about the .spec
files.
Every file without .spec
performs a specific action, for exemple: Find List, Create List, Remove List, Find Item, Add Item ....
Each of these specific actions, are our use cases, and each of them, reflects an action exposed by the Domain to the end user.
Internally, a use case is responsible for controlling the interaction between entities, repositories and other domain components.
Now, let's understand a use case structure, but to se more about use cases visit: What's a Use Case?
We are going to take a look in createList.js
use case thats interact with the List entity, remember that this use case file is auto generated in previous step when we run herbs update
.
Create List
Let's understand the use case to create a list that is generated by the CLI. For that, we are going to walk through the following topics:
- Use Case Name
- Request
- Response
- Setup / DI
- Authorize
- Steps - Basic
- Step return (Ok, Err)
- Use Case return (ctx.ret)
- Exporting with herbarium
Learn more about create usecases.
Use Case Name
First, the name is set for the usecase.
// src/domain/usecases/user/createList.js
const { usecase } = require('@herbsjs/herbs')
const createList = injection => usecase('Create List', {})
Request
Now, we have to specify what are the parameters accepted from the user on request.
In this case, we need lits's name and description, which are both String
since we want a text.
Here we can use any other Object
type, like: Boolean
, Number
, etc. To set up an array, we have to add brackets around the type, like: [String]
.
// src/domain/usecases/list/createList.js
const { usecase } = require('@herbsjs/herbs')
const createList = injection => usecase('Create List', {
// Input/Request metadata and validation
request: {
name: String,
description: String
},
})
If we were using an Entity in the request field, the validations set in the Entity will also be checked there.
Response
Once we have the request object specified, we must specify the response model.
If your use case does not need to return a response and just perform operations, you can ommit this field.
In this case, it will return the List Entity.
// src/domain/usecases/list/createList.js
const { usecase } = require('@herbsjs/herbs')
const List = require('../entities/list');
const createList = injection => usecase('Create List', {
// Input/Request metadata and validation
request: {
name: String,
description: String
},
// Output/Response metadata
response: List,
})
Setup / DI
The use case is divided by steps, they run one-by-one and can share a context object. You are free to use this object setting useful values between the steps. Besides that, you can also have an initial object setup, where you can set initially required values for steps, like repositories (it's what makes possible the interaction with the database).
In each use case step (which we are going to set soon), a context object is provided to handle the "data sharing" between different steps. And other useful data, such as repositories, the request values, etc.
On the setup
function, we can manually add values to this context, so we can use them later in the steps.
In this case of the CLI, it was not necessary, but wel could do something like this:
// src/domain/usecases/list/createList.js
const { usecase } = require('@herbsjs/herbs')
const List = require('../entities/list');
const ListRepository = require('../../../infra/data/repositories/listRepository')
const dependency = { ListRepository }
const createList = injection => usecase('Create List', {
// Input/Request metadata and validation
request: {
name: String,
description: String
},
// Output/Response metadata
response: List,
// Pre-run setup
setup: ctx => (ctx.di = Object.assign({}, dependency, injection)),
})
Authorize
Use cases may also have an authorize
function, which can be implemented with any logic and must return Ok()
if user is authorized to perform that operation and Err()
otherwise.
The authorize
function is runned before any use case. Use cases will only run if the authorize
return Ok()
.
The CLI default function just allows all requests by constant returning Ok()
, but you can implement any logic for that. For example, we can suppose that the user
object has a property called canCreateList
.
Learn more: Use Cases Features - Authorize
// src/domain/usecases/list/createList.js
const { Ok, Err, usecase } = require('@herbsjs/herbs')
const List = require('../entities/list');
const createList = injection => usecase('Create List', {
// Input/Request metadata and validation
request: {
name: String,
description: String
},
// Output/Response metadata
response: List,
// Authorization with Audit
// authorize: (user) => (user.canCreateList ? Ok() : Err()),
authorize: () => Ok(),
})
In every use case, you can set up the authorize
which gets a user
object and must return Ok
for authorized and Err
for unauthorized.
It is simple like that, you can implement any logic and if Ok
were returned the use case keep running, but if Err
were returned, the use case is interrupted.
const createList = injection => usecase('Create List', {
// Input/Request metadata and validation
request: {
name: String,
description: String
},
// Output/Response metadata
response: List,
// Authorization with Audit
authorize: async (user) => (user.canCreateList ? Ok() : Err()),
...
Steps
Since we have the request, response, setup and authorization set, we can finally start writing the actual logic in the steps.
Steps are the building blocks of a use case. Their main goal is to generate metadata before and during use case execution like the code intention, audit trail, etc. The first thing to note is that we encourage steps description with the business intent (never the technical intent).
Learn more about usecase steps.
// src/domain/usecases/list/createList.js
const { Ok, Err, usecase } = require('@herbsjs/herbs')
const List = require('../entities/list');
const ListRepository = require('../../../infra/data/repositories/listRepository')
const dependency = { ListRepository }
// Here, we receive an object `injection` with the dependencies we will need it.
// So we can interact with the database.
const createList = injection => usecase('Create List', {
// Input/Request metadata and validation
request: {
name: String,
description: String
},
// Output/Response metadata
response: List,
// Authorization with Audit
authorize: async (user) => (user.canCreateList ? Ok() : Err()),
// Pre-run setup - will inject in context object ListRepository
setup: ctx => (ctx.di = Object.assign({}, dependency, injection)),
// Step description and function
'Check if the List is valid': step(ctx => {
// Creates a new list from the request.
// And stores it in the context.
ctx.list = List.fromJSON(ctx.req)
// Check if the fields are valid.
if (!ctx.list.isValid())
return UserNotValidError('List ', 'The List entity is invalid', ctx.list.errors)
// returning Ok continues to the next step. Err stops the use case execution.
return Ok()
}),
'Save the List': step(async ctx => {
// instances a repository to store data from di (dependency injection)
const repo = new ctx.di.ListRepository(injection)
// get previus validate list from context
const list = ctx.list
// Insert the new list to the repository
// and then return it to the client using
// the `ret` property of context object.
// ctx.ret is the return value of a use case
return (ctx.ret = await repo.insert(list))
})
})
Exporting
As we previous make with entities, exporting throught herbarium, we'll make the same for usecases, but have some changes, we defined in metadata a new type of operation, herbarium.crud.create
.
It's a important attention point, because is this way, where we make usecase known for application and make it avaible to receive request throught GragphQL or Rest.
module.exports =
herbarium.usecases
.add(createList, 'CreateList')
.metadata({ group: 'List', operation: herbarium.crud.create, entity: List })
.usecase
To help you analyze
To help you in your daily development routine, in the metadata analysis of a use case, let's take a brief look at .auditTrail and .doc()
Auditing
You can retrieve useful information about a use case execution with usecase.auditTrail
.
const request = { name: 'The best list' }
// Run the use case
const response = await createListt.run(request)
// Log their information
console.log(createListt.auditTrail)
{
// object type
type: 'use case',
// use ase description
description: 'Create List',
// unique Id for each use case execution
transactionId: '9985fb70-f56d-466a-b466-e200d1d4848c',
// total use case execution time in nanosecods
elapsedTime: 1981800n,
// the same user (object) provided on `usecase.authorize(user)`
user: { name: 'John', id: '923b8b9a', isAdmin: true },
// `usecase.authorize(user)` return
authorized: true,
// use case request
request: { name: 'The best list', description: 'simple todo list' },
// use case result
return: {
Ok: { id: 1, name: 'The best list', description: 'simple todo list' }
},
// steps
steps: [
{
// object type
type: 'step',
// use ase description
description: 'Check if the List is valid',
// total step execution time in nanosecods
elapsedTime: 208201n ,
// step result
return: {}
},
...
]
}
Refer to Audit with HerbsJS to know more.
Generate a use case self documentation
You can also use uc.doc()
to get an Object like this:
{
type: 'use case',
description: 'Create List',
request: { name: String, description: String },
response: List,
steps: [
{ type: 'step', description: 'Check if the List is valid', steps: null },
]
}
Next Step
HerbsJs provides us with a quick way to create api's, notice that after we created our entities, we ran the 'herbs-update' command and the 'herbs-cli' did the heavy lifting by creating the use case files needed to perform the CRUD operations.
In this step we saw createList.js and how use cases works, with this example you will be able to adapt any use case that was generated or create new ones.
Our next step is to learn about the '.spec' and how to use it.