first commit
This commit is contained in:
177
docs/identity/auth/better-auth-surrealdb/README.md
Normal file
177
docs/identity/auth/better-auth-surrealdb/README.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# @suaveplan/better-auth-surrealdb - Better-Auth Adapter for SurrealDB v3 — bundles the canonical 17-table SurrealQL schema, compiles Better-Auth Where[] filters to parameterised SurrealQL, auto-prefixes record-link fields on write, strips prefixes on read, supports record-id/plain/ulid id strategies, and ships an OTel-instrumented conformance harness for cross-package consumption.
|
||||
|
||||
Better-Auth Adapter for SurrealDB v3 — bundles the canonical 17-table
|
||||
SurrealQL schema, compiles Better-Auth `Where[]` filters to parameterised
|
||||
SurrealQL, auto-prefixes record-link fields on write, strips prefixes on
|
||||
read, supports `record-id` / `plain` / `ulid` id strategies, and ships an
|
||||
OTel-instrumented conformance harness for cross-package reuse.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [adapter](./src/adapter/adapter.md) — The package's flagship export. `createBetterAuthSurrealAdapter(options)` returns a Better-Auth `Adapter` whose 8 contra…
|
||||
- [adapter](./src/adapter/adapter.md) — The package's flagship export. `createBetterAuthSurrealAdapter(options)` returns a Better-Auth `Adapter` whose 8 contra…
|
||||
- [internals](./src/adapter/internals.md) — Shared internal helpers consumed by the adapter shell (`adapter.ts`) and the per-method operation runners (`operations.…
|
||||
- [model-to-table](./src/adapter/model-to-table.md) — Memoising mapper from Better-Auth model name to SurrealDB table name. The mapper function is invoked once per distinct…
|
||||
- [operations](./src/adapter/operations.md) — Hosts the five primitive runners (`runCreate`, `runFindMany`, `runUpdateMany`, `runDeleteMany`, `runCount`) that transl…
|
||||
- [v1.6](./src/adapter/v1.6.md) — `betterAuthSurrealAdapter(options)` is the Better-Auth **v1.6** integration surface for `@suaveplan/better-auth-surreal…
|
||||
- [compile-select](./src/compile/compile-select.md) — Compiles a Better-Auth `select?: string[]` projection into a SurrealQL column list. Used inside the adapter's `SELECT`…
|
||||
- [compile-sort](./src/compile/compile-sort.md) — Compiles a Better-Auth `sortBy?: { field; direction }` argument into a SurrealQL `ORDER BY` fragment, validating the fi…
|
||||
- [compile-where](./src/compile/compile-where.md) — Translates Better-Auth's `Where[]` filter union into a parameterised SurrealQL `WHERE` fragment plus a binding record.…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — Defines the closed registry of error codes raised from every public path of `@suaveplan/better-auth-surrealdb`. Stable…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — Defines the closed registry of error codes raised from every public path of `@suaveplan/better-auth-surrealdb`. Stable…
|
||||
- [id-strategy](./src/id-strategy/id-strategy.md) — Encapsulates the three Better-Auth record-id strategies the adapter supports. SurrealDB's native record-id format is `<…
|
||||
- [id-strategy](./src/id-strategy/id-strategy.md) — Encapsulates the three Better-Auth record-id strategies the adapter supports. SurrealDB's native record-id format is `<…
|
||||
- [link-fields](./src/link-fields/link-fields.md) — Encapsulates the central design decision of this package: SurrealDB stores record references as typed `record<target>`…
|
||||
- [link-fields](./src/link-fields/link-fields.md) — Encapsulates the central design decision of this package: SurrealDB stores record references as typed `record<target>`…
|
||||
- [otel](./src/otel/instruments.md) — Wires the package's OpenTelemetry metrics surface and exposes a thin `instrument(op)` helper the adapter wraps around e…
|
||||
- [instruments](./src/otel/instruments.md) — Wires the package's OpenTelemetry metrics surface and exposes a thin `instrument(op)` helper the adapter wraps around e…
|
||||
- [schema](./src/schema/schema.md) — Bundles the canonical 17-table Better-Auth SurrealQL schema as a frozen string constant `BETTER_AUTH_SURREAL_SCHEMA` an…
|
||||
- [schema](./src/schema/schema.md) — Bundles the canonical 17-table Better-Auth SurrealQL schema as a frozen string constant `BETTER_AUTH_SURREAL_SCHEMA` an…
|
||||
- [link-fields](./src/schemas/link-fields.md) — Zod boundary schema asserting the structural shape of a `LinkFieldMap`. Used to validate the optional `linkFields` argu…
|
||||
- [options](./src/schemas/options.md) — Zod boundary schema for `BetterAuthSurrealAdapterOptions`. Drives the runtime validation step inside `createBetterAuthS…
|
||||
- [testing](./src/testing/run-adapter-conformance.md) — Exposes `runAdapterConformance(factory)` — the reusable suite that pins every Better-Auth Adapter scenario this package…
|
||||
- [run-adapter-conformance](./src/testing/run-adapter-conformance.md) — Exposes `runAdapterConformance(factory)` — the reusable suite that pins every Better-Auth Adapter scenario this package…
|
||||
- [types](./src/types/adapter.md) — Type-only foundation for the entire package. Defines the public option shape (`BetterAuthSurrealAdapterOptions`), the r…
|
||||
- [adapter](./src/types/adapter.md) — Type-only foundation for the entire package. Defines the public option shape (`BetterAuthSurrealAdapterOptions`), the r…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Quick start
|
||||
|
||||
```ts
|
||||
import { SurrealAdapter } from "@suaveplan/surrealdb";
|
||||
import { applyBetterAuthSchema } from "@suaveplan/better-auth-surrealdb/schema";
|
||||
import { betterAuthSurrealAdapter } from "@suaveplan/better-auth-surrealdb/adapter/v1.6";
|
||||
import { betterAuth } from "better-auth";
|
||||
|
||||
const client = new SurrealAdapter({
|
||||
url: "ws://localhost:8000",
|
||||
namespace: "auth",
|
||||
database: "auth",
|
||||
username: "root",
|
||||
password: process.env.SURREAL_PASSWORD,
|
||||
});
|
||||
await client.connect();
|
||||
await applyBetterAuthSchema(client);
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: betterAuthSurrealAdapter({ client }),
|
||||
secret: process.env.BETTER_AUTH_SECRET,
|
||||
baseURL: "http://localhost:3000",
|
||||
emailAndPassword: { enabled: true },
|
||||
});
|
||||
```
|
||||
|
||||
### Advanced — direct conformance harness usage
|
||||
|
||||
The v1.4-shape `createBetterAuthSurrealAdapter` returns the 10-method
|
||||
adapter directly (without the `createAdapterFactory` wrapper). This is the
|
||||
surface the `./testing` conformance harness consumes and the right
|
||||
choice when you want to bypass Better-Auth's field-transformation layer:
|
||||
|
||||
```ts
|
||||
import { createBetterAuthSurrealAdapter } from "@suaveplan/better-auth-surrealdb/adapter";
|
||||
|
||||
const adapter = createBetterAuthSurrealAdapter({ client });
|
||||
await adapter.create({ model: "user", data: { id: "u1", email: "a@b" } });
|
||||
```
|
||||
|
||||
## Bundled schema — 17 Better-Auth tables
|
||||
|
||||
Mirrored byte-for-byte from
|
||||
`/mnt/f/surrealdb/surql/features/02-auth/02-better-auth.surql`. Schema
|
||||
version moves in lockstep with this package version. Upstream changes to
|
||||
the canonical schema require a coordinated package version bump.
|
||||
|
||||
| Table | `userId` shape | Notes |
|
||||
|-----------------------|-------------------------|--------------------------------------------------------|
|
||||
| `user` (extended) | n/a | Extends host's user table with `emailVerified`/`name`/`image` |
|
||||
| `session` | `record<user>` | Has default `linkFields.session.userId = "user"` |
|
||||
| `account` | `string` | Flat-string userId — NOT in `DEFAULT_BETTER_AUTH_LINK_FIELDS` |
|
||||
| `verification` | n/a | Single-use token storage |
|
||||
| `passkey` | `record<user>` | WebAuthn credentials |
|
||||
| `jwks` | n/a | Server JWK pairs |
|
||||
| `sso_provider` | `option<record<user>>` | Optional record-link |
|
||||
| `oauth_application` | `string` (option) | Flat-string userId |
|
||||
| `oauth_access_token` | `string` (option) | Flat-string userId |
|
||||
| `oauth_consent` | `string` (option) | Flat-string userId |
|
||||
| `organization` | n/a | Slug-unique tenant root |
|
||||
| `member` | record × 2 | `userId`+`organizationId` both record-links |
|
||||
| `invitation` | record × 3 | `inviterId`/`organizationId`/`teamId` record-links |
|
||||
| `team` | `record<organization>` | Belongs to org |
|
||||
| `team_member` | record × 2 | `userId`+`teamId` |
|
||||
| `two_factor` | `record<user>` | TOTP secret + backup codes |
|
||||
| `api_key` | `record<user>` | Long-lived API keys with rate-limit fields |
|
||||
|
||||
Plus 13 indexes (account provider, verification identifier, SSO provider
|
||||
id, oauth client/access/refresh tokens, organization slug, passkey
|
||||
credential, user phone, api_key prefix/user, member/team uniqueness,
|
||||
invitation email/org).
|
||||
|
||||
## `linkFields` — overriding the default record-link map
|
||||
|
||||
`DEFAULT_BETTER_AUTH_LINK_FIELDS` covers every `record<...>` column in
|
||||
the bundled schema. Pass your own `linkFields` when your schema differs:
|
||||
|
||||
```ts
|
||||
import { createBetterAuthSurrealAdapter } from "@suaveplan/better-auth-surrealdb/adapter";
|
||||
|
||||
// Flat-string schema everywhere — disable prefix manipulation:
|
||||
createBetterAuthSurrealAdapter({ client, linkFields: {} });
|
||||
|
||||
// Mixed override:
|
||||
createBetterAuthSurrealAdapter({
|
||||
client,
|
||||
linkFields: {
|
||||
session: { userId: "user" }, // keep default
|
||||
member: { userId: "user", organizationId: "tenant" }, // rename target
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## `idStrategy` — record-id format
|
||||
|
||||
| Strategy | Behaviour |
|
||||
|-------------|--------------------------------------------------------------------------------------------|
|
||||
| `record-id` (default) | `data.id` becomes the SurrealDB record component (`<table>:<id>`). Rejects `id` containing `:`. Returned `id` is bare. |
|
||||
| `plain` | `id` stored as a normal field; SurrealDB auto-generates the storage id. |
|
||||
| `ulid` | When `data.id` missing, generates a 26-char Crockford-base32 ULID and applies `record-id`. |
|
||||
|
||||
## Conformance harness
|
||||
|
||||
```ts
|
||||
import { SurrealDBFixture } from "@suaveplan/testing-integration-surrealdb";
|
||||
import { runAdapterConformance } from "@suaveplan/better-auth-surrealdb/testing";
|
||||
|
||||
const fixture = new SurrealDBFixture();
|
||||
let adapter: ReturnType<typeof createBetterAuthSurrealAdapter>;
|
||||
let client: SurrealAdapter;
|
||||
|
||||
beforeAll(async () => {
|
||||
const cfg = await fixture.start();
|
||||
client = new SurrealAdapter(cfg);
|
||||
await client.connect();
|
||||
await applyBetterAuthSchema(client);
|
||||
adapter = createBetterAuthSurrealAdapter({ client });
|
||||
}, 120_000);
|
||||
|
||||
afterAll(async () => {
|
||||
await client.disconnect();
|
||||
await fixture.stop();
|
||||
});
|
||||
|
||||
runAdapterConformance(() => ({ client, adapter }));
|
||||
```
|
||||
|
||||
## Peer deps
|
||||
|
||||
- `better-auth` ^1.4.0 — supplied by the consuming app. Type-only at this
|
||||
package's boundary; never imported at runtime so the adapter ships
|
||||
cleanly to apps using any 1.4+ minor.
|
||||
|
||||
## Subpath exports
|
||||
|
||||
`.`, `./adapter`, `./adapter/v1.6`, `./compile`, `./id-strategy`,
|
||||
`./link-fields`, `./schema`, `./schemas/options`, `./schemas/link-fields`,
|
||||
`./testing`, `./error-codes`, `./otel`, `./types`.
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
# adapter
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The package's flagship export. `createBetterAuthSurrealAdapter(options)`
|
||||
returns a Better-Auth `Adapter` whose 8 contract methods plus
|
||||
`consumeOne` and `id` are wired against a SurrealDB v3 connection. The
|
||||
adapter validates options through Zod at the boundary, dispatches every
|
||||
CRUD method through OTel-instrumented spans, applies link-field
|
||||
prefix/strip on writes/reads, and compiles `Where[]` filters into
|
||||
parameterised SurrealQL — no string interpolation, no mocks, no
|
||||
`vi.mock`.
|
||||
|
||||
## Features
|
||||
|
||||
- Stable `id` constant `"@suaveplan/better-auth-surrealdb"`.
|
||||
- All 8 Better-Auth methods: `create`, `findOne`, `findMany`, `count`,
|
||||
`update`, `updateMany`, `delete`, `deleteMany`, plus the `consumeOne`
|
||||
primitive Better-Auth v1.6+ adds atop the older 8 methods.
|
||||
- Link-field-aware writes and reads — `userId: "u1"` becomes
|
||||
`userId: "user:u1"` at rest for `record<user>` columns.
|
||||
- Three id-strategies (`record-id` default, `plain`, `ulid`).
|
||||
- OTel counter + histogram via `instrument()`.
|
||||
- Zod-validated options at the boundary; failures raise
|
||||
`BetterAuthSurrealError(INVALID_INPUT)` with the `ZodError` as `cause`.
|
||||
- Connection-readiness check: every method validates `client.isConnected`
|
||||
and raises `CONNECTION_NOT_READY` when the underlying SurrealAdapter
|
||||
has not connected.
|
||||
- Underlying `SurrealError` failures wrap in `QUERY_FAILED` preserving the
|
||||
`cause` chain.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { SurrealAdapter } from "@suaveplan/surrealdb";
|
||||
import { applyBetterAuthSchema } from "@suaveplan/better-auth-surrealdb/schema";
|
||||
import { createBetterAuthSurrealAdapter } from "@suaveplan/better-auth-surrealdb/adapter";
|
||||
|
||||
const client = new SurrealAdapter({ url: "ws://localhost:8000" });
|
||||
await client.connect();
|
||||
await applyBetterAuthSchema(client);
|
||||
|
||||
const adapter = createBetterAuthSurrealAdapter({ client });
|
||||
|
||||
await adapter.create({
|
||||
model: "session",
|
||||
data: { id: "s1", userId: "u1", token: "t" },
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { createBetterAuthSurrealAdapter } from "@suaveplan/better-auth-surrealdb/adapter";
|
||||
|
||||
const adapter = createBetterAuthSurrealAdapter({
|
||||
client: surrealAdapter,
|
||||
idStrategy: "ulid",
|
||||
modelToTable: (m) => `bauth_${m}`,
|
||||
linkFields: {
|
||||
// override default — match a flat-string schema:
|
||||
session: {},
|
||||
member: { organizationId: "organization" },
|
||||
},
|
||||
});
|
||||
|
||||
const user = await adapter.findOne({
|
||||
model: "user",
|
||||
where: [{ field: "email", value: "x@y" }],
|
||||
select: ["id", "email"],
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `createBetterAuthSurrealAdapter(options): BetterAuthSurrealAdapter`
|
||||
- `BETTER_AUTH_SURREAL_ADAPTER_ID = "@suaveplan/better-auth-surrealdb"`
|
||||
- `BetterAuthSurrealAdapter` — structural type satisfying Better-Auth's
|
||||
`DBAdapter` minus `transaction` / `createSchema` (the host's own
|
||||
transaction primitive remains reachable via `SurrealAdapter.transaction`).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The adapter's `client.query()` calls pass the binding record as
|
||||
`[vars]` so both `SurrealAdapter` (which flattens a trailing record) and
|
||||
hand-rolled `SurrealClientView` implementations receive the same shape.
|
||||
`update` is implemented atop `updateMany`'s `RETURN AFTER` row stream and
|
||||
returns the first row — `count() FROM <table> WHERE … GROUP ALL` returns
|
||||
a `{ c: <num> }` row by aliasing; bare `count()` returns the count
|
||||
unnamed which trips SurrealDB v3's stricter row shape. `consumeOne`
|
||||
fetches the row first, then deletes by the same predicate; this is the
|
||||
fallback path Better-Auth's factory installs when an adapter does not
|
||||
expose a native consume primitive, and gives the same race semantics
|
||||
under SurrealDB's read-then-delete pipeline.
|
||||
@@ -0,0 +1,66 @@
|
||||
# adapter/internals
|
||||
|
||||
## Purpose
|
||||
|
||||
Shared internal helpers consumed by the adapter shell (`adapter.ts`) and the
|
||||
per-method operation runners (`operations.ts`). Encapsulates option
|
||||
normalisation (Zod validation + default fill-in), connection-readiness
|
||||
assertion, parameter packaging for the SurrealDB client surface, error
|
||||
wrapping, and the read-side row post-processing that strips link prefixes
|
||||
and normalises the returned `id`.
|
||||
|
||||
## Features
|
||||
|
||||
- `normaliseOptions(options): NormalisedOptions` — Zod-parses the boundary
|
||||
options, applies the default `LinkFieldMap` when none supplied, defaults
|
||||
`idStrategy` to `"record-id"`, instantiates a `ModelToTableCache`.
|
||||
- `ensureConnected(client)` — throws `CONNECTION_NOT_READY` when the client
|
||||
reports `isConnected === false`.
|
||||
- `paramsFromVars(vars)` — packs the binding record as a single trailing
|
||||
argument so both `SurrealAdapter.query` and bespoke `SurrealClientView`
|
||||
implementations see identical call shapes.
|
||||
- `wrapQuery(err)` — converts an underlying `SurrealError` (or any throw) to
|
||||
`BetterAuthSurrealError(QUERY_FAILED)` preserving the cause chain.
|
||||
- `postRead(row, table, linkFields, model, idStrategy)` — strips link
|
||||
prefixes, normalises the returned `id` per the strategy.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { normaliseOptions, ensureConnected } from "./internals.js";
|
||||
|
||||
const opts = normaliseOptions(userSuppliedOptions);
|
||||
ensureConnected(opts.client);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { wrapQuery, postRead } from "./internals.js";
|
||||
|
||||
try {
|
||||
const rows = await opts.client.query("SELECT * FROM user;");
|
||||
return rows.map((r) => postRead(r, "user", opts.linkFields, "user", opts.idStrategy));
|
||||
} catch (err) {
|
||||
wrapQuery(err); // never returns
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface NormalisedOptions { client; idStrategy; linkFields; modelCache; }`
|
||||
- `normaliseOptions(opts): NormalisedOptions`
|
||||
- `ensureConnected(client): void`
|
||||
- `paramsFromVars(vars): unknown[]`
|
||||
- `wrapQuery(err): never`
|
||||
- `postRead(row, table, linkFields, model, idStrategy): Row`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`normaliseOptions` re-validates through Zod on every call so test seams
|
||||
that mutate options between adapter instantiations still surface boundary
|
||||
issues. The function returns the originally-supplied `client` reference
|
||||
(not Zod's parsed clone) to preserve method identity — Zod's structural
|
||||
`custom` validator does not reconstruct the input. `wrapQuery` is typed
|
||||
`never` so callers can call it inside a `try/catch` and TypeScript still
|
||||
narrows the surrounding block to the success path.
|
||||
@@ -0,0 +1,72 @@
|
||||
# adapter/model-to-table
|
||||
|
||||
## Purpose
|
||||
|
||||
Memoising mapper from Better-Auth model name to SurrealDB table name. The
|
||||
mapper function is invoked once per distinct model and the resolved name
|
||||
is validated against the SurrealQL identifier rule
|
||||
(`/^[a-zA-Z_][a-zA-Z0-9_]*$/`). Caching avoids re-running operator-supplied
|
||||
prefix logic on every call site while validation prevents accidental
|
||||
injection through a hand-rolled `modelToTable` that returns SurrealQL
|
||||
syntax.
|
||||
|
||||
## Features
|
||||
|
||||
- Identity-default — `new ModelToTableCache()` maps model→model verbatim.
|
||||
- Caches resolved name in a `Map<string,string>`.
|
||||
- Identifier-rule validation: invalid names raise `INVALID_MODEL` on first
|
||||
use of the offending model.
|
||||
- Decoupled from the adapter shell so unit tests can exercise mapping
|
||||
semantics in isolation.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { ModelToTableCache } from "@suaveplan/better-auth-surrealdb/adapter";
|
||||
|
||||
const cache = new ModelToTableCache();
|
||||
cache.resolve("user"); // "user"
|
||||
cache.resolve("session"); // "session"
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { ModelToTableCache } from "@suaveplan/better-auth-surrealdb/adapter";
|
||||
|
||||
let calls = 0;
|
||||
const cache = new ModelToTableCache((m) => {
|
||||
calls++;
|
||||
return `bauth_${m}`;
|
||||
});
|
||||
|
||||
cache.resolve("user"); // "bauth_user" — calls === 1
|
||||
cache.resolve("user"); // "bauth_user" — calls still 1 (cached)
|
||||
cache.resolve("session"); // "bauth_session" — calls === 2
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class ModelToTableCache`
|
||||
- `constructor(resolver?: ModelToTableResolver)` — defaults to identity.
|
||||
- `resolve(model: string): string` — cached + validated.
|
||||
- `type ModelToTableResolver = (model: string) => string`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The cache is a plain `Map` because invalidation is unnecessary: model
|
||||
names are stable Better-Auth contracts and the adapter is constructed per
|
||||
process. Throwing on first use of an invalid name (rather than at adapter
|
||||
construction) preserves the "lazy" semantics so callers wiring a
|
||||
multi-tenant prefix function can return different names per request
|
||||
without paying the validation cost upfront. The identifier check uses
|
||||
the same regex as every other validator in this package
|
||||
(`/^[a-zA-Z_][a-zA-Z0-9_]*$/`) so operators get consistent feedback when
|
||||
a name shape is wrong — whether the offending input is a model alias, a
|
||||
SurrealDB table name, a link-field target, or a `select` projection
|
||||
entry.
|
||||
|
||||
The class field is `protected` rather than `private` so subclasses can
|
||||
introspect the cache during debugging without paying the indirection cost
|
||||
of an additional accessor method — useful when wiring per-tenant audit
|
||||
trails that need to log every resolved table name on first use.
|
||||
@@ -0,0 +1,73 @@
|
||||
# adapter/operations
|
||||
|
||||
## Purpose
|
||||
|
||||
Hosts the five primitive runners (`runCreate`, `runFindMany`,
|
||||
`runUpdateMany`, `runDeleteMany`, `runCount`) that translate Better-Auth
|
||||
method arguments into parameterised SurrealQL and dispatch through the
|
||||
configured `SurrealClientView`. Decoupling the runners from the
|
||||
`createBetterAuthSurrealAdapter` shell keeps each Better-Auth public method
|
||||
trivially thin (it composes runners + OTel instrumentation) and makes per-
|
||||
operation behaviour testable in isolation.
|
||||
|
||||
## Features
|
||||
|
||||
- `runCreate(opts, { model, data })` — `CREATE <target> CONTENT $data RETURN
|
||||
AFTER`. Applies link-field prefixes before binding and strips them on
|
||||
return. Honours the configured `idStrategy`.
|
||||
- `runFindMany(opts, args)` — full `SELECT … FROM <table> [WHERE …] [ORDER
|
||||
BY …] [LIMIT …] [START …]` pipeline. Empty trailing clauses are elided
|
||||
by post-template whitespace collapse.
|
||||
- `runUpdateMany(opts, args)` — `UPDATE <table> MERGE $update [WHERE …]
|
||||
RETURN AFTER` with predicate placeholders namespaced `$p0/$p1/…` to
|
||||
avoid colliding with the `$update` MERGE binding.
|
||||
- `runDeleteMany(opts, args)` — `DELETE <table> [WHERE …] RETURN BEFORE`;
|
||||
returns the number of pre-image rows so the public `delete`/`deleteMany`
|
||||
methods can derive their semantics.
|
||||
- `runCount(opts, args)` — `SELECT count() AS c FROM <table> [WHERE …]
|
||||
GROUP ALL`; tolerates SurrealDB returning an empty result set or a
|
||||
string-typed count.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { runCreate, runFindMany } from "./operations.js";
|
||||
|
||||
const row = await runCreate(opts, { model: "user", data: { id: "u1" } });
|
||||
const list = await runFindMany(opts, {
|
||||
model: "user",
|
||||
where: [{ field: "email", value: "x" }],
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { runCount, runUpdateMany } from "./operations.js";
|
||||
|
||||
const updated = await runUpdateMany(opts, {
|
||||
model: "session",
|
||||
where: [{ field: "expiresAt", value: now, operator: "lt" }],
|
||||
update: { active: false },
|
||||
});
|
||||
|
||||
const total = await runCount(opts, { model: "session" });
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `runCreate(opts, args): Promise<Row>`
|
||||
- `runFindMany(opts, args): Promise<Row[]>`
|
||||
- `runUpdateMany(opts, args): Promise<Row[]>`
|
||||
- `runDeleteMany(opts, args): Promise<number>`
|
||||
- `runCount(opts, args): Promise<number>`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
All runners single-step replace whitespace runs with one space then trim
|
||||
to keep generated SurrealQL readable in EXPLAIN output without sprinkling
|
||||
conditionals at every splice site. `runUpdateMany` uses an explicit
|
||||
`placeholderPrefix: "p"` so the merged binding object can keep `$update`
|
||||
disjoint from the predicate bindings. `runCount` aliases `count() AS c`
|
||||
because SurrealDB v3's `GROUP ALL` projection returns the aggregate under
|
||||
the function name otherwise, which is fragile across point releases.
|
||||
121
docs/identity/auth/better-auth-surrealdb/src/adapter/v1.6.md
Normal file
121
docs/identity/auth/better-auth-surrealdb/src/adapter/v1.6.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# `betterAuthSurrealAdapter` — Better-Auth v1.6 factory
|
||||
|
||||
## Purpose
|
||||
|
||||
`betterAuthSurrealAdapter(options)` is the Better-Auth **v1.6** integration
|
||||
surface for `@suaveplan/better-auth-surrealdb`. It returns a
|
||||
`DBAdapterInstance` — a function `(BetterAuthOptions) => DBAdapter` —
|
||||
that `betterAuth({ database })` accepts directly. The factory is a thin,
|
||||
type-safe wrapper around `createBetterAuthSurrealAdapter` (the v1.4 shape
|
||||
this package has always exported) plus `createAdapterFactory` from
|
||||
`@better-auth/core/db`, the official escape-hatch Better-Auth provides for
|
||||
custom database adapters.
|
||||
|
||||
The wrapper does no real work of its own beyond schema validation,
|
||||
forwarding, transaction setup, and exposing the bundled SurrealQL DDL via
|
||||
`createSchema()` so Better-Auth's CLI generator (`@better-auth/cli
|
||||
generate`) can emit a canonical schema file.
|
||||
|
||||
## Features
|
||||
|
||||
- Better-Auth v1.6 `DBAdapterInstance` return type — works with
|
||||
`betterAuth({ database: betterAuthSurrealAdapter({ client }) })` out of
|
||||
the box.
|
||||
- Same `BetterAuthSurrealAdapterOptions` shape and Zod schema as the v1.4
|
||||
surface — `client`, `modelToTable`, `idStrategy`, `linkFields`,
|
||||
`logger` are all honoured identically.
|
||||
- Transaction support: when `options.client` is a `SurrealAdapter`, the
|
||||
factory wires `config.transaction` to delegate into
|
||||
`SurrealAdapter.transaction` (a `BEGIN/COMMIT/CANCEL TRANSACTION`
|
||||
block). The transaction-scoped `DBTransactionAdapter` is built by
|
||||
re-instantiating the underlying adapter against the transaction's
|
||||
`DatabaseLike` handle so writes inside `transaction(fn)` participate in
|
||||
the surrounding transaction.
|
||||
- `createSchema()` returns `BETTER_AUTH_SURREAL_SCHEMA` verbatim — the
|
||||
same 17-table DDL string this package ships under `./schema`. Wrapped
|
||||
in `DBAdapterSchemaCreation` shape (`{ code, path: "./schema.surql",
|
||||
overwrite: true }`).
|
||||
- OTel: emits a `better_auth.surrealdb.operations_total{model:
|
||||
"_transaction", operation: "transaction", outcome}` counter point per
|
||||
`transaction(...)` invocation. The underlying CRUD methods continue to
|
||||
emit their own per-operation counter points unchanged; the wrapper
|
||||
layer never double-counts.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { SurrealAdapter } from "@suaveplan/surrealdb";
|
||||
import { betterAuthSurrealAdapter } from "@suaveplan/better-auth-surrealdb/adapter/v1.6";
|
||||
import { applyBetterAuthSchema } from "@suaveplan/better-auth-surrealdb/schema";
|
||||
import { betterAuth } from "better-auth";
|
||||
|
||||
const client = new SurrealAdapter({
|
||||
url: "ws://localhost:8000",
|
||||
namespace: "auth",
|
||||
database: "auth",
|
||||
username: "root",
|
||||
password: process.env.SURREAL_PASSWORD,
|
||||
});
|
||||
await client.connect();
|
||||
await applyBetterAuthSchema(client);
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: betterAuthSurrealAdapter({ client }),
|
||||
secret: process.env.BETTER_AUTH_SECRET,
|
||||
baseURL: "http://localhost:3000",
|
||||
emailAndPassword: { enabled: true },
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { betterAuthSurrealAdapter } from "@suaveplan/better-auth-surrealdb/adapter/v1.6";
|
||||
|
||||
// Custom model-to-table mapping + link-field overrides
|
||||
const adapter = betterAuthSurrealAdapter({
|
||||
client,
|
||||
modelToTable: (model) => (model === "user" ? "people" : model),
|
||||
linkFields: {
|
||||
session: { userId: "people" },
|
||||
member: { userId: "people", organizationId: "tenant" },
|
||||
},
|
||||
idStrategy: "ulid",
|
||||
});
|
||||
|
||||
// Pass to Better-Auth — supports the full v1.6 surface including
|
||||
// `auth.api.signUpEmail`, `auth.api.signInEmail`, `auth.api.getSession`,
|
||||
// transactions, schema generation via `@better-auth/cli generate`, etc.
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `betterAuthSurrealAdapter(options: BetterAuthSurrealAdapterV16Options):
|
||||
DBAdapterInstance` — the factory. Options are validated through
|
||||
`BetterAuthSurrealAdapterOptionsSchema`; failures throw
|
||||
`BetterAuthSurrealError(BETTER_AUTH_SURREALDB_INVALID_INPUT)`.
|
||||
- `BetterAuthSurrealAdapterV16Options` — type alias for
|
||||
`BetterAuthSurrealAdapterOptions`. Re-exported for clarity at the v1.6
|
||||
call site.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The wrapper construction is two-step: (1) build the 10-method underlying
|
||||
adapter via `createBetterAuthSurrealAdapter` once at factory call time;
|
||||
(2) inside the per-call `(options) => DBAdapter` returned by
|
||||
`createAdapterFactory`, return a `CustomAdapter` whose methods forward
|
||||
to the cached underlying adapter. Better-Auth's factory adds
|
||||
`transformInput`/`transformOutput` field whitelisting, default-value
|
||||
application, and date/boolean/JSON normalisation on top — that's the
|
||||
whole point of the wrapper layer.
|
||||
- `disableIdGeneration: true` is set in the factory config so user-provided
|
||||
`id` values survive Better-Auth's id-handling layer. The underlying
|
||||
adapter still applies its `idStrategy` (record-id / plain / ulid).
|
||||
- `create` calls forward with `forceAllowId: true` so the user-supplied
|
||||
`id` field is preserved end-to-end (Better-Auth otherwise nulls it on
|
||||
the v1.6 `create` path).
|
||||
- `transaction` is only wired when the client is a `SurrealAdapter`
|
||||
instance. For generic `SurrealClientView` callers, the factory falls
|
||||
back to `createAsIsTransaction` (sequential execution against the
|
||||
outer adapter, matching Better-Auth's documented degraded-transaction
|
||||
behaviour).
|
||||
@@ -0,0 +1,63 @@
|
||||
# compile-select
|
||||
|
||||
## Purpose
|
||||
|
||||
Compiles a Better-Auth `select?: string[]` projection into a SurrealQL
|
||||
column list. Used inside the adapter's `SELECT` template. Identifier-
|
||||
validates every entry so the projection cannot inject SurrealQL syntax.
|
||||
|
||||
## Features
|
||||
|
||||
- `undefined` and `[]` both compile to `"*"`.
|
||||
- Validates every field against `/^[a-zA-Z_][a-zA-Z0-9_]*$/`.
|
||||
- Pure / side-effect-free.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { compileSelect } from "@suaveplan/better-auth-surrealdb/compile";
|
||||
|
||||
compileSelect(["id", "email"]); // "id, email"
|
||||
compileSelect(undefined); // "*"
|
||||
compileSelect([]); // "*"
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { compileSelect } from "@suaveplan/better-auth-surrealdb/compile";
|
||||
|
||||
function buildProjection(model: string, select?: string[]) {
|
||||
const cols = compileSelect(select);
|
||||
return `SELECT ${cols} FROM ${model}`;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `compileSelect(select: ReadonlyArray<string> | undefined): string`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The default `*` keeps Better-Auth's existing first-party adapters'
|
||||
behaviour: when no projection is supplied, the row is returned in full so
|
||||
that Better-Auth's read-transform layer can shape it client-side. The
|
||||
identifier validator runs eagerly so the error surfaces as an
|
||||
`INVALID_INPUT` near the adapter boundary instead of at the SurrealDB
|
||||
query layer with a less actionable message. The function is pure and
|
||||
side-effect-free — both invocations with identical input return the same
|
||||
string output, which lets the adapter cache projection strings per
|
||||
`(model, select-fingerprint)` tuple if a future optimisation pass wants
|
||||
to dedupe across high-traffic call sites.
|
||||
|
||||
Identifier validation runs in O(n) over the input array. The check rejects
|
||||
SurrealDB constructs that would otherwise compile but introduce attack
|
||||
surface: dotted paths (`foo.bar`), function calls (`count()`), subscripts,
|
||||
back-tick-quoted identifiers. Operators wanting to project subscripted
|
||||
fields should compile their own projection string and bypass this helper;
|
||||
this package treats that as out-of-band and intentionally narrow.
|
||||
|
||||
Empty-array vs `undefined` behave identically by design — Better-Auth's
|
||||
upstream type permits both to mean "all fields". This package collapses
|
||||
both to `"*"` rather than emitting a zero-column SurrealQL `SELECT` that
|
||||
would surface as a parse error against SurrealDB v3.
|
||||
@@ -0,0 +1,63 @@
|
||||
# compile-sort
|
||||
|
||||
## Purpose
|
||||
|
||||
Compiles a Better-Auth `sortBy?: { field; direction }` argument into a
|
||||
SurrealQL `ORDER BY` fragment, validating the field name against the
|
||||
identifier rule to prevent any injection vector. Returns the empty string
|
||||
when `sortBy` is `undefined` so the adapter can unconditionally splice the
|
||||
result into its SELECT template.
|
||||
|
||||
## Features
|
||||
|
||||
- Default direction handling: only `"desc"` flips to `DESC`; everything
|
||||
else (including the explicit `"asc"`) emits `ASC`.
|
||||
- Identifier validation: field MUST match `/^[a-zA-Z_][a-zA-Z0-9_]*$/`,
|
||||
else `BETTER_AUTH_SURREALDB.INVALID_INPUT` is thrown.
|
||||
- Pure / side-effect-free.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { compileSortBy } from "@suaveplan/better-auth-surrealdb/compile";
|
||||
|
||||
const fragment = compileSortBy({ field: "createdAt", direction: "desc" });
|
||||
// "ORDER BY createdAt DESC"
|
||||
|
||||
const none = compileSortBy(undefined); // ""
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { compileSortBy } from "@suaveplan/better-auth-surrealdb/compile";
|
||||
|
||||
function buildPaginatedSelect(table: string, sortBy?: { field: string; direction: "asc" | "desc" }) {
|
||||
return `SELECT * FROM ${table} ${compileSortBy(sortBy)} LIMIT 25`.replace(/\s+/g, " ");
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `compileSortBy(sortBy: SortBy | undefined): string`
|
||||
- `SortBy` — `{ field: string; direction: "asc" | "desc" }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
SurrealDB tolerates identifiers in `ORDER BY` only when they match its
|
||||
identifier regex. We pre-validate rather than wrap in backticks so the
|
||||
generated query is human-readable in EXPLAIN output and to surface a
|
||||
typed error close to the offending input rather than at the DB boundary.
|
||||
Returning the empty string for `undefined` simplifies the adapter's SELECT
|
||||
template — the splice site can use a single template-literal slot with no
|
||||
nullish checks.
|
||||
|
||||
The direction comparison is strict equality against the literal string
|
||||
`"desc"` to keep Better-Auth's `"asc" | "desc"` union the authoritative
|
||||
contract; any other input (e.g. uppercase, abbreviated) silently defaults
|
||||
to `ASC`. This matches Better-Auth's own first-party adapters which never
|
||||
emit non-canonical direction strings, but it does mean operators
|
||||
hand-rolling a `sortBy` object with mistyped direction will not observe
|
||||
a descending sort — a deliberate trade-off favouring forward-compatibility
|
||||
against future Better-Auth direction extensions over strict input
|
||||
validation at this layer.
|
||||
@@ -0,0 +1,76 @@
|
||||
# compile-where
|
||||
|
||||
## Purpose
|
||||
|
||||
Translates Better-Auth's `Where[]` filter union into a parameterised
|
||||
SurrealQL `WHERE` fragment plus a binding record. Every clause maps to a
|
||||
unique placeholder (`$w0`, `$w1`, …) so SurrealDB binds values via its safe
|
||||
parameter path — never via string interpolation. Link-field values are
|
||||
pre-resolved against the configured `LinkFieldMap` before binding so a
|
||||
`{ field: "userId", value: "u1" }` clause on `session` compiles to a
|
||||
`WHERE userId = $w0` clause where `$w0 = "user:u1"`, satisfying the
|
||||
`record<user>` column type.
|
||||
|
||||
## Features
|
||||
|
||||
- 11 operators: `eq` (default), `ne`, `gt`, `gte`, `lt`, `lte`, `in`,
|
||||
`not_in`, `contains`, `starts_with`, `ends_with`.
|
||||
- 2 connectors: `AND` (default), `OR`. The first clause omits the connector.
|
||||
- Unique placeholder naming via positional index — never collides within
|
||||
one compile, and the `placeholderPrefix` option lets the adapter keep
|
||||
`$update`-vs-predicate bindings disjoint when assembling UPDATE queries.
|
||||
- Link-field pre-resolution: scalar values get `<target>:<v>`; array values
|
||||
map element-wise (preserving `in` / `not_in` semantics).
|
||||
- Unknown operators raise `BETTER_AUTH_SURREALDB.INVALID_OPERATOR`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { compileWhere } from "@suaveplan/better-auth-surrealdb/compile";
|
||||
|
||||
const { clause, vars } = compileWhere(
|
||||
[{ field: "age", value: 18, operator: "gte" }],
|
||||
{ model: "user", linkFields: {} }
|
||||
);
|
||||
// clause === "age >= $w0"
|
||||
// vars === { w0: 18 }
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { compileWhere } from "@suaveplan/better-auth-surrealdb/compile";
|
||||
import { DEFAULT_BETTER_AUTH_LINK_FIELDS } from "@suaveplan/better-auth-surrealdb/link-fields";
|
||||
|
||||
const compiled = compileWhere(
|
||||
[
|
||||
{ field: "userId", value: "u1" },
|
||||
{ field: "expiresAt", value: new Date(), operator: "lt", connector: "OR" },
|
||||
],
|
||||
{
|
||||
model: "session",
|
||||
linkFields: DEFAULT_BETTER_AUTH_LINK_FIELDS,
|
||||
placeholderPrefix: "p",
|
||||
}
|
||||
);
|
||||
// compiled.clause === "userId = $p0 OR expiresAt < $p1"
|
||||
// compiled.vars.p0 === RecordId("user", "u1") (pre-resolved record link)
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `compileWhere(where, options): { clause: string; vars: Record<string, unknown> }`
|
||||
- `WhereOperator` — closed union of 11 operators.
|
||||
- `WhereConnector` — `"AND" | "OR"`.
|
||||
- `WhereClause` — `{ field; value; operator?; connector? }`.
|
||||
- `CompileWhereOptions` — `{ model; linkFields; placeholderPrefix? }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The operator-to-SurrealQL table covers every Better-Auth `whereOperators`
|
||||
entry. The function returns `{ clause: "", vars: {} }` for empty input,
|
||||
which lets the adapter splice the result into a SELECT without conditional
|
||||
branching at the call site. `placeholderPrefix` defaults to `"w"` to match
|
||||
the spec's `$w0/$w1/…` convention; the adapter uses `"u"` when compiling
|
||||
MERGE-side bindings (`$update` is its own scope but predicate placeholders
|
||||
must stay disjoint from any caller-defined params).
|
||||
@@ -0,0 +1,78 @@
|
||||
# error-codes
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines the closed registry of error codes raised from every public path of
|
||||
`@suaveplan/better-auth-surrealdb`. Stable string codes let downstream callers
|
||||
branch on failure reasons without depending on stack-trace text or Better-Auth
|
||||
internals. Each code is namespaced under `BETTER_AUTH_SURREALDB.*` so log
|
||||
aggregators and OTel dashboards can pivot on a single attribute regardless of
|
||||
which package raised the failure.
|
||||
|
||||
## Features
|
||||
|
||||
- 7 fully-qualified codes: `INVALID_INPUT`, `INVALID_MODEL`,
|
||||
`INVALID_OPERATOR`, `QUERY_FAILED`, `INVALID_ID_FORMAT`,
|
||||
`INVALID_LINK_FIELD_VALUE`, `CONNECTION_NOT_READY`.
|
||||
- `BetterAuthSurrealError` extends `SuaveplanError<BetterAuthSurrealErrorCode>`
|
||||
so `instanceof` narrows the `code` to the union literal.
|
||||
- Codes built via `defineCodes("BETTER_AUTH_SURREALDB", […])` so the runtime
|
||||
object is frozen and the type is fully inferred.
|
||||
- Carry `cause` and `context` from `SuaveplanErrorOptions` — `QUERY_FAILED`
|
||||
pins the underlying `SurrealError`, `INVALID_INPUT` pins the `ZodError`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
BETTER_AUTH_SURREALDB_CODES,
|
||||
BetterAuthSurrealError,
|
||||
} from "@suaveplan/better-auth-surrealdb/error-codes";
|
||||
|
||||
throw new BetterAuthSurrealError(
|
||||
BETTER_AUTH_SURREALDB_CODES.INVALID_OPERATOR,
|
||||
"unknown operator: weird"
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { ZodError } from "zod";
|
||||
import {
|
||||
BETTER_AUTH_SURREALDB_CODES,
|
||||
BetterAuthSurrealError,
|
||||
} from "@suaveplan/better-auth-surrealdb/error-codes";
|
||||
|
||||
function assertValid<T>(parsed: { success: boolean; error?: ZodError; data?: T }): T {
|
||||
if (!parsed.success) {
|
||||
throw new BetterAuthSurrealError(
|
||||
BETTER_AUTH_SURREALDB_CODES.INVALID_INPUT,
|
||||
"adapter options failed validation",
|
||||
{ cause: parsed.error }
|
||||
);
|
||||
}
|
||||
return parsed.data as T;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `BETTER_AUTH_SURREALDB_CODES` — frozen record `{ [Reason]: "BETTER_AUTH_SURREALDB.<Reason>" }`.
|
||||
- `BetterAuthSurrealErrorCode` — union literal of the 7 codes.
|
||||
- `class BetterAuthSurrealError extends SuaveplanError<BetterAuthSurrealErrorCode>`
|
||||
— public constructor `(code, message, options?)`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`defineCodes` returns a `Object.freeze`d record so the runtime cannot be
|
||||
mutated; the type is fully inferred from the tuple literal. The class
|
||||
constructor is widened to `public` (the base is `protected`) so callers in
|
||||
sibling feature modules construct errors directly without an intermediate
|
||||
factory. The class deliberately does NOT carry an OTel span event handle —
|
||||
instrumentation is the caller's responsibility via `toSpanEvent` from
|
||||
`@suaveplan/error`.
|
||||
@@ -0,0 +1,77 @@
|
||||
# id-strategy
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Encapsulates the three Better-Auth record-id strategies the adapter
|
||||
supports. SurrealDB's native record-id format is `<table>:<id>`, but
|
||||
Better-Auth callers may pass either a bare `id` (record-id mode), no
|
||||
`id` at all (ulid mode auto-generates), or a row that stores `id` as a
|
||||
regular field (plain mode). This module resolves the strategy choice into
|
||||
concrete CREATE-target-and-data tuples and normalises the returned id
|
||||
on read.
|
||||
|
||||
## Features
|
||||
|
||||
- `prepareCreate(strategy, table, data) → PreparedCreate` — dispatches on
|
||||
the strategy and returns `{ target, data, assignedId }`.
|
||||
- `record-id` strategy: requires non-empty `data.id`, rejects colons, emits
|
||||
`<table>:\`<id>\`` to keep arbitrary identifier characters safe.
|
||||
- `ulid` strategy: when `data.id` is absent, generates a 26-char Crockford
|
||||
base32 ULID (10 chars time + 16 chars random) via `crypto.getRandomValues`.
|
||||
- `plain` strategy: keeps `id` as a row field; SurrealDB assigns the
|
||||
storage id; row's `assignedId` echoes `data.id` when present.
|
||||
- `normaliseReturnedId` — strips the `<table>:` prefix from SurrealDB's
|
||||
return shape (`RecordId` object, `<table>:<id>` string, or bare value).
|
||||
- Throws `INVALID_MODEL` when the table name fails the identifier rule.
|
||||
- Throws `INVALID_ID_FORMAT` when an id-strategy contract is violated.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { prepareCreate } from "@suaveplan/better-auth-surrealdb/id-strategy";
|
||||
|
||||
const p = prepareCreate("record-id", "user", { id: "u1", email: "x@y" });
|
||||
// p.target === "user:`u1`"
|
||||
// p.data === { email: "x@y" }
|
||||
// p.assignedId === "u1"
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { prepareCreate, generateUlid } from "@suaveplan/better-auth-surrealdb/id-strategy";
|
||||
|
||||
// Inject deterministic id for tests by composing your own table+id outside
|
||||
// of prepareCreate; or use the ulid generator directly:
|
||||
const id = generateUlid();
|
||||
const p = prepareCreate("record-id", "session", { id, userId: "user:u1" });
|
||||
|
||||
// Plain mode for schemas that store id as a regular column:
|
||||
const flat = prepareCreate("plain", "session", { id: "s1", token: "t" });
|
||||
// flat.target === "session"
|
||||
// flat.data === { id: "s1", token: "t" }
|
||||
// flat.assignedId === "s1"
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `IdStrategy = "record-id" | "plain" | "ulid"`
|
||||
- `PreparedCreate = { target: string; data: Row; assignedId: string | null }`
|
||||
- `prepareCreate(strategy, table, data): PreparedCreate`
|
||||
- `generateUlid(now?): string` — 26-char Crockford base32 ULID.
|
||||
- `normaliseReturnedId(value, table): string` — strips `<table>:` from SurrealDB
|
||||
return shapes.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The ULID alphabet matches the published spec (Crockford base32 minus I, L,
|
||||
O, U). Random bytes come from `crypto.getRandomValues` so the generator is
|
||||
universal (works in Bun, Node ≥18, and browsers). The record-id
|
||||
target is wrapped in backticks so SurrealDB accepts ULIDs containing digits
|
||||
at position 1 (otherwise the parser may treat the value as a number). The
|
||||
`prepareCreate` switch is exhaustive; TypeScript narrows `IdStrategy` to
|
||||
the three known cases at compile time.
|
||||
@@ -0,0 +1,95 @@
|
||||
# link-fields
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Encapsulates the central design decision of this package: SurrealDB stores
|
||||
record references as typed `record<target>` columns where the value at rest
|
||||
is `<target>:<bare-id>`, but Better-Auth hands the adapter a bare `userId:
|
||||
"u1"` regardless of which underlying table declares the field as a record
|
||||
link. Without per-field prefix handling, writes to record-link columns would
|
||||
fail SurrealDB's type assertion and reads would return `user:u1` strings
|
||||
that break Better-Auth's identity comparisons. This module provides the
|
||||
default field map, the per-value `apply`/`strip` primitives, and the
|
||||
row-level walkers the adapter dispatches.
|
||||
|
||||
## Features
|
||||
|
||||
- `DEFAULT_BETTER_AUTH_LINK_FIELDS` — frozen default matching every
|
||||
`record<...>` field in the bundled schema (9 model entries).
|
||||
- `linkTargetFor(map, model, field)` — pure lookup returning the configured
|
||||
target table or `null`.
|
||||
- `applyLinkPrefix(target, value)` — adds `<target>:` to plain strings;
|
||||
refuses values already containing `:` with `INVALID_LINK_FIELD_VALUE`.
|
||||
- `stripLinkPrefix(target, value)` — strips `<target>:` from prefixed
|
||||
strings; idempotent on bare strings.
|
||||
- `applyLinkPrefixesToWrite(map, model, data)` — clones `data` and prefixes
|
||||
every declared link field in one pass.
|
||||
- `stripLinkPrefixesFromRead(map, model, row)` — clones `row` and strips
|
||||
every declared link field in one pass.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
DEFAULT_BETTER_AUTH_LINK_FIELDS,
|
||||
applyLinkPrefixesToWrite,
|
||||
stripLinkPrefixesFromRead,
|
||||
} from "@suaveplan/better-auth-surrealdb/link-fields";
|
||||
|
||||
const onDisk = applyLinkPrefixesToWrite(
|
||||
DEFAULT_BETTER_AUTH_LINK_FIELDS,
|
||||
"session",
|
||||
{ userId: "u1", token: "t" }
|
||||
);
|
||||
// onDisk.userId === "user:u1"
|
||||
|
||||
const returned = stripLinkPrefixesFromRead(
|
||||
DEFAULT_BETTER_AUTH_LINK_FIELDS,
|
||||
"session",
|
||||
{ userId: "user:u1", token: "t" }
|
||||
);
|
||||
// returned.userId === "u1"
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
applyLinkPrefix,
|
||||
stripLinkPrefix,
|
||||
linkTargetFor,
|
||||
} from "@suaveplan/better-auth-surrealdb/link-fields";
|
||||
|
||||
const map = { session: { userId: "user" } } as const;
|
||||
|
||||
// Per-value primitives — used by compileWhere for Where[] bindings.
|
||||
const target = linkTargetFor(map, "session", "userId"); // "user"
|
||||
if (target !== null) {
|
||||
const bound = applyLinkPrefix(target, "u1"); // "user:u1"
|
||||
const bare = stripLinkPrefix(target, bound); // "u1"
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `DEFAULT_BETTER_AUTH_LINK_FIELDS: LinkFieldMap`
|
||||
- `linkTargetFor(map, model, field): string | null`
|
||||
- `applyLinkPrefix(target, value): unknown` — throws `INVALID_LINK_FIELD_VALUE`
|
||||
when the value already contains `:`.
|
||||
- `stripLinkPrefix(target, value): unknown`
|
||||
- `applyLinkPrefixesToWrite(map, model, data): Record<string, unknown>`
|
||||
- `stripLinkPrefixesFromRead(map, model, row): Record<string, unknown>`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The default map is built with nested `Object.freeze` so callers cannot
|
||||
accidentally mutate the shared singleton. Non-string values pass through
|
||||
both `apply` and `strip` (the schema permits `option<record<user>>` to be
|
||||
`null`). `applyLinkPrefix` refuses values containing `:` because the
|
||||
adapter would otherwise emit `user:user:u1` on write — defending against
|
||||
double-application is critical because Better-Auth's higher layers may
|
||||
synthesise rows from joined-read output that already carries prefixes.
|
||||
@@ -0,0 +1,76 @@
|
||||
# otel/instruments
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Wires the package's OpenTelemetry metrics surface and exposes a thin
|
||||
`instrument(op)` helper the adapter wraps around every CRUD method. The
|
||||
helper records one counter point (with `outcome: "success" | "error"`)
|
||||
plus one histogram observation per call, so dashboards can pivot on
|
||||
model/operation/outcome and also see the per-operation latency
|
||||
distribution.
|
||||
|
||||
## Features
|
||||
|
||||
- `better_auth.surrealdb.operations_total` counter — attributes:
|
||||
`model`, `operation`, `outcome`.
|
||||
- `better_auth.surrealdb.operation_duration_ms` histogram — attribute:
|
||||
`operation`.
|
||||
- Lazy meter resolution: the meter and instruments are constructed on
|
||||
first use (not at module load) so tests can install a real
|
||||
`MeterProvider` via `setGlobalMeterProvider` before the adapter
|
||||
begins issuing operations.
|
||||
- Universal: uses `performance.now()` when available, falls back to
|
||||
`Date.now()` when running under bundlers that strip `performance`.
|
||||
- `resetInstrumentsForTesting()` clears the lazy cache so a re-imported
|
||||
test environment resolves the new provider.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { instrument } from "@suaveplan/better-auth-surrealdb/otel";
|
||||
|
||||
const row = await instrument({
|
||||
model: "session",
|
||||
operation: "create",
|
||||
run: async () => insertSessionRow(),
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { instrument } from "@suaveplan/better-auth-surrealdb/otel";
|
||||
import { metrics } from "@opentelemetry/api";
|
||||
import { MeterProvider, PeriodicExportingMetricReader, InMemoryMetricExporter, AggregationTemporality } from "@opentelemetry/sdk-metrics";
|
||||
|
||||
// Install provider BEFORE first instrument() call:
|
||||
const exporter = new InMemoryMetricExporter(AggregationTemporality.CUMULATIVE);
|
||||
const reader = new PeriodicExportingMetricReader({ exporter, exportIntervalMillis: 60_000 });
|
||||
const provider = new MeterProvider({ readers: [reader] });
|
||||
metrics.setGlobalMeterProvider(provider);
|
||||
|
||||
await instrument({ model: "user", operation: "findOne", run: async () => ({ ok: true }) });
|
||||
await provider.forceFlush();
|
||||
const collected = exporter.getMetrics();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `instrument<T>(op): Promise<T>` — wraps a `() => Promise<T>` with counter +
|
||||
histogram observations; rethrows on failure.
|
||||
- `InstrumentedOp<T>` — `{ model: string; operation: string; run: () => Promise<T> }`.
|
||||
- `AdapterOutcome` — `"success" | "error"`.
|
||||
- `resetInstrumentsForTesting()` — invalidates the lazy cache.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The lazy-init pattern is critical: importing OTel `metrics` at module load
|
||||
returns a noop meter unless `setGlobalMeterProvider` has been called by the
|
||||
host. Caching the meter+instruments after first use keeps subsequent calls
|
||||
allocation-free. `performance.now()` is preferred for sub-millisecond
|
||||
resolution but `Date.now()` is a correct fallback because the histogram
|
||||
records millisecond observations regardless.
|
||||
@@ -0,0 +1,75 @@
|
||||
# schema
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Bundles the canonical 17-table Better-Auth SurrealQL schema as a frozen
|
||||
string constant `BETTER_AUTH_SURREAL_SCHEMA` and exposes a one-shot
|
||||
`applyBetterAuthSchema(client)` helper for callers who do not run their
|
||||
DDL through `@suaveplan/api-migrations`. Schema content is mirrored
|
||||
byte-for-byte from the canonical source-of-truth file
|
||||
`/mnt/f/surrealdb/surql/features/02-auth/02-better-auth.surql`; future
|
||||
upstream changes require a coordinated package version bump.
|
||||
|
||||
## Features
|
||||
|
||||
- 17 DDL targets: `user` field extensions, plus `session`, `account`,
|
||||
`verification`, `passkey`, `jwks`, `sso_provider`, `oauth_application`,
|
||||
`oauth_access_token`, `oauth_consent`, `organization`, `member`,
|
||||
`invitation`, `team`, `team_member`, `two_factor`, `api_key`.
|
||||
- 13 indexes (account provider, verification identifier, SSO provider id,
|
||||
oauth client/access/refresh tokens, organization slug, passkey
|
||||
credential, user phone, api_key prefix/user, member/team uniqueness,
|
||||
invitation email/org).
|
||||
- Idempotent on re-application — every `DEFINE TABLE`/`DEFINE FIELD`/
|
||||
`DEFINE INDEX` uses the `OVERWRITE` flag.
|
||||
- `applyBetterAuthSchema(client)` executes the schema via the
|
||||
`SurrealClientView.execute()` surface and wraps any underlying failure
|
||||
in `BetterAuthSurrealError(QUERY_FAILED)`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { SurrealAdapter } from "@suaveplan/surrealdb";
|
||||
import { applyBetterAuthSchema } from "@suaveplan/better-auth-surrealdb/schema";
|
||||
|
||||
const client = new SurrealAdapter({ url: "ws://localhost:8000" });
|
||||
await client.connect();
|
||||
await applyBetterAuthSchema(client);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { BETTER_AUTH_SURREAL_SCHEMA } from "@suaveplan/better-auth-surrealdb/schema";
|
||||
|
||||
// Pipe the schema through @suaveplan/api-migrations
|
||||
import { createSurrealqlAdapter } from "@suaveplan/api-migrations";
|
||||
|
||||
const migrations = createSurrealqlAdapter({
|
||||
steps: [
|
||||
{ id: "001-better-auth", up: BETTER_AUTH_SURREAL_SCHEMA },
|
||||
],
|
||||
});
|
||||
|
||||
await migrations.run(client);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `BETTER_AUTH_SURREAL_SCHEMA: string` — frozen DDL string.
|
||||
- `applyBetterAuthSchema(client: SurrealClientView): Promise<void>` — executes
|
||||
the schema via `client.execute(...)`; wraps failures in `QUERY_FAILED`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The constant is a single string literal so bundlers can tree-shake it out
|
||||
of consumers that import only `applyBetterAuthSchema` from sibling
|
||||
modules. Newlines are preserved (`\n`) so EXPLAIN output and DB logs stay
|
||||
human-readable. The helper executes the whole DDL in one `execute()`
|
||||
call; SurrealDB v3 parses multi-statement scripts natively and runs them
|
||||
sequentially within a single response. Failure capture preserves the
|
||||
underlying `SurrealError` via `cause` for upstream telemetry.
|
||||
@@ -0,0 +1,75 @@
|
||||
# schemas/link-fields
|
||||
|
||||
## Purpose
|
||||
|
||||
Zod boundary schema asserting the structural shape of a `LinkFieldMap`. Used
|
||||
to validate the optional `linkFields` argument passed to
|
||||
`createBetterAuthSurrealAdapter` and to gate any callers that hand-roll their
|
||||
own override map at runtime. Validation rejects non-string targets, empty
|
||||
keys, and identifiers that would break the generated SurrealQL identifier
|
||||
rule (`/^[a-zA-Z_][a-zA-Z0-9_]*$/`).
|
||||
|
||||
## Features
|
||||
|
||||
- Identifier-rule enforcement on model name, field name, and target table.
|
||||
- Empty-string rejection (model/field/target must be `min(1)`).
|
||||
- Composable: re-exported as `LinkFieldMapSchema` from `./schemas` and
|
||||
embedded inside `BetterAuthSurrealAdapterOptionsSchema`.
|
||||
- Type-level `LinkFieldMapSchemaInput` / `LinkFieldMapSchemaOutput` exports
|
||||
for downstream consumers that derive their own option shapes.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { LinkFieldMapSchema } from "@suaveplan/better-auth-surrealdb/schemas/link-fields";
|
||||
|
||||
const parsed = LinkFieldMapSchema.safeParse({
|
||||
session: { userId: "user" },
|
||||
});
|
||||
if (!parsed.success) throw parsed.error;
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { LinkFieldMapSchema } from "@suaveplan/better-auth-surrealdb/schemas/link-fields";
|
||||
|
||||
// Reject non-string targets at the wire boundary
|
||||
const bad = LinkFieldMapSchema.safeParse({
|
||||
session: { userId: 5 },
|
||||
});
|
||||
console.log(bad.success); // false
|
||||
|
||||
// Custom map merging
|
||||
const merged = LinkFieldMapSchema.parse({
|
||||
...{ session: { userId: "user" } },
|
||||
...{ api_key: { userId: "user" } },
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `LinkFieldMapSchema: z.ZodType` — accepts `Record<string, Record<string, string>>`.
|
||||
- `LinkFieldMapSchemaInput` — pre-parse type.
|
||||
- `LinkFieldMapSchemaOutput` — post-parse type.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The schema uses `z.record(IdentString, z.record(IdentString, IdentString))`.
|
||||
Keys go through Zod's `z.record(keySchema, valueSchema)` 4-arg form so the
|
||||
identifier check applies to the model/field names as well as the target
|
||||
table. Empty-object inputs (`{}`) parse successfully — operators commonly
|
||||
pass `linkFields: {}` to disable prefix manipulation entirely.
|
||||
|
||||
The `IdentString` schema delegates to `z.string().min(1).regex(...)` so
|
||||
the inner regex never has to deal with empty strings (which would slip
|
||||
through the regex via the implicit anchored-success path). The error
|
||||
message attached to the regex check is human-readable and surfaces in
|
||||
`ZodError.issues[i].message` for downstream error reporters, while the
|
||||
issue path lets log aggregators pivot on offending fields without
|
||||
parsing the message.
|
||||
|
||||
Type-level exports `LinkFieldMapSchemaInput` and `LinkFieldMapSchemaOutput`
|
||||
are identical in this case (the schema is `transform`-free) but the
|
||||
distinction is preserved so future extensions (e.g. coercing aliases to
|
||||
canonical table names) can change one without breaking the other.
|
||||
@@ -0,0 +1,72 @@
|
||||
# schemas/options
|
||||
|
||||
## Purpose
|
||||
|
||||
Zod boundary schema for `BetterAuthSurrealAdapterOptions`. Drives the runtime
|
||||
validation step inside `createBetterAuthSurrealAdapter` so callers cannot
|
||||
construct an adapter against a malformed configuration. Failure raises
|
||||
`BetterAuthSurrealError(INVALID_INPUT)` carrying the `ZodError` as `cause`,
|
||||
which keeps the original field path + issue list available to error
|
||||
reporters and OTel adapters.
|
||||
|
||||
## Features
|
||||
|
||||
- Structural validation of `client` (asserts `query`/`execute` methods).
|
||||
- Optional-field gating for `modelToTable`, `idStrategy`, `linkFields`,
|
||||
`logger`.
|
||||
- Embedded `LinkFieldMapSchema` reuse — invalid link-field shape surfaces
|
||||
inside the parent schema's `cause.issues`.
|
||||
- Identifier-safe id-strategy union `"record-id" | "plain" | "ulid"`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { BetterAuthSurrealAdapterOptionsSchema } from "@suaveplan/better-auth-surrealdb/schemas/options";
|
||||
|
||||
BetterAuthSurrealAdapterOptionsSchema.parse({
|
||||
client: surrealAdapterInstance,
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { BetterAuthSurrealAdapterOptionsSchema } from "@suaveplan/better-auth-surrealdb/schemas/options";
|
||||
|
||||
const result = BetterAuthSurrealAdapterOptionsSchema.safeParse({
|
||||
client: surrealAdapterInstance,
|
||||
idStrategy: "ulid",
|
||||
linkFields: { session: { userId: "user" } },
|
||||
modelToTable: (m) => `auth_${m}`,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
console.error(result.error.issues);
|
||||
} else {
|
||||
const opts = result.data; // typed
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `BetterAuthSurrealAdapterOptionsSchema: z.ZodObject` — boundary schema.
|
||||
- `BetterAuthSurrealAdapterOptionsInput` — pre-parse type alias.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`client` is validated via `z.custom` with a structural predicate (checks
|
||||
`query` and `execute` are both functions) rather than `z.instanceof`,
|
||||
because the spec accepts both `SurrealAdapter` instances and bespoke
|
||||
`SurrealClientView` objects. The schema strips no keys (the parse pass is
|
||||
permissive in case Better-Auth v1.x minor releases extend the contract);
|
||||
the factory still narrows by destructuring only the fields it uses.
|
||||
|
||||
The validator deliberately returns the originally-supplied client
|
||||
reference (Zod's `z.custom` preserves the input identity) rather than
|
||||
reconstructing it — this preserves method identity and any private
|
||||
state the supplied `SurrealAdapter` holds (connection pool, retry
|
||||
counter, OTel context). Failing options surface a `ZodError` with the
|
||||
full `issues` array, which the `createBetterAuthSurrealAdapter` factory
|
||||
re-raises as `BetterAuthSurrealError(INVALID_INPUT)` with the `ZodError`
|
||||
attached as `cause` so downstream log aggregators and OTel exporters get
|
||||
the field-level diagnostics without rebuilding them from text.
|
||||
@@ -0,0 +1,86 @@
|
||||
# testing/run-adapter-conformance
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Exposes `runAdapterConformance(factory)` — the reusable suite that pins
|
||||
every Better-Auth Adapter scenario this package implements against a real
|
||||
SurrealDB testcontainer. Downstream test files (and consumers of the
|
||||
package who maintain their own adapter implementations) import this
|
||||
harness once and get an exhaustive contract sweep without rewriting per-
|
||||
scenario boilerplate.
|
||||
|
||||
## Features
|
||||
|
||||
- Single `describe("Better-Auth adapter conformance")` block exercising
|
||||
create, findOne, findMany (with limit/offset/sortBy), update,
|
||||
updateMany, count, delete, deleteMany, and `consumeOne`.
|
||||
- Validates link-field roundtrips on `session.userId` (`record<user>`).
|
||||
- Validates `applyBetterAuthSchema` succeeds against the live container.
|
||||
- Factory injection: callers supply `{ adapter, client }` so the harness
|
||||
can hit both surfaces without re-importing module-private state.
|
||||
- Every `it()` carries `{ timeout: 60000 }` to absorb container startup +
|
||||
network jitter typical of dockerised SurrealDB.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { runAdapterConformance } from "@suaveplan/better-auth-surrealdb/testing";
|
||||
|
||||
runAdapterConformance(async () => {
|
||||
// Caller is responsible for starting the testcontainer, applying the
|
||||
// schema, and constructing the adapter.
|
||||
const { adapter, client } = await bootstrapTestEnv();
|
||||
return { adapter, client };
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { SurrealDBFixture } from "@suaveplan/testing-integration-surrealdb";
|
||||
import { SurrealAdapter } from "@suaveplan/surrealdb";
|
||||
import { applyBetterAuthSchema } from "@suaveplan/better-auth-surrealdb/schema";
|
||||
import { createBetterAuthSurrealAdapter } from "@suaveplan/better-auth-surrealdb/adapter";
|
||||
import { runAdapterConformance } from "@suaveplan/better-auth-surrealdb/testing";
|
||||
|
||||
const fixture = new SurrealDBFixture();
|
||||
let cfg: Awaited<ReturnType<typeof fixture.start>>;
|
||||
let client: SurrealAdapter;
|
||||
|
||||
beforeAll(async () => {
|
||||
cfg = await fixture.start();
|
||||
client = new SurrealAdapter(cfg);
|
||||
await client.connect();
|
||||
await applyBetterAuthSchema(client);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await client.disconnect();
|
||||
await fixture.stop();
|
||||
});
|
||||
|
||||
runAdapterConformance(() => ({
|
||||
client,
|
||||
adapter: createBetterAuthSurrealAdapter({ client }),
|
||||
}));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `runAdapterConformance(factory: ConformanceFactory): void`
|
||||
- `ConformanceFactory = () => Promise<ConformanceContext> | ConformanceContext`
|
||||
- `ConformanceContext = { adapter: BetterAuthSurrealAdapter; client: SurrealClientView }`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The harness uses the runner-agnostic `@suaveplan/testing/runner` exports
|
||||
so it works under both Vitest and Bun's `bun:test`. The factory is
|
||||
invoked inside each `it()` so callers may choose either to share one
|
||||
adapter across tests (return the same `ctx` reference) or to construct a
|
||||
fresh adapter per scenario. Tests use stable `id` values prefixed
|
||||
`u_conf_*` / `s_conf_*` to avoid cross-suite collisions when multiple
|
||||
harness invocations share a database.
|
||||
@@ -0,0 +1,81 @@
|
||||
# types
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Type-only foundation for the entire package. Defines the public option shape
|
||||
(`BetterAuthSurrealAdapterOptions`), the record-id strategy union
|
||||
(`IdStrategy`), the per-model record-link configuration (`LinkFieldMap`), and
|
||||
the structural SurrealDB client view (`SurrealClientView`). Keeping these in a
|
||||
zero-runtime module lets every other module import types without dragging in
|
||||
Zod, OTel, or error-class side-effects.
|
||||
|
||||
## Features
|
||||
|
||||
- `IdStrategy` — closed union `"record-id" | "plain" | "ulid"`.
|
||||
- `LinkFieldMap` — readonly nested record `{ model: { field: targetTable } }`.
|
||||
- `BetterAuthSurrealAdapterOptions` — full factory option surface.
|
||||
- `SurrealClientView` — structural view satisfied by `SurrealAdapter` AND any
|
||||
hand-rolled client exposing the same `query` / `execute` signatures.
|
||||
- `Row` — alias for `Record<string, unknown>` reused across method signatures.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import type { BetterAuthSurrealAdapterOptions } from "@suaveplan/better-auth-surrealdb/types";
|
||||
import { SurrealAdapter } from "@suaveplan/surrealdb";
|
||||
|
||||
const opts: BetterAuthSurrealAdapterOptions = {
|
||||
client: new SurrealAdapter({ url: "ws://localhost:8000" }),
|
||||
};
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import type {
|
||||
BetterAuthSurrealAdapterOptions,
|
||||
LinkFieldMap,
|
||||
SurrealClientView,
|
||||
} from "@suaveplan/better-auth-surrealdb/types";
|
||||
|
||||
const customLinks: LinkFieldMap = {
|
||||
session: { userId: "user" },
|
||||
api_key: { userId: "user" },
|
||||
};
|
||||
|
||||
const customClient: SurrealClientView = {
|
||||
isConnected: true,
|
||||
async query<T>() { return [] as T[]; },
|
||||
async execute() { return { rowsAffected: 0 }; },
|
||||
};
|
||||
|
||||
const opts: BetterAuthSurrealAdapterOptions = {
|
||||
client: customClient,
|
||||
linkFields: customLinks,
|
||||
idStrategy: "ulid",
|
||||
};
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `type IdStrategy = "record-id" | "plain" | "ulid"`
|
||||
- `type LinkFieldMap = Readonly<Record<string, Readonly<Record<string, string>>>>`
|
||||
- `type Row = Record<string, unknown>`
|
||||
- `interface SurrealClientView { isConnected; query; execute; }`
|
||||
- `interface BetterAuthSurrealAdapterOptions { client; modelToTable?; idStrategy?; linkFields?; logger? }`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
All maps are `Readonly` so callers cannot mutate the link-field config after
|
||||
construction — the adapter holds the same reference and treats it as
|
||||
immutable. `SurrealClientView` is intentionally structural so subclasses of
|
||||
`SurrealAdapter`, test fakes, and bespoke SurrealDB v3 clients all satisfy
|
||||
it. The `LoggerLike` import is type-only; no runtime dep on `@suaveplan/types`
|
||||
is introduced by this file. The `Row` alias is exported separately from
|
||||
`Record<string, unknown>` so future hardening (e.g. branding rows that
|
||||
have been through the link-prefix stripper to distinguish them from raw
|
||||
DB output) becomes a single-file change.
|
||||
Reference in New Issue
Block a user