Version Packages#747
Merged
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
July 19, 2026 13:38
7e38f31 to
5a69445
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
July 20, 2026 01:20
5a69445 to
6daa75b
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
July 20, 2026 02:05
6daa75b to
4c71f8c
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
July 20, 2026 02:36
4c71f8c to
68d74d6
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
July 20, 2026 03:25
68d74d6 to
80a618d
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
July 20, 2026 07:20
80a618d to
4203ded
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
July 20, 2026 07:50
4203ded to
918c9d0
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
July 20, 2026 09:56
918c9d0 to
8e29794
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
July 20, 2026 10:10
8e29794 to
537b9b5
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
July 20, 2026 11:00
537b9b5 to
9c5d1a0
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
July 20, 2026 11:28
9c5d1a0 to
08789ce
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
July 20, 2026 11:33
08789ce to
eaf136d
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
July 20, 2026 13:09
eaf136d to
0210b51
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
July 20, 2026 14:19
0210b51 to
0216f16
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
July 21, 2026 10:15
12701fb to
b89ab13
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
July 21, 2026 10:35
b89ab13 to
3a5fa39
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
2 times, most recently
from
July 21, 2026 10:50
cc2d6ec to
ecea9c8
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
July 21, 2026 11:09
ecea9c8 to
47a7d6f
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
July 21, 2026 11:14
47a7d6f to
46cd728
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
July 21, 2026 11:41
46cd728 to
eae1853
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
July 21, 2026 11:51
eae1853 to
3bb3cc4
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
July 21, 2026 12:03
3bb3cc4 to
a12512b
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
July 21, 2026 12:15
a12512b to
e9bd5a1
Compare
Contributor
Author
Coverage Report for Core Package Coverage (./packages/core)
File CoverageNo changed files found. |
Contributor
Author
Coverage Report for UI Package Coverage (./packages/ui)
File CoverageNo changed files found. |
Contributor
Author
Coverage Report for CLI Package Coverage (./packages/cli)
File CoverageNo changed files found. |
Contributor
Author
Coverage Report for Auth Package Coverage (./packages/auth)
File CoverageNo changed files found. |
Contributor
Author
Coverage Report for Storage Package Coverage (./packages/storage)
File CoverageNo changed files found. |
Contributor
Author
Coverage Report for RAG Package Coverage (./packages/rag)
File CoverageNo changed files found. |
Contributor
Author
Coverage Report for Storage S3 Package Coverage (./packages/storage-s3)
File CoverageNo changed files found. |
Contributor
Author
Coverage Report for Storage Vercel Package Coverage (./packages/storage-vercel)
File CoverageNo changed files found. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.
Releases
@opensaas/stack-core@0.31.0
Minor Changes
#750
047487aThanks @borisno2! - Add abulkDeleteserver action for list-level bulk deletioncontext.serverActionnow accepts{ listKey, action: 'bulkDelete', ids }. Itdeletes each id row-by-row through the secured context, honouring Silent failure
(a denied or missing row returns
nulland is not counted; one row's error doesnot abort the rest), and returns
{ deleted, total }.The result is deliberately a count shape rather than the single-op
{ success }shape, so a UI
serverActionwrapper that redirects on a single-item success(the item-form pattern) does not hijack a list-level bulk operation.
#755
9cd06ddThanks [@list({](https://github.com/list({)! - Admin chrome polish: opt-in nav counts and avatar label cells (#735)Two per-list opt-ins for the admin UI, both off by default.
Nav counts — set
ui.navCount: trueon a list to show an access-scopedrecord count next to its nav item. The count is fetched through the secured
context, so it only ever reflects what the current session may see; no count
query runs for lists that don't opt in, and a list whose query access is
statically denied renders no count rather than a misleading zero.
Avatar label cells — set
ui.avatar: trueto render a list's label columnwith a deterministic initials bubble ahead of the emphasized Item label. The
initials and colour derive from the row; the palette is Theme-token-derived (no
raw hex). A per-field cell override (
ui.cell) on the label field still wins.New exports:
@opensaas/stack-core:resolveNavCounts,isListQueryStaticallyDenied@opensaas/stack-ui:Avatarprimitive,AvatarLabelCell, and thegetInitials,getAvatarTone,AVATAR_TONEShelpers. New Slots:avatar,cell-avatar-label,nav-count.#759
b190813Thanks @borisno2! - Add custom Bulk actions from list config (admin list view)A list can now declare list-specific Bulk actions under
ui.listView.bulkActions. Each action's button renders in the list view's selection bar (in declaration order) alongside the built-in Delete. The action's server-sidehandlerreceives the selected ids and the secured context, so all its work runs through access control and hooks — a denied row is a Silent failure absorbed into the outcome, never leaked.Only serialisable metadata (
key/label/variant/destructive) crosses to the client; thehandler/hasAccessfunctions stay on the server. Clicking the button sends thekeyand selected ids back through the generic server action, which looks the handler up and runs it with a freshly-rebuilt secured context. Selection is enabled for a list that has custom actions even when Delete is denied. CSV export is documented as a recipe using this surface rather than shipping as a built-in.#754
f67cd79Thanks @borisno2! - Add the Filter builder input UI for the admin list view (#731)The admin list view now ships a
FilterBuilderthat constructs the?search=filter query the filter engine already consumes (ADR-0017) — a free-text search
box plus structured field / operator / value rows. Available fields, operators,
and value suggestions are derived entirely from each field's self-contained
getFilterSpec(via the serializablecollectFilterSuggestionsmetadata), sothere is no field-type
switchand no functions cross the server/clientboundary. Applied filters flow through the same secured
context.db, sofiltering can only ever narrow what a session may see.
@opensaas/stack-coregainsserializeFilterQuery(tokens)— the exact inverseof
parseFilterQuery— so the builder produces the grammar the engine parseswith the quoting and operator-prefix rules kept next to the parser.
The
FilterBuilderis composable (exported from@opensaas/stack-uiand@opensaas/stack-ui/standalone) with theme-token styling anddata-slotpartsfor extension:
The list view wires this in automatically; existing
?search=URLs keepworking unchanged.
#746
dcb10e2Thanks @borisno2! - Add the admin UI filter engine: a Filter spec field-builder contract and URL-driven server-side list filtering (ADR-0017).Fields now declare their filtering capability through a new optional
getFilterSpecmethod — a peer ofgetPrismaType/getTypeScriptTypeon the field-builder contract. It reports the operators a field supports, a pure token→condition mapper, and serializable suggestion metadata. Core field types implement it (text contains + free text, integer/decimal/timestamp/calendarDay comparisons, select/checkbox equality against enumerated values, relationship by label lookup). A field without a spec —password,json,virtual, or any third-party field that hasn't adopted one — is simply not filterable, so the addition degrades gracefully everywhere.The admin list view now parses the URL filter query (the list's
searchparam) through the engine and merges the result into the access-controlled query via the secured context, so filtering runs server-side and can only ever narrow — never widen — what a session may see. This replaces the previous hard-codedtype === 'text'search; free-text behavior is now driven by each text field's Filter spec.Grammar (ADR-0017): implicit-AND tokens, quoted multi-word values,
>/>=/</<=comparisons on numeric/date fields, and bare words as free text. Unknown syntax degrades to free text, never errors.Multi-word free-text UX shift (intentional, per ADR-0017): bare words now combine with AND, so
hello worldrequires each word to match separately (not the literal substringhello world). To match a contiguous phrase, quote it:"hello world". A pasted URL such ashttp://xis treated as a single free-text token and searched verbatim — thehttp:prefix is not parsed as a field.New exports from
@opensaas/stack-core:Third-party field authors can implement
FilterSpec(exported from@opensaas/stack-core/extend) to make their field filterable.#760
f8b6f02Thanks @borisno2! - Add inline cell editing to admin Relationship tablesCells in a to-many Relationship table on the item view are now editable in place.
Click a cell to edit it, commit with Enter or blur, cancel with Escape. Each
commit is a single-field update on the related row through the secured
context, so the related list's own operation- and field-level update access plus
its hooks/validation apply — never the parent's. The update is optimistic and
reverts, with a visible reason, on a Silent failure (access denied / row gone) or
a validation error (inline field errors surface too). Committed values re-render
through the Cell registry, so select cells stay coloured badges.
A field the session cannot write — or a table whose related-list update access is
statically denied — renders read-only with no edit affordance; row-level
(filter-scoped) denials surface at commit as a revert. Non-editable cells keep
click-to-navigate; main list tables are unchanged (this is Relationship-table
only).
@opensaas/stack-core: the generic server action gains a distinctupdateRelatedresult shape ({ updated, error?, fieldErrors? }), andcheckFieldAccessis exposed on@opensaas/stack-core/internalso the UI candecide the edit affordance without a parallel field-access evaluator.
@opensaas/stack-ui:RelationshipTableClientacceptseditableColumns; theeditable cell reuses the field-component registry for its editor and the Cell
registry for its display (new Slots:
relationship-table-cell-display,relationship-table-cell-editor,relationship-table-cell-edit-trigger,relationship-table-cell-error).#757
c05701eThanks @borisno2! - Add a pre-linked create drawer to read-only Relationship tables (issue #738)The item view's read-only Relationship tables now offer a "+ Add" control that
opens a drawer hosting the related list's create form, with the back-reference to
the current record preset and hidden. On submit the new row is created through
the secured context already linked to the parent, then the drawer closes and the
table refreshes.
Create-and-link semantics (ADR-0018): the create runs on the RELATED list, so
the related list's own
createaccess control, hooks, and field-level accessapply — never the parent's. The back-reference is set on the server from the
field/parent id (never trusted from the client payload). The "+ Add" is shown
only when a back-reference exists to preset the link and the related list's
createaccess is not statically denied; a filter/function-scoped denialsurfaces at commit time as a generic error (no denied-vs-absent leak).
New generic server action (
@opensaas/stack-core):The drawer (
RelationshipCreateDrawerfrom@opensaas/stack-ui) mounts on theexisting
relationship-table-toolbarseam and reuses the shared item-form engineand field-component registry, so the related list's full validation and required
fields are enforced even when a required field is not one of the table's columns.
#756
8199238Thanks [@list({](https://github.com/list({)! - Add relationship-table row removal to the admin item view (ADR-0018)Each read-only Relationship table row now has a ✕ removal control. By default it
disconnects the related row from the current record (non-destructive — the
row survives and still appears on its own list), gated on the related list's
update access. A per-relationship opt-in truly deletes the related row (behind a
confirmation, gated on the related list's delete access), or hides the control
entirely. Where the schema makes disconnect impossible (a required foreign key on
the related side) the control is hidden unless delete is opted in. Removals run
through the secured context, so an access-denied removal is a Silent failure: the
row stays with a visible reason.
Configure per relationship via
ui.itemView.removeAction:@opensaas/stack-coreadds aremoveRelatedserver action (distinct{ removed }result shape, likebulkDelete, so a redirect-on-success wrappernever hijacks an in-place removal) and the
RelationshipItemViewConfig.removeActionoption.
#745
4d99e91Thanks @borisno2! - Add a Cell registry with default cells for core field typesList tables now render every value through a Cell resolved by a
cell-component registry that mirrors the form-field registry's priority chain:
per-field override → custom type registry → field-type registry → plain-text
fallback. Each core field type ships a default Cell — text (plain), integer
(tabular figures), select (coloured Badge), timestamp (formatted date), checkbox
(mark), and to-one relationship (Item label link). Unknown/third-party types
without a registered Cell fall back to plain text.
Select options gain optional, additive per-option UI metadata mapping a value to
a badge variant. Existing options keep working unchanged; unmapped options render
the neutral badge.
Register a Cell for a custom/third-party field exactly as you register its form
component, or override a single field's Cell:
#751
20459b5Thanks [@list({](https://github.com/list({)! - Derive the admin item view from the list shape, with read-only Relationship tables and a totals footer (#734)A record's edit page now derives its layout from the list's shape. Scalar and
to-one fields stay in a details card (whole-form Save/Cancel, unchanged), and
each to-many relationship renders as a read-only Relationship table: one
to-many relationship gives a two-column split, none gives a single centered
card, several stack. Table columns default to the related list's own column
curation minus the back-reference to the parent, cells come from the cell
registry, and a totals footer always shows the row count plus sums for any
explicitly-configured numeric columns (each formatted by that column's Cell).
Rows are fetched through the secured context, so only access-visible data shows.
Rows are read-only here — a row click navigates to the related record.
@opensaas/stack-coregains additive item-view config (no breaking changes):New
@opensaas/stack-uiexports:RelationshipTable,RelationshipTableClient,and the pure
deriveItemViewLayouthelper (withItemViewLayout,ItemViewArrangement,RelationshipTableSection). The Relationship table shipsnamed Slots (
relationship-table,relationship-table-toolbar,relationship-table-row,relationship-table-cell,relationship-table-footer)as extension seams for the follow-up inline-edit, create-drawer, and row-removal
work.
#774
62a1612Thanks @borisno2! - Bound the admin item-view Relationship tables with atakeand a "showing N of M" footerThe read-only Relationship tables on a record's edit page (issue #734) previously
fetched every related row unbounded. They now fetch a bounded page of related rows
and surface the full access-scoped total in the footer.
of related rows (
DEFAULT_ITEM_VIEW_TAKE, 10), overridable per relationship viaui.itemView.take. Rows are still fetched through the secured context, so onlyaccess-visible rows come back.
Showing N of M rows,where N is the rendered (bounded) count and M is the full access-scoped total,
fetched via a filtered
_countthat folds the related list's ownqueryaccessin (mirroring the list view's count columns). A fully-denied related list reads
Showing 0 of 0and never leaks a true total. The row count is always shown,including the zero-column footer path.
Core:
mergeIncludeWithAccessControlnow preserves a caller-suppliedtakeon ato-many relation include (it only narrows the fetch, never widening past the access
where), so the securedfindUnique/findManyinclude can bound related-row reads.#764
c210319Thanks [@list({](https://github.com/list({)! - Admin list view: to-many relationship columns render an access-visible count, sort by relation count, and filter by numeric count comparisons (issue #732). Virtual fields render via their Cell but are excluded from sorting and filtering.A to-many relationship used as a list column now shows the count of the related rows the session may see — fetched in the SAME query via a filtered Prisma
_count, with the related list'squeryaccess folded into the count'swhere, so it never counts rows the session cannot read and issues no per-row query. Clicking the column header sorts by relation_count, and its Filter spec offers numeric comparisons on the count (posts:>5) in the filter builder and in shared URLs.Because Prisma cannot compare a relation count in a
where, a to-many relationship's Filter spec emits a structured count marker that is resolved to an access-scoped{ id: { in } }before the query runs, through the secured context.New
@opensaas/stack-coreexports:buildRelationshipCountSelect,resolveRelationshipCountFilters,isToManyRelationshipField, andRELATIONSHIP_COUNT_FILTER_KEY(with theRelationshipCountFilterMarkertype).Patch Changes
#773
5a60291Thanks @borisno2! - HardencreateRelatedserver action: reject malformed calls that supply only one offield/parentId, and validate that the back-reference names a relationship field before injecting the parent connect.#775
2fcb582Thanks @borisno2! - Return a generic "Action failed" message (and log the real error server-side) when a custom bulk-action handler throws an unexpected non-Prisma error, instead of surfacing its internal message to the client#771
85c7fc3Thanks @borisno2! - Harden relationship count-filter resolution: preserve any sibling conditions co-present in a_countFiltermarker's AND-member instead of replacing it wholesale, and document why the secured count read intentionally keeps its full projection (context.dbdoes not honourselect).#780
55d55e0Thanks @borisno2! - Fix a to-one relationship filter token (e.g.author:Ada) leaking related-list data by ANDing the related list'squeryaccess filter into the nested condition instead of running it unscoped.#794
96e1067Thanks @borisno2! - Fix multi-column fields (e.g. storageimage()/file()in Keystone-parity mode) writing an unrecognised value silently instead of failing validation. The column split now runs aftervalidateFieldRules, not before, at the top-level and nested write paths.@opensaas/stack-storage-vercel@0.31.0
Minor Changes
#777
7098878Thanks @borisno2! - Honourpublic: falsewith real private-blob support. Uploads now mappublic: falsetoaccess: 'private'(previously ignored);download()reads both public and private blobs through@vercel/blob's authorizedget()path instead of a plainfetch(url), and rejects with a descriptive "File not found" error for a missing blob instead of an unrelated failure; a newgetSignedUrl(filename, expiresIn?)returns a time-limited signed URL for serving private files through developer-controlled routes.This also bumps the
@vercel/blobdependency from^2.3.1to^2.6.1, which adds theissueSignedToken/presignUrlAPIsgetSignedUrl()relies on.#786
7cd3eb4Thanks @borisno2! - AddallowOverwriteconfig option tovercelBlobStorage. The Vercel Blob API rejects uploads to an existing pathname unless this is sent, which made stable-filename replace workflows (generateUniqueFilenames: false, e.g. a field withcleanupOnReplace) throw "blob already exists".allowOverwritenow defaults totruewhengenerateUniqueFilenamesisfalse, andfalseotherwise; an explicit setting always wins.#778
5f8fd73Thanks @borisno2! - Add Vercel OIDC authentication support to the Vercel Blob storage providerThe provider no longer requires a static read-write token. New
storeIdandoidcTokenconfig options enable Vercel OIDC auth — the@vercel/blobSDK exchanges the deployment'sVERCEL_OIDC_TOKENfor blob credentials automatically:Credential precedence (resolved by the SDK on every call): explicit
token→ OIDC token (oidcTokenorVERCEL_OIDC_TOKEN) plus store id (storeIdorBLOB_STORE_ID) →BLOB_READ_WRITE_TOKENenvironment variable.The constructor no longer throws when no static token is configured — previously this blocked OIDC-authenticated deployments before the SDK's credential resolution could run. When no credentials are available at all, the SDK now throws a descriptive error on the first storage operation instead. Requires
@vercel/blob2.4.1+ (dependency floor bumped from 2.3.1).Patch Changes
#779
4a1d9beThanks @borisno2! - FixgetUrl()returning a fabricated host that never resolves. It now derives the store ID from the configured token/store ID (the same way the SDK does internally) and embeds it along with the access mode, matching the real URL the SDK returns at upload time. A token that doesn't yield a store ID now throws a descriptive error instead of producing a silently wrong URL.#792
fe1ed5cThanks @borisno2! - Fix unique filename generation to usepath.extnamesemantics (matching the local provider) so extension-less originals no longer get the whole name appended, and pass through an explicitcacheControlMaxAge: 0instead of dropping it.#776
030d540Thanks @borisno2! - Fixdelete()/download()for@vercel/blobv2, which rejectshead()withBlobNotFoundErrorfor missing blobs instead of resolvingnull.delete()of a missing blob is now a no-op and no longer round-trips throughhead().@opensaas/stack-ui@0.31.0
Minor Changes
#755
9cd06ddThanks [@list({](https://github.com/list({)! - Admin chrome polish: opt-in nav counts and avatar label cells (#735)Two per-list opt-ins for the admin UI, both off by default.
Nav counts — set
ui.navCount: trueon a list to show an access-scopedrecord count next to its nav item. The count is fetched through the secured
context, so it only ever reflects what the current session may see; no count
query runs for lists that don't opt in, and a list whose query access is
statically denied renders no count rather than a misleading zero.
Avatar label cells — set
ui.avatar: trueto render a list's label columnwith a deterministic initials bubble ahead of the emphasized Item label. The
initials and colour derive from the row; the palette is Theme-token-derived (no
raw hex). A per-field cell override (
ui.cell) on the label field still wins.New exports:
@opensaas/stack-core:resolveNavCounts,isListQueryStaticallyDenied@opensaas/stack-ui:Avatarprimitive,AvatarLabelCell, and thegetInitials,getAvatarTone,AVATAR_TONEShelpers. New Slots:avatar,cell-avatar-label,nav-count.#759
b190813Thanks @borisno2! - Add custom Bulk actions from list config (admin list view)A list can now declare list-specific Bulk actions under
ui.listView.bulkActions. Each action's button renders in the list view's selection bar (in declaration order) alongside the built-in Delete. The action's server-sidehandlerreceives the selected ids and the secured context, so all its work runs through access control and hooks — a denied row is a Silent failure absorbed into the outcome, never leaked.Only serialisable metadata (
key/label/variant/destructive) crosses to the client; thehandler/hasAccessfunctions stay on the server. Clicking the button sends thekeyand selected ids back through the generic server action, which looks the handler up and runs it with a freshly-rebuilt secured context. Selection is enabled for a list that has custom actions even when Delete is denied. CSV export is documented as a recipe using this surface rather than shipping as a built-in.#754
f67cd79Thanks @borisno2! - Add the Filter builder input UI for the admin list view (#731)The admin list view now ships a
FilterBuilderthat constructs the?search=filter query the filter engine already consumes (ADR-0017) — a free-text search
box plus structured field / operator / value rows. Available fields, operators,
and value suggestions are derived entirely from each field's self-contained
getFilterSpec(via the serializablecollectFilterSuggestionsmetadata), sothere is no field-type
switchand no functions cross the server/clientboundary. Applied filters flow through the same secured
context.db, sofiltering can only ever narrow what a session may see.
@opensaas/stack-coregainsserializeFilterQuery(tokens)— the exact inverseof
parseFilterQuery— so the builder produces the grammar the engine parseswith the quoting and operator-prefix rules kept next to the parser.
The
FilterBuilderis composable (exported from@opensaas/stack-uiand@opensaas/stack-ui/standalone) with theme-token styling anddata-slotpartsfor extension:
The list view wires this in automatically; existing
?search=URLs keepworking unchanged.
#746
dcb10e2Thanks @borisno2! - Add the admin UI filter engine: a Filter spec field-builder contract and URL-driven server-side list filtering (ADR-0017).Fields now declare their filtering capability through a new optional
getFilterSpecmethod — a peer ofgetPrismaType/getTypeScriptTypeon the field-builder contract. It reports the operators a field supports, a pure token→condition mapper, and serializable suggestion metadata. Core field types implement it (text contains + free text, integer/decimal/timestamp/calendarDay comparisons, select/checkbox equality against enumerated values, relationship by label lookup). A field without a spec —password,json,virtual, or any third-party field that hasn't adopted one — is simply not filterable, so the addition degrades gracefully everywhere.The admin list view now parses the URL filter query (the list's
searchparam) through the engine and merges the result into the access-controlled query via the secured context, so filtering runs server-side and can only ever narrow — never widen — what a session may see. This replaces the previous hard-codedtype === 'text'search; free-text behavior is now driven by each text field's Filter spec.Grammar (ADR-0017): implicit-AND tokens, quoted multi-word values,
>/>=/</<=comparisons on numeric/date fields, and bare words as free text. Unknown syntax degrades to free text, never errors.Multi-word free-text UX shift (intentional, per ADR-0017): bare words now combine with AND, so
hello worldrequires each word to match separately (not the literal substringhello world). To match a contiguous phrase, quote it:"hello world". A pasted URL such ashttp://xis treated as a single free-text token and searched verbatim — thehttp:prefix is not parsed as a field.New exports from
@opensaas/stack-core:Third-party field authors can implement
FilterSpec(exported from@opensaas/stack-core/extend) to make their field filterable.#760
f8b6f02Thanks @borisno2! - Add inline cell editing to admin Relationship tablesCells in a to-many Relationship table on the item view are now editable in place.
Click a cell to edit it, commit with Enter or blur, cancel with Escape. Each
commit is a single-field update on the related row through the secured
context, so the related list's own operation- and field-level update access plus
its hooks/validation apply — never the parent's. The update is optimistic and
reverts, with a visible reason, on a Silent failure (access denied / row gone) or
a validation error (inline field errors surface too). Committed values re-render
through the Cell registry, so select cells stay coloured badges.
A field the session cannot write — or a table whose related-list update access is
statically denied — renders read-only with no edit affordance; row-level
(filter-scoped) denials surface at commit as a revert. Non-editable cells keep
click-to-navigate; main list tables are unchanged (this is Relationship-table
only).
@opensaas/stack-core: the generic server action gains a distinctupdateRelatedresult shape ({ updated, error?, fieldErrors? }), andcheckFieldAccessis exposed on@opensaas/stack-core/internalso the UI candecide the edit affordance without a parallel field-access evaluator.
@opensaas/stack-ui:RelationshipTableClientacceptseditableColumns; theeditable cell reuses the field-component registry for its editor and the Cell
registry for its display (new Slots:
relationship-table-cell-display,relationship-table-cell-editor,relationship-table-cell-edit-trigger,relationship-table-cell-error).#757
c05701eThanks @borisno2! - Add a pre-linked create drawer to read-only Relationship tables (issue #738)The item view's read-only Relationship tables now offer a "+ Add" control that
opens a drawer hosting the related list's create form, with the back-reference to
the current record preset and hidden. On submit the new row is created through
the secured context already linked to the parent, then the drawer closes and the
table refreshes.
Create-and-link semantics (ADR-0018): the create runs on the RELATED list, so
the related list's own
createaccess control, hooks, and field-level accessapply — never the parent's. The back-reference is set on the server from the
field/parent id (never trusted from the client payload). The "+ Add" is shown
only when a back-reference exists to preset the link and the related list's
createaccess is not statically denied; a filter/function-scoped denialsurfaces at commit time as a generic error (no denied-vs-absent leak).
New generic server action (
@opensaas/stack-core):The drawer (
RelationshipCreateDrawerfrom@opensaas/stack-ui) mounts on theexisting
relationship-table-toolbarseam and reuses the shared item-form engineand field-component registry, so the related list's full validation and required
fields are enforced even when a required field is not one of the table's columns.
#756
8199238Thanks [@list({](https://github.com/list({)! - Add relationship-table row removal to the admin item view (ADR-0018)Each read-only Relationship table row now has a ✕ removal control. By default it
disconnects the related row from the current record (non-destructive — the
row survives and still appears on its own list), gated on the related list's
update access. A per-relationship opt-in truly deletes the related row (behind a
confirmation, gated on the related list's delete access), or hides the control
entirely. Where the schema makes disconnect impossible (a required foreign key on
the related side) the control is hidden unless delete is opted in. Removals run
through the secured context, so an access-denied removal is a Silent failure: the
row stays with a visible reason.
Configure per relationship via
ui.itemView.removeAction:@opensaas/stack-coreadds aremoveRelatedserver action (distinct{ removed }result shape, likebulkDelete, so a redirect-on-success wrappernever hijacks an in-place removal) and the
RelationshipItemViewConfig.removeActionoption.
#745
4d99e91Thanks @borisno2! - Add a Cell registry with default cells for core field typesList tables now render every value through a Cell resolved by a
cell-component registry that mirrors the form-field registry's priority chain:
per-field override → custom type registry → field-type registry → plain-text
fallback. Each core field type ships a default Cell — text (plain), integer
(tabular figures), select (coloured Badge), timestamp (formatted date), checkbox
(mark), and to-one relationship (Item label link). Unknown/third-party types
without a registered Cell fall back to plain text.
Select options gain optional, additive per-option UI metadata mapping a value to
a badge variant. Existing options keep working unchanged; unmapped options render
the neutral badge.
Register a Cell for a custom/third-party field exactly as you register its form
component, or override a single field's Cell:
#750
047487aThanks @borisno2! - Add row selection and a built-in Bulk action Delete to the admin list viewThe list table now renders a selection checkbox column when the list's delete
access is not statically false. The header checkbox toggles the visible page,
per-row checkboxes accumulate an explicit id set across pages, and the selection
clears when the filter changes. A selection bar shows the count, a Clear action,
a named
data-slot="selection-actions"seam for future custom bulk actions, and— only when delete access allows — a Delete that confirms first, deletes each
selected row through the secured context honouring Silent failure, and reports
"N of M deleted" (partial access denials are visible without revealing which or
why).
The admin list view also honours an optional
?pageSize=URL param, preservedacross sorting, searching and paging.
New exports:
RowSelectionBar(withRowSelectionBarProps/RowSelectionBarClassNames) and theuseRowSelectionhook plus the pureisPageFullySelected/getPageCheckboxStatehelpers.#751
20459b5Thanks [@list({](https://github.com/list({)! - Derive the admin item view from the list shape, with read-only Relationship tables and a totals footer (#734)A record's edit page now derives its layout from the list's shape. Scalar and
to-one fields stay in a details card (whole-form Save/Cancel, unchanged), and
each to-many relationship renders as a read-only Relationship table: one
to-many relationship gives a two-column split, none gives a single centered
card, several stack. Table columns default to the related list's own column
curation minus the back-reference to the parent, cells come from the cell
registry, and a totals footer always shows the row count plus sums for any
explicitly-configured numeric columns (each formatted by that column's Cell).
Rows are fetched through the secured context, so only access-visible data shows.
Rows are read-only here — a row click navigates to the related record.
@opensaas/stack-coregains additive item-view config (no breaking changes):New
@opensaas/stack-uiexports:RelationshipTable,RelationshipTableClient,and the pure
deriveItemViewLayouthelper (withItemViewLayout,ItemViewArrangement,RelationshipTableSection). The Relationship table shipsnamed Slots (
relationship-table,relationship-table-toolbar,relationship-table-row,relationship-table-cell,relationship-table-footer)as extension seams for the follow-up inline-edit, create-drawer, and row-removal
work.
#774
62a1612Thanks @borisno2! - Bound the admin item-view Relationship tables with atakeand a "showing N of M" footerThe read-only Relationship tables on a record's edit page (issue #734) previously
fetched every related row unbounded. They now fetch a bounded page of related rows
and surface the full access-scoped total in the footer.
of related rows (
DEFAULT_ITEM_VIEW_TAKE, 10), overridable per relationship viaui.itemView.take. Rows are still fetched through the secured context, so onlyaccess-visible rows come back.
Showing N of M rows,where N is the rendered (bounded) count and M is the full access-scoped total,
fetched via a filtered
_countthat folds the related list's ownqueryaccessin (mirroring the list view's count columns). A fully-denied related list reads
Showing 0 of 0and never leaks a true total. The row count is always shown,including the zero-column footer path.
Core:
mergeIncludeWithAccessControlnow preserves a caller-suppliedtakeon ato-many relation include (it only narrows the fetch, never widening past the access
where), so the securedfindUnique/findManyinclude can bound related-row reads.#764
c210319Thanks [@list({](https://github.com/list({)! - Admin list view: to-many relationship columns render an access-visible count, sort by relation count, and filter by numeric count comparisons (issue #732). Virtual fields render via their Cell but are excluded from sorting and filtering.A to-many relationship used as a list column now shows the count of the related rows the session may see — fetched in the SAME query via a filtered Prisma
_count, with the related list'squeryaccess folded into the count'swhere, so it never counts rows the session cannot read and issues no per-row query. Clicking the column header sorts by relation_count, and its Filter spec offers numeric comparisons on the count (posts:>5) in the filter builder and in shared URLs.Because Prisma cannot compare a relation count in a
where, a to-many relationship's Filter spec emits a structured count marker that is resolved to an access-scoped{ id: { in } }before the query runs, through the secured context.New
@opensaas/stack-coreexports:buildRelationshipCountSelect,resolveRelationshipCountFilters,isToManyRelationshipField, andRELATIONSHIP_COUNT_FILTER_KEY(with theRelationshipCountFilterMarkertype).#788
613902cThanks @borisno2! - StandaloneListTablenow routes every cell through the shared cell registry (CellRenderer), matchingListView. The bespoke relationship renderer andfieldTypes[column] === 'relationship'branch are gone in favour ofRelationshipCell, which already handles link navigation andstopPropagation.A new optional
fieldOptionsprop letsselectcolumns resolve label mapping andui.variantbadge colour, exactly likeListView:Existing
ListTablecall sites keep working unchanged.Patch Changes
#780
55d55e0Thanks @borisno2! - Fix a to-one relationship filter token (e.g.author:Ada) leaking related-list data by ANDing the related list'squeryaccess filter into the nested condition instead of running it unscoped.#775
2fcb582Thanks @borisno2! - Exclude json columns from relationship-table inline editing so an unchanged json cell no longer wastes a secured update round-trip@opensaas/stack-auth@0.31.0
Patch Changes
be5772bThanks @borisno2! - Add a regression test locking the generated Session/Account user FK shape (no@@index([userId]),onDelete: Cascade) so future drift from better-auth parity is caught.@opensaas/stack-cli@0.31.0
Patch Changes
047487a,9cd06dd,b190813,f67cd79,dcb10e2,f8b6f02,c05701e,5a60291,2fcb582,85c7fc3,8199238,4d99e91,20459b5,62a1612,c210319,55d55e0,96e1067]:@opensaas/stack-storage@0.31.0
Patch Changes
#795
0e603b9Thanks @borisno2! - Add integration-style tests proving image()/file() reject unrecognised write value shapes end-to-end in bothdb.columns: 'keystone'(multi-column) and default single-column (JSON) modes, pinning the #789 fix as a regression guard.#776
030d540Thanks @borisno2! -StorageProvider.delete()is now documented as idempotent; the local provider swallowsENOENTon delete instead of throwing.@opensaas/stack-tiptap@0.31.0
Patch Changes
db7079aThanks @borisno2! - Bump@tiptap/pmand@tiptap/extension-placeholderto^3.28.0to match@tiptap/starter-kit, fixing a TypeScript build failure caused by duplicate@tiptap/pmversions in the dependency tree.@opensaas/stack-rag@0.31.0
@opensaas/stack-storage-s3@0.31.0