orphans

package
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 28, 2026 License: MIT Imports: 5 Imported by: 0

Documentation

Overview

Package orphans models how Payload's Postgres adapter clears NESTED rows on an update, so PayCLI can predict orphan rows before a write, detect them after one, and remove them without deleting the document.

The adapter's write path (@payloadcms/drizzle 3.87.1, verified)

Every block TYPE has ONE table per collection (`pages_blocks_bento_item`), whatever depth its rows sit at. A block row's `_parentID` is the DOCUMENT id, not the id of the row it is nested in, and `_path` records where it sits (`layout.0.blocks.0.blocks`). An array row's `_parentID` is the id of the row that holds it, with ON DELETE CASCADE.

On update, upsertRow (dist/upsertRow/index.js ~l.516) deletes, for every table in `blocksToDelete`, ALL rows `WHERE _parentID = <document id>` and then inserts the new rows. transform/write/traverseFields.js ~l.130 adds a blocks field's tables to that set when the TRAVERSAL of the new data reaches the field — before looking at the field's value. The traversal always reaches every top-level field (Payload hands the adapter the whole merged document), but it reaches a blocks field inside a block (or an array row) only by walking a row of that parent type. So:

  • top-level blocks tables are always cleared (top-level swaps are clean);
  • a nested block type's table is cleared only when the new document still contains a row of a type whose nested blocks field accepts it;
  • nested arrays are removed by the cascade from their parent row, never on their own.

A restructure such as group → bento → bentoItem ⇒ group → grid → text leaves every bentoItem row in place. On read (transform/read, createBlocksMap) rows are grouped by `_path` across ALL block tables, so the stale rows reappear inside whatever row now sits at their old path: as `{}` when that field does not accept their type, as real rows when it does, or invisibly when no field has that path.

Nothing here performs I/O; values are the generic shapes encoding/json produces and inputs are never mutated.

Index

Constants

View Source
const (
	// FindEmptyRow is a `{}` row where the body sent a real row (or none).
	FindEmptyRow = "empty_row"
	// FindExtraRow is a row the body did not send.
	FindExtraRow = "extra_row"
	// FindNoBlockType is a row that lost the blockType the body gave it.
	FindNoBlockType = "no_block_type"
	// FindTypeSequence is a list whose blockType sequence differs from the
	// body's.
	FindTypeSequence = "type_sequence"
)

Finding kinds.

View Source
const MaxPhases = 6

MaxPhases caps the carrier phases a remedy may plan. Each phase peels one level of nesting; real documents need one or two.

View Source
const RootKind = "root"

RootKind is the parent kind of a list that is not inside any row.

Variables

This section is empty.

Functions

func BlockKind

func BlockKind(slug string) string

BlockKind is the row kind of a block row of type slug.

func CloneDoc

func CloneDoc(doc map[string]any) map[string]any

CloneDoc deep-copies a document.

func EmptyRows

func EmptyRows(doc map[string]any) []string

EmptyRows lists the paths of every `{}` row in doc. Payload never returns an empty object for a real row (every row carries at least its id), so each one is a row the adapter read from a table the field at that path does not accept — an orphan.

func Lookup

func Lookup(doc map[string]any, path string) (any, bool)

Lookup resolves a dotted path of object keys ("layout", "hero.links").

func SetAt

func SetAt(doc map[string]any, path string, v any)

SetAt sets a dotted path of object keys, creating objects on the way.

func SlugOf

func SlugOf(kind string) string

SlugOf is the block type of a block kind ("" for any other kind).

func WithoutEmptyRows

func WithoutEmptyRows(doc map[string]any) map[string]any

WithoutEmptyRows is a copy of doc with every `{}` row removed from its list — the document as the body described it, for the §10.2 leaf diff, which would otherwise pair the body's rows with the orphans by index and report them as dropped input. doc itself is returned when it has none.

Types

type Accepts

type Accepts map[Container][]string

Accepts maps a list definition to block types known to be accepted in it. Use Container{Parent: RootKind, Field: "layout"} for a top-level field and Container{Parent: BlockKind("group"), Field: "blocks"} for a nested one.

type Analysis

type Analysis struct {
	// Risks are the block types the write leaves behind, sorted.
	Risks []Risk
	// Empty are `{}` rows already present in the stored document.
	Empty []string
	// Unresolved are `{}` rows whose block type no source could name, so no
	// carrier can clear them.
	Unresolved []string
	// Phases are the carrier sets of the remedy, in order. Empty when the
	// plain write leaves nothing behind.
	Phases [][]Carrier
	// Incomplete is set when MaxPhases was reached with carriers still
	// needing a table cleared.
	Incomplete bool
}

Analysis is Analyze's answer.

func Analyze

func Analyze(stored, next map[string]any, history []map[string]any, accepts Accepts) *Analysis

Analyze predicts which rows of `stored` — the row the write overwrites, as it is stored now — survive writing `next` — the whole document the write will store (Payload's merge of the body into the newest version) — and plans the carrier phases that remove them. history holds older states of the same document (versions); they only serve to name the type of a `{}` row and to find rows to copy as carriers.

A block type's table is cleared by the write when some list it was seen in is reached by the traversal of `next`: a top-level list always is, a nested list only when `next` still has a row of the list's parent kind. Which types a list accepts is learnt from every row seen in it (stored, next, history), so an accepted-but-never-seen type is treated as not cleared — the safe direction: it costs a carrier, never an orphan.

accepts adds what is known about list definitions from elsewhere (PayCLI's block census: which block types were observed in each nested blocks field of each block type, across every document of the collection). It only ever widens the accepted sets, so it can only remove false risks.

func (*Analysis) ClearedTypes

func (a *Analysis) ClearedTypes() []string

ClearedTypes is every block type the remedy's carriers clear, sorted.

func (*Analysis) NeedsReset

func (a *Analysis) NeedsReset() bool

NeedsReset reports whether a plain write would leave rows behind (or already has).

type Ancestor

type Ancestor struct {
	// Path is the ancestor row's own path, List the list it sits in.
	Path, List string
	// Kind is the ancestor's row kind.
	Kind string
	// Field is the list inside the ancestor that leads to the next row.
	Field string
	// Row is the ancestor row itself (not copied; never mutated here).
	Row map[string]any
}

Ancestor is one row on the way down to a nested row.

type Carrier

type Carrier struct {
	// List is the top-level list the carrier is appended to ("layout").
	List string `json:"list"`
	// Chain is the carrier's block types from the top-level row down.
	Chain []string `json:"chain"`
	// Clears is the block type the carrier exists to make the adapter clear.
	Clears string `json:"clears"`
	// Row is the carrier row itself: a copy of real rows with every id
	// removed and every nested blocks list emptied except the one link that
	// leads down the chain.
	Row map[string]any `json:"-"`
}

Carrier is one row appended to a top-level list for one remedy phase.

type Container

type Container struct {
	Parent string
	Field  string
}

Container identifies a list by what holds it: the kind of the enclosing row (RootKind outside every row) and the list's path inside that row, through named groups ("blocks", "section.items", or "layout" / "hero.links" at the root). Two lists with the same Container are the same field definition.

func (Container) String

func (c Container) String() string

String renders "parent>field".

type Finding

type Finding struct {
	Kind string `json:"kind"`
	Path string `json:"path"`
	// BlockType is the extra row's type, when it has one.
	BlockType string `json:"block_type,omitempty"`
	// Sent/Stored are the blockType sequences of a type_sequence finding.
	Sent   []string `json:"sent,omitempty"`
	Stored []string `json:"stored,omitempty"`
}

Finding is one orphan symptom.

func EchoCheck

func EchoCheck(sent, stored map[string]any) []Finding

EchoCheck compares every row list in the request body with the same list in a stored document (the echo, or a read-back) and reports orphan symptoms: `{}` rows, rows the body did not send, rows that lost their blockType, and blockType sequences that differ. A list the server returned SHORTER than sent is not reported here — that is a dropped input (§10.2), not an orphan — and neither is anything below it.

type Risk

type Risk struct {
	BlockType string `json:"block_type"`
	// Paths are the rows at risk (in the stored document).
	Paths []string `json:"paths"`
	// Causes name the nested rows whose change makes the adapter skip the
	// table ("layout[0].blocks[0]: bento → grid").
	Causes []string `json:"causes,omitempty"`
}

Risk is one block type whose rows in the stored document the write will leave behind.

type Row

type Row struct {
	// Path is the row's path (`layout[0].blocks[1]`), List its list's path.
	Path, List string
	// Kind is BlockKind(blockType) for a block row, "array:<container>" for an
	// array row, and "" for an empty object.
	Kind string
	// BlockType is the row's blockType ("" for array rows and {} rows).
	BlockType string
	// Container is the field definition the row belongs to.
	Container Container
	// Depth is the number of rows enclosing this one (0 = a top-level list).
	Depth int
	// Empty marks a row that is `{}` — how Payload reads back an orphan row
	// whose type the field at its old path does not accept.
	Empty bool
	// Anc are the enclosing rows, outermost first.
	Anc []Ancestor
	// Value is the row itself (not copied).
	Value map[string]any
}

Row is one element of a row list (a blocks or array field value).

func Rows

func Rows(doc map[string]any) []Row

Rows lists every row of every row list in doc, depth first, keys sorted. Rich-text editor states and relationship references are not rows.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL