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.allreads 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 aResponse(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 :
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). deepreads other tables : each needs a rule ofdata.read. So does a path through a column which refers to another table (customer_id.emailinselect,findorsort).$user._idis a value of the visitor's row, as a text (anObjectIdof MongoDB is its hex text).- A visitor without the field a rule names (
$user.customer_idfor 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.
ctx.fetch : other servers¶
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
$uservalues 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.