zappier-edge/src/db/customer-repo.ts

112 lines
4.5 KiB
TypeScript

import Database from 'better-sqlite3';
import { Customer, CustomerRepo } from '../auth';
export class SqliteCustomerRepo implements CustomerRepo {
constructor(private db: Database.Database, seed: Customer[] = []) {
this.db.exec(`
CREATE TABLE IF NOT EXISTS customers (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
tier_id TEXT NOT NULL,
api_key TEXT NOT NULL UNIQUE,
stripe_customer_id TEXT,
multiplier_override REAL
)
`);
// Idempotent migrations for pre-existing databases.
this.ensureColumn('billing_type', `billing_type TEXT NOT NULL DEFAULT 'stripe'`);
this.ensureColumn('email', 'email TEXT');
this.ensureColumn('password_hash', 'password_hash TEXT');
this.ensureColumn('totp_secret', 'totp_secret TEXT');
this.ensureColumn('totp_enabled', 'totp_enabled INTEGER NOT NULL DEFAULT 0');
this.ensureColumn('balance_cents', 'balance_cents INTEGER NOT NULL DEFAULT 0');
this.ensureColumn('email_invoicing', 'email_invoicing INTEGER NOT NULL DEFAULT 0');
const { n } = this.db.prepare('SELECT COUNT(*) AS n FROM customers').get() as { n: number };
if (n === 0) {
for (const c of seed) this.save(c);
}
}
private ensureColumn(name: string, ddl: string): void {
const cols = this.db.prepare('PRAGMA table_info(customers)').all() as { name: string }[];
if (!cols.some((c) => c.name === name)) {
this.db.exec(`ALTER TABLE customers ADD COLUMN ${ddl}`);
}
}
findByApiKey(apiKey: string): Customer | undefined {
const r = this.db.prepare('SELECT * FROM customers WHERE api_key = ?').get(apiKey) as
| Record<string, unknown>
| undefined;
return r ? toCustomer(r) : undefined;
}
findByEmail(email: string): Customer | undefined {
const r = this.db
.prepare('SELECT * FROM customers WHERE lower(email) = lower(?)')
.get(email) as Record<string, unknown> | undefined;
return r ? toCustomer(r) : undefined;
}
list(): Customer[] {
const rows = this.db.prepare('SELECT * FROM customers ORDER BY id').all() as Record<
string,
unknown
>[];
return rows.map(toCustomer);
}
save(customer: Customer): void {
this.db
.prepare(
`INSERT INTO customers (id, name, tier_id, api_key, stripe_customer_id, multiplier_override, billing_type, email, password_hash, totp_secret, totp_enabled, balance_cents, email_invoicing)
VALUES (@id, @name, @tierId, @apiKey, @stripeCustomerId, @multiplierOverride, @billingType, @email, @passwordHash, @totpSecret, @totpEnabled, @balanceCents, @emailInvoicing)
ON CONFLICT(id) DO UPDATE SET
name = @name,
tier_id = @tierId,
api_key = @apiKey,
stripe_customer_id = @stripeCustomerId,
multiplier_override = @multiplierOverride,
billing_type = @billingType,
email = @email,
password_hash = @passwordHash,
totp_secret = @totpSecret,
totp_enabled = @totpEnabled,
balance_cents = @balanceCents,
email_invoicing = @emailInvoicing`,
)
.run({
id: customer.id,
name: customer.name,
tierId: customer.tierId,
apiKey: customer.apiKey,
stripeCustomerId: customer.stripeCustomerId ?? null,
multiplierOverride: customer.multiplierOverride ?? null,
billingType: customer.billingType ?? 'stripe',
email: customer.email ?? null,
passwordHash: customer.passwordHash ?? null,
totpSecret: customer.totpSecret ?? null,
totpEnabled: customer.totpEnabled ? 1 : 0,
balanceCents: customer.balanceCents ?? 0,
emailInvoicing: customer.emailInvoicing ? 1 : 0,
});
}
}
function toCustomer(r: Record<string, unknown>): Customer {
return {
id: r.id as string,
name: r.name as string,
tierId: r.tier_id as string,
apiKey: r.api_key as string,
stripeCustomerId: (r.stripe_customer_id as string | null) ?? undefined,
multiplierOverride: (r.multiplier_override as number | null) ?? undefined,
billingType: ((r.billing_type as string | null) ?? 'stripe') as Customer['billingType'],
email: (r.email as string | null) ?? undefined,
passwordHash: (r.password_hash as string | null) ?? undefined,
totpSecret: (r.totp_secret as string | null) ?? undefined,
totpEnabled: ((r.totp_enabled as number | null) ?? 0) === 1,
balanceCents: (r.balance_cents as number | null) ?? 0,
emailInvoicing: ((r.email_invoicing as number | null) ?? 0) === 1,
};
}