What are actions
An action is a set of outcomes (as defined in the action schema) which is executed when action endpoint is invoked.
An action can have a set of required input fields or none at all. Actions can also be built to handle callbacks/webhooks from other services (like payment gateway server to server notification).
Actions can be thought of as follows:
- A set of inputs (key value pair, extracted from query params and request body)
- A set of outcomes based on the inputs
What are actions and why do I need this
Create/Read/Update/Delete (CRUD) APIs are only basic APIs exposed on the database, and you would rarely want to make those API available to your end user. Reasons could be multiple
- The end user doesn't (immediately) owe the data they create
- Creating a "row"/"data entry" entry doesn't signify completion of a process or a flow
- Usually a "set of entities" is to be created and not just a single entity (when you create a user, you also want to create a usergroup also and associate the user to usergroup)
- You could allow user to update only some fields of an entity and not all fields (eg user can change their name, but not email)
- Changes based on some entity (when you are going through a project, a new item should automatically belong to that project)
Actions provide a powerful abstraction over the CRUD and to handle a variety of use cases. Actions can also make use of operations imported from OpenAPI Specs of other services.
To quickly understand what actions are, lets see what happened when you "signed up" on Daptin.
Take a look at how "Sign up" action is defined. We will go through each part of this definition
An action is performed on an entity. Let's also remember that world
is an entity itself.
Action schema
Name: signup
Label: Sign up
InstanceOptional: true
OnType: user_account
InFields:
- Name: name
ColumnType: label
IsNullable: false
- Name: email
ColumnType: email
IsNullable: false
- Name: password
ColumnType: password
IsNullable: false
- Name: Password Confirm
ColumnName: passwordConfirm
ColumnType: password
IsNullable: false
Validations:
- ColumnName: email
Tags: email
- ColumnName: name
Tags: required
- ColumnName: password
Tags: eqfield=InnerStructField[passwordConfirm],min=8
Conformations:
- ColumnName: email
Tags: email
- ColumnName: name
Tags: trim
OutFields:
- Type: user_account
Method: POST
Reference: user
Attributes:
name: "~name"
email: "~email"
password: "~password"
confirmed: '0'
- Type: usergroup
Method: POST
Reference: usergroup
Attributes:
name: "!'Home group for ' + user.name"
- Type: user_user_id_has_usergroup_usergroup_id
Method: POST
Reference: user_usergroup
Attributes:
user_id: "$user.reference_id"
usergroup_id: "$usergroup.reference_id"
- Type: client.notify
Method: ACTIONRESPONSE
Attributes:
type: success
title: Success
message: Signup Successful
- Type: client.redirect
Method: ACTIONRESPONSE
Attributes:
location: "/auth/signin"
window: self
Action Name
Name: "signup",
Name of the action, this should be unique for each actions. Actions are identified by this name
Action Label
Label: "Sign up",
Label is for humans
OnType
OnType: "user_account",
The primary type of entity on which the action happens. This is used to know where the actions should come up on the UI
Action instance
InstanceOptional: true,
If the action requires an "instance" of that type on which the action is defined (more about this below). So "Sign up" is defined on "user" table, but an instance of "user" is not required to initiate the action. This is why the "Sign up" doesnt ask you to select a user (which wouldn't make sense either)
Input fields
InFields: []api2go.ColumnInfo
This is a set of inputs which the user need to fill in to initiate that action. As we see here in case of "Sign up", we ask for four inputs
- Name
- Password
- Confirm password
Note that the ColumnInfo structure is the same one we used to define tables.
Validations
Validations: []ColumnTag
Validations validate the user input and rejects if some validation fails
{
"ColumnName": "email",
"Tags": "email"
}
This tells that the "email" input should actually be an email.
One of the more interesting validations is cross field check
{
ColumnName: "password",
Tags: "eqfield=InnerStructField[passwordConfirm],min=8",
},
This tells that the value entered by user in the password field should be equal to the value in passwordConfirm field. And the minimum length should be 8 characters.
Conformations
Conformations: []ColumnTag
Conformations help to clean the data before the action is carried out. The frequently one used are trim
and email
.
- Trim: trim removes white spaces, which are sometimes accidently introduced when entering data
- Email: email conformation will normalize the email. Things like lowercase + trim
OutFields
OutFields: []Outcome
OutFields are the list of outcomes which the action will result in. The outcomes are evaluated in a top to bottom manner, and the result of one outcome is accessible when evaluating the next outcome.
We have defined three outcomes in our "Sign Up" action.
- Create a user
{ Type: "user_account", Method: "POST", Reference: "user", Attributes: map[string]interface{}{ "name": "~name", "email": "~email", "password": "~password", "confirmed": "0", }, },
This tells us that, the first outcome is of type "user". The outcome is a "New User" (POST). It could alternatively have been a Update/Find/Delete operation.
The attributes maps the input fields to the fields of our new user.
~name
will be the value entered by user in the name field~email
will be the entered in the email field, and so on
If we skip the ~
, like "confirmed": "0"
Then the literal value is used.
Reference: "user",
We have this to allow the "outcome" to be referenced when evaluating the next outcome. Let us see the other outcomes
JavaScript in fields - "!..."
{
Type: "usergroup",
Method: "POST",
Reference: "usergroup",
Attributes: map[string]interface{}{
"name": "!'Home group for ' + user.name",
},
},
Daptin embeds the otto js engine. An exclamation mark sets to evaluate the rest of the string as Javascript.
'Home group for ' + user.name
becomes "Home group for parth"
Referencing previous outcomes
{
Type: "user_user_id_has_usergroup_usergroup_id",
Method: "POST",
Reference: "user_usergroup",
Attributes: map[string]interface{}{
"user_id": "$user.reference_id",
"usergroup_id": "$usergroup.reference_id",
},
},
the $
sign is to refer the reference variables. Here this outcome adds the newly created user to the newly created usergroup.