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
- func BlockKind(slug string) string
- func CloneDoc(doc map[string]any) map[string]any
- func EmptyRows(doc map[string]any) []string
- func Lookup(doc map[string]any, path string) (any, bool)
- func SetAt(doc map[string]any, path string, v any)
- func SlugOf(kind string) string
- func WithoutEmptyRows(doc map[string]any) map[string]any
- type Accepts
- type Analysis
- type Ancestor
- type Carrier
- type Container
- type Finding
- type Risk
- type Row
Constants ¶
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.
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.
const RootKind = "root"
RootKind is the parent kind of a list that is not inside any row.
Variables ¶
This section is empty.
Functions ¶
func EmptyRows ¶
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 WithoutEmptyRows ¶
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 ¶
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 ¶
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 ¶
ClearedTypes is every block type the remedy's carriers clear, sorted.
func (*Analysis) NeedsReset ¶
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 ¶
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.
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 ¶
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).