Repository navigation
1.13.0 — SilverStripe 5.2 line
Fixed
-
(#201) A composition's
page.match.idresolving to the wrong page used to silently
reparent every element in the payload whoseexternalIdalready existed anywhere on the
site —ExternalIdResolver::tryFind()is a global lookup by design, and composition force-
writes the resolved area's id onto whatever record it matches, with no check that the record
already belonged to a different page.RecordWriter::write()now rejects (new
CROSS_PAGE_REPARENTerror code, 409) when a server-derivedParentIDassignment (composition
only — an explicit clientfields.ParentIDwrite, e.g. viacontent_batch, is unaffected and
still governed solely by the existing element-type-allowed-on-page check) would move an
already-parented element onto a different page's area. Confirmed live (Rockline Industrial):
recovery needed raw SQL, since the API forbidsdeleteby design. The owner comparison checks
both id and base class —getOwnerPage()resolves against anyElementalAreasExtension
owner, not justSiteTree, so two owners from different base tables can share a numeric id.
Documented indocs/en/12_error-codes.md,docs/en/08_page-compositions.md, and the MCP tool
schema (schema/endpoints.jsonv1.17). -
(#202) A
content_batchhas_manyadd,remove, orset(theremoveAll()half) that
touched an already-published, clean related record left it desynced from LIVE with nothing to
notice or fix it —HasManyListunconditionally repoints/clears the related record's foreign
key via an ordinary draft write regardless of its Versioned state, leaving an attach stranded
modifiedOnDraftor a detach's LIVE row still pointing at the old parent.RecordWriter::write()
now republishes any already-published, NOT-already-modifiedOnDraftrecord a has_many relation
write touches this way, when the operation's ownpublish/defaultPublishisn'tnone—
authorization-checked against the related record's own classactionverb first, the same way a
subtree/ownspublish cascade checks every non-root record it touches. A record that was never
published, or that already had unrelated in-progress draft edits before this write touched it, is
left exactly as it was — this never forces an editor's own draft to LIVE. A many_manyaddwas
already unaffected for the related item (it only writes it when it isn't already in the
database). -
(#203)
dryRun: true(and a genuine atomic-failure rollback) already correctly roll back
an ordinary DB-onlyonBeforeWrite()/ValueTransformerside effect — verified with new
regression coverage rather than assumed. What no DB transaction can ever undo is a side effect
OUTSIDE the database (an HTTP call, a queued job, an external cache write) — confirmed live,
ElementOembed's oEmbed lookup left orphanEmbedObjectrows behind both a rolled-back
composition and adryRunprobe. AddedDynamic\ContentApi\Write\DryRunContext::isActive(),
a static flag any project write hook orValueTransformercan check to skip a non-DB side
effect during a dry run — this module can't intercept a project class's side effect on its
behalf, only make the dry-run state visible to it. -
(#204) Investigated whether non-image (PDF) asset uploads have a working route — they do;
AssetServicehas never hardcodedImage, andcontent_asset_uploadcorrectly resolves a
.pdf(or any other extension) to plainFilevia the framework's own
File::get_class_for_file_extension(). The confirmed field incident's second half was real
though:AssetService::ingest()letDBFile::setFromString()'sValidationException(a
disallowed extension,File.allowed_extensions) escape unmapped, surfacing as500 SERVER_ERRORwith a raw exception message instead of the422 VALIDATION_FAILEDevery other
write path in this module gives for the same failure class. Now mapped the same way. It never
silently reported success on a failed write — the controller's own top-levelThrowablecatch
already turns an uncaught exception into a proper (if less specific) error envelope. -
(#191, #195, #192) Four write-path gaps that used to accept a request and silently do
something other than what was asked, found via a field audit of real usage on two production
consumer projects (~1,500 recorded MCP calls mined from session transcripts). All four now
reject rather than silently drop or coerce:WriteApplicator::applyFields()(and, on the colymba/apiwrite surface,
WriteGuardExtension::onBeforeWrite()) now validates a DBEnum-backed field's value against
its own declared list before writing —DBEnum::setValue()never validates, and MySQL
itself silently coerces an out-of-list value to''rather than rejecting it. New
INVALID_VALUEerror code.DBMultiEnum(aset-backed multi-select Enum subclass) is
handled correctly — each comma-separated value is checked independently.- (#191) A has_one relation named under a
relationspayload (has_many/many_many only) is
now rejected — with an actionable message — before the record is written, not after: a
relations: {"Parent": 74}on a create used to silently leaveParentIDuntouched, then
fail on unrelated-looking root-levelSiteTreevalidation instead of ever surfacing that
Parentwas the actual problem. - (#195)
LinkTransformer::transform()now rejects an unrecognized key in a structured
link payload, and a key that's only valid for a different link type (e.g.fileIdwith
"type": "ExternalLink") — both used to be silently dropped (DataObject::setCastedField()
falls back to a bare, never-persisted dynamic property when the resolved link class has no
matching column), leaving an empty or partially-populated link record with no signal
anything was wrong. - (#192)
CompositionService::compose()now also acceptsareaRelationat the request's
top level, as a defensive fallback for a caller reaching the endpoint directly over HTTP
(page.areaRelationremains the one documented shape); a mismatch between the two locations
is reported as a warning rather than resolved silently.
Compatibility note: three of these are newly-rejecting a write that previously returned
200 — a consumer currently sending a payload that happened to hit one of these gaps (an
out-of-list enum value, a has_one underrelations, an unrecognized/wrong-type link key) will
now get a 422 instead of a silent no-op. -
(#198)
GenerateContentApiExposureTask/ExposureScaffoldergeneratedapi_access: 'GET,POST,PUT'for every class — no HTTP method maps to theaction(publish/unpublish/
archive) verb, so a project that pasted the generator's own output in as-is had a class that
looked fully CRUD-configured but could never actually publish a write through this API. Now
generates'GET,POST,PUT,action'. -
The generator's output now also documents (as a comment) the separate
Colymba\RESTfulAPI\ QueryHandlers\DefaultQueryHandler.modelsentry — the base both this module's own
/content-api/v1endpoints and colymba's generic/apisurface merge from, per
ClassRegistry's own docblock, withClassRegistry.modelsas the narrower, content-api-only
overlay. A class registered only under the overlay resolves forcontent_schema_classwhile
every actual/api/$Modelrequest 404s or is silently invisible.