feat(db): mesh data model — meshes, members, invites, audit log

- pgSchema "mesh" with 4 tables isolating the peer mesh domain
- Enums: visibility, transport, tier, role
- audit_log is metadata-only (E2E encryption enforced at broker/client)
- Cascade on mesh delete, soft-delete via archivedAt/revokedAt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-04-04 21:19:32 +01:00
commit d3163a5bff
1384 changed files with 314925 additions and 0 deletions

View File

@@ -0,0 +1,89 @@
#!/usr/bin/env tsx
/**
* Backfill customer records for existing users without them.
* Run once after deploying the new schema.
*
* Usage: pnpm with-env pnpm dlx tsx packages/db/src/scripts/backfill-customers.ts
*/
import { eq, isNull } from "drizzle-orm";
import { generateId } from "@turbostarter/shared/utils";
import { creditTransaction, customer, user } from "../schema";
import { db } from "../server";
const DEFAULT_CREDITS = 100;
async function backfillCustomers() {
console.log("Starting customer backfill...\n");
// Find users without customer records using a left join
const usersWithoutCustomers = await db
.select({
id: user.id,
email: user.email,
name: user.name,
})
.from(user)
.leftJoin(customer, eq(user.id, customer.userId))
.where(isNull(customer.id));
console.log(
`Found ${usersWithoutCustomers.length} users without customer records\n`,
);
if (usersWithoutCustomers.length === 0) {
console.log("No users to backfill. Done!");
return;
}
let created = 0;
let errors = 0;
for (const u of usersWithoutCustomers) {
const customerId = generateId();
try {
await db.transaction(async (tx) => {
await tx.insert(customer).values({
id: customerId,
userId: u.id,
customerId: `backfill_${u.id}`,
status: "active",
plan: "free",
credits: DEFAULT_CREDITS,
});
await tx.insert(creditTransaction).values({
id: generateId(),
customerId,
amount: DEFAULT_CREDITS,
type: "signup",
reason: "Backfill: Welcome credits for existing user",
balanceAfter: DEFAULT_CREDITS,
});
});
console.log(`✓ Created customer for ${u.email} (${u.name})`);
created++;
} catch (error) {
console.error(
`✗ Failed for ${u.email}:`,
error instanceof Error ? error.message : error,
);
errors++;
}
}
console.log(`\nBackfill complete!`);
console.log(` Created: ${created}`);
console.log(` Errors: ${errors}`);
}
backfillCustomers()
.then(() => process.exit(0))
.catch((error) => {
console.error("Backfill failed:", error);
process.exit(1);
});

View File

@@ -0,0 +1,18 @@
import { reset } from "drizzle-seed";
import { logger } from "@turbostarter/shared/logger";
import * as schema from "../schema";
import { db } from "../server";
async function main() {
await reset(db, schema);
logger.info("Database reset successfully!");
process.exit(0);
}
main().catch((error) => {
logger.error(error);
process.exit(1);
});

View File

@@ -0,0 +1,12 @@
import { logger } from "@turbostarter/shared/logger";
function main() {
/**
* Place your seeding logic here
*/
logger.info("Database seeded successfully!");
process.exit(0);
}
main();

View File

@@ -0,0 +1,101 @@
import { sql } from "drizzle-orm";
import fs from "fs";
import path from "path";
import { logger } from "@turbostarter/shared/logger";
import { db } from "../server";
interface JournalEntry {
idx: number;
version: string;
when: number;
tag: string;
breakpoints: boolean;
}
interface JournalFile {
version: string;
dialect: string;
entries?: JournalEntry[];
}
const JOURNAL_PATH = path.resolve("migrations/meta/_journal.json");
function loadJournalEntries(): JournalEntry[] {
if (!fs.existsSync(JOURNAL_PATH)) {
throw new Error(`Migrations journal not found at ${JOURNAL_PATH}`);
}
const journalFile = fs.readFileSync(JOURNAL_PATH, "utf-8");
const parsed = JSON.parse(journalFile) as JournalFile;
return [...(parsed.entries ?? [])].sort((a, b) => a.idx - b.idx);
}
function toTimestamp(value: unknown): number | null {
if (typeof value === "number") {
return value;
}
if (typeof value === "string") {
const parsed = Number(value);
return Number.isNaN(parsed) ? null : parsed;
}
if (typeof value === "bigint") {
return Number(value);
}
if (value instanceof Date) {
return value.getTime();
}
return null;
}
async function fetchAppliedMigrationTimestamps(): Promise<number[]> {
const result = await db.execute(
sql`SELECT created_at FROM "drizzle"."__drizzle_migrations" ORDER BY created_at`,
);
return Array.from(result)
.map((row) => toTimestamp((row as { created_at?: unknown }).created_at))
.filter((timestamp): timestamp is number => timestamp !== null);
}
async function main() {
const journalEntries = loadJournalEntries();
const appliedTimestamps = await fetchAppliedMigrationTimestamps();
const appliedTimestampSet = new Set(appliedTimestamps);
const appliedMigrations = journalEntries.filter((entry) =>
appliedTimestampSet.has(entry.when),
);
const pendingMigrations = journalEntries.filter(
(entry) => !appliedTimestampSet.has(entry.when),
);
logger.info("\nApplied migrations:");
if (appliedMigrations.length === 0) {
logger.info("(none)");
} else {
appliedMigrations.forEach((entry) => logger.info(`- ${entry.tag}`));
}
logger.info("\nPending migrations:");
if (pendingMigrations.length === 0) {
logger.info("(none)");
} else {
pendingMigrations.forEach((entry) => logger.info(`- ${entry.tag}`));
}
}
void main()
.catch((err) => {
logger.error(err);
process.exit(1);
})
.then(() => {
process.exit(0);
});