Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions src/cbor/partial.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { SurqlQueryBindings, WithPartiallyEncodeValues } from "../types";
import type { Replacer } from "./constants";
import { type EncoderOptions, encode } from "./encoder";
import { CborFillMissing } from "./error";
Expand Down Expand Up @@ -39,14 +40,17 @@ export class PartiallyEncoded {
}
}

export function partiallyEncodeObject(
object: Record<string, unknown>,
export function partiallyEncodeObject<Q extends string>(
object: SurqlQueryBindings<Q> | Record<never, never>,
options?: EncoderOptions<true>,
): Record<string, PartiallyEncoded> {
): WithPartiallyEncodeValues<SurqlQueryBindings<Q>, PartiallyEncoded> {
// it can happen that the bindings are undefined and typescript complains
// so we need to add a little check
const o = object ?? {};
return Object.fromEntries(
Object.entries(object).map(([k, v]) => [
Object.entries(o).map(([k, v]) => [
k,
encode(v, { ...options, partial: true }),
]),
);
) as WithPartiallyEncodeValues<SurqlQueryBindings<Q>, PartiallyEncoded>;
}
6 changes: 6 additions & 0 deletions src/data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,9 @@ export {
GeometryPolygon,
} from "./types/geometry.ts";
export { encodeCbor, decodeCbor } from "./cbor.ts";

export type {
AssertValidSurqlValue,
SurqlQueryBindingValue,
SurqlFuture,
} from "./types/querybindingvalues.ts";
7 changes: 4 additions & 3 deletions src/data/types/future.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Value } from "../value";
import type { SurqlFuture } from "./querybindingvalues";

/**
* An uncomputed SurrealQL future value.
*/
export class Future extends Value {
constructor(readonly inner: string) {
export class Future<F extends string> extends Value {
constructor(readonly inner: F) {
super();
}

Expand All @@ -17,7 +18,7 @@ export class Future extends Value {
return this.toString();
}

toString(): string {
toString(): SurqlFuture<F> {
return `<future> ${this.inner}`;
}
}
88 changes: 88 additions & 0 deletions src/data/types/querybindingvalues.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import type { Gap } from "../../cbor";
import type { RecordId, StringRecordId } from "./recordid";
import type { Table } from "./table";
import type { Uuid } from "./uuid";
export type SurqlFuture<F extends string> = `<future> ${F}`;

export type SocketConnectionVariables<K extends string, V> = Record<K, V>;

type BrandedError<Msg extends string, T> = T & { __brand: Msg };
type SqlKeyword =
| "SELECT"
| "INSERT"
| "UPDATE"
| "DELETE"
| "CREATE"
| "DROP"
| "select"
| "insert"
| "update"
| "delete"
| "create"
| "drop";
type IsSurqlQuery<T extends string> = T extends `${SqlKeyword} ${string}`
? true
: false;

type IsRecord<T extends string> = T extends `${string}:${string}`
? true
: false;

type IsUuid<T extends string> =
T extends `${string}-${string}-${string}-${string}-${string}` ? true : false;

type IsIsoDate<T extends string> =
T extends `${number}-${number}-${number}T${number}:${number}:${number}.${number}Z`
? true
: T extends `${number}-${number}-${number}T${number}:${number}:${number}Z` // Without milliseconds
? true
: false;

export type SurqlQueryBindingValue =
| SurqlFuture<string>
| Date
| string
| StringRecordId
| RecordId
| Uuid
| boolean
| Gap
| Table
| number;

export type AssertValidSurqlValue<
T extends string,
V extends SurqlQueryBindingValue = SurqlQueryBindingValue,
> = IsIsoDate<T> extends true
? BrandedError<"Use d`…` for ISO-dates", T>
: IsUuid<T> extends true
? BrandedError<"Use u`…` instead for UUIDs", T>
: IsRecord<T> extends true
? BrandedError<"Use r`…` instead for record IDs", T>
: IsSurqlQuery<T> extends true
? BrandedError<"Use f`…` instead for futures", T>
: V;

export type MustBeSurqlValue<V> = V extends Date
? V
: V extends Table
? V
: V extends boolean
? V
: V extends Gap
? V
: V extends number
? V
: V extends Uuid
? V
: V extends StringRecordId
? V
: V extends Date
? V
: V extends RecordId
? V
: V extends string
? AssertValidSurqlValue<V>
: V extends unknown
? V
: never;
186 changes: 149 additions & 37 deletions src/surreal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,32 @@ import {
decodeCbor,
encodeCbor,
} from "./data";
import type { SurqlQueryBindingValue } from "./data";
import {
type AbstractEngine,
ConnectionStatus,
EngineContext,
type EngineEvents,
type Engines,
} from "./engines/abstract.ts";
import { Emitter } from "./util/emitter.ts";
import { PreparedQuery } from "./util/prepared-query.ts";
import { versionCheck } from "./util/version-check.ts";

import type {
ActionResult,
ConnectOptions,
ExecuteRawSurqlQuery,
ExecuteSurqlQuery,
ExportOptions,
LiveHandler,
MapQueryResult,
ParallelSurqlQueryBindingsArray,
Patch,
Prettify,
QueryParameters,
RpcResponse,
SurqlQueryBindings,
} from "./types.ts";
import { Emitter } from "./util/emitter.ts";
import { PreparedQuery } from "./util/prepared-query.ts";
import { versionCheck } from "./util/version-check.ts";

import { AuthController } from "./auth.ts";
import { type Fill, partiallyEncodeObject } from "./cbor";
Expand Down Expand Up @@ -234,6 +238,7 @@ export class Surreal extends AuthController {
* @param key - Specifies the name of the variable.
* @param val - Assigns the value to the variable name.
*/

async let(variable: string, value: unknown): Promise<true> {
const res = await this.rpc("let", [variable, value]);
if (res.error) throw new ResponseError(res.error.message);
Expand Down Expand Up @@ -332,43 +337,150 @@ export class Surreal extends AuthController {
}

/**
* Runs a set of SurrealQL statements against the database.
* @param query - Specifies the SurrealQL statements.
* @param bindings - Assigns variables which can be used in the query.
*/
async query<T extends unknown[]>(
...args: QueryParameters
): Promise<Prettify<T>> {
const raw = await this.queryRaw<T>(...args);
return raw.map(({ status, result }) => {
if (status === "ERR") throw new ResponseError(result);
return result;
}) as T;
* Executes a single SurrealQL query against the database with optional variable bindings.
*
* This method allows dynamic SurrealQL execution with parameter substitution. Variables in the query
* should be prefixed with `$` (e.g., `$id`) and are replaced by values provided in the `bindings` object.
*
* Example usage:
* ```ts
* const result = await db.query(
* 'SELECT * FROM user WHERE id = $id',
* { id: '123' }
* ).execute();
* ```
*
* @template Q - A SurrealQL query string.
* @template B - A bindings object corresponding to variables in the query string.
*
* @param {...QueryParameters<Q, B>} args - A tuple consisting of a query string and its associated bindings object.
* @returns {{
* execute<T>(): Promise<[T]>
* }} An object with an `execute` method that performs the query and returns the result in a typed array.
*/
query<Q extends string, B extends SurqlQueryBindings<Q>>(
...args: QueryParameters<Q, B>
): ExecuteSurqlQuery {
const self = this;
return {
async execute<T extends unknown[T]>(): Promise<Prettify<[T]>> {
const raw = await self.queryRaw<Q>(...args).execute<T>();
return raw.map(({ status, result }) => {
if (status === "ERR") throw new ResponseError(result);
return result;
}) as [T];
},
};
}

/**
* Runs a set of SurrealQL statements against the database.
* @param query - Specifies the SurrealQL statements.
* @param bindings - Assigns variables which can be used in the query.
*/
async queryRaw<T extends unknown[]>(
...[q, b]: QueryParameters
): Promise<Prettify<MapQueryResult<T>>> {
const params =
q instanceof PreparedQuery
? [
q.query,
partiallyEncodeObject(q.bindings, {
fills: b as Fill[],
replacer: replacer.encode,
}),
]
: [q, b];

await this.ready;
const res = await this.rpc<MapQueryResult<T>>("query", params);
if (res.error) throw new ResponseError(res.error.message);
return res.result;
queryRaw<Q extends string>(
...[q, b]: QueryParameters<Q>
): ExecuteRawSurqlQuery {
const self = this;
return {
async execute<T extends unknown[T]>(): Promise<
Prettify<MapQueryResult<T>>
> {
const params =
q instanceof PreparedQuery
? [
q.query,
partiallyEncodeObject(q.bindings, {
fills: b as Fill[],
replacer: replacer.encode,
}),
]
: [q, b];

await self.ready;
const res = await self.rpc<MapQueryResult<T>>("query", params);
if (res.error) throw new ResponseError(res.error.message);
return res.result;
},
};
}

/**
* Executes multiple SurrealQL queries against the database, with optional bindings for each query.
*
* This function takes an array of SurrealQL query strings and a corresponding array of binding objects.
* It substitutes variables in each query using the bindings, concatenates all queries into a single SurrealQL string,
* and prepares an executable object that runs the combined query when `.execute()` is called.
*
* Each variable in the query string should be prefixed with `$` (e.g. `$id`), and will be replaced by the corresponding
* value in the bindings object. If no bindings are provided for a query, it will be executed as-is.
*
* Example usage:
* ```ts
* const result = await db.parallelQuery(
* ['SELECT * FROM user WHERE id = $id', 'SELECT * FROM post WHERE title = $title'],
* [{ id: '123' }, { title: 'Hello' }]
* ).execute();
* ```
*
* @template Qs - A readonly tuple of query strings.
* @template Bindings - A readonly tuple of bindings corresponding to the query strings.
*
* @param {readonly [...Qs]} queries - An array of SurrealQL queries to execute.
* @param {readonly [...Bindings]} bindings - An array of bindings for each query, where each binding is an object mapping keys to values used in the corresponding query.
* @returns {{
* execute<T>(): Promise<[T]>
* }} An object with an `execute` method that runs the constructed query and returns the typed result.
*/
queryMany<
const Qs extends readonly string[],
const Bindings extends ParallelSurqlQueryBindingsArray<Qs>,
>(
queries: readonly [...Qs],
bindings: readonly [...Bindings],
): ExecuteSurqlQuery {
let finalQuery = "";

const finalBindings: Record<string, SurqlQueryBindingValue> = {};

for (let i = 0; i <= queries.length - 1; i++) {
const query = queries[i];
const binding = bindings[i];
if (typeof query !== "string") {
continue;
}
if (typeof query !== "string" && typeof binding === "undefined") {
continue;
}
if (typeof binding === "undefined") {
if (query.endsWith(";")) {
finalQuery += query;
continue;
}
finalQuery += `${query};`;
continue;
}
for (const key of Object.keys(binding)) {
finalBindings[key] = binding[key];
}
if (query.endsWith(";")) {
finalQuery += query;
} else {
finalQuery += `${query};`;
}
}
const self = this;
return {
async execute<T extends unknown[T]>(): Promise<Prettify<[T]>> {
const raw = await self
.queryRaw(finalQuery, finalBindings as unknown as undefined)
.execute<T>();
return raw.map(({ status, result }) => {
if (status === "ERR") throw new ResponseError(result);
return result;
}) as [T];
},
};
}

/**
Expand All @@ -378,9 +490,9 @@ export class Surreal extends AuthController {
* @deprecated Use `queryRaw` instead
*/
async query_raw<T extends unknown[]>(
...args: QueryParameters
): Promise<Prettify<MapQueryResult<T>>> {
return this.queryRaw<T>(...args);
...args: QueryParameters<string>
): Promise<Prettify<MapQueryResult<[T]>>> {
return await this.queryRaw(...args).execute<T>();
}

/**
Expand Down
Loading