Skip to content

Reading

matter.table(name) gives a table of the app’s space, by name. Each read goes through one of its indexes.

const deals = matter.table('Deals')
const page = await deals.query({ index: 'by_stage', eq: { Stage: 'Lead' }, limit: 50 })
// { rows: [{ id, title, values, createdBy, createdAt, updatedAt, cursor }], nextCursor, prevCursor }
await deals.count({ index: 'by_owner_stage', eq: { Owner: '$viewer', Stage: ['Lead', 'Won'] } })
await deals.exists({ index: 'by_email', eq: { Email: 'sam@acme.com' } })
await deals.groupCount({ index: 'by_stage', by: 'Stage' }) // { groups: [{ values: { Stage: 'Lead' }, count }], more }
await deals.ids({ index: 'by_stage', eq: { Stage: 'Won' } }) // { ids, nextCursor }
await deals.get(id)
await deals.getMany(ids)
Read What it gives
query(read) A page of rows, with cursors for the next and previous pages.
count(read) How many rows match. Without an index, every row of the table.
exists(read) Whether any row matches.
groupCount({ index, eq?, by }) Rows per value of the index’s match fields named in by. eq fixes the match fields before them. more says there were more groups than the limit.
ids(read) Row ids in index order, with a cursor. Cheaper than query when you only need ids.
get(id), getMany(ids) Rows by id. get gives null for a row that doesn’t exist.

exists, groupCount and ids need an index.

  • index: the index to read through, by name.
  • eq: a value for every match field of the index. A list means “any of these”, and null matches an empty value.
  • range: { field, gt, gte, lt, lte }: bounds on the index’s first order field.
  • filter: [{ field, operator, value }]: checks on the rows the index finds.
  • order: 'asc', the index’s own order (the default), or 'desc', its reverse.
  • limit: up to 1,000. The default is 50 for query and 100 for ids and groupCount.
  • after and before: a row’s cursor, to read the rows after or before it.
  • until: a row’s cursor, to read up to and including it. Use it to re-read a page exactly as it grows.

Filter operators are is, is_not, contains, not_contains, is_empty, is_not_empty, gt, gte, lt, lte, before, after, on_or_before and on_or_after.

Anything a read doesn’t recognise is an error, not ignored. A misspelt option would otherwise read more than you meant.

A read without an index scans the table in sort order:

await deals.query({
sort: [{ field: 'Amount', direction: 'desc' }],
filter: [{ field: 'Stage', operator: 'is', value: 'Lead' }],
})

That only works for a small table, one under 10,000 rows. Once a table grows past that, it’s read through the server: a sort, or a filter without an index, is refused, and a read without an index pages through the table in its own order. See Small and large tables.

Rows give values by field name, the same as in Matter’s tables:

  • options by name;
  • people as { id, name }, and '$viewer' means the member reading;
  • linked rows as { id, title };
  • dates as YYYY-MM-DD;
  • attachments as { id, name, size, mime_type }.

A person or relation field that holds one value gives one object; one that allows several gives a list. Empty values are left out of values.

In eq, range and filter, people can be given by id, by name or as '$viewer', and linked rows by id or title. An option must exist: a read that names one the field doesn’t have fails, and lists the options.

Page with cursors:

let after
do {
const page = await deals.query({ index: 'by_stage', eq: { Stage: 'Lead' }, limit: 200, after })
handle(page.rows)
after = page.nextCursor
} while (after)

To keep a page live as rows arrive, use live reads.