Composing models
- Creating composables to share features across some of your models
- Creating your own model factory with predefined features for all of your models
Sometimes, you may want to share common features across your models. To solve this, you may use one of the two solutions proposed by Foscia:
- Composition, to share features across some of your models
- Factory, to share features across all of your models
Composition
When you need to share features across some of your models, you should use composition.
Using CLI
You can generate a new composable using @foscia/cli
.
- NPM
- YARN
- PNPM
- Bun
npx foscia make composable publishable
yarn foscia make composable publishable
pnpm foscia make composable publishable
bun foscia make composable publishable
Defining a composable
The first step is to create a composable with the features you want to share.
This is done through makeComposable
and uses the same syntax as makeModel
.
The created composable will get its custom properties (e.g. published
below)
rewritten to protect their descriptor. This allows using spread syntax when
using composable in other models' definition.
import { attr, makeComposable, toDateTime } from '@foscia/core';
export default makeComposable({
publishedAt: attr(toDateTime()).nullable(),
get published() {
return !!this.publishedAt;
},
});
Using a composable
The easiest way to use your composable is to object-spread it inside your model's definition.
import { makeModel } from '@foscia/core';
import publishable from '../composables/publishable';
export default class Post extends makeModel('posts', {
publishable,
/** post definition */
}) {}
You may also use the model extend
method as follows:
import { makeModel } from '@foscia/core';
import publishable from '../composables/publishable';
export default class Post extends makeModel('posts').extend({ publishable }) {}
Using hooks
Composables share the models' hooks system. Each hooks you define on a composable will be added to models which use the composable. This is useful when defining common behaviors, such as a UUID keyed model with automatic generation before first saving.
import { attr, makeComposable, onCreating } from '@foscia/core';
import { v4 as uuidV4 } from 'uuid';
const uuidID = makeComposable({
id: attr<string | null>(),
});
onCreating(uuidID, (instance) => {
instance.id = instance.id ?? uuidV4();
});
export default uuidID;
Be aware that you cant unregister hooks from a composable, because hooks are
shallow cloned when extending the composable.
Hook registration function (such as onCreating
) returned callback will
only unregister hooks on composable, not on extending models.
Here is another example where we automatically track creation and last update timestamp for every record.
import { attr, makeComposable, onSaving, toDateTime } from '@foscia/core';
const timestamps = makeComposable({
timestamps: true,
createdAt: attr(toDateTime()),
updatedAt: attr(toDateTime()),
});
onSaving(timestamps, (instance) => {
if (instance.timestamps) {
instance.updatedAt = new Date();
if (!instance.$exists) {
instance.createdAt = instance.updatedAt;
}
}
});
export default timestamps;
Typechecking composables
You can easily typecheck for models or instances using some of your composables
by defining type aliases. You can also use isModelUsing
or isInstanceUsing
functions to check for composition existing on a model or instance:
import { attr, makeComposable, toDateTime, isInstanceUsing, isModelUsing, ModelInstanceUsing, ModelUsing } from '@foscia/core';
const publishable = makeComposable({
publishedAt: attr(toDateTime()).nullable(),
});
// Defining type aliases for your composable.
export type PublishableInstance = ModelInstanceUsing<typeof publishable>;
export type PublishableModel = ModelUsing<typeof publishable>;
export default publishable;
// Use type aliases for your function.
function somethingRequiringAPublishable(instance: PublishableInstance) {
console.log(instance.publishedAt);
}
// Checking if a model/instance is implementing a composable.
isInstanceUsing(someInstance, publishable); // `someInstance` extends `publishable`.
isModelUsing(SomeModel, publishable); // `SomeModel` extends `publishable`.
isInstanceUsing
and isModelUsing
will only check that the model definition
contain at some time the composable, but won't guaranty that the composable
properties are not overwritten afterward.
Factory
Using CLI
You can generate a new factory using @foscia/cli
.
- NPM
- YARN
- PNPM
- Bun
npx foscia make model-factory
yarn foscia make model-factory
pnpm foscia make model-factory
bun foscia make model-factory
Defining a factory
When you need to share features across all of your models, you should use a
custom model factory. It will replace the Foscia's makeModel
function.
import { attr, makeModelFactory, toDateTime } from '@foscia/core';
export default makeModelFactory({
/* ...common configuration */
}, {
createdAt: attr(toDateTime()),
updatedAt: attr(toDateTime()),
get wasChangedSinceCreation() {
return this.createdAt.getTime() === this.updatedAt.getTime();
},
});
Once your factory is ready, you can use it in replacement of the default
makeModel
provided by Foscia.
import makeModel from './makeModel';
export default class Post extends makeModel('posts', {
/* definition */
}) {}
Using hooks
Models factories share the models' hooks system. Each hooks you define on a factory will be added to models which are defined using the factory. This is useful when defining common behaviors, such as a UUID keyed model with automatic generation before first saving.
import { attr, makeModelFactory, onCreating } from '@foscia/core';
import { v4 as uuidV4 } from 'uuid';
const makeModel = makeModelFactory({}, {
id: attr<string | null>(),
});
onCreating(makeModel, (instance) => {
instance.id = instance.id ?? uuidV4();
});
export default makeModel;
Be aware that you cant unregister hooks from a factory, because hooks are
shallow cloned when creating a model.
Hook registration function (such as onCreating
) returned callback will
only unregister hooks on factory, not on created models.