Remote / foldkit-remote-server
A backend held in memory
RemoteServer.memory serves rows you give it through the same handlers a
real server uses, so a client sees what production would send: only the
fields asked for, and nested relations resolved in one read. Use it for a
first run, a test, a story or a demo, when there is no database yet.
const backend = RemoteServer.memory({
domain: Data, // or the definition from Remote.define
rows: {
User: [{ id: 'u1', name: 'Ada' }],
Project: [{ id: 'p1', name: 'Apollo', status: 'active', owner: 'User:u1' }],
},
// Optional: each mutation writes through the store, and the next read sees it.
mutations: store => [
RemoteServer.mutation(RenameProject, ({ input }) => {
store.write('Project', input.id, { name: input.name })
return Effect.succeed({
output: { id: input.id },
entities: [Remote.patch(Project, input.id, { name: input.name })],
})
}),
],
})
backend.layer // provide it where the real RemoteClient would gobackend.server is the server definition the layer answers through. Serve
it over a real transport to try a client against HTTP, or to test another
server against it:
const served = RemoteRpc.toLayer(RemoteServer.handlers(backend.server, undefined))- Rows are in their wire shape: a relation is its ref key (
'User:u1'), and a to-many relation is a list of them. - A query declared with
Query.defineis answered by running its body over the rows withevaluate, the reference interpreter, and paged with offset cursors. A query with no body has nothing to run and is refused when the backend is made, unless you pass a source for it inqueries. - Every field is readable unless
authorizenames the entity; see below. It pushes no live changes, and it is not a server to deploy.
To try authorization without a database, give authorize per entity name,
the function RemoteServer.entity takes, and the principal the layer
answers as. The principal's type is inferred from them, as RemoteServer.make
infers it from its sources:
const guarded = RemoteServer.memory({
domain: Data,
rows: { Project: [{ id: 'p1', name: 'Apollo', privateNotes: 'Internal' }] },
principal: { isAdmin: false },
authorize: {
Project: (principal: Principal, fields) =>
fields.filter(field => field !== 'privateNotes' || principal.isAdmin),
},
})
// The layer reads as a non-admin; the server answers as anyone.
const asAdmin = RemoteServer.handlers(guarded.server, { isAdmin: true })A withheld field is settled, as from any server: the client stops asking for
it, and a selection that needs it fails as Unavailable. A name in
authorize the domain does not declare is refused when the backend is made.
filter takes the same per-entity form. readable decides which rows a
principal may read, once per read, given each row's stored values; its second
argument looks another stored row up by id, for a rule that follows a
relation:
// Only an admin reads projects.
const adminsOnly = RemoteServer.memory({
domain: Data,
rows: { Project: [{ id: 'p1', name: 'Apollo', privateNotes: 'Internal' }] },
principal: { isAdmin: false },
readable: (principal: Principal) =>
Effect.succeed(entity => principal.isAdmin || entity !== 'Project'),
})A row it may not read is absent from a read by id (the client reads it as
not found) and from every query, and a ref to it is dropped from a relation:
a list leaves it out and a one ref reads null, as a database binding's
visible hides it.
An entity no row backs, answered from who is asking (foldkit-guard's
Me, say), is a Source of its own beside the rows, in entities. Leave it
out of the rows' domain: a Source for an entity the domain declares is
refused when the backend is made, as two Sources for one entity are.
const Whoami = Entity.make('Whoami', Schema.Struct({ id: Schema.String, admin: Schema.Boolean }))
const WhoamiSource = RemoteServer.entity<Principal>(Whoami, {
read: ({ ids, principal }) =>
Effect.succeed(ids.map(id => ({ id, values: { admin: principal.isAdmin } }))),
})
// No principal: served through `serveFetch` or `handlers`, as each request's.
const { server } = RemoteServer.memory({
domain: Data, // Project only; Whoami is the Source's
rows: { Project: [{ id: 'p1', name: 'Apollo', privateNotes: 'Internal' }] },
entities: [WhoamiSource],
})
const asVisitor = RemoteServer.handlers(server, { isAdmin: false })principal is only who layer answers as. Without it (and a principal type
that does not admit undefined), the backend has no layer, only its store
and server, so nothing can read the rows as nobody.