Records and queries
A record is one piece of your app's data: a to-do, a vote, a message. This page shows how to write records, change them, delete them, and find them again with a query that stays live as they change.
The examples use the todos collection from Building an app.
Writing a record
put makes a new record and gives it back:
const todo = await node.records.put(space.id, todos, { text: 'Buy milk', done: false });
todo.key; // 'akthxqofxox3iszhpi5j3zhl6a.mfrggzdfmztwq2lknnwg23tpoa'The record gets a fresh key. The key stays the same for the record's whole
life, so it is what you keep, read by and link to.
Pass the collection's definition, as here, and TypeScript checks the body as you write it. You can also pass a collection's name as a string, and the body is untyped.
Editing a record
update writes the record's next version:
await node.records.update(space.id, todo.key, { text: 'Buy milk', done: true });It takes the whole new body, not a patch. Every edit is a new version, and it syncs to everyone in the space.
Deleting a record
await node.records.delete(space.id, todo.key);A delete is a version too, marked deleted, and it syncs like any other.
Who may write
A write your account isn't allowed to make throws, with the reason in plain words, and nothing is signed. To ask first, for example to hide a button:
const mayEdit = await node.records.can(space.id, 'edit', todo.key);The action is 'create', 'edit' or 'delete'. For 'create', name the
collection instead of a key. In React, useCan(space, action, target) gives
the same answer.
Who may write what is set by the collection's rules and the space's roles. See Collections.
Links
A record can point at other records. Here a vote points at the poll it is
about, and linked finds what points at the poll:
const poll = await node.records.put(space.id, 'app.poll', { question: 'Where to?' });
await node.records.put(space.id, 'app.vote', { choice: 0 }, { links: [{ rel: 'about', to: poll.key }] });
const votes = await node.records.linked(space.id, poll.key, { rel: 'about' });rel is the link's role. The roles a record may carry are declared on its
collection (Collections).
update keeps a record's links unless you pass new ones.
Querying
A query asks for the records of one collection, filtered and sorted:
const { records } = await node.records.query(space.id, {
collection: todos,
where: { done: false },
sort: { '@createdAt': 'desc' },
limit: 20,
});A query is plain JSON, so you can store it, send it, or have an agent write it. A malformed query throws, saying what to fix.
Filters
where names fields and what they must be. A bare value means "equals":
await node.records.query(space.id, {
collection: todos,
where: { done: false, text: { $contains: 'milk' } },
});- A plain name is a field of the body (
text,address.city), or one of the collection's computed fields. - A name starting with
@is about the record itself:@key,@author,@root,@createdBy,@createdAt,@updatedAt,@seq. link:<role>is where the record's first link of that role points.
The operators are $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin,
$exists and $contains. Combine filters with $and, $or and $not.
$ne is true for a missing field. $contains is a case-insensitive
substring, or an element of a list. It is not full-text search.
Every operator and field is in The query format.
Sorting and paging
sort takes several fields, applied in order. Ties break on the key.
When there are more records than limit, the result has a cursor. Pass it
back to get the next page. It is null on the last one.
const page1 = await node.records.query(space.id, { collection: todos, limit: 20 });
const page2 = await node.records.query(space.id, {
collection: todos,
limit: 20,
cursor: page1.cursor ?? undefined,
});Following links
include brings related records along with each result. Here each poll comes
with its votes, and the number of reactions to it:
const { records } = await node.records.query(space.id, {
collection: 'app.poll',
include: {
votes: { rel: 'about', from: 'app.vote' },
reactions: { rel: 'about', from: 'std.reaction', count: true },
},
});
records[0]?.included.votes;By default an include finds records that link to this one
(direction: 'in'). direction: 'out' finds the ones this record links to.
Each include takes its own where, limit and nested include, or
count: true for just the number.
Typed results
Name collections by their definitions, in collection and from, and the
results are typed: bodies, includes and computed fields. where and sort
are typed too, so a field the collection doesn't have, or a value of the wrong
type, is a compile error. A collection named by a string is untyped.
While a space loads
The result's complete is false while a node that holds only part of a
space is still fetching a collection the query needs. Show "Loading…", not
"Nothing here".
Live results
watch runs a query now, and again whenever the space's records change, here
or on someone else's device:
const stop = node.records.watch(space.id, { collection: todos }, (result) => {
console.log(result.records.length, 'to-dos');
});
stop();In React, useQuery(space, query) does the same. useRecord(space, key)
keeps one record current, and useLinked(space, key, { rel }) what links to
it.
For anything else, subscribe to the node's events:
const unsubscribe = node.subscribe((event) => {
if (event.type === 'records') console.log('records changed in', event.space);
});What a record looks like
Every record the node gives you is a
NodeRecord: its key,
its body, its links, who wrote it and whether it checks out.
A query gives each as a
QueryRecord, which
adds what the collection's computed fields work out on this
device and what its include found.
Show root or createdBy as the author. author is the app's key that
signed, not the person. node.spaces.profiles(space) maps accounts to names.
Keys
A record's key says whose it is. Every key starts with its owner's part, a hash of the account, so no one else can create a record there.
Most records just take the fresh key put gives them. Some records there is
one of per account, like settings. For those, pass a name:
await node.records.put(space.id, 'app.settings', { theme: 'dark' }, { key: 'settings' });
const mine = await node.records.keyFor(space.id, 'settings'); // 'akthxqofxox3iszhpi5j3zhl6a.settings'
const theirs = await node.records.keyFor(space.id, 'settings', did); // someone else's, if they wrote one
await node.records.get(space.id, theirs);- The name goes in your account's namespace, so two accounts that both
choose
settingsget two records. - Names are 1–101 characters of
a–z,0–9and: . _ -. - Read and link records by their whole
key, asputgives it, not by the name. - A name is the same key in every collection, and a record keeps its
collection for life. So
putrefuses a name you already hold in another collection, deleted or not. Put the collection in the name (app.todo.settings) so two of your apps never clash. - Writing a deleted key brings the record back. Pass the whole key when it is already yours.
- A name of the form
one:<hex>is refused outside aonePercollection.
In a collection with a onePer rule, the node works out the key for you
from the parts the rule names, and node.records.onePerKey tells you what it
is. With @author the key is the writer's. Without, it is a shared one:…
key (Collections). In a private space it is keyed with
the space's key, so it says nothing to someone who can't read the space.
An account's line in a collection, for contracts, has the key
lineKey(account, collection), exported by @minca/core.
Every peer refuses a first version at a key the writer doesn't own. The rule is in spec 02 §4.1.
When two devices edit at once
Each edit raises the record's seq by one. The version with the higher seq
wins. On a tie, the version id decides. Clocks play no part, so two devices
that edited apart agree on the same winner.
Edits inside one record are not merged. Design records so that people edit different ones: one record per vote, per card, per reaction.
History
node.records.history(space, key) gives the versions this node keeps whole,
newest first. That is the current one, or every one in a collection defined
with history: 'all'. Otherwise an earlier version's body is forgotten once
it is replaced.