165 lines
5.2 KiB
TypeScript
165 lines
5.2 KiB
TypeScript
import {
|
|
BETTER_AUTH_SURREALDB_CODES,
|
|
BetterAuthSurrealError,
|
|
} from "../error-codes/error-codes.js";
|
|
import type { IdStrategy, Row } from "../types/adapter.js";
|
|
|
|
/**
|
|
* Crockford-base32 alphabet (per ULID spec).
|
|
* I, L, O, U are intentionally absent.
|
|
*/
|
|
const CROCKFORD32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
|
|
/**
|
|
* Generate a 26-character Crockford-base32 ULID:
|
|
* - 10 chars time (48-bit ms-since-epoch),
|
|
* - 16 chars random (80 bits via `crypto.getRandomValues`).
|
|
*/
|
|
export function generateUlid(now: () => number = Date.now): string {
|
|
let time = now();
|
|
const timeChars: string[] = new Array(10);
|
|
for (let i = 9; i >= 0; i--) {
|
|
timeChars[i] = CROCKFORD32[time % 32] as string;
|
|
time = Math.floor(time / 32);
|
|
}
|
|
const rand = new Uint8Array(16);
|
|
crypto.getRandomValues(rand);
|
|
const randChars = new Array(16) as string[];
|
|
for (let i = 0; i < 16; i++) {
|
|
randChars[i] = CROCKFORD32[(rand[i] as number) % 32] as string;
|
|
}
|
|
return timeChars.join("") + randChars.join("");
|
|
}
|
|
|
|
/**
|
|
* Outcome of `prepareCreate` — fully-resolved CREATE arguments.
|
|
*
|
|
* - `target`: the SurrealQL target — either `<table>:<id>` (record-id /
|
|
* ulid) or just `<table>` (plain). The adapter splices this verbatim
|
|
* into `CREATE <target> CONTENT $data RETURN AFTER`.
|
|
* - `data`: row content excluding the id (record-id) or including it
|
|
* (plain).
|
|
* - `assignedId`: the id Better-Auth should see after the create returns
|
|
* (either supplied, ULID-generated, or `null` when SurrealDB will assign
|
|
* one in `plain` mode).
|
|
*/
|
|
export interface PreparedCreate {
|
|
readonly target: string;
|
|
readonly data: Row;
|
|
readonly assignedId: string | null;
|
|
}
|
|
|
|
const IDENT = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
|
|
/**
|
|
* Resolve a `(strategy, table, data)` tuple into the CREATE-arguments
|
|
* Better-Auth's adapter needs. Encapsulates the three id-strategy
|
|
* branches.
|
|
*/
|
|
export function prepareCreate(
|
|
strategy: IdStrategy,
|
|
table: string,
|
|
data: Row
|
|
): PreparedCreate {
|
|
switch (strategy) {
|
|
case "record-id":
|
|
return prepareRecordId(table, data, null);
|
|
case "ulid":
|
|
return prepareRecordId(table, data, generateUlid);
|
|
case "plain":
|
|
return preparePlain(table, data);
|
|
}
|
|
}
|
|
|
|
function prepareRecordId(
|
|
table: string,
|
|
data: Row,
|
|
fallback: (() => string) | null
|
|
): PreparedCreate {
|
|
const rawId = data["id"];
|
|
let id: string;
|
|
if (rawId === undefined || rawId === null || rawId === "") {
|
|
if (fallback === null) {
|
|
throw new BetterAuthSurrealError(
|
|
BETTER_AUTH_SURREALDB_CODES.INVALID_ID_FORMAT,
|
|
"idStrategy='record-id' requires data.id to be a non-empty string"
|
|
);
|
|
}
|
|
id = fallback();
|
|
} else {
|
|
if (typeof rawId !== "string") {
|
|
throw new BetterAuthSurrealError(
|
|
BETTER_AUTH_SURREALDB_CODES.INVALID_ID_FORMAT,
|
|
`data.id must be a string, got ${typeof rawId}`
|
|
);
|
|
}
|
|
if (rawId.includes(":")) {
|
|
throw new BetterAuthSurrealError(
|
|
BETTER_AUTH_SURREALDB_CODES.INVALID_ID_FORMAT,
|
|
`data.id "${rawId}" must not contain ":" — pass the bare id`
|
|
);
|
|
}
|
|
id = rawId;
|
|
}
|
|
if (!IDENT.test(table)) {
|
|
throw new BetterAuthSurrealError(
|
|
BETTER_AUTH_SURREALDB_CODES.INVALID_MODEL,
|
|
`table "${table}" is not a valid SurrealQL identifier`
|
|
);
|
|
}
|
|
const { id: _omit, ...rest } = data;
|
|
return {
|
|
target: `${table}:\`${id}\``,
|
|
data: rest,
|
|
assignedId: id,
|
|
};
|
|
}
|
|
|
|
function preparePlain(table: string, data: Row): PreparedCreate {
|
|
if (!IDENT.test(table)) {
|
|
throw new BetterAuthSurrealError(
|
|
BETTER_AUTH_SURREALDB_CODES.INVALID_MODEL,
|
|
`table "${table}" is not a valid SurrealQL identifier`
|
|
);
|
|
}
|
|
const assignedId =
|
|
typeof data["id"] === "string" ? (data["id"] as string) : null;
|
|
return {
|
|
target: table,
|
|
data: { ...data },
|
|
assignedId,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Normalise an `id` value returned by SurrealDB. SurrealDB returns either
|
|
* a `RecordId` object (`{ tb, id }`), a string `<table>:<id>`, or a
|
|
* bare value. This function returns the bare id portion as a string.
|
|
*/
|
|
export function normaliseReturnedId(value: unknown, table: string): string {
|
|
if (value === null || value === undefined) return "";
|
|
if (typeof value === "object") {
|
|
const maybeRecord = value as { id?: unknown; tb?: unknown };
|
|
if (typeof maybeRecord.id === "string") return maybeRecord.id;
|
|
if (
|
|
typeof maybeRecord.id === "object" &&
|
|
maybeRecord.id !== null &&
|
|
"String" in (maybeRecord.id as Record<string, unknown>)
|
|
) {
|
|
return String(
|
|
(maybeRecord.id as Record<string, unknown>)["String"]
|
|
);
|
|
}
|
|
return String(value);
|
|
}
|
|
if (typeof value !== "string") return String(value);
|
|
const prefix = `${table}:`;
|
|
if (value.startsWith(prefix)) {
|
|
const stripped = value.slice(prefix.length);
|
|
return stripped.startsWith("`") && stripped.endsWith("`")
|
|
? stripped.slice(1, -1)
|
|
: stripped;
|
|
}
|
|
return value;
|
|
}
|