Remote / foldkit-remote-server

Queries

A Query Source answers the ordered connection itself. The client's select travels with the request, and the handlers return the selected fields of the page's items with the edges, so one response carries both:

ts
import { Query } from 'foldkit-remote'

const ProjectsByOwner = Query.make('ProjectsByOwner', {
  Input: Schema.Struct({ ownerId: Schema.String }),
  Result: Query.connection(Project),
})

const ProjectsByOwnerSource = RemoteServer.query(
  ProjectsByOwner,
  ({ input, window, principal, select }) =>
    projectsPage({ ownerId: input.ownerId, window, principal }),
)

const Server = RemoteServer.make({
  entities: [ProjectSource],
  queries: [ProjectsByOwnerSource],
})

A Query returns:

ts
{
  edges,
  start,
  end,
}

A Source answers membership only; it may ignore select. The handlers fetch the selected fields through the entity sources (authorization included) and return them as entities/settled beside the page. Query membership and entity data stay separate on the way in and travel together on the way out.

start and end are Remote Boundary values (Terminal, Cursor, or Unknown). The client uses those explicit boundaries when merging pages instead of guessing from row count.

Query input is decoded through the Query's own Schema before your callback runs. The callback receives the decoded input, the requested QueryWindow, the select slice when the client sent one, and the principal.

Running a query body over rows you already hold

A descriptor declared with Query.define carries its meaning as a value, and evaluate runs that value directly:

ts
import { evaluate } from 'foldkit-entity'

evaluate(PostsBySlug.body!, { slug: 'intro' }, rows)
// the matching rows, in the order the body asks for

It lives in foldkit-entity, beside the IR it interprets, because it is that IR's reference semantics rather than anything this package does — and because a client judging rows it already holds needs it without depending on a server package. This package re-exports it, so importing it from either place means the same thing.

It is pure — it reads the rows it is given and nothing else — and it is the reference the compiled interpreters are checked against. foldkit-remote-drizzle turns the same body into SQL; the test suite runs both over the same rows in a real SQLite and requires the same ids in the same order. That is what makes a body source-neutral in fact rather than in principle.

It follows SQL, not JavaScript. The case that matters is null: null = null is unknown in SQL and matches no row, where JavaScript would call the two equal. A comparison keeps a row only when it is true, so a null on either side drops it.

What it refuses rather than guesses:

  • Ordering by a column that is null in some row. SQLite sorts nulls first, Postgres sorts them last for asc: the databases disagree with each other, so there is no answer to be conformant to and picking one would make this wrong against the other.
  • Comparing values it has no order for, such as a number against a string.
  • Searching text that holds a NUL character, which Postgres text cannot hold and SQLite's like stops at.

Each throws where the query runs, naming the query. An ordering is checked before anything is sorted: every term over every matched row, in row order, so the error names the first row that breaks it (number and string when a later row holds text), the same on every engine, whichever pair a sort would have compared first. One matched row, or none, has nothing to order and is never refused.

Checking an interpreter against the semantics

evaluate is the reference, and cases is what it is a reference to: the conformance suite, exported so an interpreter written elsewhere can be held to the same semantics.

ts
import { Subject, cases, rows } from 'foldkit-remote-server'

for (const { what, body, input, expected } of cases) {
  expect(yourInterpreter(body, input, rows).map(row => row.id)).toEqual(expected)
}

Every case is chosen to make interpreters disagree, which is the only kind that tests anything: mixed case, nulls on both sides of a comparison, % and _ as literal text, the empty search, a predicate compared to a boolean either way round. That is not caution for its own sake — contains reached a released package meaning three different things because every value in the original fixture happened to be lowercase.

Both interpreters here run it: this package over rows in memory, and foldkit-remote-drizzle compiled to SQL against a real SQLite.