Repository navigation
v6.4.0
Highlights
-
@byline/core,@byline/admin,@byline/host-tanstack-start— Declared field requirements are now enforced on writes. The form validator only ever walked top-level fields, and the server write path normalised and persisted without comprehensive schema validation — so a group missing a required child produced no error in the editor, and an SDK create could persist a document with no title at all. Core lifecycle services now validate the prepared document immediately before writing a content version, recursively through groups, array items and declared block variants, and after hooks, normalization, counter assignment and rich-text preparation, so the check describes exactly what is about to be stored. The admin runs the same walk before submission, exempting condition-hidden fields and pending uploads. Errors travel asERR_VALIDATIONwithdetails.reason: 'invalid_document_fields'and anissuesarray of{ field, message, kind }, decoded safely bygetDocumentFieldValidationDetailsfrom@byline/core; array and block paths carry stable item identities (content[id=abc].title), nested errors contribute to tab badges, and a server rejection preserves the editor's unsaved edits rather than discarding them.kindisrequiredfor a declared value that is absent andinvalidfor everything else, so a consumer can separate what is missing from what is wrong. See Migrations — this applies to existing documents. -
@byline/core— Document creation can no longer bypass publication permissions. Creation forwarded the caller's requested status straight through, checking only thecreateability: an actor holding justcreateandreadcould create a document that was immediately readable as published, and an undeclared status was accepted outright. Creation now rejects a status the collection's workflow does not declare, and a non-default initial status requireschangeStatus, pluspublishwhen selecting published. The checks cover both the explicit option and the legacydata.statusfallback in the field envelope, and run before hooks and again after preparation, since a hook can rewrite the value in between. Creating at the workflow's configured default remains authorized bycreatealone, so published-only workflows such asSINGLE_STATUS_WORKFLOWkeep their intended automatic publication. -
@byline/core— Restoring a historical version is deliberately exempt from that validation gate. A historical version cannot be opened and corrected before it is restored, so enforcing today's rules on it would make the content permanently unrecoverable. Two independent things put a version out of step: the collection's schema may have tightened, and the validation rules may have changed while the schema stood still — the correctedemailandurlrules in this release are exactly that case. The exemption is tied toaction: 'restore'rather than a caller-supplied flag, so a write cannot obtain it without also declaring itself a restore in the audit trail. Authorization, revision and version-ownership checks, hooks and storage constraints all still apply, and the next ordinary save is validated in full. Validation still runs for reporting: the issues come back asvalidationIssuesonRestoreVersionResultandRestoreSingletonVersionResult, and the admin names the offending fields in a warning toast so the next save's refusal is understandable. Two consequences are accepted recovery semantics rather than guarantees: abeforeUpdatehook on a restore runs inside the exemption, and a workflow whosedefaultStatusispublishedwill republish restored content that fails current validation. Duplication and locale copies keep the gate — their source is the current version, which the editor can correct — and now name their invalid fields instead of showing a generic failure. -
@byline/admin— Built-in widgets honourreadOnlyconsistently. The flag was previously per-widget and only partly implemented; the standard text widget, among others, rendered an editable input regardless. Scalar controls, file and image mutation controls, relations, arrays, blocks and their descendants now all respect it, including structural actions — adding, removing, reordering and drag handles — while preview, download and collapse stay usable. Custom field components and rich-text editor replacements remain responsible for implementing the hint themselves. This is still a presentation hint, never server-side immutability: enforce that in abilities, hooks or lifecycle services.
Bug Fixes
-
@byline/admin— An older asynchronous field hook could overwrite newer input. Every change started an independent hook pipeline that committed its captured value on completion regardless of ordering, so typingoldthennewand having the newer hook resolve first left the store holdingold. Each invocation now carries a generation token and superseded results — values and errors — are discarded. Submission waits for outstanding field changes before validating. -
@byline/admin— Submit-time hook errors are no longer swallowed.runFieldHooks()published its errors and the immediately followingvalidateForm()replaced the whole error array, so a hook that blocked submission left the editor with no explanation at all. Both sources are now combined into one published validation result, and the errors reach the field and its tab badge. -
@byline/admin— A partially failed upload batch no longer re-uploads its successes. When any upload failed, the orchestrator returned before adopting successful results or clearing their pending entries, so retrying re-transported files that had already stored — repeating storage work and upload hooks, and leaving orphaned objects behind. Successes are now adopted individually and only failed entries are retained for retry; a selection replaced mid-flight is not overwritten. -
@byline/admin— Form subscriptions no longer miss writes between render and subscription.useFieldValue()read its initial value during render and subscribed in a passive effect without rechecking, so a component writing in a layout effect left the field rendering a stale value. Form state moved touseSyncExternalStore, and nested writes now clone their containing objects and arrays so previous snapshots and unrelated references stay stable. -
@byline/admin— A parked system-field confirmation cannot be replaced by re-entry. Submission released its synchronous in-flight guard when it parked a payload awaiting confirmation, so a second call could rerun validation and swap the held payload while the modal was still open. One captured payload is now held across the confirm/cancel transition, and a cancelled callback cannot submit it. -
@byline/core,@byline/admin— A schema-author callback that throws is reported rather than escaping.validate,condition, and acustomentry invalidation.rulesall run author-written code against data it may not anticipate — a validator callingvalue.trim()meets a historical version lacking the field. An exception aborted the validation walk, discarding issues already collected, and surfaced as an opaque error instead of a field result. All three sites are isolated (including Zod schema construction, where a malformedpatternthrows fromnew RegExpbefore any value is parsed), and a failure is recorded as aninvalidissue. Exception text never reaches the browser — the editor sees a fixedcould not be validatedmessage while the real error goes to the server log, because an unexpected exception can carry internal diagnostics. A message a validator deliberately returns is authored for the editor and is kept verbatim. A throwingconditionnow leaves its field visible in the render path, at submit time and in the walker alike, rather than taking the render down. -
@byline/admin— Editing a field no longer re-renders the whole form layout.useFormTabs()subscribed to the entire form even when no tab declared a visibility condition, so every keystroke notified the layout walk and its un-memoised field renderers. Tab visibility now observes predicate results and tab errors observe the error store. No latency benchmark is claimed for this.
Chores
- monorepo — The Turborepo
testtask now declaresdependsOn: ["^build"], matching itstypecheckandtest:integrationsiblings. Without it a package's test cache key excluded its workspace dependencies, so a change in@byline/corewith no change in@byline/adminreplayed a stale pass — a group-validation assertion had in fact been failing while the suite reported green. Any cross-package change could have been masked the same way. Localpnpm testruns now rebuild dependencies first. @byline/admin— Reload admission and focus recovery moved into auseDocumentReloadRecoveryhook, and the status-button and dropdown transitions share one callback.FormRendererProps.onSubmitis now typed with the submission payload rather thanany.@byline/host-tanstack-start— Create and patch transport inputs receive runtime shape checks, separate from authoritative document validation. The field-validation decoder follows the existing allowlist pattern and is covered over the installed server-function serializer.- monorepo — The review that produced this cycle, its rejected intermediate positions, and the deferred schema-aware restoration design are recorded under
specs/.
Migrations
Four changes can alter how an existing schema or existing data behaves. None of them require a code change before the release will run, but each can cause a write that previously succeeded to be refused.
-
emailandurlvalidation rules are now enforced. They previously called Zod's.describe()rather than.email()/.url(), so they validated nothing at all. A collection declaringvalidation: { rules: [{ type: 'email' }] }over data that was never checked will begin refusing saves. Audit affected collections and correct the data, or drop the rule. -
Declared field requirements apply to every content save, including drafts. A document written before a field became required stays readable, but must supply that field on its next content save. Restoring a historical version is the exception described above. If incomplete drafts are intentional for a collection, mark those fields
optional— and where a field is required only in particular circumstances, express that withoptional: trueplus avalidatecallback rather thancondition, which the lifecycle does not evaluate. Seedocs/10-api-reference/03-fields.md. -
A schema-author callback that throws now blocks the save. A
validateorconditionfunction, or acustomrule, that throws on data it did not anticipate yields acould not be validatedfield error where it previously escaped as an opaque failure. Check the server log for the underlying exception; it is no longer included in the editor-facing message. -
SDK callers passing an explicit
statustocreate()now need the matching abilities —changeStatus, pluspublishfor published. Ordinary admin creation is unaffected: it uses the collection's configured default status. Seeds, importers and migration scripts that create published content should run with an actor holding those abilities.
One correction worth noting for anyone enforcing publication preconditions: the fields reference previously suggested beforeUpdate, which does not run on a status transition. Use beforeStatusChange, which both changeStatus and scheduled publication invoke.
All other @byline/* packages bumped to 6.4.0 in lockstep with no behavioural changes this cycle.