@anysoftinc/anydb-sdk
Version:
AnyDB TypeScript SDK for querying and transacting with Datomic databases
443 lines (442 loc) • 16.4 kB
JavaScript
import { kw, sym, uuid as uuidEdn } from "./client";
import { v4 as uuidv4 } from "uuid";
/**
* Creates NextAuth cookie configuration with proper security settings
* Uses __Host- prefix and secure cookies in production, regular cookies in development
*/
export function createNextAuthCookies(cookieIdentifier) {
const isProduction = process.env.NODE_ENV === "production";
const prefix = isProduction ? "__Host-" : "";
const secure = isProduction;
return {
sessionToken: {
name: `${prefix}${cookieIdentifier}.session-token`,
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
secure,
},
},
callbackUrl: {
name: `${prefix}${cookieIdentifier}.callback-url`,
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
secure,
},
},
csrfToken: {
name: `${prefix}${cookieIdentifier}.csrf-token`,
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
secure,
},
},
};
}
// Datomic schema idents
const USER = {
id: "anydb.auth.user.v1/id", // uuid identity
name: "anydb.auth.user.v1/name",
email: "anydb.auth.user.v1/email",
emailVerified: "anydb.auth.user.v1/email-verified",
image: "anydb.auth.user.v1/image",
};
const ACCOUNT = {
id: "anydb.auth.account.v1/id", // uuid identity
userId: "anydb.auth.account.v1/user-id", // uuid ref by value
type: "anydb.auth.account.v1/type",
provider: "anydb.auth.account.v1/provider",
providerAccountId: "anydb.auth.account.v1/provider-account-id",
refreshToken: "anydb.auth.account.v1/refresh-token",
accessToken: "anydb.auth.account.v1/access-token",
expiresAt: "anydb.auth.account.v1/expires-at",
tokenType: "anydb.auth.account.v1/token-type",
scope: "anydb.auth.account.v1/scope",
idToken: "anydb.auth.account.v1/id-token",
sessionState: "anydb.auth.account.v1/session-state",
};
const SESSION = {
id: "anydb.auth.session.v1/id", // uuid identity
sessionToken: "anydb.auth.session.v1/session-token",
userId: "anydb.auth.session.v1/user-id",
expires: "anydb.auth.session.v1/expires",
};
const VTOKEN = {
id: "anydb.auth.vtoken.v1/id", // uuid identity
identifier: "anydb.auth.vtoken.v1/identifier",
token: "anydb.auth.vtoken.v1/token",
expires: "anydb.auth.vtoken.v1/expires",
};
function toUser(m) {
const get = (k) => m && (m[k] ?? m[`:${k}`]);
// USER.id is ":auth.user.v1/id", so slice(1) gives "auth.user.v1/id"
const idKey = USER.id; // "auth.user.v1/id"
const idVal = get(idKey);
const id = typeof idVal === "string" ? idVal : idVal?.value ?? String(idVal);
const emailVerified = get(USER.emailVerified);
return {
id,
name: get(USER.name) || null,
email: get(USER.email) || null,
emailVerified: emailVerified ? new Date(emailVerified) : null,
image: get(USER.image) || null,
};
}
/**
* Creates a NextAuth.js adapter for AnyDB/Datomic
*
* @param db - DatomicDatabase instance
* @returns NextAuth.js Adapter
*
* @example
* ```typescript
* import { createDatomicDatabase } from '@anysoftinc/anydb-sdk';
* import { AnyDBAdapter } from '@anysoftinc/anydb-sdk/nextauth-adapter';
*
* const db = createDatomicDatabase(client, 'storage', 'auth-db');
*
* export default NextAuth({
* adapter: AnyDBAdapter(db),
* // ... other config
* });
* ```
*/
export function AnyDBAdapter(db) {
async function getUserByEmail(email) {
const norm = typeof email === "string" ? email.toLowerCase() : email;
const q = {
find: [
[
sym("pull"),
sym("?e"),
[
kw(USER.id),
kw(USER.name),
kw(USER.email),
kw(USER.emailVerified),
kw(USER.image),
],
],
],
where: [[sym("?e"), kw(USER.email), norm]],
};
const res = await db.query(q);
const rows = Array.isArray(res) ? res : [];
return rows.length ? toUser(rows[0][0]) : null;
}
async function getUser(id) {
const q = {
find: [
[
sym("pull"),
sym("?e"),
[
kw(USER.id),
kw(USER.name),
kw(USER.email),
kw(USER.emailVerified),
kw(USER.image),
],
],
],
where: [[sym("?e"), kw(USER.id), uuidEdn(id)]],
};
const res = await db.query(q);
const rows = Array.isArray(res) ? res : [];
if (!rows.length)
return null;
return toUser(rows[0][0]);
}
return {
// Users
async createUser(data) {
// Check if user already exists by email
if (data.email) {
const existing = await getUserByEmail(data.email);
if (existing) {
return existing;
}
}
const id = uuidv4();
const tx = {
[USER.id]: uuidEdn(id),
...(data.name ? { [USER.name]: data.name } : {}),
...(data.email ? { [USER.email]: String(data.email).toLowerCase() } : {}),
...(data.emailVerified
? { [USER.emailVerified]: data.emailVerified }
: {}),
...(data.image ? { [USER.image]: data.image } : {}),
};
await db.transact([tx]);
return {
id,
name: data.name ?? null,
email: (data.email ? String(data.email).toLowerCase() : null),
emailVerified: data.emailVerified ?? null,
image: data.image ?? null,
};
},
getUser,
getUserByEmail,
async getUserByAccount({ provider, providerAccountId }) {
const q = {
find: [
[
sym("pull"),
sym("?u"),
[
kw(USER.id),
kw(USER.name),
kw(USER.email),
kw(USER.emailVerified),
kw(USER.image),
],
],
],
where: [
[sym("?a"), kw(ACCOUNT.provider), provider],
[sym("?a"), kw(ACCOUNT.providerAccountId), providerAccountId],
[sym("?a"), kw(ACCOUNT.userId), sym("?uid")],
[sym("?u"), kw(USER.id), sym("?uid")],
],
};
const res = await db.query(q);
const rows = Array.isArray(res) ? res : [];
return rows.length ? toUser(rows[0][0]) : null;
},
async updateUser(data) {
if (!data.id)
throw new Error("updateUser requires id");
const tx = {
[USER.id]: uuidEdn(data.id),
...(data.name !== undefined ? { [USER.name]: data.name } : {}),
...(data.email !== undefined
? { [USER.email]: data.email ? String(data.email).toLowerCase() : null }
: {}),
...(data.emailVerified !== undefined
? { [USER.emailVerified]: data.emailVerified }
: {}),
...(data.image !== undefined ? { [USER.image]: data.image } : {}),
};
await db.transact([tx]);
const u = await getUser(data.id);
return u;
},
async deleteUser(id) {
await db.transact([[kw("db/retractEntity"), [USER.id], uuidEdn(id)]]);
return null;
},
// Accounts
async linkAccount(account) {
const id = uuidv4();
const tx = {
[ACCOUNT.id]: uuidEdn(id),
[ACCOUNT.userId]: uuidEdn(account.userId),
[ACCOUNT.type]: account.type,
[ACCOUNT.provider]: account.provider,
[ACCOUNT.providerAccountId]: account.providerAccountId,
...(account.refresh_token
? { [ACCOUNT.refreshToken]: account.refresh_token }
: {}),
...(account.access_token
? { [ACCOUNT.accessToken]: account.access_token }
: {}),
...(account.expires_at
? { [ACCOUNT.expiresAt]: account.expires_at }
: {}),
...(account.token_type
? { [ACCOUNT.tokenType]: account.token_type }
: {}),
...(account.scope ? { [ACCOUNT.scope]: account.scope } : {}),
...(account.id_token ? { [ACCOUNT.idToken]: account.id_token } : {}),
...(account.session_state
? { [ACCOUNT.sessionState]: account.session_state }
: {}),
};
await db.transact([tx]);
return account;
},
async unlinkAccount({ provider, providerAccountId }) {
const q = {
find: [sym("?id")],
where: [
[sym("?a"), kw(ACCOUNT.provider), provider],
[sym("?a"), kw(ACCOUNT.providerAccountId), providerAccountId],
[sym("?a"), kw(ACCOUNT.id), sym("?id")],
],
};
const res = await db.query(q);
const id = Array.isArray(res) && res[0]?.[0];
if (id) {
await db.transact([[kw("db/retractEntity"), [ACCOUNT.id], id]]);
}
},
// Sessions
async createSession(session) {
const id = uuidv4();
let expiresDate;
if (session.expires instanceof Date &&
!isNaN(session.expires.getTime())) {
expiresDate = session.expires;
}
else if (session.expires) {
const d = new Date(session.expires);
expiresDate = isNaN(d.getTime())
? new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
: d;
}
else {
expiresDate = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
}
const tx = {
[SESSION.id]: uuidEdn(id),
[SESSION.userId]: uuidEdn(session.userId),
[SESSION.sessionToken]: session.sessionToken,
[SESSION.expires]: expiresDate,
};
await db.transact([tx]);
return { ...session, expires: expiresDate };
},
async getSessionAndUser(sessionToken) {
const q = {
find: [
[
sym("pull"),
sym("?s"),
[
kw(SESSION.id),
kw(SESSION.sessionToken),
kw(SESSION.userId),
kw(SESSION.expires),
],
],
[
sym("pull"),
sym("?u"),
[
kw(USER.id),
kw(USER.name),
kw(USER.email),
kw(USER.emailVerified),
kw(USER.image),
],
],
],
where: [
[sym("?s"), kw(SESSION.sessionToken), sessionToken],
[sym("?s"), kw(SESSION.userId), sym("?uid")],
[sym("?u"), kw(USER.id), sym("?uid")],
],
};
const res = await db.query(q);
const row = Array.isArray(res) ? res[0] : null;
if (!row)
return null;
const s = row[0];
const u = row[1];
const getS = (k) => s && (s[k] ?? s[`:${k}`]);
const expiresRaw = getS(SESSION.expires);
const expires = expiresRaw instanceof Date ? expiresRaw : new Date(expiresRaw);
const sessionResult = {
sessionToken: getS(SESSION.sessionToken),
userId: (getS(SESSION.userId)?.value ?? getS(SESSION.userId)),
expires,
};
return { session: sessionResult, user: toUser(u) };
},
async updateSession(partial) {
let expiresDate;
if (partial.expires instanceof Date) {
expiresDate = isNaN(partial.expires.getTime())
? undefined
: partial.expires;
}
else if (partial.expires) {
const d = new Date(partial.expires);
expiresDate = isNaN(d.getTime()) ? undefined : d;
}
if (!partial.sessionToken) {
throw new Error("updateSession requires sessionToken to upsert by identity");
}
const tx = {
...(partial.sessionToken
? { [SESSION.sessionToken]: partial.sessionToken }
: {}),
...(partial.userId
? { [SESSION.userId]: uuidEdn(partial.userId) }
: {}),
...(expiresDate ? { [SESSION.expires]: expiresDate } : {}),
};
await db.transact([tx]);
return {
...partial,
...(expiresDate ? { expires: expiresDate } : {}),
};
},
async deleteSession(sessionToken) {
const q = {
find: [sym("?id")],
where: [
[sym("?s"), kw(SESSION.sessionToken), sessionToken],
[sym("?s"), kw(SESSION.id), sym("?id")],
],
};
const res = await db.query(q);
const id = Array.isArray(res) && res[0]?.[0];
if (id) {
await db.transact([[kw("db/retractEntity"), [SESSION.id], id]]);
}
},
// Verification tokens (email sign-in, passwordless)
async createVerificationToken(token) {
const id = uuidv4();
const tx = {
[VTOKEN.id]: uuidEdn(id),
[VTOKEN.identifier]: token.identifier,
[VTOKEN.token]: token.token,
[VTOKEN.expires]: token.expires,
};
await db.transact([tx]);
return token;
},
async useVerificationToken(params) {
const q = {
find: [
[
sym("pull"),
sym("?e"),
[
kw(VTOKEN.id),
kw(VTOKEN.identifier),
kw(VTOKEN.token),
kw(VTOKEN.expires),
],
],
],
where: [
[sym("?e"), kw(VTOKEN.identifier), params.identifier],
[sym("?e"), kw(VTOKEN.token), params.token],
],
};
const res = await db.query(q);
const rows = Array.isArray(res) ? res : [];
if (!rows.length)
return null;
const m = rows[0][0];
const get = (k) => m && (m[k] ?? m[`:${k}`]);
const entityId = get(VTOKEN.id);
// Delete the token after using it
await db.transact([[kw("db/retractEntity"), entityId]]);
return {
identifier: get(VTOKEN.identifier),
token: get(VTOKEN.token),
expires: new Date(get(VTOKEN.expires)),
};
},
};
}