Business Logic & Hooks
We know that the code generated by Temple won't necessarily satisfy every use case.
That's why we made it easy to modify and extend the application logic, without sacrificing the ability to regenerate code after.
This short guide will walk you through how to add custom business logic to your application.
We'll be using the ExampleProject
from the Getting Started guide, and we'll assume you have a basic familiarity with Go syntax.
Adding custom logic
In the example-service
directory, you'll find the following files and folders:
example-service├── Dockerfile├── config.json├── dao│ ├── dao.go│ ├── datastore.go│ └── errors.go├── example-service.go├── go.mod├── hook.go├── setup.go└── util└── util.go
We're most interested in setup.go
for this guide, which is where you can add additional logic that won't be lost if you need to regenerate your Templefile.
This file will start off looking fairly empty:
package mainimport "github.com/gorilla/mux"func (env *env) setup(router *mux.Router) {// Add user defined code here}
The setup
method defined on env
here is invoked before the HTTP server is started.
It gives you, the developer, the opportunity to:
- register hooks, to be executed before or after any database calls
- register new endpoints with the router
Hooks provide an interface for you to define custom logic that is executed before or after a specific database interaction. This may include logic for additional validation of request parameters or providing values to be stored that are not included in the request (see Value Annotations for more on this).
Registering a hook
Within the env
object that the setup
method is defined on, you will find an attribute called hook
.
Hook is a struct that is defined in hook.go
, which defines two methods for each endpoint. These are named Before<endpoint>
and After<endpoint>
, where <endpoint>
may be any one of Create
, Read
, Update
, Delete
, List
or Identify
.
More information on these endpoints can be found in Service Architecture.
For our example project, the two methods for the Create
endpoint are as follows:
func (h *Hook) BeforeCreate(hook func(env *env, req createExampleServiceRequest, input *dao.CreateExampleServiceInput) *HookError) {...}func (h *Hook) AfterCreate(hook func(env *env, exampleService *dao.ExampleService) *HookError) {...}
By defining a function that matches the argument type defined here, then passing it to the function, we are able to execute arbitrary code before or after the datastore interaction for each endpoint.
The types of each hook vary, depending on the operation they are defined for.
For example, where a request body is not provided, such as in a GET request, the req
argument is omitted from the hook.
If we modify our setup.go
to include a new function called ourCustomHook
, where the arguments to the function match those for the BeforeCreate
argument, we can register a new hook that will be invoked every time a new create request is issued.
Our code will now read:
package mainimport ("github.com/gorilla/mux""github.com/temple/tutorial/example-service/dao")func (env *env) setup(router *mux.Router) {env.hook.BeforeCreate(ourCustomBeforeHook)}func ourCustomBeforeHook(env *env, req createExampleServiceRequest, input *dao.CreateExampleServiceInput) *HookError {return nil}
Modifying the DAO request
Now that we have defined our custom hook, we can start populating it with additional logic. To start, we will show how you can modify the DAO request. The DAO provides a common interface to access a backing store, without directly exposing the implementation details of doing so. More information about this can be found in Service Architecture.
The custom hook we defined takes 3 parameters:
- the environment,
env
- the request provided by the user,
req
- the input to the datastore request,
input
The input object is passed as a pointer, which gets directly passed to the DAO after the hook invocation and will be used as the query for the datastore.
This means if we update any attributes of this object, the data store call will be updated too.
This may be particularly useful if you're using attribute annotations such as @server
or @serverSet
, as defined in the Value Annotations guide, since these attributes are not provided in the request, but are stored in the datastore.
In the following example, we modify all Create
requests to the datastore so that each foo
property contains the string "Hello, World!".
package mainimport ("github.com/gorilla/mux""github.com/temple/tutorial/example-service/dao")func (env *env) setup(router *mux.Router) {env.hook.BeforeCreate(ourCustomBeforeHook)}func ourCustomBeforeHook(env *env, req createExampleServiceRequest, input *dao.CreateExampleServiceInput) *HookError {input.Foo = "Hello, world!"return nil}
If we perform some example requests, we see that our hook updates the object that is stored, irrespective of what was passed in the request:
# Create a new object❯❯❯ curl -X POST $KONG_ENTRY/api/example-service -d '{"foo": "abcd", "bar": 10}'{"id":"43cc65f5-823c-11ea-9dc4-0242ac180003","foo":"Hello, world!","bar":10}# Retrieve that same object❯❯❯ curl -X GET $KONG_ENTRY/api/example-service/43cc65f5-823c-11ea-9dc4-0242ac180003{"id":"43cc65f5-823c-11ea-9dc4-0242ac180003","foo":"Hello, world!","bar":10}
Conditionally updating the datastore input
As well as updating the datastore request, we could also use the user's request to conditionally update certain fields:
package mainimport ("github.com/gorilla/mux""github.com/temple/tutorial/example-service/dao")func (env *env) setup(router *mux.Router) {env.hook.BeforeCreate(ourCustomBeforeHook)}func ourCustomBeforeHook(env *env, req createExampleServiceRequest, input *dao.CreateExampleServiceInput) *HookError {if (req.Bar == 5) {input.Foo = "Hello, world!"}return nil}
This will only update the value of Foo
if the value 5 is passed for bar:
# Create a new object where Bar != 5❯❯❯ curl -X POST $KONG_ENTRY/api/example-service -d '{"foo": "abcd", "bar": 10}'{"id":"e8e2e06e-823c-11ea-84ea-0242ac170003","foo":"abcd","bar":10}# Create a new object where Bar == 5❯❯❯ curl $KONG_ENTRY/api/example-service -d '{"foo": "abcd", "bar": 5}'{"id":"f244e8f0-823c-11ea-84ea-0242ac170003","foo":"Hello, world!","bar":5}
Modifying the response to the client
As well as creating a hook that is invoked before the datastore call, we are able to define a hook that is invoked after the datastore call.
For the same ExampleService
as the previous examples, a hook invoked after creating an object would look like:
package mainimport ("github.com/gorilla/mux""github.com/temple/tutorial/example-service/dao")func (env *env) setup(router *mux.Router) {env.hook.AfterCreate(ourCustomAfterHook)}func ourCustomAfterHook(env *env, exampleService *dao.ExampleService) *HookError {return nil}
There are only two arguments to this function:
- the environment,
env
- the newly created object,
exampleService
We are able to modify the object that has just been created, before it is returned to the client, by updating the attributes of the object exampleService
:
package mainimport ("github.com/gorilla/mux""github.com/temple/tutorial/example-service/dao")func (env *env) setup(router *mux.Router) {env.hook.AfterCreate(ourCustomAfterHook)}func ourCustomAfterHook(env *env, exampleService *dao.ExampleService) *HookError {exampleService.Bar = 42return nil}
Since this only modifies the response to the client, and not what's stored in the datastore, the value will not be modified in any subsequent GET requests:
# Create a new object❯❯❯ curl $KONG_ENTRY/api/example-service -d '{"foo": "abcd", "bar": 10}'{"id":"12bff66e-8243-11ea-9908-0242ac180003","foo":"abcd","bar":42}# Retrieve that same object❯❯❯ curl -X GET $KONG_ENTRY/api/example-service/12bff66e-8243-11ea-9908-0242ac180003{"id":"12bff66e-8243-11ea-9908-0242ac180003","foo":"abcd","bar":10}
Making additional DAO calls
One thing we have not yet discussed is the env
argument to each of the hooks.
The environment gives you access to the DAO as well as methods for accessing cross service communication.
This means you can perform additional database requests as part of your Before
or After
hook.
These can include the predefined database calls, or your own. For more information on this, see adding DAO functions.
For example, before updating a given entry, you may want to check what is already stored, and modify the update request accordingly:
package mainimport ("github.com/gorilla/mux""github.com/temple/tutorial/example-service/dao")func (env *env) setup(router *mux.Router) {env.hook.BeforeUpdate(ourCustomBeforeUpdateHook)}func ourCustomBeforeUpdateHook(env *env, req updateExampleServiceRequest, input *dao.UpdateExampleServiceInput) *HookError {current, _ := env.dao.ReadExampleService(dao.ReadExampleServiceInput{ID: input.ID,})// Modify the update query if what is currently stored is > 10if current.Bar > 10 {input.Bar = current.Bar}return nil}
Aborting Requests
Finally, Hooks give you the ability to abort requests early, by returning a HookError
from the hook.
For example, we could use this to disallow any requests where the value of Bar
is greater than 10:
package mainimport ("errors""net/http""github.com/gorilla/mux""github.com/temple/tutorial/example-service/dao")func (env *env) setup(router *mux.Router) {env.hook.BeforeCreate(ourCustomBeforeHook)}func ourCustomBeforeHook(env *env, req createExampleServiceRequest, input *dao.CreateExampleServiceInput) *HookError {if input.Bar > 10 {return &HookError{statusCode: http.StatusBadRequest,error: errors.New("The value of bar must be less than or equal to 10"),}}return nil}
Running some example requests:
# An example request where bar <= 10❯❯❯ curl $KONG_ENTRY/api/example-service -d '{"foo": "abcd", "bar": 5}'{"id":"1ee638c1-8246-11ea-aef0-0242ac180003","foo":"abcd","bar":5}# An example request where bar > 10❯❯❯ curl $KONG_ENTRY/api/example-service -d '{"foo": "abcd", "bar": 15}'{"error":"The value of bar must be less than or equal to 10"}