Skip to content

Объединения

Объединение - один список над несколькими моделями: лента из постов и рекламы, холст из разных фигур, смешанные результаты поиска. Используйте его, когда сущности разных видов текут через один экран, сохраняя свои поля и поведение.

Варианты идентифицируются ссылками на модели - никаких строк-«типов», которые нужно придумывать и держать уникальными.

Примеры на этой странице выполняются в активном скоупе - см. Коллекции и экземпляры.

Объявление объединения

ts
import { union } from "@virentia/core/models";

const Timestamped = trait({
  data: { createdAt: f.date().indexed("ord") },
});

const Post = staticModel({
  with: [Timestamped],
  data: { title: f.string(), likes: f.number(0) },
});

const Ad = staticModel({
  with: [Timestamped],
  data: { budget: f.number(0), active: f.boolean(true) },
});

const FeedItem = union(Post, Ad).by((json) =>
  "budget" in json ? Ad : Post,
);

const feed = collection(FeedItem);

by говорит объединению, какому варианту принадлежит JSON-объект. Union-коллекция - представление: экземпляры живут в collection(Post) и collection(Ad), и обе стороны видят одни и те же данные.

Добавление через объединение

ts
feed.add({ title: "привет", likes: 3 }); // by → Post
feed.add({ budget: 100 });               // by → Ad

feed.count;             // 2
collection(Post).count; // 1
collection(Ad).count;   // 1

Что происходит:

  • неизвестные id идут через by - дискриминация по форме на полных объектах;
  • известный id мержится в свой вариант напрямую, без by - частичным обновлениям различительные ключи не нужны;
  • полный объект другого варианта с занятым id - ошибка: варианты не мигрируют;
  • без .by(...) у объединения нет add - ни в типах, ни в рантайме.

Общие поля

Поле общее для объединения, когда каждый вариант получает его из одного трейта:

ts
feed.where(FeedItem.createdAt.gte(today)).count;
feed.sort(FeedItem.createdAt.desc).items;

Оба работают, потому что Post и Ad компонуют Timestamped. Каждый вариант использует свой индекс; отсортированные результаты сливаются между вариантами.

Совпадение имён - не общность: две модели, каждая со своим createdAt в data, не дают FeedItem.createdAt, и обращение к нему об этом скажет. Кладите общие поля в трейт.

Фильтр по вариантам: match

Вариант-специфичные условия идут через match - по ветке на вариант:

ts
feed.match(
  [Post, (p) => p.likes.gte(100)],  // p - дескрипторы Post
  [Ad, (a) => a.active.eq(true)],
);

Ветка возвращает предикат либо true/false - оставить или выкинуть вариант целиком. match исчерпывающий: пропущенный вариант - ошибка компиляции (сообщение его называет) и ошибка рантайма. match свободно сочетается в цепочке с where, sort и take:

ts
feed
  .where(FeedItem.createdAt.gte(today))
  .match([Post, (p) => p.likes.gte(10)], [Ad, () => false])
  .sort(FeedItem.createdAt.desc)
  .take(20);

Сужение элементов

Результаты типизированы объединением экземпляров вариантов; in сужает:

ts
for (const item of feed.items) {
  if ("likes" in item) {
    item.likes.value;  // Post
  } else {
    item.budget.value; // Ad
  }
}

Объединение в связях

Целью связи может быть объединение - значением или санком:

ts
const Bookmark = staticModel({
  data: { target: refs.one(FeedItem) },
});

bookmark.target.value = post; // экземпляр любого варианта
bookmark.json().target;       // id

Когда связь записывают экземпляром, вариант известен точно - всё остаётся надёжным, даже когда два варианта делят одну строку id. Id, загруженный из JSON, разрешается поиском по вариантам при первом чтении; если он есть в двух - чтение падает с понятной ошибкой: по одному только id вариант не определить.

Дети с union-целью встраивают JSON каждого ребёнка, а by ре-дискриминирует их на загрузке.

Контракт

ts
function union(...variants: Model[]): Union;

// дискриминатор входа: json типизирован как Dto<V1> | Dto<V2> | ...
// пока .by(...) не объявлен, у объединения нет add - ни в типах, ни в рантайме
union.by((json) => Variant): Union;

// union-коллекция - Query по всем вариантам, плюс:
feed.add(json)      // маршрут через by / мерж по известному id
feed.get(id)        // ищет по вариантам
feed.remove(id)
feed.match(...[Variant, (descriptors) => Predicate | boolean][])

Общие дескрипторы (FeedItem.field) существуют для полей, которые каждый вариант несёт из одного трейта.

Частые кейсы

Используйте объединения для:

  • лент и таймлайнов, смешивающих виды сущностей;
  • холстов и деревьев слоёв из разнородных элементов;
  • поиска сразу по нескольким моделям;
  • полиморфных ссылок - refs.one(FeedItem) вместо поля-тега типа.

Связанные разделы