Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
export {
flattenRows,
paramsToRecord,
wrapQueryError,
} from "./query-helpers.js";

View File

@@ -0,0 +1,79 @@
# query-helpers
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`query-helpers` holds the small, stateless utility functions shared by every public method on `SurrealAdapter` and its sibling CRUD/transaction modules: positional-parameter conversion, error normalisation, and result-shape flattening.
Centralising these helpers keeps `adapter.ts` focused on the `DatabaseAdapter` lifecycle (connect/disconnect/state) and lets the CRUD and transaction modules each consume the same battle-tested implementations without duplication.
## Features
- `paramsToRecord(params?)` — turn `readonly unknown[]` into `{ p0, p1, ... }` (or `undefined` for empty input)
- `wrapQueryError(err, sql)` — coerce arbitrary thrown values into structured `SurrealError(QUERY_FAILED)` with cause preservation
- `flattenRows<T>(results)` — flatten the surrealdb.js `[[row, ...]]` nested shape into `T[]`
- All helpers are pure, allocation-free on the empty-input fast paths, and free of any client/IO coupling
## Usage: Basic Example
```ts
import {
paramsToRecord,
wrapQueryError,
flattenRows,
} from "@suaveplan/surrealdb/adapter/query-helpers";
const vars = paramsToRecord(["alice", 42]);
// vars === { p0: "alice", p1: 42 }
try {
// ...driver call...
} catch (err) {
throw wrapQueryError(err, "SELECT * FROM user");
}
```
## Usage: Advanced Example
```ts
import {
flattenRows,
wrapQueryError,
} from "@suaveplan/surrealdb/adapter/query-helpers";
async function safeQuery<T>(
client: { query<U>(sql: string): Promise<U[]> },
sql: string
): Promise<T[]> {
try {
const results = await client.query<T>(sql);
return flattenRows<T>(results);
} catch (err) {
throw wrapQueryError(err, sql);
}
}
```
## API Reference
### `paramsToRecord(params?: readonly unknown[]): Record<string, unknown> | undefined`
Returns `undefined` when `params` is missing or empty. Otherwise returns a fresh record keyed `p0`, `p1`, ... in order.
### `wrapQueryError(err: unknown, sql: string): SurrealError`
Returns `err` unchanged when it is already a `SurrealError`. Otherwise wraps it as `SurrealError(SURREAL.QUERY_FAILED, "Query failed [<sql>]: <message>", { cause })` where `cause` is preserved only for `Error` instances.
### `flattenRows<T>(results: unknown): T[]`
If `results` is `[[...]]` returns the inner array. If `results` is `[T, ...]` returns it as-is. For empty or non-array inputs returns `[]`.
## Implementation Notes
- `paramsToRecord` allocates a fresh object on every call to keep callers safe from accidental mutation; the empty-input branch avoids the allocation entirely.
- `wrapQueryError` mirrors the original behaviour from `SurrealAdapter._wrapQueryError` — it must NOT swallow `SurrealError` subclasses, since callers rely on the code discriminator surviving the `await` boundary.
- `flattenRows` accepts `unknown` so call-sites can pass the raw driver return value without an upfront cast; the structural test exhausts both observed shapes (single-statement and multi-statement query responses).
- These functions are deliberately leaf utilities: they import only `../../errors/index.js`, never the driver or adapter. That keeps the dependency graph acyclic when the CRUD and transaction modules depend on them.

View File

@@ -0,0 +1,72 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { SURREAL_CODES, SurrealError } from "../../errors/index.js";
import {
flattenRows,
paramsToRecord,
wrapQueryError,
} from "./query-helpers.js";
describe("paramsToRecord", () => {
it("returns undefined for missing params", { timeout: 5000 }, () => {
expect(paramsToRecord(undefined)).toBeUndefined();
});
it("returns undefined for empty array", { timeout: 5000 }, () => {
expect(paramsToRecord([])).toBeUndefined();
});
it("maps positional params to p0/p1/...", { timeout: 5000 }, () => {
expect(paramsToRecord(["a", 1, null])).toEqual({
p0: "a",
p1: 1,
p2: null,
});
});
});
describe("wrapQueryError", () => {
it("passes SurrealError through unchanged", { timeout: 5000 }, () => {
const original = new SurrealError(SURREAL_CODES.NOT_CONNECTED, "nope");
expect(wrapQueryError(original, "SELECT 1")).toBe(original);
});
it("wraps Error with cause", { timeout: 5000 }, () => {
const cause = new Error("boom");
const wrapped = wrapQueryError(cause, "SELECT 1");
expect(wrapped).toBeInstanceOf(SurrealError);
expect(wrapped.code).toBe(SURREAL_CODES.QUERY_FAILED);
expect(wrapped.message).toContain("SELECT 1");
expect(wrapped.message).toContain("boom");
expect((wrapped as { cause?: unknown }).cause).toBe(cause);
});
it("wraps non-Error throw using String() and no cause", {
timeout: 5000,
}, () => {
const x: unknown = "string error";
const wrapped = wrapQueryError(x, "SELECT 1");
expect(wrapped.code).toBe(SURREAL_CODES.QUERY_FAILED);
expect(wrapped.message).toContain("string error");
expect((wrapped as { cause?: unknown }).cause).toBeUndefined();
});
});
describe("flattenRows", () => {
it("returns inner array for nested shape", { timeout: 5000 }, () => {
expect(flattenRows([[1, 2, 3]])).toEqual([1, 2, 3]);
});
it("returns flat array unchanged", { timeout: 5000 }, () => {
expect(flattenRows([{ a: 1 }, { a: 2 }])).toEqual([{ a: 1 }, { a: 2 }]);
});
it("returns [] for empty input", { timeout: 5000 }, () => {
expect(flattenRows([])).toEqual([]);
});
it("returns [] for non-array input", { timeout: 5000 }, () => {
expect(flattenRows(undefined)).toEqual([]);
expect(flattenRows(null)).toEqual([]);
expect(flattenRows("scalar")).toEqual([]);
});
});

View File

@@ -0,0 +1,49 @@
import { SURREAL_CODES, SurrealError } from "../../errors/index.js";
/**
* Convert a positional `readonly unknown[]` parameter list into the named
* `{ p0, p1, ... }` record shape that the surrealdb.js driver expects.
*
* Returns `undefined` when the input is missing or empty so the driver
* can skip the `vars` argument entirely (matches the no-params overload).
*/
export function paramsToRecord(
params?: readonly unknown[]
): Record<string, unknown> | undefined {
if (params === undefined || params.length === 0) return undefined;
const record: Record<string, unknown> = {};
for (let i = 0; i < params.length; i++) {
record[`p${i}`] = params[i];
}
return record;
}
/**
* Wrap an arbitrary thrown value from the surrealdb.js driver into a
* structured `SurrealError(QUERY_FAILED)`, preserving the original `Error`
* as `cause` when possible.
*
* Pass-through for values that are already `SurrealError` instances so we
* do not double-wrap.
*/
export function wrapQueryError(err: unknown, sql: string): SurrealError {
if (err instanceof SurrealError) return err;
return new SurrealError(
SURREAL_CODES.QUERY_FAILED,
`Query failed [${sql}]: ${err instanceof Error ? err.message : String(err)}`,
{ cause: err instanceof Error ? err : undefined }
);
}
/**
* Flatten the doubly-nested result shape returned by `surrealdb.js`
* (`[[row, row, ...]]`) into a flat `T[]`, also accepting an already-flat
* `T[]` for the cases where the driver returns only one statement's rows.
*/
export function flattenRows<T>(results: unknown): T[] {
if (Array.isArray(results) && results.length > 0) {
const first = results[0];
return Array.isArray(first) ? (first as T[]) : (results as T[]);
}
return [];
}