Skip to content

v6.4.0

Choose a tag to compare

@58bits 58bits released this 15 Sep 22:21
· 97 commits to develop since this release

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 as ERR_VALIDATION with details.reason: 'invalid_document_fields' and an issues array of { field, message, kind }, decoded safely by getDocumentFieldValidationDetails from @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. kind is required for a declared value that is absent and invalid for 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 the create ability: an actor holding just create and read could 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 requires changeStatus, plus publish when selecting published. The checks cover both the explicit option and the legacy data.status fallback 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 by create alone, so published-only workflows such as SINGLE_STATUS_WORKFLOW keep 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 corrected email and url rules in this release are exactly that case. The exemption is tied to action: '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 as validationIssues on RestoreVersionResult and RestoreSingletonVersionResult, 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: a beforeUpdate hook on a restore runs inside the exemption, and a workflow whose defaultStatus is published will 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 honour readOnly consistently. 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 typing old then new and having the newer hook resolve first left the store holding old. 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 following validateForm() 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 to useSyncExternalStore, 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 a custom entry in validation.rules all run author-written code against data it may not anticipate — a validator calling value.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 malformed pattern throws from new RegExp before any value is parsed), and a failure is recorded as an invalid issue. Exception text never reaches the browser — the editor sees a fixed could not be validated message 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 throwing condition now 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 test task now declares dependsOn: ["^build"], matching its typecheck and test:integration siblings. Without it a package's test cache key excluded its workspace dependencies, so a change in @byline/core with no change in @byline/admin replayed 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. Local pnpm test runs now rebuild dependencies first.
  • @byline/admin — Reload admission and focus recovery moved into a useDocumentReloadRecovery hook, and the status-button and dropdown transitions share one callback. FormRendererProps.onSubmit is now typed with the submission payload rather than any.
  • @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.

  • email and url validation rules are now enforced. They previously called Zod's .describe() rather than .email() / .url(), so they validated nothing at all. A collection declaring validation: { 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 with optional: true plus a validate callback rather than condition, which the lifecycle does not evaluate. See docs/10-api-reference/03-fields.md.

  • A schema-author callback that throws now blocks the save. A validate or condition function, or a custom rule, that throws on data it did not anticipate yields a could not be validated field 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 status to create() now need the matching abilities — changeStatus, plus publish for 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.