Skip to main content

schema.Entity

If you already have classes for your data-types, schema.Entity mixin may be for you.

import { schema } from '@data-client/graphql';

export class Article {
  id = '';
  title = '';
  content = '';
  tags: string[] = [];
}

export class ArticleEntity extends schema.Entity(Article) {}

Options

The second argument to the mixin can be used to conveniently customize construction. If not specified the Base class' static members will be used. Alternatively, just like with Entity, you can always specify these as static members of the final class.

class User {
  username = '';
  createdAt = Temporal.Instant.fromEpochSeconds(0);
}
class UserEntity extends schema.Entity(User, {
  pk: 'username',
  key: 'User',
  schema: { createdAt: Temporal.Instant.from },
}) {}

pk: string | (value, parent?, key?) => string | undefined = 'id'

Specifies the Entity.pk

A string indicates the field to use for pk.

A function is used just like Entity.pk, but the first argument (value) is this

Defaults to 'id'; which means pk is a required option unless the Base class has a serializable id member.

multi-column primary key
class Thread {
  forum = '';
  slug = '';
  content = '';
}
class ThreadEntity extends schema.Entity(Thread, {
  pk(value) {
    return [value.forum, value.slug].join(',');
  },
}) {}

key: string

Specifies the Entity.key

schema: {[k:string]: Schema}

Specifies the Entity.schema

Methods

schema.Entity has the same methods as Entity with an improved mergeWithStore() lifecycle.

This method uses shouldReorder() to handle race conditions rather than useIncoming(), which is better able to handle partial field entities.

Eventually Entity will also be converted to use this default implementation. You can prepare for this by copying the mergeWithStore default implementation below.

static mergeWithStore(existingMeta, incomingMeta, existing, incoming): mergedValue

static mergeWithStore(
existingMeta: {
date: number;
fetchedAt: number;
},
incomingMeta: { date: number; fetchedAt: number },
existing: any,
incoming: any,
) {
const useIncoming = this.useIncoming(
existingMeta,
incomingMeta,
existing,
incoming,
);

if (useIncoming) {
// distinct types are not mergeable (like delete symbol), so just replace
if (typeof incoming !== typeof existing) {
return incoming;
} else {
return this.shouldReorder(
existingMeta,
incomingMeta,
existing,
incoming,
)
? this.merge(incoming, existing)
: this.merge(existing, incoming);
}
} else {
return existing;
}
}

mergeWithStore() is called during normalization when a processed entity is already found in the store.

This calls useIncoming(), shouldReorder() and potentially merge()

static useIncoming(existingMeta, incomingMeta, existing, incoming): boolean

static useIncoming(
existingMeta: { date: number; fetchedAt: number },
incomingMeta: { date: number; fetchedAt: number },
existing: any,
incoming: any,
) {
return true;
}

Preventing updates

useIncoming can also be used to short-circuit an entity update.

import deepEqual from 'deep-equal';

class ArticleEntity extends schema.Entity(
class {
id = '';
title = '';
content = '';
published = false;
},
) {
static useIncoming(
existingMeta: { date: number; fetchedAt: number },
incomingMeta: { date: number; fetchedAt: number },
existing: any,
incoming: any,
) {
return !deepEqual(incoming, existing);
}
}

static shouldReorder(existingMeta, incomingMeta, existing, incoming): boolean

static shouldReorder(
existingMeta: { date: number; fetchedAt: number },
incomingMeta: { date: number; fetchedAt: number },
existing: any,
incoming: any,
) {
return incomingMeta.fetchedAt < existingMeta.fetchedAt;
}

true return value will reorder incoming vs in-store entity argument order in merge. With the default merge, this will cause the fields of existing entities to override those of incoming, rather than the other way around.

Example

class LatestPriceEntity extends schema.Entity(
  class {
    id = '';
    updatedAt = 0;
    price = '0.0';
    symbol = '';
  },
) {
  static shouldReorder(
    existingMeta: { date: number; fetchedAt: number },
    incomingMeta: { date: number; fetchedAt: number },
    existing: { updatedAt: number },
    incoming: { updatedAt: number },
  ) {
    return incoming.updatedAt < existing.updatedAt;
  }
}

static merge(existing, incoming): mergedValue

static merge(existing: any, incoming: any) {
return {
...existing,
...incoming,
};
}

Merge is used to handle cases when an incoming entity is already found. This is called directly when the same entity is found in one response. By default it is also called when mergeWithStore() determines the incoming entity should be merged with an entity already persisted in the Reactive Data Client store.

const vs class

If you don't need to further customize the entity, you can use a const declaration instead of extend to another class.

There is a subtle difference when referring to the class token in TypeScript - as class declarations will refer to the instance type; whereas const tokens refer to the value, so you must use typeof, but additionally typeof gives the class type, so you must layer InstanceType on top.

import { schema } from '@data-client/graphql';

export class Article {
  id = '';
  title = '';
  content = '';
  tags: string[] = [];
}

export class ArticleEntity extends schema.Entity(Article) {}
export const ArticleEntity2 = schema.Entity(Article);

const article: ArticleEntity = ArticleEntity.fromJS();
const articleFails: ArticleEntity2 = ArticleEntity2.fromJS();
const articleWorks: InstanceType<typeof ArticleEntity2> =
  ArticleEntity2.fromJS();