Skip to content

feat(plugins): add a Typesense driver with collection browsing and a request console (#2629) - #2648

Merged
datlechin merged 5 commits into
mainfrom
feat/typesense-driver
Sep 6, 2026
Merged

feat(plugins): add a Typesense driver with collection browsing and a request console (#2629)#2648
datlechin merged 5 commits into
mainfrom
feat/typesense-driver

Conversation

@datlechin

@datlechin datlechin commented Sep 6, 2026

Copy link
Copy Markdown
Member

Adds a Typesense driver plugin: collections browse and edit as tables, and a request console that takes a method, a path and a JSON body the way the Typesense docs write their curl examples. Registry-only, like Elasticsearch.

Fixes #2629

What the server actually does

Every number and rule the driver hard-codes was measured against a real Typesense server (29.0 and 26.0, both downloaded and run locally), not read out of the docs. Five of them changed the design:

Measured Consequence in the driver
per_page and limit cap at 250 hits, and TablePro's default page is 1,000 rows A page is split into 250-row searches. offset answers correctly past a million, so no cursor or point-in-time is needed, unlike Elasticsearch.
multi_search carries up to 50 searches; the 51st fails the whole batch The chunks of a page go over in one multi_search, so a 1,000-row page is a single round trip and a 100,000-row page is eight.
sort_by on a field whose schema says sort: false is a 400 that fails the whole search, id is never sortable, and at most 3 sort fields are accepted Sorts are filtered against the schema's sort flag and capped at 3. A column that cannot sort stays unsorted instead of blanking the grid.
filter_by backticks have no escape tag:=`odd` || year:>0 matched every document in the collection. A filter value containing a backtick is refused with a message.
A GET .../documents/search URL over 4,096 bytes answers 400, which an IN filter reaches at 265 values Every search goes over as a POST /multi_search body instead of a query string.

Three more shaped smaller decisions: a string field under >, <, >= or <= silently matches nothing rather than raising (so those are refused on a non-numeric field); a numeric or boolean field rejects a backticked literal (so quoting is driven by the declared type); and nested object fields are reported both as the object parent and as dotted leaves, alongside a .* entry on an auto-schema collection (so columns are the leaves, with the parents and the wildcard dropped and id prepended).

scripts/check-typesense-limits.sh re-checks all of that against a live server, so a future Typesense release cannot move one of these numbers silently.

Filter mapping

= != > >= < <= BETWEEN IN NOT IN CONTAINS NOT CONTAINS STARTS WITH all map onto filter_by. Six do not exist in Typesense at all: IS NULL, IS NOT NULL, IS EMPTY, IS NOT EMPTY, REGEX and ENDS WITH (which needs a field created with infix: true). Those are refused with a localized message naming the operator rather than silently returning the wrong rows.

String matching in Typesense always ignores case and nothing turns that off, so the plugin declares caseSensitivityStyle = .unsupported. .driverManaged would have left the filter bar's case toggle enabled over a choice the driver cannot honour.

Defects found in review and fixed here

Codex was out of usage limit, so a security reviewer and a correctness reviewer read the diff instead. Six findings, all in this branch's own code, all verified against the running server before fixing:

  1. API key exfiltration through the console (high). //attacker.example/x is an RFC 3986 network-path reference, so URL(string:relativeTo:) resolved it to a different host while the X-TYPESENSE-API-KEY header rode along; GET also classified .safe, so read-only mode and the MCP gate both passed it. Measured with a local listener: the admin key arrived. Every request now resolves through TypesensePathEncoding.resolve, which refuses anything leaving the connection's scheme, host and port. Re-measured: blocked, listener saw nothing, ordinary console requests unaffected. The same shape exists in the Elasticsearch driver on main and is listed below rather than changed here.
  2. Read-only and MCP gate bypass through the request body (medium). The classifier substring-scanned the whole statement, so POST /collections/c/documents/import carrying "note": "/multi_search" in a field value classified as a read. It now parses the header line, cuts the path at ?, and matches the path itself.
  3. Filter injection on numeric and boolean fields (medium). Those branches send the value unquoted and validated nothing, so year:=1900 || year:>0 reached the server as filter syntax, on a branch chosen from a server-declared field type. Both now parse-check the value.
  4. A collection named a/b was unreachable. Typesense accepts one; .urlPathAllowed let the slash through, so the driver asked for /collections/a/b and got a 404 while the collection listed in the sidebar. One shared path-segment encoder now encodes / and ., which also stops a document id of .. from ever acting as a path segment. Proven end to end: that collection now browses, counts and deletes.
  5. Every value of a nested object[] column rendered blank. Typesense reports variants.sku as a schema field but returns variants as an array of objects, and the flattener only walked dictionaries. A dotted path now reads across an array's elements, which is the same shape Typesense gives the leaf. Measured: ["A1","B2"] where the grid previously showed nothing.
  6. The driver never registered at all (found by taking the screenshot). The bundle did not declare TableProProvidesDatabaseTypeIds, so PluginManifest could not read its type without loading it, isDriverInstalled answered false, and picking Typesense in the form offered to download a plugin that was already installed. The app logs it: "declared no TableProProvides* capability keys in Info.plist; eager loading will block startup". Fixed and pinned by three tests that read the plist from the repository.

Feature scope, and the four things that were broken

An audit against the full PluginDatabaseDriver surface found that the driver was at Elasticsearch parity (24 of 251 requirements) and that four features the UI already offered did not work, because the app composes SQL whenever a driver declines to spell an operation itself:

Action What the app sent Before
Export a collection SELECT * FROM books refused by the console parser, while supportsExport said yes
Drop a collection DROP TABLE books refused
Truncate DELETE FROM books reached the server as a real DELETE /FROM books
Stream an export none streamRows was never implemented

All four now map to Typesense requests, measured against a live server: export streams GET /collections/:c/documents/export as JSONL and returned 1,200 rows in 3 batches with the right columns; truncate empties a collection and keeps its schema; drop removes the collection and leaves its neighbours; and DELETE FROM books is now refused instead of becoming an HTTP request, while a query string holding a space still parses.

Two Typesense-native surfaces were added on top:

  • Users & Roles lists API keys, since Typesense has no user accounts. Each key shows the collections it reaches and the actions it holds. Keys can be created and deleted; editing is deliberately unavailable, because Typesense has no endpoint that changes a key and its value is returned exactly once, so an edit would have to be a delete and a recreate that silently rotates the value every client is using.
  • Server Dashboard reads /metrics.json and /stats.json: system and process memory, fragmentation, disk, requests per second, search and write latency, pending write batches and cache hit ratio.

Compaction (POST /operations/db/compact) is reachable in the console but deliberately not published through supportedMaintenanceOperations: both of the app's maintenance surfaces are scoped to the selected table, and compaction acts on the whole database, so listing it there would offer a per-collection item that silently acts on everything.

Verification

  • verify.sh build, verify.sh generate: PASS.
  • verify.sh test over the new suites plus every suite that owns a type this change touches: 90 + 95 + 77 cases, all passing, including a test per finding above.
  • verify.sh plugins (AllPlugins): TypesenseDriverPlugin.tableplugin compiles, links and carries the right principal class and versions. The aggregate itself reports FAIL from oracle-nio's @TaskLocal macro under the local toolchain, a known local-only break unrelated to this change; the only two errors in the 8,744-line log are both in that package.
  • verify.sh lint: 0 violations.
  • docs/scripts/check-writing-style.sh and check-docs-against-source.py: both clean, including the engine count moving 29 → 30 across driver-counts.mdx, index.mdx and databases/index.mdx.
  • scripts/ci/check-plugin-manifest.py: 30 plugins agree with the manifest. shellcheck --severity=warning clean over the new script.
  • End to end against a live Typesense 29.0 server, driving the real TypesensePluginDriver from a swiftc harness: connect, collection list with document counts, columns (nested leaves flattened, parents dropped), browse, browse sorted on a sortable and on an unsortable column, filtered browse and filtered count, a 600-row page arriving as 600 unique rows in order across three chunked searches, an offset page, the backtick filter refused, IS NULL refused, the console over GET /collections, POST /multi_search, JSONL export and /health, and an insert / update / read-back / delete round trip.
  • The same probes run identically on Typesense 26.0, which is the floor the docs page states.

No UI automation: the flow needs a live Typesense server and a published registry binary, neither of which a deterministic UI test can assume.

Screenshot

docs/images/typesense-request-console.png and its dark twin are real shots of the driver running against a local Typesense 29.0, captured at the canonical 3024x1722, not placeholders. Driving the app to get them is what surfaced finding 6.

https://claude.ai/code/session_01LikBUDFCMFdFRg1iZ9WWcw

@mintlify

mintlify Bot commented Sep 6, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
TablePro 🟢 Ready View Preview Sep 6, 2026, 2:28 AM

💡 Tip: Enable Automations to automatically generate PRs for you.

@datlechin

Copy link
Copy Markdown
Member Author

The docs screenshots are now real shots of the driver, captured against a local Typesense 29.0 at the canonical 3024x1722.

Request console, light and dark

Typesense request console, light

Typesense request console, dark

A POST /multi_search with filter_by: "year:>1960 && in_print:true" and sort_by: "year:desc", answered in 6 ms and rendered as a grid of 8 documents. It shows three things the diff claims: the single-search multi_search renders as a grid rather than raw JSON, columns come from the collection schema with id first, and the sidebar groups objects under Collections.

Driving the app to take these is what found the sixth defect: the plugin loaded but never registered its type, so the form offered to download a plugin that was already installed. The connection form itself is worth a look too, since it is the only place the hidesPassword + hidesUsername pair is visible: Typesense has no user accounts, so API Key replaces both credential rows, and there is no Database field and no SSH or tunnel pane.

@datlechin
datlechin merged commit e74f1ba into main Sep 6, 2026
9 checks passed
@datlechin
datlechin deleted the feat/typesense-driver branch September 6, 2026 04:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Database request: Typesense

1 participant