Models relations
- 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
.
- NPM
- YARN
- PNPM
- Bun
npx foscia make loader
yarn foscia make loader
pnpm foscia make loader
bun 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.
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']);
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
2. Related model action
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.
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']);
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"
}
]
}
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.
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');
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.
import { makeRefreshIncludeLoader, loaded } from '@foscia/core';
import action from './action';
export default makeRefreshIncludeLoader(action, {
exclude: loaded,
// other options
});