Skip to content

v4.11.0

Latest

Choose a tag to compare

@58bits 58bits released this 30 Jul 17:44

A minor release that changes what a soft delete means. Deleting a document now
releases its path back to the collection's live namespace while retaining the
path value for history, and stops deleting uploaded files from object storage.
The path change requires a one-time schema upgrade on existing installations. The
storage change requires no migration, but it does mean deleted media now
accumulates — see Migrations for both.

Highlights

  • monorepo — a soft-deleted document no longer holds its path hostage. Until
    now, byline_document_paths enforced uniqueness over
    (collection_id, locale, path) regardless of whether the owning document was
    deleted, so a deleted document kept its path reserved forever while
    findByPath — which reads the deleted-filtering current-documents view —
    reported nothing at that address. Re-creating a document at the same path
    failed with ERR_PATH_CONFLICT and no supported way to recover it. The table
    now carries a nullable deleted_at and a stored generated
    alive column (true when deleted_at IS NULL, otherwise NULL), and the
    unique key spans (collection_id, locale, path, alive). Because both
    PostgreSQL and MySQL permit many NULLs in a unique index, two live
    documents still cannot share a path, while any number of deleted documents may
    retain one. Path lookup filters to alive = true, so a deleted document at a
    higher-priority locale is skipped in favour of a live fallback. Callers never
    write alive themselves.

  • @byline/db-postgres, @byline/db-mysql — a new storage primitive,
    restoreSoftDeletedDocument({ document_id }), reactivates every path row and
    every version of a fully soft-deleted document in one transaction under the
    same collection-then-document locks that soft delete takes. The live unique
    key revalidates the retained path on the way back in: if another document has
    claimed it in the meantime, the whole restoration rolls back rather than
    half-reviving the document. It returns 0 — a no-op, not an error — for a
    document that is missing, versionless, already live, or in a legacy
    partially-live state, so it never implicitly "repairs" inconsistent data. This
    is a storage primitive for trusted tooling: it does not rebuild tree
    placement or search and cache projections, and no editorial trash-and-restore
    surface is wired to it yet.

  • @byline/core — path-conflict errors now say which operation failed and
    that a live document owns the requested path, rather than reporting a bare
    "path already in use". Because the unique key is now live-only, a violation
    can only mean a live occupant, so the wording is derived rather than guessed —
    no extra lookup is issued and the occupant's document id is never exposed. The
    ERR_PATH_CONFLICT code and the public details shape (path, locale,
    constraint) are unchanged.

  • monorepo — duplicating a document onto a path that only deleted rows
    retain now keeps the requested path instead of appending a short-UUID suffix.
    A live occupant still triggers the single suffixed retry, so the two cases are
    finally distinguishable.

  • monorepo — writing a new version of an existing document now takes a
    row-scoped lock on that document before checking its liveness. Concurrent saves
    to the same document serialise with each other and with soft delete and
    un-delete; writes to unrelated documents remain fully concurrent. A fully
    deleted document can no longer gain a live version by any route other than
    whole-document un-delete. No action is required — this happens automatically —
    but it is a behavioural change worth knowing about if you drive concurrent
    writes at the same document.

Bug Fixes

  • @byline/core — a path conflict raised while updating a document reported
    the collection's configured default content locale instead of the document's
    own source_locale, which is the locale the conflicting path row is actually
    keyed by. Any document whose source locale differs from the collection default
    produced a misleading error. Create and duplicate were already correct — a
    create is constrained to the default locale, and a duplicate writes a new
    document — so the fix is confined to the two update paths.

Chores

  • monorepo — both database adapters' Drizzle migration streams were squashed
    to a single fresh-install baseline for this release, and the @byline/cli
    bundled templates were re-synchronised from them. Two guards now protect this:
    a drift contract asserting the CLI templates match the adapter sources
    byte-for-byte, and an append-only history check asserting every previously
    released hand-written sql/ script is still present and unmodified.

  • monorepo — the reference documentation importer
    (apps/webapp/byline/scripts/import-docs.ts) lost its --force flag, which
    existed only to work around the reserved-path problem this release removes.
    With it went a PostgreSQL advisory lock, version-tombstone mutation, staging
    snapshots and manual hook replay — and the importer's only dependency on
    PostgreSQL adapter internals. It is now dialect-neutral and runs against
    MySQL, having previously been this repository's own counter-example to the
    adapter abstraction. The flag is rejected as unknown rather than ignored.
    Delete followed by re-import now starts a new logical document at the same
    path; the old document and its audit trail stay deleted.

Migrations

  • @byline/db-postgres, @byline/db-mysql — existing installations must
    apply the numbered native upgrade script for their provider:

    # PostgreSQL
    psql "$DATABASE_URL" -f packages/db-postgres/sql/0006_soft_delete_path_liveness.sql
    
    # MySQL
    mysql -u "$DB_USER" -p "$DB_NAME" < packages/db-mysql/sql/0001_soft_delete_path_liveness.sql

    Each script adds the columns, backfills deleted_at for path rows whose
    document has no live version, and rebuilds the unique key without renaming it.
    A path row whose document has no versions at all is deliberately left live,
    because nothing was ever deleted there. Both scripts are idempotent and safe
    to rerun. The PostgreSQL script runs the backfill and constraint replacement
    in one transaction and raises on a failed post-condition; MySQL DDL
    auto-commits, so its script guards each step through information_schema,
    asserts both directions of path/version liveness afterwards, and fails loudly
    rather than printing a diagnostic you might miss.

    Do not apply this release's development Drizzle migration or the squashed
    fresh-install baseline to an occupied database — those describe the schema from
    scratch and are for new installations only.

  • monorepo — soft delete no longer removes uploaded sources or generated
    variants from object storage. Source and variant paths are immutable
    historical references that immutable versions and duplicated documents can
    share, so deleting one document is not evidence that nobody else references
    the object. The practical consequence is that storage now grows monotonically:
    there is no purge or reference-safe reclamation operation yet, and no orphan
    sweeper. Issue #72
    owns the design — variant identity, persisted generation recipes,
    provider-neutral source reads, shared-reference analysis, and eventual
    cleanup. Plan capacity accordingly if you delete media at volume.

Breaking Changes

  • @byline/corebreaking for out-of-tree adapters only:
    IDocumentCommands now requires restoreSoftDeletedDocument({ document_id }).
    Both built-in adapters ship it, so applications using @byline/db-postgres or
    @byline/db-mysql need no change. If you maintain your own IDbAdapter, add
    the command: it must atomically reactivate every version and every path row of
    a fully deleted document, return 0 for missing, versionless, already-live and
    legacy partially-live documents, and let a live-path unique violation roll the
    whole operation back.

  • @byline/core, @byline/host-tanstack-startbreaking for code
    that narrows on the delete side-effect phase union: storageCleanup was
    removed from DeleteDocumentSideEffectPhase, leaving afterTreeChange and
    afterDelete. Soft delete performs no object-storage cleanup, so the phase can
    no longer occur and was not reserved. The transport layer still sanitises an
    unrecognised phase to 'unknown', so a stale value arriving from an older
    client degrades rather than throwing.

All other @byline/* packages bumped to 4.11.0 in lockstep with no behavioural changes this cycle.