PROD-2446: make companion-field reference scanning schema-driven - #208
Merged
Conversation
PROD-2431/2435/2442 fixed ContentFieldMapper's remap of a linked-content field's companion column (LinkeContentDropdownValueField/SortIDFieldName), but two sibling utilities that scan the same content-item field data for different purposes were never updated to match — both still assumed a reference is only ever shaped as a contentid/contentID/sortids object key, with no knowledge of an arbitrarily-named companion field. 1. has-unresolved-content-references.ts (the PROD-2309 pre-push guard): collectUnresolvedContentReferences() now takes the source model and also checks each Content field's companion column. Previously an unresolved reference living only in a companion field shipped to the target undetected — the field mapper leaves it as the SOURCE id and only warns (not errors), which content-batch-processor.ts doesn't log or block on — reproducing the exact opaque server NullReferenceException PROD-2309 was built to prevent. 2. collect-content-id-references.ts (push-order dependency detection): collectContentIDReferences() now takes the source model too, so an item referenced only through a companion field is recognized as a dependency and promoted to push first (get-content-item-types.ts resolves each item's own source model via modelMapper to supply it). Extracted the companion-field-pair lookup (previously private inside ContentFieldMapper) into a shared src/lib/content/linked-content-companion-fields.ts (getLinkedContentCompanionFields/findFieldKey), used by all three now instead of duplicating the logic a third time. Added regression tests for both utilities (arbitrarily-named companion, SortIDFieldName companion, case-insensitive match, sentinel/no-model back-compat) plus a get-content-item-types push-order test proving an item referenced only via a companion field gets promoted to linked. Full suite: 105 suites / 1924 tests passing. npm run build compiles clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
DerekAgility
approved these changes
Aug 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes PROD-2446 — a follow-up audit after PROD-2431/2435/2442 found that the schema-driven companion-field fix landed in
ContentFieldMapperonly, while two sibling utilities that scan the exact same content-item field data for other purposes were never updated to match. Both still assumed a linked-content reference is only ever shaped as acontentid/contentID/sortidsobject key, with no knowledge of aContent-typed field'sLinkeContentDropdownValueField/SortIDFieldNamecompanion setting.1 — PROD-2309 guard is blind to companion fields
has-unresolved-content-references.ts→collectUnresolvedContentReferences()only recognizedcontentid/contentID/sortidskeys. Called fromcontent-batch-processor.ts(~line 416) before the field mapper runs, as the guard whose whole purpose is to throw before shipping a payload guaranteed to fail server-side with aNullReferenceException.sourceModelwas already in scope at that call site but was never passed to the guard.Worse:
ContentFieldMapper.mapContentFields()(called right after) does find the companion field via the schema-driven pass, but when the reference can't be resolved it only increments awarning, not anerror— andcontent-batch-processor.tsonly logsvalidationErrors, never warnings. So an item with an unresolved companion-field reference sailed past the guard entirely and shipped to target with a dangling source ID — reproducing the exact opaque NullReferenceException PROD-2309 was built to prevent, via the companion-field path instead of the plaincontentid/sortidspath.2 — Push-order dependency detection is blind to companion fields
collect-content-id-references.tsfeedsget-content-item-types.ts's push-order logic (an item another item depends on is promoted to push first, so its source→target mapping exists in time). Same structural-only sniffing —collectContentIDReferences()didn't even take amodelparameter, so it couldn't consult companion settings without a signature change. An item that references another item only through a companion field was never promoted to push-first, which can produce exactly the unresolved-reference scenario in #1.Fix
ContentFieldMapper) into a sharedsrc/lib/content/linked-content-companion-fields.ts(getLinkedContentCompanionFields/findFieldKey), so there's one source of truth instead of three near-duplicates.collectUnresolvedContentReferences()now accepts the sourcemodeland also checks each Content field's companion column (only at the top level — companions are always siblings of the main field, never nested).collectContentIDReferences()now accepts the sourcemodeltoo;get-content-item-types.tsresolves each item's own source model viamodelMapper(mirroring the existinghasValidMappingspattern) to supply it.content-batch-processor.tsnow passessourceModelinto the PROD-2309 guard call.Testing
collect-content-id-references.test.tsandhas-unresolved-content-references.test.ts: arbitrarily-named companion,SortIDFieldNamecompanion, sentinel/no-model back-compat, and (for the unresolved-reference guard) case-insensitive match and top-level-only scanning.get-content-item-types.test.tsproving an item referenced only through a companion field is promoted tolinkedContentItems(push-first).npm test— 105 suites / 1924 tests passing, no regressions.npm run build— compiles clean.🤖 Generated with Claude Code