Skip to main content

Composing models

What you'll learn
  • 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.

npx 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.

composables/publishable.ts
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.

models/post.ts
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:

models/post.ts
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.

composables/uuidID.ts
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;
warning

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.

composables/timestamps.ts
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:

composables/publishable.ts
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`.
warning

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.

npx 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.

makeModel.ts
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.

makeModel.ts
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;
warning

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.