Skip to main content

Models relations

What you'll learn
  • Checking if relations are loaded
  • Creating functions which will load your relations
  • Configuring those functions to match your needs

Checking loading state

If you want to check if an instance's relations (or deep relations) are loaded, you can use the loaded function.

loaded recursively inspect relations. This means it will only return true if all relations of the instance (and sub-instances for deep relations) are loaded.

import { loaded } from '@foscia/core';

// True if comments is loaded.
loaded(myPost, 'comments');
// True if comments and each author of comments are loaded.
loaded(myPost, 'comments.author');

Loading relations

Since Foscia uses a functional approach for your action, you are able to load a relation using different ways depending on your data source implementation.

For this, Foscia provides multiple loader factories providing various behaviors and options.

Using CLI

You can generate a new loader using @foscia/cli.

npx foscia make loader

1. Instance refreshing

This is the simplest way of loading relations if your data source implementation provides an inclusion of relations and a way to filter models based on IDs (e.g. JSON:API).

This loader will target the model index and include the requested relations. It supports nested relations keys if your data source supports them.

Here is an example when using a JSON:API backend with an ids filter available.

loaders/loadWithRefresh.ts
import { makeRefreshIncludeLoader } from '@foscia/core';
import { filterBy } from '@foscia/jsonapi';
import action from './action';

export default makeRefreshIncludeLoader(action, {
prepare: (action, { instances }) => action.use(filterBy({ ids: instances.map((i) => i.id) })),
});

You can now use the loader on any instance.

import loadWithRefresh from './loaders/loadWithRefresh';

await loadWithRefresh(myPost, 'comments');
await loadWithRefresh(myPostsArray, ['comments', 'comments.author']);
info

This loader is recommended as it will only run one action for many models and relations. But, be aware that you should implement an adapted prepare to filter the fetched models (this will avoid overloading your data source with useless records fetching).

It should support polymorphism if your data provider does.

Options

prepare: (action: Action, context: { instances: ModelInstance[]; relations: string[] }): Awaitable<void>

A function to execute before running action allowing you to prepare the refresh action (e.g. to avoid fetching a full list of models by filtering on instances' IDs).

chunk: (instances: ModelInstance[]): ModelInstance[][]

A function to split the instances array to multiple arrays (e.g. to avoid hitting pagination limit).

Array chunk function example

function chunk<T>(items: T[], size: number) {
const chunks = [] as T[][];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + chunkSize));
}

return chunks;
}
exclude: (instance: ModelInstance, relations: ModelRelationDotKey): bool

A function to exclude some relation fetching (e.g. to avoid fetching already loaded relations). Notice the following:

  • If an instance is excluded for all relations, it will not be refreshed
  • If a relation is excluded for all instances, it will not be included during refresh
version: v0.8.2+

This method is best suited for multiple instance relation loading with implementations providing IDs instead of included relations, such as some REST implementations. If nested relations are passed (such as comments.author), it will include the nested relations during the root model action.

loaders/loadWithModelQuery.ts
import { makeQueryModelLoader } from '@foscia/core';
import action from './action';

export default makeQueryModelLoader(action, {
prepare: (action, { ids }) => action.use(param('ids', ids)),
});

You can now use the loader on any instance.

import loadWithModelQuery from './loaders/loadWithModelQuery';

await loadWithModelQuery(myPost, 'comments');
await loadWithModelQuery(myPostsArray, ['comments', 'comments.author']);
info

This loader is recommended as it will only run one action per direct relation.

It supports polymorphic relations when related models are correctly defined on relations and extract option correctly retrieve IDs.

Options

extract: (instance: ModelInstance, relation: ModelRelationKey): Arrayable<ModelIdType> | null | undefined

A function to extract the related IDs and types from an instance. Default behavior is to use the raw record value for relation's key and and build one or many objects containing type and ID.

Extract function example and side notes

Here is the default implementation for extract function using makeQueryModelLoaderExtractor helper, as an example of implementation.

import { makeQueryModelLoaderExtractor, normalizeKey } from '@foscia/core';

const extract = makeQueryModelLoaderExtractor(
(instance, relation) => instance.$raw[normalizeKey(instance.$model, relation)],
(value) => (typeof value === 'object' ? { id: value.id, type: value.type } : { id: value }),
);

It will support extracting both raw JSON data related IDs extraction (basic and polymorphic relations):

{
"id": "1",
"title": "Foo",
"comments": ["1", "2"]
}
{
"type": "posts",
"id": "1",
"title": "Foo",
"relatedContents": [
{
"type": "posts",
"id": "2"
},
{
"type": "galleries",
"id": "1"
}
]
}
info

Notice that Foscia will try to deserialize each relations were record is an object or array of objects value (such as in the 2nd example). This will mark the relation as loaded even if the related value attributes are not loaded. To avoid this, you can disable the relation's syncing on pull (e.g. hasOne().sync('push')), as it will disable relation deserialization.

prepare: (action: Action, context: { ids: ModelIdType[]; relations: string[] }): Awaitable<void>

A function to execute before running action allowing you to prepare the fetch action (e.g. to avoid fetching a full list of models by filtering on related IDs).

chunk: (ids: ModelIdType[]): ModelIdType[][]

A function to split the related IDs array to multiple arrays (e.g. to avoid hitting pagination limit).

Array chunk function example

function chunk<T>(items: T[], size: number) {
const chunks = [] as T[][];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + chunkSize));
}

return chunks;
}
exclude: (instance: ModelInstance, relations: ModelRelationDotKey): bool

A function to exclude some relation fetching (e.g. to avoid fetching already loaded relations).

3. Relation action

This method is best suited for one instance relation loading with implementations providing relation reading through a dedicated endpoint/query, such as JSON:API. If nested relations are passed (such as comments.author), it will include the nested relations during the root relation action.

loaders/loadWithRelationQuery.ts
import { makeQueryRelationLoader } from '@foscia/core';
import action from './action';

export default makeQueryRelationLoader(action);

You can now use the loader on any instance.

import loadWithRelationQuery from './loaders/loadWithRelationQuery';

await loadWithRelationQuery(myPost, 'comments');
warning

Because this loader will run one action per instance and relation, it is only recommended for one instance's relation loading, not many.

It should support polymorphism if your data provider does.

Options

exclude: (instance: ModelInstance, relations: ModelRelationDotKey): bool

A function to exclude some relation fetching (e.g. to avoid fetching already loaded relations).

disablePerformanceWarning: boolean

makeQueryRelationLoader will log a warning if you are using it to load multiple instances relations in one call, because it may cause performance issues (multiple actions would run).

You can pass this option with true to avoid this warning.

Loading missing relations only

Every loader factory provides an exclude option which can remove some instances or relations before running actions.

This is useful to avoid loading already loaded relations, and can be done easily using the loaded function.

loaders/loadMissingWithRefresh.ts
import { makeRefreshIncludeLoader, loaded } from '@foscia/core';
import action from './action';

export default makeRefreshIncludeLoader(action, {
exclude: loaded,
// other options
});