SurrealX
A strongly typed SurrealDB client.
SurrealX is a CLI and library that generates a strongly typed client for SurrealDB queries
from your running Surreal database. SurrealX extends the basic Surreal instance
from the surrealdb package with X
variants (e.g. select
becomes selectX
)
that is aware of your active tables in your database.
Furthermore it provides a very basic migration setup. The SurrealDB team is working on a built-in migration tool, so our migration tool is only prelimenary, and should probably not be used in production.
Example
Say you have made the following queries to your Surreal database (possible created with our migration tool)
-- Schemaless table
CREATE post SET title = "My first post";
-- Schemafull table
DEFINE TABLE user SCHEMAFULL;
DEFINE FIELD age ON TABLE user TYPE int ASSERT $value != NONE;
DEFINE FIELD name ON TABLE user TYPE object;
DEFINE FIELD name.first ON TABLE user TYPE string ASSERT $value != NONE;
DEFINE FIELD name.last ON TABLE user TYPE string;
DEFINE FIELD comments ON TABLE user TYPE array;
DEFINE FIELD comments.* ON TABLE user TYPE object ASSERT $value != NONE;
DEFINE FIELD comments.*.id ON TABLE user TYPE string ASSERT $value = /^comment:.*/;
DEFINE FIELD comments.*.title ON TABLE user TYPE string;
And then generate the client lib with surrealx generate --output gen.ts
.
Then you will have a fully typechecked client lib that can do the following, where the tablenames table records, update statements and so on are type checked.
// gen.ts
import { Post, SurrealX, User } from './gen.ts';
/**
* type Post = Record<string, unknown>;
*
* type User = {
* age: number;
* comments?: {
* id?: string;
* title?: string;
* }[];
* name?: {
* first: string;
* last?: string;
* };
* };
*/
// SETUP
const db = new SurrealX('http://127.0.0.1:8000/rpc');
await db.signin({ user: 'root', pass: 'root' });
await db.use('test', 'test');
// selectX and selectAllX
await db.selectAllX('user'); // type: User[]
await db.selectAllX('user:123'); // typeError
await db.selectAllX('user:123', ['name.first', 'age', 'id']); // type: { age: number, name?: { first: string } }[]
await db.selectX('user:123'); // type: User | undefined
await db.selectX('user'); // typeError
await db.selectX('post:123'); // type: Record<string, unknown>
// createX, with type checked data insert
await db.createX('user', { age: 20, name: { first: 'Ben' } }); // type: User
// updateX and updateAllX, with type checked data insert
await db.updateX('user:123', { age: 20, name: { first: 'Ben' } }); // type: User
await db.updateAllX('user', { age: 20, name: { first: 'Ben' } }); // type: User[]
// changeX and changeAllX, with type checked data insert (there are deep partial)
await db.changeX('user:123', { name: { first: 'Ben' } }); // type: User
await db.changeAllX('user', { name: { first: 'Ben' } }); // type: User[]
// deleteX, with type checked table name, like the others
await db.deleteX('user:123'); // type; void
// modifyX, modifyAllX
await db.modifyX('user:123', [{ op: 'replace', path: '/age', value: 20 }]);
// You can always remove the `X` from the end of the method, which will use the built in Surreal method
Docs
You can either use surrealX as a CLI or a library (the bin is located in ./bin/mod.ts
and the library is exported from ./mod.ts
). The usage is very similar for both, so these docs show the CLI usage:
You can see how to use the CLI by running
deno run https://deno.land/x/surrealx/bin/mod.ts --help
Which will yield the following
surrealx <cmd> [options]
Commands:
surrealx migrate <subcommand> Group of commands for creating and running migr
ations
surrealx generate Generate a SurrealDB client from the database
surrealx database Group of commands for interacting with the data
base
Options:
--version Show version number [boolean]
--url The url with which to connect with SurrealDB
[string] [default: "http://127.0.0.1:8000/rpc"]
--token The token with which to connect with SurrealDB[string]
-u, --user [string] [default: "root"]
-p, --pass, --password [string] [default: "root"]
--ns, --namespace [string] [default: "test"]
--db, --database [string] [default: "test"]
--help Show help [boolean]
From there you can either add migrations with migrate add <description>
, run pending migrations with migrate run
or generate the surrealX client with generate --output <output.file>
. You can also reset your database with database reset
.
Migrations
To add a new migration file run
surrealx migrate add <description>
which will create a migration file with the name <timestamp>_<description>.sql
(e.g. 20230206192324_initial_migration.sql
). You can then write whatever SurrealDB statements you want. However because of the current implementation you HAVE TO END ALL YOUR STATEMENTS WITH SEMICOLONS;
We are working on making the migrations implementation better.
After you have written all your statements, you can run any pending migrations with
surrealx migrate run
Notes and considerations
[REPLACED]
bits in comments.
To every field in a tables type we add a comment specifying how the field was defined. However, sometimes you might have defined a field like this
DEFINE FIELD comment ON TABLE post TYPE string ASSERT $value = /^comment:.*/;
or similar. The issue is that this includes the string */
which is the same
as the closing tag of the ts doc comment. There are currently
no workaround for this, so
hence the [REPLACED]
.
null
and undefined
SurrealDB distinguishes between their Null
and None
type, which are similar to JS's null
and undefined
. For instance, if you have the following table
DEFINE TABLE user SCHEMAFULL;
DEFINE FIELD age ON TABLE user TYPE int ASSERT $value != NONE;
DEFINE FIELD name ON TABLE user TYPE string;
and then query it, you get the following results depending on what fields you query for
/**
* Can return things like
* [
* { age: 1, name: "a" },
* { age: 2 }
* ]
*/
await db.selectAllX('user');
/**
* Can return things like
* [
* { age: 1, name: "a" },
* { age: 2, name: null }
* ]
*/
await db.selectAllX('user', ['age', 'name']);
/**
* And even querying for a non-existing field can return things like
* [
* { age: 1, profession: null },
* { age: 2, profession: null }
* ]
*/
await db.selectAllX('user', ['age', 'professions']);
So depending on what fields you want to query you either get null
or undefined
. We could potentially support this in surrealX, but have not gotten around to it yet.
In the mean time we strongly suggest using user.name == null
for checking if things are null
or undefined
rather than ===
(potentially enforced using the eqeqeq linting rule).