Skip to main content

Creating an action enhancer

What you'll learn
  • Defining a custom action enhancer
  • Providing an extension property to your enhancer

Foscia tries to be agnostic of your data source, so sometimes you may require a custom enhancer to avoid code duplication.

This is a simple guide on defining a custom enhancer, but you may also inspire from any existing Foscia enhancers.

Goal

Since Foscia is pagination agnostic, providing a first enhancer is not possible. Here is what we want our new first enhancer to do:

  • Select the model just like the find enhancer, but not a record ID
  • Limit the pagination to the first page and one record only

In this example, we will admit a JSON:API is used with the following query parameters working:

  • page[number] describes the number of the page to fetch
  • page[size] describes the count of records to fetch (aka. limit)
info

This guide is an enhancer version of the first runner described in the custom runners guide.

Defining the function

Our implementation of first will target the model and paginate the context.

action/enhancers/first.ts
import { Action, Model, ModelInstance, forModel } from 'foscia/core';
import { paginate } from 'foscia/jsonapi';

export default function first<
Context extends {},
ContextDefinition extends {},
ContextInstance extends ModelInstance<ContextDefinition>,
ContextModel extends Model<ContextDefinition, ContextInstance>,
>(model: ContextModel) {
return (action: Action<Context>) =>
action.use(forModel(model)).use(paginate({ number: 1, size: 1 }));
}
caution

Please note that when defining custom enhancers or runners, you should always correctly define generic types. This is very important as it will allow the context propagation through other enhancers and runners.

Using the function

Once your enhancer is ready, you may use it like any other Foscia enhancer.

import { one } from 'foscia/core';
import action from './action';
import first from './action/enhancers/first';
import Post from './models/post';

const post = await action()
.use(first(Post))
.run(one());

Defining the extension

Our current enhancer can only be used through an import and the use method of our action. To make it available for the builder pattern style calls, we must define an extension for it.

There is currently a limitation of the TypeScript language (Higher Order types are not available for now) which forces us to declare each extension manually. The goal of an extension definition is to get a type safe feature directly available on our action (and so provide autocomplete, context propagation, etc.).

Once your enhancer extension is ready, you will be able to use it as any other enhancers of Foscia.

action/enhancers/first.ts
import {
Action,
ActionParsedExtension,
ConsumeId,
ConsumeModel,
forModel,
makeEnhancersExtension,
Model,
ModelInstance,
} from 'foscia/core';
import { paginate } from 'foscia/jsonapi';

// Our previous enhancer code.
export default function first<
Context extends {},
ContextDefinition extends {},
ContextInstance extends ModelInstance<ContextDefinition>,
ContextModel extends Model<ContextDefinition, ContextInstance>,
>(model: ContextModel) {
return (action: Action<Context>) =>
action.use(forModel(model)).use(paginate({ number: 1, size: 1 }));
}

// The extension typing.
type FirstEnhancerExtension = ActionParsedExtension<{
first<Context extends {}, Extension extends {}, ContextModel extends Model>(
this: Action<Context, Extension>,
model: ContextModel,
): Action<Context & ConsumeModel<ContextModel> & ConsumeId, Extension>;
}>;

// The extension value.
first.extension = makeEnhancersExtension({ first }) as FirstEnhancerExtension;
caution

Here again, correctly typing our enhancer extension is really important to get context and action's extension propagation.

Transforming it to a runner

Wish first was not selecting a model but directly running the action and fetching a result?

Check out the custom runner guide which describe how to code a first action runner.