/** * This Source Code is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * * Copyright (c) Infonomic Company Limited */ /** * Relationship population service. * * `populateDocuments` walks a set of reconstructed documents, finds every * relation leaf that matches a caller-supplied populate spec, batches * fetches against each target collection (one DB round-trip per depth * level per target collection), and replaces each leaf in place with the * populated document. Missing targets become a `{ _resolved: false }` * stub; already-visited targets become a `{ _cycle: true }` stub. * * Consumed by both `@byline/client` (external read API with `populate` * and `depth` options) and the admin webapp's API-preview server fn. * * A request-scoped `ReadContext` is threaded through the walk. Its * `visited` set and `readCount` budget guard against recursive reads — * particularly the A→B→A failure mode that appears when future * `afterRead` hooks invoke their own reads from within populated * documents. The guard is in place from day one so that the hook work * in Phase 4+ cannot reintroduce the problem. * * See docs/04-collections/03-relationships.md for the full design rationale. * * --------------------------------------------------------------------- * DSL summary * --------------------------------------------------------------------- * * The populate DSL has two independent axes: * * 1. **Scope** — which relations in the *source* document to walk. * 2. **Projection** — which fields of each *target* document to load. * * The top-level `populate` value selects scope + (optionally) a uniform * projection across the whole tree: * * - `populate: true` → walk every relation leaf, * default projection at every * depth. See below for exactly * what the default projection * returns. * - `populate: '*'` → walk every relation leaf, full * document projection at every * depth. Symmetric with the * sub-spec shorthand and intended * for tools like the admin API * preview where the whole tree * should be visible. * - `populate: { name: … }` → walk only the named relations. * - `populate: undefined` → skip populate entirely (no-op). * * Default projection — exactly what comes back for `true` (and for any * sub-spec whose `select` is omitted): * * - **Document row metadata, always present** (lives on the * `document_versions` row, not in the `store_*` tables, so it is * returned regardless of the `fields` projection): * `document_version_id`, `document_id`, `collection_id`, `path`, * `status`, `created_at`, `updated_at`. * - **The `useAsTitle` field** (schema-declared identity field; * falls back to the first declared text field when `useAsTitle` * is not set on the `CollectionDefinition`). This is the one * entry added to the `fields` object. * * In effect "default projection" is "enough to identify and label the * target" — document metadata for wiring, plus one user-defined field * (typically `title`) for a human-readable label. Callers wanting more * use `'*'` (full doc) or `{ select: [...] }` (explicit fields). * * Each matched leaf carries a `PopulateFieldSpec` that selects projection: * * - `true` → default projection: the target's * identity field (`useAsTitle`, * falling back to the first text * field). Document metadata * (`document_id`, `collection_id`, * `path`, `status`, timestamps) is * always included for free — it * lives on the row, not in the * store_* tables. * - `'*'` → full document: every field of * the target is loaded. * - `{ select: [...] }` → explicit field list, merged with * the identity field so downstream * UI always has a label to render. * - `{ populate: {...} }` → nested populate for the next * depth level. Combinable with * `select`. * * Examples: * * populate: true * → every relation, default projection at every depth level. * * populate: '*' * → every relation, full projection at every depth level * (use for API previews / debug views that want the whole tree). * * populate: { heroImage: true } * → only heroImage, default projection. If heroImage's own * relations exist, they populate at the next depth with `true`. * * populate: { heroImage: '*' } * → only heroImage, full document. If heroImage's own relations * exist, they populate at the next depth with `'*'` (consistent * with how `true` propagates). * * populate: { author: { select: ['name'] } } * → only author; fetch `name` + identity field. * * populate: { author: { select: ['name'], populate: { employer: '*' } } } * → author with `name` at depth 1; employer fully populated at depth 2. * * Notes: * * - `'*'` belongs on the sub-spec (or as the whole top-level spec), * not inside `select`. `select` is always an explicit field list. * - Projection defaults are transitive at every depth: `true` propagates * `true` into nested levels; `'*'` propagates `'*'`. Explicit * `{ populate: {...} }` maps take precedence when declared. * - Multiple leaves pointing at the same target document are batched * into a single fetch; their projection specs are merged (any `'*'` * wins; otherwise selects union together, identity field is always * added). */ import { type RequestContext } from '@byline/auth'; import type { CollectionDefinition, FieldSet, IDbAdapter, PopulateFieldSpec, PopulateSpec, ReadContext, ReadMode, RelatedDocumentValue, RelationField } from '../@types/index.js'; export type { ReadContext } from '../@types/index.js'; /** Build a fresh ReadContext. */ export declare function createReadContext(overrides?: Partial): ReadContext; export type { PopulateFieldOptions, PopulateFieldSpec, PopulateMap, PopulateSpec, } from '../@types/populate-types.js'; export type { CycleRelationValue, PopulatedRelationValue, RelationFieldReadValue, RelationReadValue, UnpopulatedRelationValue, UnresolvedRelationValue, } from '../@types/relation-types.js'; export interface PopulateOptions { db: IDbAdapter; /** Every collection definition in the app — needed to resolve target fields. */ collections: readonly CollectionDefinition[]; /** The source collection id for `documents`. */ collectionId: string; /** * Documents to populate, as returned from a read operation. Must carry * `document_id` and `fields`. Mutated in place — every relation leaf * in `fields` that is walked becomes an envelope: the original * `{ targetDocumentId, targetCollectionId, relationshipType?, * cascadeDelete? }` refs are preserved, and discriminator fields * (`_resolved`, `_cycle`) plus an optional `document` property are * layered on top. See the `PopulatedRelationValue` / * `UnresolvedRelationValue` / `CycleRelationValue` interfaces below. */ documents: Array>; /** What to populate. Omit to no-op. */ populate?: PopulateSpec; /** * Max walk depth. Defaults to 1 when `populate` is present, 0 otherwise. * Clamped to `readContext.maxDepth`. */ depth?: number; /** Locale forwarded to the batch fetch. */ locale?: string; /** * Read mode forwarded to `getDocumentsByDocumentIds`. Selects whether * populated targets are resolved from `current_documents` (default, * `'any'`) or `current_published_documents` (`'published'`). Public * consumers of `@byline/client` typically want `'published'` so a * populated target that currently has a newer draft still resolves to * its last published version rather than leaking a draft. */ readMode?: ReadMode; /** * Request-scoped recursion guard. Omit to create a fresh context for * this top-level call. Threaded through by future read-side hooks to * prevent A→B→A infinite loops. */ readContext?: ReadContext; /** * Request-scoped auth context. Required when any target collection in the * walk has a `beforeRead` hook configured. Each target's hook is invoked * (and cached in private authority-bound state) before its batch fetch, * and the resulting predicate is ANDed onto the fetch's WHERE. When * omitted — most synthetic / test call paths — `beforeRead` hooks are * skipped entirely; the production read paths all forward this through * from `CollectionHandle`. Low-level synthetic calls still receive an * anonymous operation context in `afterRead`, so the hook contract never * lacks identity state. */ requestContext?: RequestContext; /** Private cache domain explicitly shared by every read path in one client instance. */ securityDomain?: object; /** * Skip `beforeRead` hook resolution on every target collection. The * top-level read's `_bypassBeforeRead` flag rides through to populate * here so admin tooling sees the same unscoped tree on populated * relations as it does on the source document. */ bypassBeforeRead?: true; /** * Registered richtext server-side populate function (typically resolved * once at the top level from `ServerConfig.fields.richText.populate`). * Threaded through populate so each materialised target also gets its * rich-text leaves populated before its `afterRead` hook fires — * ensuring user-land hooks observe fully populated content regardless * of whether the target arrived via a relation field on the source or * via a richtext document link / inline image. Omit when no richtext * adapter is registered. */ richTextPopulate?: import('../@types/index.js').RichTextPopulateFn; } /** * Populate relation leaves in `opts.documents` in place, one DB * round-trip per depth level per target collection. */ export declare function populateDocuments(opts: PopulateOptions): Promise; declare function visitedKey(collectionId: string, documentId: string): string; /** * A single relation leaf pending populate. `parent[key]` currently holds a * `RelatedDocumentValue`; after processing it holds either a populated * document or a stub (cycle / unresolved). */ interface RelationLeafRef { parent: Record; key: string; field: RelationField; value: RelatedDocumentValue; /** * Per-leaf populate sub-spec resolved from the PopulateMap. * * - `true` → default projection (identity field only). * - `'*'` → full document (all fields). * - object → explicit `select` and/or nested `populate`. */ sub: PopulateFieldSpec; } /** * Collect every relation leaf whose name matches `populate`. Structure * field names do not scope the match — if `populate: { author: true }` * is given, every `author` relation found anywhere in the tree matches. * * Tree traversal is delegated to the shared `walkFieldTree` walker; this * function applies the relation-specific filters: populate-spec match, * envelope shape, and "skip already-populated" (via the `_resolved` * discriminator left behind by a previous populate pass). */ declare function collectRelationLeaves(fields: Record, fieldDefs: FieldSet, populate: PopulateSpec, acc: RelationLeafRef[]): void; declare function matchesPopulate(fieldName: string, populate: PopulateSpec): PopulateFieldSpec | undefined; /** * Build the `fields` array for a batch fetch against a single target * collection. * * - Any leaf with `sub === '*'` → `undefined` (fetch all fields). * - Otherwise → union of explicit `select` lists from each leaf, * merged with the target's identity field (`useAsTitle`, falling * back to the first text field). `sub === true` contributes no * selects, so a batch of only-`true` leaves collapses to the * identity field alone — the default projection. * * Document metadata (`document_id`, `collection_id`, `path`, `status`, * timestamps) lives on the row itself and is always returned — it does * not need to appear in the `fields` list. */ declare function buildBatchSelect(leaves: RelationLeafRef[], targetDef: CollectionDefinition | undefined): string[] | undefined; /** * The field that represents a target document's identity for populate's * default projection. Prefers `useAsTitle` (server-safe schema-level * config), falling back to the first declared text field. */ export declare function resolveIdentityField(def: CollectionDefinition): string | undefined; /** * Merge the per-leaf sub-populate specs for all leaves pointing at a * single (now-populated) target document into a single PopulateSpec for * the next level's walk of that document. Returns `undefined` if the * leaves don't request any nested population (in which case the populated * document's own relations stay as raw refs). * * Sub-spec semantics at the next level: * - `'*'` → propagate `'*'` (scope=all, full projection, recursive). * `'*'` wins over `true` when both appear in the same batch * so the caller's "full document" intent is preserved. * - `true` → recurse into every relation of the target (scope=all, * default projection). * - object → forward any nested `populate` map; ignore `select`. */ declare function reduceChildPopulate(leaves: RelationLeafRef[], targetDocumentId: string): PopulateSpec | undefined; export declare const __internal: { collectRelationLeaves: typeof collectRelationLeaves; matchesPopulate: typeof matchesPopulate; buildBatchSelect: typeof buildBatchSelect; reduceChildPopulate: typeof reduceChildPopulate; visitedKey: typeof visitedKey; };