Skip to content

v0.22.0

Choose a tag to compare

@github-actions github-actions released this 21 Sep 17:04
· 1877 commits to main since this release

Added

  • Linking an account returns the provider's profile picture. verify
    already fetches a photo from every provider that has one, and both sign-in
    branches pass it to updateUser — but POST /auth/link/:provider asked for
    the same profile, got the same answer and dropped it. An app whose users link
    an account after signing up therefore had no route to a picture at all,
    which is the normal case wherever the account came first. Both branches now
    answer with photoURL, the idempotent already-linked one included, so a
    re-link is also a way to refresh it. Returned rather than stored on purpose:
    these URLs are signed and expire (LinkedIn's within weeks), so a column
    holding one is a column that turns into a broken image later — copy the bytes
    somewhere you own while the URL is still good.

  • beforeQuery narrows which rows a read asks for. afterRead sees rows
    after they are fetched, so a tenant scope or a per-role row filter could only
    be an RLS policy or a condition pushed into every caller. A collection's
    callbacks.beforeQuery, or a global one on initializeRebaseBackend, returns
    a filter or a logical group that is AND-ed into the read before it is
    compiled. It can only narrow: nothing it returns can drop or replace the
    caller's own conditions. It applies to the listing, the single get, the count,
    the aggregate, search, vector reads, nested paths, the realtime refetch behind
    .listen(), and the rows loaded for a relation or an ?include=, where the
    target collection's hook is the one that applies. A hook filter that names a
    column the table does not have refuses the request with
    400 UNKNOWN_FILTER_FIELD, whatever configureUnknownFilterFields is set to.
    Postgres only: a MongoDB or Firestore collection that declares one fails at
    boot rather than serving every row.

  • search.mode can be "hybrid", which finds substrings and folds accents
    in one collection.
    Full-text search matches whole words, so seb never
    found Sebastian and audit never found Lead Auditor. The plain search
    finds substrings but cannot fold accents, so munoz never found Muñoz.
    mode: "hybrid" ORs the indexed full-text match with an accent-folded
    substring match over the same search.fields, and ranks full-text hits above
    substring-only ones. It changes the query and not the generated column, so it
    can be turned on for a live collection without the rebuild a changed search
    block otherwise needs; it adds the unaccent extension and one helper
    function if they are missing. The substring half cannot use the GIN index,
    which is why "fts" stays the default.

  • Five plugin slots that rendered nowhere now render.
    entity.field.before and entity.field.after render around every form
    field, entity.row.actions in each table row's hover tools, global.search
    in the app bar beside the breadcrumbs, and shell.toolbar at the end of the
    app bar. All five were declared and documented, and registering for one did
    nothing. UNRENDERED_SLOTS is now empty, and a test that scans for render
    sites fails if a slot is declared without one again.

  • A component override can be named by module path. An override in a
    collection's admin.components took only a component, so declaring one meant
    importing React into config/collections/*.ts, which the backend loads too.
    Component now takes a path or a lazy import(), as admin.Field does, and
    the collections Vite plugin turns the path into a lazy import. An override
    that cannot be resolved renders the built-in component instead of nothing.

Changed

  • The record dialog has its Create and Save buttons at the bottom. In a
    collection opened with openEntityMode: "dialog", such as users in a
    scaffolded project, Create, "Create and close", Save and Discard sat in the
    right-hand corner of the top bar. They are now in a footer under the form,
    where every other dialog in the admin keeps its buttons. The top bar keeps the
    title, the overflow menu and the window controls. The side panel and the
    full-screen record still keep Save in the top bar, where it stays in view
    while a long form scrolls.

  • A form of four fields or fewer is one column. The derived layout paired
    fields by type, so the new-user form put Email beside Name and left Roles
    alone on the row below. Short forms now stack. A span set on a property is
    still honoured, and longer forms keep their columns.

  • The entity.row.actions and field slot props describe what their render
    sites have.
    In EntityRowActionsProps, entityId is string | number,
    path, collection and selectionController are optional, and
    parentCollectionSlugs and parentEntityIds are gone. In
    EntityFieldSlotProps, collection is optional. A relation picker renders
    rows with no collection path, a table can hide selection, and a field can be
    rendered outside a collection form. A component typed against the old props
    has to handle the optional ones.

Removed

  • PropertyIdCopyTooltip and LabelWithIconAndTooltip are gone from
    @rebasepro/cms.
    Use PropertyKeyHint to show a property's key beside a
    label of your own, or LabelWithIcon with its new propertyKey prop.

  • The collection.filter-panel and dashboard.widget slots, with
    CollectionFilterPanelProps and DashboardWidgetProps.
    Neither ever
    rendered. The admin has no filter sidebar for the first to render into; put
    filter UI in collection.toolbar or collection.widgets. The second named no
    position on the page; the home page has home.children.start,
    home.children.end, home.cards and home.card.widget.

Fixed

  • A search for two words finds a row that keeps them in two fields.
    Searching sebastian melendez on a collection with first_name and
    last_name returned nothing: the default search compiled one
    ILIKE '%sebastian melendez%' per column, and no column holds both words.
    Typing sebastian returned nothing either — the trailing space was part of
    the pattern. The search string is now split into terms, OR-ed across the
    searchable fields and AND-ed across the terms, which is what
    websearch_to_tsquery already did for a collection that declared a search
    block, so both paths answer the same question. Double quotes keep a run
    together as a phrase, a one-word search compiles to exactly the SQL it always
    did, and no search that used to match stops matching. Same fix in the Mongo
    driver and in the offline evaluator, which had the same shape.

  • The service key is no longer rate limited. The data API buckets callers
    as API key, signed-in user, then IP — and identify saw the service key's
    uid: "service" and gave it the ordinary user allowance of 1000 requests per
    15 minutes, shared across every service-key caller. It is the deployment's own
    credential and already reaches every row, so the limit protected nothing that
    was not already open to whoever holds the key; what it stopped was the work
    the key exists for. A backfill of 7,925 rows died at row ~1000 with a 429, and
    a managed deployment has no rateLimit surface to raise. The service identity
    now skips the limiter entirely, without spending the bucket of whoever shares
    its key. API keys keep their own rate_limit, users the user allowance,
    anonymous traffic the IP bucket.

  • A refused Google sign-in onto an existing account now tells the visitor
    what to do, in their language.
    Signing in with a provider onto an address
    whose account cannot be linked automatically showed the API's message on the
    login screen, word for word: "…Sign in with your existing method, then POST
    to /auth/link/google to link google to your account." LoginView now shows
    "An account with this email already exists. Sign in with your email and
    password instead." when that account holds a password, and "Sign in the way
    you did before." otherwise, in all seven locales. The server's response is
    unchanged for API callers, and the EMAIL_NOT_VERIFIED error now carries
    details: { reason, provider }, where reason is
    "local-account-unverified" or "provider-email-unverified", so your own
    login screen can do the same.

  • Starring a collection on the admin home page no longer draws a star the
    size of a card.
    The favourites chip passed size="small" to a lucide icon,
    which writes size straight into the SVG's width and height. small is
    not a length, so the browser dropped it and the icon filled the chip. The
    same mistake made four more icons render oversized: the join-path delete
    button in the collection editor's relations tab, the export button, the
    empty avatar in the user picker, and "Log in anonymously" on the Firebase
    login screen. All five now pass iconSize, and a lint rule
    (rebase/lucide-icon-numeric-size) refuses a named size on a lucide icon.

  • "New user" in the admin saves again when users.ts does not mark the
    secret columns.
    A project's own users collection replaces the default,
    and a copy scaffolded before excludeFromApi was in the template leaves it
    off passwordHash and emailVerificationToken. The server adds the flag at
    boot, but the admin built its collections from the file as written, so the
    new-user form sent both fields as null. The server then refused the create:
    "'passwordHash', 'emailVerificationToken' are excluded from the API on
    'users' and cannot be written through it". Neither field is shown in the
    form. Every CollectionRegistry now applies the same rule when it copies an
    auth collection, so the admin and the server agree on which columns are the
    server's, whether or not the file says so. The boot warning asking you to add
    excludeFromApi: true is unchanged.

  • Tab moves to the next field in a record dialog or side panel. Tab from a
    field could land on the dialog itself, so the next Tab started again from the
    header buttons, and Shift+Tab did the same. Leaving the field closed a tooltip
    rendered inside the dialog. Radix's focus trap treats any node removed while
    focus is between two elements as the focused element being deleted: it
    focused the dialog, and the browser abandoned the move. The kit's Dialog and
    Sheet now complete a move interrupted this way, whatever removed the node.

  • The property ID no longer covers the form. Text, date, switch, geopoint,
    binary and vector fields opened a "Property ID" tooltip over their own label
    as soon as they took focus, and again on hover. The key now appears in small
    type beside the label when you rest the pointer on it, and clicking it copies
    it. It covers nothing, never opens on focus, and is not a Tab stop.

  • Global callbacks run. The callbacks passed to initializeRebaseBackend,
    and export const callbacks from config/index.ts on the managed runtime,
    were stored on the backend's own collection registry. No driver reads that
    one: each builds a registry of its own at boot and looks for global callbacks
    there. So a global afterRead, beforeSave, afterSave, beforeDelete or
    afterDelete was accepted and never called, on any data path. The backend now
    hands them to every driver at boot, and refuses to start if a driver returns
    no registry that can take them. If you declared global callbacks, they start
    running with this release
    — read them again before you upgrade. A global
    beforeQuery beside a MongoDB or Firestore data source also refuses to start.

  • Cloud status no longer tells a serving project to create its database.
    A managed project whose database the control plane had not recorded showed
    blockedOn: no_database beside status: active in rebase cloud status,
    and suggested rebase cloud db create --type managed, the one command that
    could provision a second, empty database and point the app at it. A project
    that has deployed once has a database, so the CLI now reports it blocked on a
    missing database only before its first deploy.

  • A time imported from Excel keeps its minute. The spreadsheet reader turns
    Excel's fractional-day dates into milliseconds by rounding down, so 12:30
    often arrived as 12:29:59.999 and a field showing minutes displayed 12:29. A
    date within a millisecond of a whole second is now put back on it; a real
    sub-second time is left alone.