In updateCMSItems.ts, skipUniqueValidation and skipItemRefValidation are coupled. Both read from opts.skipUniqueValidation:
// entrypoints/server/lib/logic/cms/items/updateCMSItems.ts, lines 408-413
skipUniqueValidation:
isSubsequentPublicationOperation ||
Boolean(opts.skipUniqueValidation),
skipItemRefValidation:
isSubsequentPublicationOperation ||
Boolean(opts.skipUniqueValidation), // <-- same flag controls bothThis means callers that set skipUniqueValidation: true also silently skip ItemRef validation, which is a data integrity risk for general-purpose callers that might be updating ItemRef fields without touching slugs.
Today two callers set opts.skipUniqueValidation:
changeCMSItemsStatus(hardcodestruebecause it only changes_draft/_archived/_noSearch)- The DDA
UpdateCMSItemsProvider(sets!changesSlug)
Both happen to be safe because neither touches ItemRef fields, but the coupling is fragile and forces callers to reason about internal validation mechanics.
Make updateCMSItems derive both skip flags from the per-item payload, using the collection's field schema (which it already has loaded). No caller needs to pass skip flags.
The logic:
- Skip slug uniqueness validation when the item's payload doesn't contain a
slugfield - Skip ItemRef validation when the item's payload doesn't contain any field slugs that map to
ItemReforItemRefSettype in the collection isSubsequentPublicationOperationstill skips both (already validated on first pass in writeToAllLive)
After collection is resolved (~line 213), build a set of ItemRef field slugs:
const collectionItemRefFieldSlugs = new Set(
(collection?.allItemRefFields || []).map((f: {slug: string}) => f.slug)
);In the per-item loop, before building validateItemInput (~line 388), inspect the payload:
const itemFieldSlugs = Object.keys(item.fields);
const changesSlug = itemFieldSlugs.includes('slug');
const changesTouchesItemRefs = itemFieldSlugs.some((slug) =>
collectionItemRefFieldSlugs.has(slug)
);Replace the coupled flags:
// Before:
skipUniqueValidation:
isSubsequentPublicationOperation || Boolean(opts.skipUniqueValidation),
skipItemRefValidation:
isSubsequentPublicationOperation || Boolean(opts.skipUniqueValidation),
// After:
skipUniqueValidation:
isSubsequentPublicationOperation || !changesSlug,
skipItemRefValidation:
isSubsequentPublicationOperation || !changesTouchesItemRefs,Remove cmsOpts.skipUniqueValidation = true; (line 92) and its comment (lines 90-91). The optimization now happens automatically in updateCMSItems because status updates only contain _draft/_archived/_noSearch, none of which are slug or ItemRef fields.
Remove skipUniqueValidation from:
CMSItemsOptstype (line 158)AdditionalOptstype (line 226)- The transform output (line 345)
entrypoints/server/test/logic/cms/items/updateCMSItems_test.ts:
- The existing test at line 1573 (
skipUniqueValidation optiondescribe block) passesskipUniqueValidation: trueon opts and asserts it propagates. Replace it with tests that verify the payload-inspection behavior:- Patch with
slugin payload:skipUniqueValidationshould befalse - Patch without
slugin payload:skipUniqueValidationshould betrue - Patch with ItemRef field slug in payload:
skipItemRefValidationshould befalse - Patch without ItemRef field slug in payload:
skipItemRefValidationshould betrue
- Patch with
entrypoints/server/test/logic/cms/items/changeCMSItemsStatus_test.ts:
- Remove or update the test at line 355 that asserts
skipUniqueValidation: trueis passed. SincechangeCMSItemsStatusno longer sets it, this test should instead verify thatupdateCMSItemsis called withoutskipUniqueValidationon opts (or just remove the test, since the behavior is now internal toupdateCMSItems).
entrypoints/server/feature-integration/server-monolith/internals/lib/logic/cms/providers/items/updateItems.test.ts:
- Remove assertions about
skipUniqueValidationin thetoCMSItemsOptscall expectations (lines 93, 127, 145, 162)
ValidateItemInput.skipUniqueValidationandValidateItemInput.skipItemRefValidationinvalidateItem.tsstay as-is. These are internal types thatupdateCMSItemsandinsertCMSItemscompute and pass.insertCMSItems.tsalready derivesskipUniqueValidation: isSubsequentPublicationOperationlocally. No change needed.SchemaValidatorOpts.skipUniqueValidationinschemaValidator/index.tsstays. It's consumed bytextField.tsfor the actual slug uniqueness check.
- PR #104123 introduced
skipUniqueValidationfor thechangeCMSItemsStatusN+1 fix - PR #112751 (aaron/dda-update-item) is adding the DDA
updateItemsmethod and surfaced this coupling via reviewer feedback: https://github.com/webflow/webflow/pull/112751#discussion_r3484320012 collection.allItemRefFieldsis a Mongoose virtual that returns fields withtype === 'ItemRef' || type === 'ItemRefSet'
Refactor updateCMSItems to derive skip-validation flags from payload inspection instead of relying on callers to pass them.
Context: In entrypoints/server/lib/logic/cms/items/updateCMSItems.ts, the _updateItems function currently reads opts.skipUniqueValidation to set both skipUniqueValidation and skipItemRefValidation on the validateItemInput. This couples the two flags and forces callers (changeCMSItemsStatus, the DDA UpdateCMSItemsProvider) to reason about internal validation mechanics.
The fix: updateCMSItems already has the collection loaded (which has allItemRefFields, a virtual returning fields with type ItemRef or ItemRefSet). In the per-item loop, inspect the item's payload field slugs to determine:
- skipUniqueValidation: true when the payload doesn't include 'slug'
- skipItemRefValidation: true when the payload doesn't include any field slugs matching the collection's ItemRef/ItemRefSet fields
- isSubsequentPublicationOperation still skips both (already handled separately)
Changes needed:
1. updateCMSItems.ts: Build a Set of ItemRef field slugs from collection.allItemRefFields after collection is resolved. In the per-item loop, compute changesSlug and changesTouchesItemRefs from Object.keys(item.fields), then use those instead of opts.skipUniqueValidation.
2. changeCMSItemsStatus.ts: Remove cmsOpts.skipUniqueValidation = true and its comment. The optimization now happens automatically.
3. toCMSItemsOpts.ts: Remove skipUniqueValidation from CMSItemsOpts type, AdditionalOpts type, and the transform output.
4. Update tests in updateCMSItems_test.ts (replace the skipUniqueValidation option test with payload-inspection tests), changeCMSItemsStatus_test.ts (remove the skipUniqueValidation assertion), and the DDA provider test updateItems.test.ts (remove skipUniqueValidation from toCMSItemsOpts call expectations).
Do NOT change: ValidateItemInput in validateItem.ts, insertCMSItems.ts (already derives its own flag), or SchemaValidatorOpts in schemaValidator/index.ts.
Branch off dev. Run the mocha integration tests for updateCMSItems and changeCMSItemsStatus after making changes: ./bin/babel-mocha entrypoints/server/test/logic/cms/items/updateCMSItems_test.ts --timeout 60000 and ./bin/babel-mocha entrypoints/server/test/logic/cms/items/changeCMSItemsStatus_test.ts --timeout 60000. Run the Jest unit test for the provider: npx nx anytest -- entrypoints/server/feature-integration/server-monolith/internals/lib/logic/cms/providers/items/updateItems.test.ts --skip-nx-cache