Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,376 @@
<!-- markdownlint-disable MD024 -->
# Capability Spec: better-auth-surrealdb
**Package**: `@suaveplan/better-auth-surrealdb`
**Status**: ACTIVE
## Requirements
### Requirement: createBetterAuthSurrealAdapter returns a valid Better-Auth Adapter with Zod-validated options
The package SHALL export `createBetterAuthSurrealAdapter(options): Adapter` returning an object that satisfies Better-Auth's published `Adapter` interface: `{ id: "@suaveplan/better-auth-surrealdb"; create; findOne; findMany; update; updateMany; delete; deleteMany; count }`. Options SHALL be Zod-validated via `BetterAuthSurrealAdapterOptionsSchema` (exported from `./schemas/options`) at the factory boundary; failures SHALL raise `BetterAuthSurrealError(BETTER_AUTH_SURREALDB_INVALID_INPUT)` carrying the `ZodError` as `cause`.
#### Scenario: factory produces all required methods
- **WHEN** `createBetterAuthSurrealAdapter({ client })` is called with a valid `SurrealAdapter`
- **THEN** the returned object has `id === "@suaveplan/better-auth-surrealdb"` and every Better-Auth `Adapter` method is a function
#### Scenario: invalid options raise INVALID_INPUT
- **WHEN** `createBetterAuthSurrealAdapter({} as never)` is called (missing `client`)
- **THEN** `BetterAuthSurrealError(BETTER_AUTH_SURREALDB_INVALID_INPUT)` is thrown with `cause instanceof ZodError`
**Verified by:** `src/adapter/adapter.test.ts::"factory produces every Better-Auth Adapter method"` AND `src/adapter/adapter.test.ts::"invalid options raise INVALID_INPUT"` AND `src/adapter/adapter.spec-conformance.test.ts::"satisfies Better-Auth Adapter type via expectTypeOf"`
---
### Requirement: linkFields auto-prefix writes and strip prefixes on reads
The package SHALL export `LinkFieldMap` type and `DEFAULT_BETTER_AUTH_LINK_FIELDS` constant. The constant SHALL cover every record-link field in the bundled 17-table schema: `session.userId`, `passkey.userId`, `member.userId/organizationId`, `invitation.inviterId/organizationId/teamId`, `team.organizationId`, `team_member.userId/teamId`, `two_factor.userId`, `api_key.userId`, `sso_provider.userId`. On `create` and `update`, the adapter SHALL prefix string values for declared link fields with `<target_table>:`. On `findOne`, `findMany`, the adapter SHALL strip the matching prefix from those fields before returning rows. On `Where[]` values for declared link fields, the adapter SHALL pre-resolve the prefix before binding.
#### Scenario: session.userId write wraps into record-link form
- **WHEN** `adapter.create({ model: "session", data: { userId: "u1", token: "t" } })` is awaited against a real SurrealDB testcontainer (schema applied) with default `linkFields`
- **THEN** the stored row's `userId` field equals the SurrealDB record-link `user:u1` AND the returned object has `userId === "u1"` (prefix stripped)
#### Scenario: where-filter pre-resolves link prefix
- **WHEN** `adapter.findMany({ model: "session", where: [{ field: "userId", value: "u1" }] })` is awaited
- **THEN** the generated SurrealQL `WHERE` clause binds `userId = user:u1` (NOT `userId = "u1"`)
#### Scenario: empty linkFields disables prefix manipulation
- **WHEN** `createBetterAuthSurrealAdapter({ client, linkFields: {} })` runs `create/findOne` on `session`
- **THEN** values pass through verbatim — no prefix added on write, none stripped on read
#### Scenario: account.userId (string-typed in schema) is NOT prefixed
- **WHEN** `adapter.create({ model: "account", data: { userId: "u1", providerId: "p", accountId: "a", password: "" } })` runs with default `linkFields`
- **THEN** the stored `userId` field equals the plain string `"u1"` (NOT `user:u1`) — `account` is absent from `DEFAULT_BETTER_AUTH_LINK_FIELDS`
**Verified by:** `src/link-fields/link-fields.test.ts::"DEFAULT_BETTER_AUTH_LINK_FIELDS covers every record-link field in the bundled schema"` AND `src/adapter/create.test.ts::"session.userId write wraps into record-link form"` AND `src/adapter/find.test.ts::"where-filter pre-resolves link prefix"` AND `src/link-fields/link-fields.test.ts::"empty linkFields disables prefix manipulation"` AND `src/link-fields/link-fields.test.ts::"account.userId is not prefixed under default config"`
---
### Requirement: create maps to SurrealQL CREATE … CONTENT … RETURN AFTER
`adapter.create<T>({ model, data })` SHALL issue `CREATE <modelToTable(model)> CONTENT $data RETURN AFTER` (or `CREATE <table>:<id> CONTENT $data` when `data.id` is supplied or `idStrategy === "record-id"`) via the configured `SurrealAdapter.query` path, return the first returned row with the `<table>:` prefix stripped from `id` AND from every declared link field. When `idStrategy: "ulid"`, an auto-generated ULID SHALL be injected into `data.id` when Better-Auth does not supply one.
#### Scenario: create injects record-id prefix in storage
- **WHEN** `adapter.create({ model: "user", data: { id: "abc", email: "x@y" } })` is awaited
- **THEN** a row exists at `user:abc` with `email === "x@y"` AND the returned object has `id === "abc"` (no `user:` prefix)
#### Scenario: ulid strategy fills missing id
- **WHEN** `adapter.create({ model: "session", data: { userId: "u1" } })` runs with `idStrategy: "ulid"` and `data.id` absent
- **THEN** the returned object has a non-empty `id` matching `/^[0-9A-Z]{26}$/`
**Verified by:** `src/adapter/create.test.ts::"create stores under table:id and strips prefix on return"` AND `src/adapter/create.test.ts::"ulid strategy fills missing id"` AND `tests/integration/conformance.test.ts::"create roundtrips against real SurrealDB"`
---
### Requirement: findOne and findMany compile Where[] to parameterised SurrealQL
`adapter.findOne<T>({ model, where, select? })` and `adapter.findMany<T>({ model, where?, limit?, offset?, sortBy?, select? })` SHALL build a parameterised `SELECT <projection> FROM <table> [WHERE …] [ORDER BY <field> <dir>] [LIMIT …] [START …]` query. `findOne` appends `LIMIT 1` and returns the first row or `null`. Every returned row's `id` AND declared link fields SHALL have prefixes stripped.
#### Scenario: findOne returns null when no match
- **WHEN** `adapter.findOne({ model: "user", where: [{ field: "email", value: "missing@x" }] })` is awaited
- **THEN** the result is `null`
#### Scenario: findMany honours limit / offset / sortBy
- **WHEN** 5 user rows exist and `adapter.findMany({ model: "user", limit: 2, offset: 1, sortBy: { field: "createdAt", direction: "asc" } })` is awaited
- **THEN** the result has length 2 starting at the second-oldest row
**Verified by:** `src/adapter/find.test.ts::"findOne returns null on no match"` AND `src/adapter/find.test.ts::"findMany honours limit/offset/sortBy"` AND `tests/integration/conformance.test.ts::"findOne and findMany conformance against real SurrealDB"`
---
### Requirement: compileWhere supports the full Better-Auth operator set
`compileWhere(where: Where[], options)` SHALL support operators `eq` (default), `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `not_in`, `contains`, `starts_with`, `ends_with` AND connectors `AND` (default), `OR`. The output SHALL be `{ clause: string; vars: Record<string, unknown> }` with placeholders `$w0`, `$w1`, …. Link-field values are pre-resolved per `options.linkFields[model]` before binding. Unknown operators SHALL raise `BetterAuthSurrealError(BETTER_AUTH_SURREALDB_INVALID_OPERATOR)`.
#### Scenario: operator maps to SurrealQL
- **WHEN** `compileWhere([{ field: "age", value: 18, operator: "gte" }], { model: "user", linkFields: {} })` is called
- **THEN** the result `clause` contains `age >= $w0` AND `vars.w0 === 18`
#### Scenario: connector composes clauses
- **WHEN** `compileWhere([{ field: "a", value: 1 }, { field: "b", value: 2, connector: "OR" }], { model, linkFields: {} })` is called
- **THEN** the clause is `a = $w0 OR b = $w1`
#### Scenario: unknown operator raises INVALID_OPERATOR
- **WHEN** `compileWhere([{ field: "x", value: 1, operator: "weird" as never }], { model, linkFields: {} })` is called
- **THEN** `BetterAuthSurrealError(BETTER_AUTH_SURREALDB_INVALID_OPERATOR)` is thrown
**Verified by:** `src/compile/compile-where.test.ts::"maps every supported operator"` AND `src/compile/compile-where.test.ts::"AND/OR connectors compose"` AND `src/compile/compile-where.test.ts::"unknown operator raises INVALID_OPERATOR"` AND `src/compile/compile-where.test.ts::"placeholder names never collide"` AND `src/compile/compile-where.test.ts::"link-field where values pre-resolved to record-link form"`
---
### Requirement: update / updateMany merge fields and return the affected count or row
`adapter.update<T>({ model, where, update })` SHALL issue `UPDATE <table> MERGE $update WHERE <compiled> RETURN AFTER` and return the first updated row (with prefixes stripped) or `null`. `adapter.updateMany(args)` SHALL return the number of rows updated.
#### Scenario: update returns the merged row
- **WHEN** a `user` exists with `email: "old"` and `adapter.update({ model: "user", where: [{ field: "id", value: "u1" }], update: { email: "new" } })` is awaited
- **THEN** the result has `email === "new"` AND `id === "u1"` (prefix stripped)
#### Scenario: updateMany returns count
- **WHEN** 3 users have `role: "free"` and `adapter.updateMany({ model: "user", where: [{ field: "role", value: "free" }], update: { role: "pro" } })` is awaited
- **THEN** the result equals 3
**Verified by:** `src/adapter/update.test.ts::"update returns merged row"` AND `src/adapter/update.test.ts::"updateMany returns affected count"` AND `tests/integration/conformance.test.ts::"update conformance against real SurrealDB"`
---
### Requirement: delete / deleteMany remove rows by Where filter
`adapter.delete({ model, where })` SHALL issue `DELETE <table> WHERE <compiled>` and return `void`. `adapter.deleteMany(args)` SHALL return the number of rows deleted.
#### Scenario: delete removes the row
- **WHEN** a session exists at `session:s1` and `adapter.delete({ model: "session", where: [{ field: "id", value: "s1" }] })` is awaited
- **THEN** a subsequent `findOne` for that id returns `null`
#### Scenario: deleteMany returns count
- **WHEN** 4 expired sessions exist and `adapter.deleteMany({ model: "session", where: [{ field: "expiresAt", value: now, operator: "lt" }] })` is awaited
- **THEN** the result equals 4
**Verified by:** `src/adapter/delete.test.ts::"delete removes the row"` AND `src/adapter/delete.test.ts::"deleteMany returns affected count"`
---
### Requirement: count uses SELECT count() GROUP ALL
`adapter.count({ model, where? })` SHALL issue `SELECT count() FROM <table> [WHERE <compiled>] GROUP ALL` and return the numeric count.
#### Scenario: count without where returns total
- **WHEN** 10 sessions exist and `adapter.count({ model: "session" })` is awaited
- **THEN** the result equals 10
#### Scenario: count with where returns filtered
- **WHEN** 10 sessions exist and 3 have `userId: "u1"`, and `adapter.count({ model: "session", where: [{ field: "userId", value: "u1" }] })` is awaited
- **THEN** the result equals 3 (where-clause pre-resolves `userId` to record-link form)
**Verified by:** `src/adapter/count.test.ts::"count without where returns total"` AND `src/adapter/count.test.ts::"count with where returns filtered"`
---
### Requirement: idStrategy controls record-ID format
The package SHALL support 3 `idStrategy` values: `"record-id"` (default), `"plain"`, `"ulid"`. `idStrategy: "record-id"` with an `id` containing a `:` character SHALL raise `BetterAuthSurrealError(BETTER_AUTH_SURREALDB_INVALID_ID_FORMAT)`.
#### Scenario: plain strategy stores id as a field
- **WHEN** `createBetterAuthSurrealAdapter({ client, idStrategy: "plain" })` runs `adapter.create({ model: "user", data: { id: "u1", email: "x" } })`
- **THEN** the stored record's SurrealDB-native id is auto-generated AND the row has fields `id: "u1"` and `email: "x"`
#### Scenario: invalid record-id format raises
- **WHEN** the default strategy receives `data.id = "abc:def"` on create
- **THEN** `BetterAuthSurrealError(BETTER_AUTH_SURREALDB_INVALID_ID_FORMAT)` is thrown
**Verified by:** `src/id-strategy/id-strategy.test.ts::"plain strategy stores id as a field"` AND `src/id-strategy/id-strategy.test.ts::"record-id strategy strips and reapplies prefix"` AND `src/id-strategy/id-strategy.test.ts::"ulid strategy fills missing id"` AND `src/id-strategy/id-strategy.test.ts::"colon in record-id raises INVALID_ID_FORMAT"`
---
### Requirement: modelToTable mapping is overridable and validated
`options.modelToTable?: (model: string) => string` SHALL default to identity. When supplied, the function output SHALL be cached per model. Returned table names SHALL match `/^[a-zA-Z_][a-zA-Z0-9_]*$/`; mismatches raise `BetterAuthSurrealError(BETTER_AUTH_SURREALDB_INVALID_MODEL)` on first use.
#### Scenario: custom mapping prefixes every table
- **WHEN** `createBetterAuthSurrealAdapter({ client, modelToTable: (m) => "bauth_" + m })` runs `adapter.create({ model: "user", data: { … } })`
- **THEN** the row is stored under `bauth_user:<id>`
#### Scenario: invalid table name raises
- **WHEN** `modelToTable` returns `"weird-name"` for some model and `create` is called
- **THEN** `BetterAuthSurrealError(BETTER_AUTH_SURREALDB_INVALID_MODEL)` is thrown
**Verified by:** `src/adapter/model-to-table.test.ts::"custom mapping prefixes tables"` AND `src/adapter/model-to-table.test.ts::"invalid table name raises INVALID_MODEL"` AND `src/adapter/model-to-table.test.ts::"mapping cached after first call"`
---
### Requirement: Bundled SurrealQL schema covers all 17 Better-Auth tables
The package SHALL export `BETTER_AUTH_SURREAL_SCHEMA` (frozen string) from `./schema` containing the DDL for all 17 Better-Auth tables: `user` (field extensions: `emailVerified`, `name`, `image`), `session`, `account`, `verification`, `passkey`, `jwks`, `sso_provider`, `oauth_application`, `oauth_access_token`, `oauth_consent`, `organization`, `member`, `invitation`, `team`, `team_member`, `two_factor`, `api_key` — plus 13 indexes (covering email uniqueness, phone uniqueness, provider+account combos, etc.). DDL SHALL use `DEFINE TABLE OVERWRITE` and `DEFINE FIELD OVERWRITE` so re-application is idempotent. `applyBetterAuthSchema(adapter): Promise<void>` SHALL execute the schema string via `adapter.execute`. Schema content matches the canonical Suaveplan target at `/mnt/f/surrealdb/surql/features/02-auth/02-better-auth.surql`.
#### Scenario: schema string contains every required table
- **WHEN** the conformance test parses `BETTER_AUTH_SURREAL_SCHEMA` for `DEFINE TABLE OVERWRITE <name>` occurrences
- **THEN** the set includes session, account, verification, passkey, jwks, sso_provider, oauth_application, oauth_access_token, oauth_consent, organization, member, invitation, team, team_member, two_factor, api_key (16 explicit `DEFINE TABLE` — `user` table is created elsewhere, only fields are extended here)
#### Scenario: applyBetterAuthSchema is idempotent
- **WHEN** `applyBetterAuthSchema(adapter)` is called twice in succession against a real SurrealDB testcontainer
- **THEN** the second call resolves without error AND the table set is unchanged
#### Scenario: applied schema accepts canonical Better-Auth row writes
- **WHEN** `applyBetterAuthSchema(adapter)` is called, then `adapter.create` is invoked for a `session` with `userId: "user:u1"` and an `account` with `userId: "u1"`
- **THEN** both writes succeed (session enforces `record<user>`; account accepts `string`)
**Verified by:** `src/schema/schema.test.ts::"BETTER_AUTH_SURREAL_SCHEMA contains every required table"` AND `src/schema/schema.test.ts::"applyBetterAuthSchema is idempotent against real SurrealDB"` AND `tests/integration/schema-write-compatibility.test.ts::"applied schema accepts canonical Better-Auth row writes"`
---
### Requirement: Zod schemas exported at ./schemas/* subpaths for cross-package consumption
The package SHALL export Zod schemas at dedicated subpaths so downstream callers consume the same types the producer asserts at the boundary: `./schemas/options` exports `BetterAuthSurrealAdapterOptionsSchema`; `./schemas/link-fields` exports `LinkFieldMapSchema`. Boundary failures SHALL raise `BetterAuthSurrealError(BETTER_AUTH_SURREALDB_INVALID_INPUT)` carrying the `ZodError` as `cause`.
#### Scenario: link-field structure rejected by LinkFieldMapSchema
- **WHEN** `LinkFieldMapSchema.safeParse({ session: { userId: 5 } })` is called (non-string target table)
- **THEN** `safeParse.success === false`
#### Scenario: createBetterAuthSurrealAdapter rejects malformed linkFields
- **WHEN** `createBetterAuthSurrealAdapter({ client, linkFields: { session: { userId: 5 as never } } })` is called
- **THEN** `BetterAuthSurrealError(BETTER_AUTH_SURREALDB_INVALID_INPUT)` is thrown with `cause instanceof ZodError`
**Verified by:** `src/schemas/options.test.ts::"BetterAuthSurrealAdapterOptionsSchema rejects missing client"` AND `src/schemas/link-fields.test.ts::"LinkFieldMapSchema rejects non-string target table"` AND `src/adapter/adapter.test.ts::"createBetterAuthSurrealAdapter rejects malformed linkFields"`
---
### Requirement: Every BETTER_AUTH_SURREALDB_* code is thrown from a production path
`BETTER_AUTH_SURREALDB_CODES` SHALL define exactly 7 codes (`INVALID_INPUT`, `INVALID_MODEL`, `INVALID_OPERATOR`, `QUERY_FAILED`, `INVALID_ID_FORMAT`, `INVALID_LINK_FIELD_VALUE`, `CONNECTION_NOT_READY`). Every code SHALL be raised by ≥1 production call site.
#### Scenario: QUERY_FAILED wraps underlying SurrealError
- **WHEN** the underlying `SurrealAdapter.query` throws a `SurrealError` and an adapter operation is in-flight
- **THEN** the operation rejects with `BetterAuthSurrealError(BETTER_AUTH_SURREALDB_QUERY_FAILED)` carrying the `SurrealError` as `cause`
#### Scenario: INVALID_LINK_FIELD_VALUE on bad link-field write
- **WHEN** `adapter.create({ model: "session", data: { userId: "abc:def" } })` is called with default `linkFields` (record-link target receives a value containing `:`)
- **THEN** `BetterAuthSurrealError(BETTER_AUTH_SURREALDB_INVALID_LINK_FIELD_VALUE)` is thrown
**Verified by:** `src/better-auth-surrealdb.spec-conformance.test.ts::"every BETTER_AUTH_SURREALDB_* code is thrown from a production path"`
---
### Requirement: OTel counter + histogram emit per operation
The package SHALL register counter `better_auth.surrealdb.operations_total` (attributes: `model`, `operation`, `outcome: "success" | "error"`) and histogram `better_auth.surrealdb.operation_duration_ms` (attribute: `operation`). Every adapter method emits both.
#### Scenario: counter emits success + error
- **WHEN** a successful `create` and a failed operation run against a real in-memory `MeterProvider`
- **THEN** `operations_total{outcome: "success"}` AND `operations_total{outcome: "error"}` both have observations; the histogram records both durations
**Verified by:** `src/otel/otel.test.ts::"counter emits success"` AND `src/otel/otel.test.ts::"counter emits error"` AND `src/otel/otel.test.ts::"histogram records per operation"`
---
### Requirement: Conformance harness exercises the full Better-Auth Adapter contract
The package SHALL export `runAdapterConformance(factory)` from `./testing`. The harness SHALL apply `BETTER_AUTH_SURREAL_SCHEMA` to the testcontainer-backed connection before running scenarios, then exercise every contract scenario Better-Auth's first-party adapters pass (CRUD + count + Where-filter combinations).
#### Scenario: harness registers every Better-Auth scenario
- **WHEN** `runAdapterConformance(() => createBetterAuthSurrealAdapter({ client }))` is invoked inside a test file backed by a real SurrealDB testcontainer
- **THEN** the resulting test run includes a "Better-Auth adapter conformance" describe block covering create / findOne / findMany / update / updateMany / delete / deleteMany / count, plus per-operator Where-filter coverage and link-field roundtrip checks
**Verified by:** `tests/integration/conformance.test.ts::"runAdapterConformance pins every Better-Auth scenario against real SurrealDB"`
---
### Requirement: Server-tier package shape
The package SHALL run in Bun + Node. `package.json` SHALL declare subpath exports per feature module (`.`, `./adapter`, `./compile`, `./id-strategy`, `./link-fields`, `./schema`, `./schemas/options`, `./schemas/link-fields`, `./testing`, `./error-codes`, `./otel`, `./types`). Every implementation `.ts` SHALL have a sibling `.md` ≥200 words. No `export *` in feature barrels. 100/100/100/100 coverage via Istanbul.
#### Scenario: every subpath is reachable
- **WHEN** `tests/dep-graph.test.ts` inspects `package.json.exports` and imports each subpath
- **THEN** each resolves to a real module
**Verified by:** `tests/dep-graph.test.ts::"every subpath export resolves"` AND `tests/dep-graph.test.ts::"every impl .ts has a sibling .md"` AND `tests/dep-graph.test.ts::"dependencies match the allowlist"`
---
### Requirement: betterAuthSurrealAdapter returns a Better-Auth v1.6 DBAdapterInstance
The package SHALL export `betterAuthSurrealAdapter(options): DBAdapterInstance` from the root barrel AND from a new `./adapter/v1.6` subpath. The return value SHALL be a function `(betterAuthOptions: BetterAuthOptions) => DBAdapter` that Better-Auth v1.6's `createBetterAuth` accepts as its `database` field. The factory SHALL Zod-validate `options` via `BetterAuthSurrealAdapterOptionsSchema` (same schema the v1.4 factory uses); failures raise `BetterAuthSurrealError(BETTER_AUTH_SURREALDB_INVALID_INPUT)`.
#### Scenario: factory consumed by createBetterAuth
- **WHEN** `createBetterAuth({ database: betterAuthSurrealAdapter({ client }), secret: "x", baseUrl: "http://localhost" })` is called against a real SurrealDB testcontainer with the schema applied
- **THEN** the call returns a `BetterAuth` instance whose `auth.api.signUpEmail({ body: { email, password, name } })` round-trips successfully (creates user, account, session)
**Verified by:** `src/adapter/v1.6.test.ts::"factory consumed by createBetterAuth signs up successfully"` AND `src/adapter/v1.6.spec-conformance.test.ts::"return type satisfies DBAdapterInstance via expectTypeOf"`
---
### Requirement: v1.6 factory wraps the shipped 10-method adapter via createAdapterFactory
The v1.6 factory SHALL internally call `createBetterAuthSurrealAdapter(options)` to build the underlying 10-method adapter and SHALL wrap it via `createAdapterFactory` from `@better-auth/core/db`. The wrapper SHALL forward every Better-Auth v1.6 method onto the underlying adapter's existing methods: `findOne`/`findMany`/`create`/`update`/`updateMany`/`delete`/`deleteMany`/`count` map 1:1; `transaction` forwards to `runSurrealTransaction` from `@suaveplan/surrealdb`; `getDefaultIdField` returns `"id"`; `createSchema` returns `BETTER_AUTH_SURREAL_SCHEMA`. Underlying error classes (`BetterAuthSurrealError`) propagate through the wrapper unchanged.
#### Scenario: error propagation through the wrapper
- **WHEN** an invalid operator surfaces in a wrapped v1.6 call (e.g. a `findMany` whose Where uses an unsupported op)
- **THEN** the rejection is a `BetterAuthSurrealError(BETTER_AUTH_SURREALDB_INVALID_OPERATOR)` (not a generic wrapper error)
#### Scenario: transaction forwards to SurrealDB
- **WHEN** Better-Auth invokes `adapter.transaction(async (txAdapter) => txAdapter.create({...}))` on the wrapped factory
- **THEN** `runSurrealTransaction` is invoked AND the inner `create` runs inside the transaction (rolled back on throw, committed on resolve)
**Verified by:** `src/adapter/v1.6.test.ts::"errors propagate from the underlying adapter"` AND `src/adapter/v1.6.test.ts::"transaction forwards to runSurrealTransaction"` AND `src/adapter/v1.6.test.ts::"getDefaultIdField returns 'id'"`
---
### Requirement: createSchema returns BETTER_AUTH_SURREAL_SCHEMA verbatim
The wrapped adapter's `createSchema` method (called by Better-Auth's CLI generator) SHALL return the exact string content of `BETTER_AUTH_SURREAL_SCHEMA`. No interpolation, no per-config customisation in v1 — operators who need a modified schema run `@better-auth/cli generate --output ./schema.surql` then edit the file.
#### Scenario: createSchema output matches BETTER_AUTH_SURREAL_SCHEMA
- **WHEN** the wrapped adapter's `createSchema()` is invoked
- **THEN** the result equals `BETTER_AUTH_SURREAL_SCHEMA` (character-for-character)
**Verified by:** `src/adapter/v1.6.test.ts::"createSchema returns BETTER_AUTH_SURREAL_SCHEMA verbatim"`
---
### Requirement: v1.6 wrapper is exercised end-to-end against real Better-Auth + SurrealDB
The v1.6 wrapper SHALL be exercised by a full Better-Auth end-to-end integration test (`tests/integration/better-auth-end-to-end.test.ts`) that constructs `betterAuth({ database: betterAuthSurrealAdapter({ client }) })` against a real SurrealDB testcontainer and rounds-trips `auth.api.signUpEmail`. The existing v1.4-shape `runAdapterConformance` harness MAY NOT run verbatim against the v1.6 DBAdapter because Better-Auth's `transformInput` whitelists fields against the in-memory Better-Auth schema, which uses column names that differ from the bundled SurrealQL schema (e.g. Better-Auth's `verification.value` vs. the package's `verification.val`). The wrapper is still smoke-tested against the testcontainer (factory builds, DBAdapter shape correct) and unit-tested at 100% line/branch/function coverage in `src/adapter/v1.6.test.ts`.
#### Scenario: v1.4-shape harness runs against the v1.4 factory
- **WHEN** the integration test runs `runAdapterConformance(() => createBetterAuthSurrealAdapter({ client }))` against the testcontainer
- **THEN** every scenario passes (no regression in the v1.4 surface)
#### Scenario: v1.6 wrapper round-trips a full Better-Auth signUpEmail flow
- **WHEN** the e2e test constructs `betterAuth({ database: betterAuthSurrealAdapter({ client }), … })` and calls `auth.api.signUpEmail({ body: { email, password, name } })` against the testcontainer
- **THEN** the call resolves with a `user` whose `email` matches the input, AND the SurrealDB `user` table contains a row with that email, AND the `account` table contains a `credential`-provider row
**Verified by:** `tests/integration/conformance.test.ts::"v1.4-shape adapter passes conformance"` AND `tests/integration/conformance.test.ts::"factory build + DBAdapter shape smoke check (full flow lives in better-auth-end-to-end.test.ts)"` AND `tests/integration/better-auth-end-to-end.test.ts::"createBetterAuth + signUpEmail round-trips against real SurrealDB"`
---
### Requirement: README v1.6 quick-start is the default; v1.4 documented as advanced
The package README SHALL show `betterAuthSurrealAdapter` (v1.6) usage as the default quick-start. The v1.4-shape `createBetterAuthSurrealAdapter` SHALL be documented under an "Advanced — direct conformance harness usage" section. New subpath `./adapter/v1.6` is listed alongside the existing `./adapter`.
#### Scenario: README quick-start uses betterAuthSurrealAdapter
- **WHEN** the docs:check / readme-lint pass scans the README
- **THEN** the first non-install code block uses `betterAuthSurrealAdapter(...)` (not the v1.4 surface)
**Verified by:** `tests/dep-graph.test.ts::"README quick-start uses betterAuthSurrealAdapter v1.6 factory"` AND `tests/dep-graph.test.ts::"./adapter/v1.6 subpath export resolves"`
<!-- markdownlint-enable MD024 -->