feat: add PostgreSQL backend

Mirrors the node:sqlite sub-backend structure with full migration support.
Uses native pg types (UUID, JSONB, TIMESTAMPTZ) and $1/$2 parameterisation
via internal ? placeholders converted at execution time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 02:00:10 +00:00
co-authored by Claude
parent dd8d31633f
commit 4521274a27
22 changed files with 1273 additions and 1 deletions
+137
View File
@@ -0,0 +1,137 @@
import type { Abode } from "../types/Abode.js";
import type { ApikeyPermissions, ClientApikey } from "../types/Apikey.js";
import type { Resident, ResidentFlags } from "../types/Resident.js";
import type { ClientUser, PartialUser, UserFlags } from "../types/User.js";
export function pgToDate(d: Date | string): string {
return d instanceof Date ? d.toISOString() : new Date(d).toISOString();
}
const defaultUserFlags: UserFlags = {};
export function pgToUserFlags(flags: unknown): UserFlags {
const out = { ...defaultUserFlags };
if (typeof flags !== "object" || !flags || Array.isArray(flags)) return out;
const f = flags as Record<string, unknown>;
if (f.admin === true) out.admin = true;
return out;
}
export function pgToPartialUser(user: {
uid: string;
name: string;
flags: unknown;
created_at: Date | string;
updated_at: Date | string;
}): PartialUser {
return {
uid: user.uid,
name: user.name,
flags: pgToUserFlags(user.flags),
created_at: pgToDate(user.created_at),
updated_at: pgToDate(user.updated_at),
};
}
export function pgToClientUser(user: {
uid: string;
email: string;
name: string;
flags: unknown;
created_at: Date | string;
updated_at: Date | string;
}): ClientUser {
return {
...pgToPartialUser(user),
email: user.email,
};
}
export function pgToAbode(abode: {
aid: string;
name: string;
created_at: Date | string;
created_by: string | null;
updated_at: Date | string;
updated_by: string | null;
}): Abode {
return {
aid: abode.aid,
name: abode.name,
created_at: pgToDate(abode.created_at),
created_by: abode.created_by,
updated_at: pgToDate(abode.updated_at),
updated_by: abode.updated_by,
};
}
const defaultResidentFlags: ResidentFlags = {};
export function pgToResidentFlags(flags: unknown): ResidentFlags {
const out = { ...defaultResidentFlags };
if (typeof flags !== "object" || !flags || Array.isArray(flags)) return out;
const f = flags as Record<string, unknown>;
if (f.admin === true) out.admin = true;
return out;
}
export function pgToResident(resident: {
uid: string;
aid: string;
flags: unknown;
created_at: Date | string;
created_by: string | null;
updated_at: Date | string;
updated_by: string | null;
}): Resident {
return {
uid: resident.uid,
aid: resident.aid,
flags: pgToResidentFlags(resident.flags),
created_at: pgToDate(resident.created_at),
created_by: resident.created_by,
updated_at: pgToDate(resident.updated_at),
updated_by: resident.updated_by,
};
}
const defaultApikeyPermissions: ApikeyPermissions = {};
export function pgToApikeyPermissions(permissions: unknown): ApikeyPermissions {
const out = { ...defaultApikeyPermissions };
if (
typeof permissions !== "object" ||
!permissions ||
Array.isArray(permissions)
)
return out;
const p = permissions as Record<string, unknown>;
if (p.admin === true) out.admin = true;
if (p.all === true) out.all = true;
for (const key of ["users", "residents", "abodes"] as const) {
if (p[key] === "r" || p[key] === "rw") out[key] = p[key] as "r" | "rw";
}
for (const key of ["restrict_users", "restrict_abodes"] as const) {
if (
Array.isArray(p[key]) &&
(p[key] as unknown[]).every((x) => typeof x === "string")
) {
out[key] = p[key] as string[];
}
}
return out;
}
export function pgToClientApikey(apikey: {
uid: string;
kid: string;
name: string;
permissions: unknown;
created_at: Date | string;
expires_at: Date | string | null;
}): ClientApikey {
return {
uid: apikey.uid,
kid: apikey.kid,
name: apikey.name,
permissions: pgToApikeyPermissions(apikey.permissions),
created_at: pgToDate(apikey.created_at),
expires_at: apikey.expires_at ? pgToDate(apikey.expires_at) : null,
};
}