Initial Commit
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
export {
|
||||
flattenRows,
|
||||
paramsToRecord,
|
||||
wrapQueryError,
|
||||
} from "./query-helpers.js";
|
||||
@@ -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.
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
@@ -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 [];
|
||||
}
|
||||
Reference in New Issue
Block a user