Actions
- Enhancing actions
- Running actions
- Extending actions for builder pattern calls
- Conditionally enhancing and running actions
- Registering hooks on actions
Before reading this guide
This guide is only about basic usage and lifecycle of actions.
- You can read more about action factories setups in the getting started guide.
- Advanced usage is different between the various implementations. You can see advanced usage of actions for your implementation inside the digging deeper actions guide.
- If you want to see some concrete examples on how actions could be used, you can check the examples documentation section.
Usage
Once your models and action factory are set up, you are ready to run actions through your models.
Foscia uses two typologies of functions to run actions, enhancers and runners, which are described in the lifecycle guide.
Examples of the documentation commonly use
variadic run
calls on actions, as this is the simplest
way of using actions.
Retrieving records
query
can target a records index using a model. You can use it in
combination with all
to retrieve a list of the model instances.
import { query, all } from '@foscia/core';
const posts = await action().run(
query(Post),
all(),
);
posts.forEach((post) => {
console.log(post.title);
});
In addition, query
also provide a way to target a record using a model and
an ID. You can use it in combination with oneOrFail
or oneOr
to retrieve
a single record and handle not found errors.
import { query, oneOrFail } from '@foscia/core';
const postOrFail = await action().run(
query(Post, '1'),
oneOrFail(),
);
const postOrNull = await action().run(
query(Post, '2'),
oneOr(() => null),
);
Finally, query
can also be used to target a record using an instance.
If the instance does not exist, this will target the records index, otherwise
it will target the record. This can be used when writing a record (e.g.
create/update) or when lazy loading a relation.
import { query, oneOrFail } from '@foscia/core';
const post = await action().run(query(somePost), oneOrFail());
Creating and updating records
You can create or update records using create
and update
functions. As a
convenience, Foscia provides a save
function which will call create
or
update
depending on the existence state of your record.
oneOrCurrent
is a runner which you can combine with those enhancers to always
retrieve an instance, even if your data source does not return the record on
those actions (such as a 204 No Content
on update). It will return a record if
there is one if your data source response, otherwise it will return the
initially provided instance.
You can also combine those functions with none
runner to not handle the data
source response.
import { create, save, fill, oneOrCurrent, none } from '@foscia/core';
const post = await action().run(
create(fill(new Post(), { title: 'Hello World!' })),
include('author'),
oneOrCurrent(),
);
post.title = 'Edited Hello World!';
await action().run(save(post), none());
Deleting records
You can use destroy
to trigger a record deletion. You can combine it with
none
to ignore the data source's response (errors are still thrown by the
adapter).
import { destroy, none } from '@foscia/core';
await action().run(destroy(post), none());
Relationships
Eager loading relations
Some implementations of Foscia support dedicated relationships operations (such as JSON:API). With those, you may read and write relationships.
Eager loading relations
Eager loading provides a way to retrieve records and eager load related models without further requests. It also alleviates the "N + 1" query problem.
To eager load a relation, you can use the include
function.
import { query, all, include } from '@foscia/core';
const posts = await action().run(
query(Post),
include('author'),
all(),
);
Lazy loading relations
Lazy loading provides a way to fetch related records for a relation with an already retrieved a record.
import { query, all, oneOrFail } from '@foscia/core';
// Fetch a post without relations.
const post = await action().run(query(Post, 1), oneOrFail());
// Fetch "comments" relation.
const comments = await action().run(query(post, 'comments'), all());
Lazy loading a record's relation won't affect the given record instance
(it won't hydrate the relation on parent instance, here post
).
Writing relations
To one
For hasOne
relationship, you can use associate
and dissociate
to change the relation value.
import { associate, dissociate, none } from '@foscia/core';
// Associate myUser as myPost's author.
await action().run(
associate(myPost, 'author', myUser),
none(),
);
console.log(myPost.author); // myUser
// Dissociate author of myPost.
await action().run(
dissociate(myPost, 'author'),
none(),
);
console.log(myPost.author); // null
Using associate
and dissociate
will update the model's property and mark
it synced.
To many
For hasMany
relationship, you can use attach
, detach
and updateRelation
to change the relation value:
attach
will attach one or many related records, without touching existing related records;detach
will detach one or many related records, without touching attaching other records;updateRelation
will sync related records to the given records (detach past records, attach given records);
import { attach, detach, updateRelation, none } from '@foscia/core';
// Attach myUser1 and myUser2 to myTeam's members.
await action().run(
attach(myTeam, 'members', [myUser1, myUser2]),
none(),
);
// Detach myUser1 and myUser2 from myTeam's members.
await action().run(
detach(myTeam, 'members', [myUser1, myUser2]),
none(),
);
// Sync myUser1 and myUser2 as myTeam's members.
await action().run(
updateRelation(myTeam, 'members', [myUser1, myUser2]),
none(),
);
Using attach
, detach
and updateRelation
will not update the model's
property.
Creating through
In some data source implementation, you may want to create a record "through"
a parent record's relation. As an example, creating a comment through a post
in a REST API, and using a POST /posts/1/comments
request.
This can be achieved using the create
enhancer.
import { create } from '@foscia/core';
// Create a comment through a post.
await action().run(
create(newComment, post, 'comments'),
oneOrCurrent(),
);
Caching
When having a cache enabled on your action, all fetched records are cached
when deserializing. You can use the cache to retrieve
an already fetched record without interacting with your data source again
using cached
and cachedOrFail
.
import { query, cached, cachedOrFail } from '@foscia/core';
const postOrNull = await action().run(query(Post, 1), cached());
const postOrFail = await action().run(query(Post, 1), cachedOrFail());
Sometimes, you may want to interact with the cache if your record does not
have been cached already. For this particular case, you can use cachedOr
.
import { query, cachedOr, oneOrFail } from '@foscia/core';
const cachedPostOrFetch = await action().run(query(Post, 1), cachedOr(oneOrFail()));
cached
runners will check for loaded relations if you are requesting
relations include
. If some relations (even deep ones) are not loaded,
it will act as the record is not inside the cache.
Conditionals
Sometimes, you may need to conditionally apply an enhancer or run an action. As
an example, you may want to sort results differently based on the user's defined
sort's direction. This can be done easily using the when
helper:
import { query, when } from '@foscia/core';
import { sortByDesc } from '@foscia/jsonapi';
action().run(
query(Post),
when(displayLatestFirst, sortByDesc('createdAt'),
);
when
returns a new enhancer or runner based on the given value's truthiness.
It will execute the first enhancer/runner only if its value is truthy. You may
pass the value as a factory function returning the value, and even a promise
value. You may also pass a second enhancer/runner which will only execute if the
value is falsy. Each callback arguments will receive the action as their first
argument and the value as their second argument. Each callback may also be
async, as any enhancers and runners.
Here are further examples:
import { changed, create, oneOrFail, when } from '@foscia/core';
const post = fill(new Post(), userInputData);
action().run(
create(post),
when(
() => /* compute a special value */,
(a, specialTruthyValue) => /* do something */,
(a, specialFalsyValue) => /* do something */,
),
when(
changed(post),
oneOrFail(),
() => post,
),
);
Lifecycle
When using variadic run
calls, Foscia actions lifecycle
is "hidden" by variadic arguments. In fact, all actions execution have
3 steps: instantiation, enhancements and execution (run).
action().run( // Instantiation.
query(Post), // `query` enhancement.
include('comments'), // `include` enhancement.
all(), // `all` run.
);
The above example is equivalent to and can be decomposed as follows:
action() // Instantiation.
.use(query(Post)) // `query` enhancement.
.use(include('comments')) // `include` enhancement.
.run(all()); // `all` run.
Instantiation
As stated in the getting started guide, actions are instantiated through your action factory. In this guide, we'll admit you have a setup action factory.
Enhancers
An action instance can receive multiple enhancements that will build an appropriate context to run requests to your data sources.
Each enhancer can be applied using the use
action method. Note that those
enhancers are not instantly applied to the action context, but during the action
run step (or context computation).
action()
// Enhance the action.
.use(query(Post))
.use(include('comments'));
Variadic use
use
also support variadic enhancers. Number of arguments might be limited,
check the function signature for details.
action().use(
query(Post),
include('comments'),
sortByDesc('publishedAt'),
// etc.
);
Enhancers API guide
Available enhancers API guide
Runners
An action instance can be run using the run
method. The runner can execute
multiple enhancers or runners internally.
When an action run, it does 3 things:
- Dequeue all enhancers since the action instantiation and builds context
- Execute the runner and each of its internal enhancers/runners (this may update the context)
- Return the runner result (might be any value, including void or an error throwing)
Internally, action running will also trigger actions hooks.
action()
.use(query(Post))
.use(include('comments'))
// Run the action.
.run(all());
Variadic run
run
also support variadic enhancers. Last argument must be a runner.
Number of arguments might be limited, check the function signature for details.
const posts = await action().run(
query(Post),
include('comments'),
sortByDesc('publishedAt'),
all(),
);
Runners API guide
Available runners API guide
Hooks
You may hook on multiple events which occurs on action instance using the hook registration function:
onRunning
: after context computation, before context runner execution.onSuccess
: after context runner successful execution (no error thrown).onError
: after context runner failed execution (error thrown).onFinally
: after context runner successful or failed execution.
To register a hook callback, you must use the registration enhancer on your building action.
import {
onRunning,
onSuccess,
onError,
onFinally,
} from '@foscia/core';
action().use(onRunning(({ context }) => /* ... */));
action().use(onSuccess(({ context, result }) => /* ... */));
action().use(onError(({ context, error }) => /* ... */));
action().use(onFinally(({ context }) => /* ... */));
Hooks' callbacks are async and executed in a sequential fashion (one by one, not parallelized).
You can temporally disable hook execution for a given action by using the
withoutHooks
function. withoutHooks
can receive a sync or async
callback: if an async callback is passed, it will return a Promise
.
import { query, all, withoutHooks } from '@foscia/core';
// Retrieve a list of User instances without action hooks running.
const users = await withoutHooks(action(), async (a) => {
return await a.use(query(User)).run(all());
});
Foscia may also register hooks internally when using some enhancers. Those provide some library features (models hooks, etc.). Be careful running actions without hooks, as those hooks will also be disable.
Extensions
Sometimes, functional programming can be frustrating, because you must always
rewrite the same words (e.g. use
) to keep a builder pattern styled code.
Extensions provide a set of properties or methods which will be added to your
actions' instances. As an action, extensions can avoid you writing use
or
run
by adding enhancers/runners methods on you action.
The first step to use one or many extensions is to update your action factory in which you should provide a second parameter.
import {
makeActionFactory,
query,
include,
all,
hooksExtensions,
} from '@foscia/core';
export default makeActionFactory(
{
// makeJsonRestAdapter(), ...etc.
},
{
...hooksExtensions(),
...query.extension(),
...include.extension(),
...all.extension(),
},
);
You can now use the extended enhancers and runners without calling use
or
run
:
import action from './action';
await action().query(Post).include('tags').all();
Every enhancers and runners of Foscia provide a .extension()
property which is
extendable by an action instance.
You may extend your action with any enhancers or runners extensions manually. Otherwise, you may also use prebuild extensions packs. Those provide multiple extensions in one exported object allowing you to extend multiple extensions at one time!
Available extensions packs API guide
Keep in mind that using extensions will avoid tree-shaking the extended enhancers or runners functions (even when those are unused in your codebase), because those are imported by their extensions.