Skip to content

v3.15.0

Choose a tag to compare

@58bits 58bits released this 29 Jun 03:48

Highlights

  • @byline/search-postgres (new package) + @byline/core + @byline/client + @byline/host-tanstack-start + @byline/richtext-lexicalfull-text search. A pluggable SearchProvider seam now lives in @byline/core (registered on ServerConfig.search, validated at initBylineCore()), with a batteries-included Postgres driver shipping as the brand-new @byline/search-postgres package — ranked search with zero new infrastructure, reusing your existing pgAdapter pool (db.pool). Core assembles each document into a type-enriched SearchDocument via buildSearchDocument(); the driver stores one weighted tsvector row per (collection_path, document_id, locale) (title → weight A, body fields → AD by their declared boost, facet terms → C), queries with websearch_to_tsquery + ts_rank, and returns ts_headline highlights. Collections opt in with a search config — { body, facets, filters, zones }, where the implementor names fields by the role they play and core derives each field's type from the schema. Querying is a first-class Client SDK method parallel to find(): client.collection('docs').search({ query, locale, limit }) returns ranked hits (title / path / score / highlights). Per-locale indexing uses the matching Postgres regconfig (enenglish, …) with a configurable defaultLocale. The index stays live through collection lifecycle hooks (indexDocument / removeFromIndex) and rebuilds via client.collection(x).reindex() or the admin Reindex button (a reusable listActions list-header slot, self-gated on the new collections.<path>.reindex ability). Published-only and safe for public readers. See Getting started with search below for setup. BM25 / vector / hybrid drivers, facet aggregation, structured where filtering, cross-collection zone queries, and attachment text-extraction are sanctioned future phases — surfaced honestly today through the driver's capabilities flags (weighting + highlights are true; the rest are false).

  • @byline/richtext-lexicallexicalToText extractor + toText server seam. Rich-text body fields flatten to plain, indexable text through the editor-agnostic ServerConfig.fields.richText.toText seam (RichTextToTextFn), implemented for Lexical as lexicalToText / lexicalEditorToTextServer (@byline/richtext-lexical/server) — a recursive text-node accumulator, no markdown. It is the search sibling of the existing lexicalToMarkdown serializer and is what lets a richtext field contribute to a collection's searchable body.

  • @byline/core + @byline/adminadmin.itemView (generalised relation picker). The per-collection relation picker config has been generalised into admin.itemView, which does triple duty — what to fetch (projection) and how to render (presentation) — reused by the relation picker, hasMany tiles, and (eventually) heterogeneous search-result rows. The old picker key is kept as a deprecated, backward-compatible alias, so existing collections keep working unchanged. A relation column formatter for list views ships alongside it, so relation fields render their target's real title rather than a bare id in admin tables.

Bug Fixes

  • @byline/host-tanstack-start — search indexing now runs through a new getSystemBylineClient() (super-admin context, no session cookies) instead of the request-scoped admin client. Calling the request-scoped client from a lifecycle hook coupled indexing to a live HTTP request and threw No StartEvent found in AsyncLocalStorage from every out-of-band write path — import scripts, seeds, migrations, the CLI, and tests. Indexing is background maintenance (it reads the published view and bypasses beforeRead), so the system context is both correct and runtime-agnostic. Lifecycle hooks that index should resolve the client with getSystemBylineClient(), not getAdminBylineClient().

  • @byline/host-tanstack-start — the listActions list-header slot (and thus the Reindex button) now renders in the tree list view as well as the default list view.

Chores

  • @byline/cli — the docs collection example template now ships the full working search vertical: @byline/search-postgres added to the install dep manifest, the provider + migrate + toText seam registered in the example server.config.ts, the docs schema's search config corrected to the current { body, facets, … } shape, lifecycle hooks moved to the loader form with a hooks.ts that indexes via getSystemBylineClient(), and a ReindexButton wired into the docs list header. Earlier in the cycle the example templates were also migrated from the deprecated picker config to admin.itemView.

Getting started with search

Search is opt-in. To enable it on an install:

  1. Install the driver (new this release):

    pnpm add @byline/search-postgres

    pg is a peer dependency — you already have it via @byline/db-postgres.

  2. Apply the search schema. @byline/search-postgres owns its own schema — numbered SQL files shipped in the package, tracked in its own byline_search_migrations table. It is not part of your app's Drizzle migration stream, so it will not be applied by your normal drizzle:migrate. Pick one per environment:

    • Run the SQL by hand (locked-down / managed Postgres — DBA-reviewable):

      psql "$DATABASE_URL" -f node_modules/@byline/search-postgres/migrations/0001_init.sql
    • Call migrate() as a deploy step (recommended for production — deterministic, explicit DDL permissions, idempotent and transactional per file):

      import { migrate } from '@byline/search-postgres'
      
      const { applied } = await migrate(db.pool, { log: (m) => logger.info(m) })
      // applied: [1]  (empty when already up to date)
    • autoMigrate at boot (development convenience):

      search: postgresSearch({ pool: db.pool, autoMigrate: true })
  3. Register the provider on ServerConfig.search, reusing the adapter's pool — no second connection:

    import { postgresSearch } from '@byline/search-postgres'
    
    search: postgresSearch({ pool: db.pool, defaultLocale: 'en' }),

    initBylineCore() fails fast if a collection opts into search but no provider is registered.

  4. Opt collections in via their search config ({ body, facets, filters, zones }) and wire indexing in their lifecycle hooks (indexDocument / removeFromIndex) — see the docs collection in the example app, or the @byline/cli docs template.

  5. Backfill content that was published before indexing existed with await client.collection('docs').reindex() (or the admin Reindex button).

Breaking Changes

None. admin.itemView is additive — the prior picker config continues to work as a deprecated alias.

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