@vantis/data
v0.2.0
Published
Vantis Data SDK — typed, server-only runtime client for the per-block Data API
Readme
@vantis/data
Store and query your app's data with full TypeScript types — no SQL, no connection strings.
import { data } from '@vantis/data';
await data.from('posts').insert({ title: 'Hello world' });
const { data: posts } = await data.from('posts').select('*');That's a real, type-checked query. Here's how to get there.
Quickstart
1. Install
npm install @vantis/dataZero dependencies.
2. Describe your data in vantis.schema.json:
{
"version": 1,
"tables": [
{
"name": "posts",
"columns": [
{ "name": "id", "type": "uuid", "primary_key": true, "default": { "kind": "builtin", "builtin": "gen_random_uuid()" } },
{ "name": "title", "type": "text", "nullable": false },
{ "name": "body", "type": "text", "nullable": true },
{ "name": "created_at", "type": "timestamptz", "nullable": false, "default": { "kind": "builtin", "builtin": "now()" } }
]
}
]
}3. Generate types
vantis data buildAdd the generated file to your tsconfig.json include array. Now every query is type-checked — wrong column names and missing required fields are compile errors, before you run anything.
Editor autocomplete while writing the schema: point your editor at the bundled JSON schema in
.vscode/settings.json:{ "json.schemas": [{ "fileMatch": ["vantis.schema.json"], "url": "./node_modules/@vantis/data/vantis.schema.schema.json" }] }
Read
const { data: posts, error } = await data.from('posts').select('*');Filter, order, and page:
const { data: recent } = await data.from('posts')
.select('id, title, created_at')
.eq('published', true)
.order('created_at', { ascending: false })
.limit(20);Pull in related rows — the shape follows your foreign keys: a single relationship
comes back as an object (null if it can be absent), and the "many" side of a
relationship comes back as an array:
const { data: withAuthor } = await data.from('posts').embed('users');
// withAuthor[0].users — the author object (or null if the post has no author)Write
// Insert — omit a required field and it won't compile
await data.from('posts').insert({ title: 'Hello world' });
// Update the rows your filters match
await data.from('posts').eq('id', postId).update({ title: 'Edited' });
// Delete the rows your filters match
await data.from('posts').eq('id', postId).delete();Chain .select() after a write to get the affected rows back:
const { data: created } = await data.from('posts').insert({ title: 'Hi' }).select('*');Change a value safely
Increment a counter, toggle a flag, or merge JSON in one atomic call — no read-then-write, no race conditions:
import { data, increment, toggle } from '@vantis/data';
await data.from('posts').modify({ views: increment(1) }, { id: postId });
await data.from('posts').modify({ pinned: toggle() }, { id: postId });Available helpers: increment, decrement, multiply, divide, min, max, toggle, serverTimestamp, concat, merge, jsonSet, setOnInsert.
Add a guard to change a row only when a condition still holds:
const res = await data.from('accounts').modify(
{ balance: decrement(10) },
{ id: accountId },
{ guard: { balance: { gte: 10 } } },
);
if (!res.error && res.data?.length === 0) {
// guard missed — balance was too low, nothing changed
}Or create the row on first touch and modify it every time after — no separate "does it exist yet?" check, and no need to repeat the id you already gave in the second argument:
await data.from('counters').modify(
{ count: increment(1) },
{ id: 'visits' },
{ upsert: {} }, // row doesn't exist yet → created with count starting at 1
);guard and upsert are mutually exclusive — use one or the other.
Search
For tables you mark searchable, full-text search is one call:
const { data: hits } = await data.from('posts').search('climbing gear');Call your functions
const { data: result } = await data.run.transfer({ from, to, amount });When something goes wrong
Every call returns { data, error } — no try/catch needed. Check error first:
const { data: post, error } = await data.from('posts').select('*').eq('id', postId).limit(1);
if (error) {
// error.message tells you what went wrong; error.code is a stable code you can branch on
return;
}
use(post);Keep your token on the server
@vantis/data reads its URL and token from your environment. The token grants access to your data — use it only on the server, never in the browser. Importing @vantis/data in browser code throws, on purpose.
The token is read when you run a query, not when you import — so server builds that bundle before your environment is set (like next build) work fine.
Need full control?
@vantis/data gives you a fast, typed, opinionated path. If you need raw database access or a different engine, run your own service on Vantis instead.
License
Apache-2.0
