npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@surrealguard/client

v0.5.3

Published

Typed SurrealQL client — literal queries resolve their result and params from generated types.

Readme

@surrealguard/client

A typed SurrealQL client. You write real SurrealQL; surrealguard generate analyses it against your schema and emits the types.

const people = await db.run(allPeople);
//    ^? Array<{ id: RecordId<"person">; name: string; age: number }>

No query builder, no second language, no manual db.query<Person[]>(...) cast.

How it works

surrealguard generate scans your source for query text, analyses each query against your .surql schema, and writes one file:

declare module "@surrealguard/client" {
  interface SurqlRegistry {
    "SELECT id, name FROM person WHERE team = $team": {
      result: [Array<{ id: RecordId<"person">; name: string }>];
      params: { team: RecordId<"team"> };
    };
  }
}

A registry keyed by exact query text, read by one conditional generic. That is the whole mechanism. Two rules keep it honest:

  • There is no permissive string overload. A literal is also a string, so a fallback overload would rescue every mis-call into unknown. There is none.
  • A miss degrades to unknown, never any. Asserted in test-d/.

Install

npm install @surrealguard/client surrealdb

surrealguard.toml:

[sources]
schema  = ["schema/**/*.surql"]
queries = ["queries/**/*.surql"]
surrealguard generate --out src/lib/surrealguard.generated.ts

A query is a value

Query text lives in exactly one place:

// src/lib/queries.ts
import { defineQuery, defineLive } from "./surrealguard.generated";

export const allPeople  = defineQuery("SELECT id, name, age FROM person");
export const peopleOf   = defineQuery("SELECT id, name FROM person WHERE team = $team");
export const addPerson  = defineQuery("CREATE person SET name = $name, joined = $joined");
export const livePeople = defineLive("SELECT id, name, age FROM person");
export const liveTeam   = defineLive("SELECT id, name FROM person WHERE team = $team");

defineQuery infers the string literal and reads the registry exactly as db.query does — same guarantee, same single conditional generic. The gain is that the literal is written once, so an SSR seed and the component that consumes it cannot drift apart byte-for-byte and silently miss the cache.

A query text the registry does not contain is a hard compile error, because it means exactly one thing — the generated file is stale:

TS2345: Argument of type 'SurqlError<"this query is not in the generated
  registry - run `surrealguard generate`">' is not assignable to …

That also catches the case where reformatting a query changed its key. defineQuery.unchecked("…") opts out and degrades to unknown[].

The client

// src/lib/db.ts
import { createClient } from "./surrealguard.generated";

export const db = createClient({
  url: "ws://localhost:8000/rpc",
  namespace: "app",
  database: "app",
});

The connection opens lazily, on first use, so a module-level db is safe and no route has to remember to await db.connect(...).

// One-shot. Single-statement queries resolve to their rows — no destructure.
const people = await db.run(allPeople);
const red    = await db.run(peopleOf, { team: new RecordId("team", "red") });

// Multi-statement queries keep the per-statement tuple. Nothing is hidden.
const [names, ages] = await db.run(twoStatements);

// A scalar result is a scalar.
const count = await db.run(peopleCount);   // ^? number

// Live, in vanilla JS, with no other package.
const stop = db.watch(livePeople, (rows) => render(rows));
//    ^? rows: Array<{ id: RecordId<"person">; name: string; age: number }>

// Writes, and telling the reactive layer about them.
await db.run(addPerson, { name: "ada", joined: new Date() });
await db.invalidate(allPeople);
await db.invalidate(peopleOf.with({ team }));   // just that binding

Params are required exactly when the query reads them, and forbidden when it does not:

await db.run(peopleOf);                      // TS2554: missing params
await db.run(allPeople, { team });           // TS2554: this query takes none
await db.run(peopleOf, { team: "team:red" }); // TS2322: string is not RecordId

Values are the SDK's values

This is the part most likely to surprise you, and it is deliberate. The SDK decodes SurrealQL values as its own classes, so that is what the generated types say:

| SurrealQL | TypeScript | | --- | --- | | record<team> | RecordId<"team"> | | datetime | Date | | duration | Duration | | uuid | Uuid | | decimal | Decimal |

row.id is a RecordId, not a string — row.id.startsWith(...) is a compile error rather than a runtime one. datetime is a native Date because createClient sets codecOptions.useNativeDates.

It matters more for writing than for reading. A RecordId parameter encodes to a record link on the wire; a plain string encodes to a SurrealQL string:

encode(new RecordId("team","red"))  ->  c8 82 …   (CBOR tag 8: a record link)
encode("team:red")                  ->  68 …      (an untagged text string)

So WHERE team = $team matches with the class and returns nothing without it.

Construct params from the generated import:

import { RecordId } from "./surrealguard.generated";
await db.run(peopleOf, { team: new RecordId("team", "red") });

Crossing a serialisation boundary

Class instances do not survive SvelteKit's load (devalue) or a React Server Component's props. Json<T> is the projection that does — it is the SDK's own Jsonify, so the mapping is theirs:

const rows = await db.runJson(allPeople);
//    ^? Array<{ id: `person:${string}`; name: string; age: number }>

db.runJson, preload, and the whole reactive layer (@surrealguard/query, @surrealguard/svelte, @surrealguard/next) are Json<T>-shaped for this reason. db.run is not.

Errors

import { SurrealGuardError } from "./surrealguard.generated";

try {
  await db.run(peopleOf, { team });
} catch (error) {
  if (error instanceof SurrealGuardError) {
    console.error(error.query, error.params);
    // The SDK's own typed error is kept, not flattened:
    if (error.cause instanceof AuthenticationError) redirectToLogin();
  }
}

Escape hatches

db.surreal                                   // the raw SDK instance
db.surreal.query(surql`SELECT * FROM ${t}`)  // fully dynamic, SDK-typed
await db.query("SELECT name FROM person")    // literal form -> the statement tuple
fromSurreal(existingSurreal)                 // wrap a connection you already own

db.query(text, params) is the lower-level form: it returns the full per-statement tuple and never unwraps. db.run is the same guarantee one level up.

If you use fromSurreal, construct the Surreal with codecOptions: { useNativeDates: true } — that is the one thing createClient does for you which cannot be recovered afterwards.

Why compose the SDK instead of extending it

class SurrealGuardClient extends Surreal does not compile. The SDK already owns run (RPC function invocation), subscribe (the event emitter) and invalidate — and Surreal.invalidate() logs the session out. tsc reports TS2416 on each. So the SDK instance lives at db.surreal, the session methods worth having (use/signin/signup/authenticate/close) are forwarded, and the data vocabulary is ours.

API

| Export | | | --- | --- | | createClient(options) | the client; connects lazily | | fromSurreal(surreal) | wrap a Surreal you already own | | defineQuery(text) / defineLive(text) | name a query; .unchecked opts out of the registry | | db.run / db.runJson / db.runLiveOnce | execute | | db.watch(live, onRows, onError?) | subscribe; returns an unsubscribe | | db.invalidate(...queries) | tell the reactive layer a write happened | | db.query(text, params?) | the literal form; per-statement tuple | | preload(db, query) | server-fetched, serialisable, self-describing | | SurrealGuardError | { query, params, cause } | | RecordId / Uuid / Duration / Decimal | the SDK value classes | | Json<T> / Rows<R> / SurqlQuery / SurqlLive / Preloaded<T> | types |

Licence

MIT OR Apache-2.0