Skip to content

Querying data

vaif.from(table) returns a query builder for one table. Filters, sorting and paging chain; the method at the end (list(), get(), create() and so on) sends the request to the data API at /v1/data/{projectId}/{table}.

Give the builder a row type to get typed results:

import { createVaifClient } from "@vaifstudio/client";
const vaif = createVaifClient({
baseUrl: "https://api.vaif.studio",
projectId: process.env.VAIF_PROJECT_ID,
apiKey: process.env.VAIF_PUBLISHABLE_KEY,
});
type Post = {
id: string;
title: string;
author_id: string;
published: boolean;
views: number;
created_at: string;
};
const posts = vaif.from<Post>("posts");
// Many rows
const { data, count } = await posts.eq("published", true).orderBy("created_at", "desc").limit(20).list();
// One row by primary key (throws VaifNotFoundError if it does not exist)
const { data: post } = await posts.get("0b9d3f4e-6c1a-4d2b-9a7e-2f1c8e5d3a10");
// The first match, or null
const newest = await posts.orderBy("created_at", "desc").first();
// Exactly one row, as { data, error } instead of throwing
const { data: one, error } = await posts.eq("title", "Hello").maybeSingle();
console.log(data.length, count, post.title, newest?.title, one, error);

list() returns { data, count, hasMore }. It returns 50 rows unless you set limit(), and at most 1000. single() returns an error when no row or several rows match; maybeSingle() returns data: null when no row matches.

Method Matches rows where
.eq(column, value) the column equals the value
.neq(column, value) the column does not equal the value
.gt(), .gte(), .lt(), .lte() the column is greater than, at least, less than, or at most the value
.like(column, pattern) the column matches a SQL LIKE pattern (% is a wildcard)
.ilike(column, pattern) the same, ignoring case
.in(column, values) the column is one of the values
.where(filter) any filter, including AND and OR groups

Chained filters are combined with AND. Use .where() for OR:

const { data: popularOrMine } = await posts
.where({
OR: [
{ field: "author_id", operator: "eq", value: "user-1" },
{ field: "views", operator: "gte", value: 1000 },
],
})
.list();
console.log(popularOrMine);
// Sort by several columns
const sorted = await posts.orderBy("published", "desc").orderBy("created_at", "desc").list();
// Page numbers
const page2 = await posts.orderBy("created_at", "desc").paginate({ page: 2, pageSize: 20 });
// Offsets
const next = await posts.limit(20).offset(40).list();
// Only some columns
const titles = await posts.select("id", "title").list();
console.log(sorted.data, page2.pageInfo, next.data, titles.data);

paginate() returns the rows and pageInfo: total, page, pageSize, pageCount, hasNextPage and hasPrevPage.

const { data: created } = await posts.create({
title: "Hello",
author_id: "user-1",
published: false,
views: 0,
created_at: new Date().toISOString(),
});
const { data: updated } = await posts.update(created.id, { published: true });
await posts.delete(created.id);
// Insert, or update the row with the same title if one exists
await posts.upsert(
{ title: "Hello", author_id: "user-1", published: true, views: 0, created_at: new Date().toISOString() },
{ conflictFields: ["title"] }
);
console.log(updated.published);

upsert() updates every column except the conflict columns, which must have a unique constraint. For many rows at once, see Batch operations.

const total = await posts.count();
const published = await posts.eq("published", true).count();
const totalViews = await posts.sum("views");
const averageViews = await posts.avg("views");
console.log(total, published, totalViews, averageViews);

min() and max() work the same way. Each call computes one aggregate, and takes equality filters only.

These methods exist on the builder but throw a VaifError with the code NOT_SUPPORTED, because the data API does not offer them:

Method Use instead
.distinct() Remove duplicates in your code
.aggregate([...]) (several at once) count(), sum(), avg(), min() and max(), one at a time
.cursorPaginate() paginate({ page, pageSize })
.raw(), .transaction() See Batch operations

numeric and bigint values can arrive as strings, because they can be larger than JavaScript numbers hold exactly. Register the column types, and the client converts them to number, or to BigInt when a value does not fit:

vaif.setColumnTypes("orders", { amount: "numeric", item_count: "bigint" });

Every read and write goes through your project’s row rules. Send the signed-in user’s access token, so rules that depend on the user apply.