Skip to content
This page View Markdown Open in ChatGPT Open in Claude

Data

A page reads its data on the server, in its loader, next to the database. The browser gets HTML : no API of yours is called from it, no token of yours travels to it.

export async function loader(ctx: PageContext) {
    const [products, categories] = await Promise.all([
        ctx.db.query({ instance: 'mongodb', database: 'shop', table: 'products', find: { active: true }, sort: { name: 1 }, limit: 24 }),
        ctx.db.distinct({ instance: 'mongodb', database: 'shop', table: 'products', distinctField: 'category' }),
    ]);
    return { products, categories };
}

export default function Products({ data }: PageProps<typeof loader>) { … }
  • The loaders of the layouts and of the page run at the same time ; Promise.all reads several things at once inside one.
  • What a loader returns may be dates, ids, numbers : the page gets them as they are.
  • A loader may answer instead of the page : redirect('/login'), notFound(), or a Response (ctx.json(...)).

Four ways to reach data

What it reaches As whom
ctx.db The tables site.config.ts lists in data.read and data.write, with the row rule of each The website, under its rules
ctx.api The APIs of the account : tables (ctx.api.db) and custom APIs (ctx.api.customApi) The visitor, as a request of its browser would be : the access of each API, its hooks
ctx.sys g.sys : everything the account may do The account. Off unless data.allowSys turns it on
ctx.fetch Another server The hosts data.fetch lists

ctx.db : the tables a website lists

ctx.db takes the words of g.sys.db : getAll, getById, query, count, distinct, distinctQuery, aggregate, saveSingleOrMultiple, masterSave, arrayOperations, updateById, updateMany, replaceById, removeById, removeByQuery, and ctx.db.gen.* for tables without a schema. A call names its instance, database and table.

A website reaches no table until site.config.ts lists it. A call to any other table is refused with 403, whatever the code asks :

site.config.ts
export default defineSite({
    data: {
        read: [
            'mongodb/shop/products',
            // A rule : the columns a read gives.
            { table: 'mongodb/shop/categories', fields: ['_id', 'name', 'slug'] },
            // The rows of the visitor : added to every query, update and removal.
            { table: 'mongodb/shop/orders', where: { customer_id: '$user._id' }, requireUser: true },
        ],
        write: [
            { table: 'mongodb/shop/orders', where: { customer_id: '$user._id' }, requireUser: true, allow: ['create'] },
            // A sign-up : a visitor may add a customer, never change nor remove one ; the new one is a customer.
            { table: 'mongodb/shop/customers', allow: ['create'], set: { groups: ['customer'] } },
        ],
    },
});
Part of a rule What it does
table instance/database/table, as the account names them. instance/database/* for every table of a database.
where Added to the conditions of every read, update and removal ($and). '$user.<field>' is a field of the visitor who signed in.
set What every row the website saves gets, over what the page sent. where's equalities by default : with where: { customer_id: '$user._id' }, every saved order is the visitor's.
requireUser Only for a visitor who signed in (401 for the others). A rule which names $user asks for one anyway.
fields The columns a read gives : a select of the page is kept to them. Give one to a table of users, without its password.
allow For data.write : create, update, delete. All three by default.

What the server checks :

  • A call by id (getById, updateById, replaceById, removeById) finds the row, and checks it against the rule : a row of another visitor is not found (404).
  • deep reads other tables : each needs a rule of data.read. So does a path through a column which refers to another table (customer_id.email in select, find or sort).
  • $user._id is a value of the visitor's row, as a text (an ObjectId of MongoDB is its hex text).
  • A visitor without the field a rule names ($user.customer_id for a user with none) reaches nothing (403).

What a visitor sent is a value, never a query

Build find from what the visitor sent as values : find: { category: String(ctx.query.category) }. Never hand an object of the query or of a form to the database as it came : { $where: … } or { $ne: null } would be operators. For a search, escape the text of a regular expression : { name: { $regex: text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), $options: 'i' } }.

ctx.api : as the visitor

ctx.api.db takes the same words as ctx.db, but calls the APIs of the account as the visitor would : the access of each API (its groups, the tokens it asks for), the API user of the website (data.apiUser), the tokens of the visitor its auth providers gave, and the hooks of the API. Use it when the APIs already say who may do what.

// A custom API of the account, with the token of the visitor.
const quote = await ctx.api.customApi('/shop/quote', { method: 'POST', body: { items } });
const orders = await ctx.api.db.query({ instance: 'mongodb', database: 'shop', table: 'orders', limit: 10 });

ctx.sys : everything the account may do

ctx.sys is g.sys of your code : secrets, Redis, events, every table. It is off : data.allowSys: true in site.config.ts turns it on. A page with ctx.sys reads whatever its code asks, so check every value a visitor sends.

const key = await ctx.sys.system.getSecret('stripe.apiKey');

ctx.fetch : other servers

site.config.ts
data: { fetch: ['api.github.com', '*.example.com'] },
const answer = await ctx.fetch('https://api.github.com/repos/apimaker/docs', { headers: { accept: 'application/json' }, timeout: 5000 });
const stars = answer.json?.stargazers_count;

ctx.fetch returns { status, headers, body, json }. A host which is not listed is refused. Answers are 10 MiB at most ; a request waits 30 seconds at most (timeout in milliseconds asks for less).

ctx.cache : values kept between requests

The cache of the website, in Redis, its keys its own :

const rates = await ctx.cache.getOrSet('rates', 600, () => ctx.fetch('https://api.example.com/rates').then((answer) => answer.json));
await ctx.cache.set('banner', 'Free delivery today', 3600);
await ctx.cache.remove('banner');

For whole pages, see caching.

JSON files

A file of JSON of the website is a module : import posts from '@/data/posts.json'. It is read when the website is built, and shared by every request : sample data, settings, translations.

What not to do

  • Never keep ctx, a visitor's data or a row in a variable of a module : the module is shared by every visitor of the engine.
  • Never write $user values into a page kept for everyone (cache.scope: 'public') : the server does not keep a page which read cookies or headers, but it can not know what a loader wrote.