> No issue, just a doc update # Checklist for submitter If some of the following don't apply, delete the relevant line. <!-- Note that API documentation changes are now addressed by the product design team. --> - [x] Manual QA for all new/changed functionality
4 KiB
Backend patterns
The backend software patterns that we follow in Fleet.
NOTE: There are always exceptions to the rules, but we try to follow these patterns as much as possible unless a specific use case calls for something else. These should be discussed within the team and documented before merging.
Table of Contents
API Inputs
Input preprocessing and validation
Validate API inputs and return a 4XX status code if invalid. If you did not do authorization checking before failing validation, skip the authorization check with svc.authz.SkipAuthorization(ctx).
Inputs corresponding to sortable or indexed DB fields should be preprocessed (trim spaces, normalize Unicode, etc.). Use utility method fleet.Preprocess(input string) string. Backend sync where discussed.
JSON unmarshaling
PATCH API calls often need to distinguish between a field being set to null and a field not being present in the JSON. Use the structs from optjson package to handle this. Backend sync where discussed. JSON unmarshaling article and example.
MySQL
Use high precision for all time fields. Precise timestamps make sure that we can accurately track when records were created and updated, keep records in order with a reliable sort, and speed up testing by not having to wait for the time to update. MySQL reference. Backend sync where discussed. Example:
CREATE TABLE `sample` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`created_at` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`updated_at` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
PRIMARY KEY (`id`)
);
Do not use goqu; use MySQL queries directly. Searching for, understanding, and debugging direct MySQL
queries is easier. If needing to modify an existing goqu query, try to rewrite it in
MySQL. Backend sync where discussed.
Data retention
Sometimes we need data from rows that have been deleted from DB. For example, the activity feed may be retained forever, and it needs user info (or host info) that may not exist anymore.
Going forward, we need to keep this data in a dedicated table(s). A reference unmerged PR is here.
The id may be the same as that of the original table. For example, if the user row is deleted, a new entry with the same user.id can be added to user_persistent_info.
Re-usable transactionable functions
Sometimes we want to encapsulate a piece of functionality in such a way that it can be use both independently and as part of a transaction. To do so, create a private function in the following way:
func myTransactionableFunction(ctx context.Context, tx sqlx.ExtContext, yourArgsHere any) error {
// some setup, statements, etc...
_, err := tx.ExecContext(ctx, stmt, args)
if err != nil {
return ctxerr.Wrap(ctx, err, "doing some stuff in a transaction")
}
}
You can then use the function as a standalone call, like so
// *sqlx.DB implements the sqlx.ExtContext interface
err := myTransactionableFunction(ctx, ds.writer(ctx), myArgs)
or as part of a transaction, like so
func (ds *Datastore) MyDSMethodWithTransaction(ctx context.Context, yourArgsHere any) error {
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
return myTransactionableFunction(ctx, tx, yourArgsHere)
})
}
See this commit for an example of this pattern.