Base file-cell uploads were hitting a parallel POST /bases/files/upload
endpoint that re-implemented the multipart parse, the size limit
handling, and the spaceId resolution that the standard page-attachment
endpoint (POST /files/upload) already does. The two diverged on minor
points (no audit log, no attachmentId support, slightly different
permission check) without good reason — bases are pages (isBase=true),
so the existing endpoint already handles them correctly.
Server: delete uploadBaseFile and the now-unused BaseRepo injection.
Client: route the file-cell uploader through the existing uploadFile
helper in page-service. The base's pageId is a valid page id, so the
server's pageRepo.findById succeeds and pageAccessService.validateCanEdit
runs — which lines up with the Base edit ability at the space-role
level (Manage Page and Manage Base track together for admins/writers,
Read for readers).
A freshly-created inline-embed used to render with only the primary
"Title" column and zero rows — visually it looks more like a broken
widget than a database, so users always had to do at least three
manual setup steps before the embed conveyed its purpose.
BaseService.create now accepts an optional `defaults` arg for
`extraTextProperties` and `defaultRows`; both extras are inserted in
the same transaction as the page/property/view so a half-built base
can never slip out. The inline-embed controller passes
{ extraTextProperties: 2, defaultRows: 1 } so the embed lands as
"Title + Text 1 + Text 2", with one empty row ready to type into.
Standalone base creation goes through the same code path with no
defaults, so its existing single-Title-column shape is unchanged.
- Add isBase to page repo baseFields so it is always selected
- Add isBase to sidebar pages query select list
- Add isBase to client IPage type
- In PageContent, render <BaseTable pageId={page.id} /> when page.isBase
is true instead of the TipTap editor path
Postgres has no per-expression stats for `cells->>'uuid' ILIKE '%…%'`,
`search_text ILIKE`, or `search_tsv @@`, so EXPLAIN Plan Rows falls
back to a default selectivity and is off by orders of magnitude — a
contains filter on a 10k-row base was reporting ~150 against thousands
of real matches. Auto-route any request whose filter tree contains a
contains/ncontains/startsWith/endsWith op or a search term to the
capped-exact path, even when the caller asked for an estimate.
Per-cell useResolvedPages([id]) calls each mounted with a unique
React Query key, so a grid with 20 page-typed cells fired 20 requests on
first paint. A shared loader now accumulates incoming ids within a
microtask, fires a single POST for the union, and fans the subset each
caller asked for back to them. Cells keep their own cache entry + null
handling; they just share the underlying network call.
Also renames /bases/pages/resolve → /bases/pages/expand — the old name
collided with other "resolve" semantics in the codebase.
Row-count display on a filtered view shouldn't force a full COUNT(*) on
every list fetch. New endpoint returns either an EXPLAIN-plan estimate
(default, ~1ms, no execution) or a LIMIT-capped exact count that short-
circuits to `{ capped: true }` once the match set passes EXACT_COUNT_CAP.
Clients call it in parallel with the rows query so the grid still paints
at its own pace.
- DTO + repo.countEstimate/countExact reusing the list predicate shape
- service picks the mode; controller mirrors the list Read ability check
- client hook keyed by filter/search/exact so a "show exact" toggle
doesn't clobber the estimate cache
The withProperties subquery hydrating /bases/info was missing a
`deleted_at IS NULL` filter, so after a delete the socket-echo
invalidation refetched and the just-deleted column rehydrated on the
originating client and never dropped on others.