- Start Date: 2023-01-17
- RFC PR: (to do)
Summary
Adds to Orionjs a new decorator @UseMiddleware(LogMiddleware)
or @Middleware(LogMiddleware, AuthMiddleware)
to extends and reuse some generic functionalities to resolvers and fields.
Basic example
Without middlewares we need to call all generic functions inside the resolver implementation
@Mutation({
params: {resourceId: {type: String}, updates: {type: UpdateResouceModel}},
returns: Resouce
})
async updateResouce(params: {resourceId: ResourceId; updates: UpdateType}, viewer: Viewer) {
const resource = await this.resourcesRepo.getResourceById(params.resourceId)
if (!resource) return
await checkWebsitePermission(resource.websiteId, viewer)
await logger({action: 'update', resolver: 'updateResource', viewer, resource})
const updatedResource = await this.tabsRepo.updateTab(params.tabId, params.updates)
... do some other logic ...
return updatedResource
}
With middlewares decorator we can attach generic functions to the resolver function
const checkWebsitePermissionMw = ({resouce, viewer}, next) => await checkWebsitePermission(resource.websiteId, viewer)
const loggerMw = ({resouce, viewer}, next) => await logger({action: 'update', resolver: 'updateResource', viewer, resource})
@Mutation({
params: {resourceId: {type: String}, updates: {type: UpdateResouceModel}},
returns: Resouce
})
@Middleware(checkWebsitePermissionMw, loggerMw)
async updateResouce(params: {resourceId: ResourceId; updates: UpdateType}, viewer: Viewer) {
const resource = await this.resourcesRepo.getResourceById(params.resourceId)
if (!resource) return
const updatedResource = await this.tabsRepo.updateTab(params.tabId, params.updates)
... do some other logic ...
return updatedResource
}
You can also adds middlewares to fields definitions on schema
const timerMw = async ({resource}, next) => {
console.time('timer')
const response = await next()
console.timeEnd('timer')
return response
}
const loggerMw = ({resource}, next) => {
console.log('access to code field')
return next()
}
const permissionMw = ({viewer}, next) => {
if(!viewer.checkRoles('admin')) return null
return next()
}
@TypedSchema()
export class Resouce {
@Prop()
_id: ResouceId
@Prop()
@Middleware(timerMw, loggerMw, permissionMw)
code: string
@Prop({optional: true})
description?: string
}
Motivation
The main motivation of using middlewares is to be able to have reusable pieces of code and use them in resolvers when required. In this way, we can extend the capabilities of a resolver or field and give power to Orionjs to be used in different contexts.
Detailed design
Middleware are pieces of reusable code that can be easily attached to resolvers and fields. By using middleware we can extract the commonly used code from our resolvers and then declaratively attach it using a decorator or even registering it globally.
The proposed API interface is:
({parent, params, viewer, info}, next) => {}
Where:
parent
: its the parent field of the resolver chain
params
: are the parameters of the resolver
viewer
: are the viewer of the request
info
: the resolver information of Apollo (optional)
next
is a function to call the next function of middleware chain
you can add a middleware with the @Middleware()
decorator.
@Middlewares(loggerMw) // with one middleware
@Middlewares(checkWebsitePermissionMw, loggerMw) // with more than one
With async middlewares you can do before and after actions:
const middleware = ({ parent, params, viewer, info }, next) => {
/* code before */
const result = await next()
/* code after */
return result
}
The inspiration comes from:
Drawbacks
One of the main disadvantages is that the new middleware decorator affects the general understanding of orionjs, it is necessary to define well the interface that the middlewares will have to make it easy to use.
In addition, the use of middleware in the fields makes it necessary to create an additional resolver (internal to orionjs) to be able to execute the resolvers before returning the value of the field.
The implementation can be complex but you have to rely on Koajs middleware, specifically compose repo.
Alternatives
The most direct alternative is to continue using the current system and to use the middlewares within the resolver implementation.
Adoption strategy
The adoption strategy is incremental, since this is not a breaking change. It can be used as needed and update old implementations to use middlewares.
How we teach this
We need to write examples in the Orionjs documentation and maybe a common patters or style guide.
Unresolved questions
- Define decorator name (or accept the proposed)
- Define middleware interface arguments (or accept the proposed)
- Define how use the middlewares with fields