Major architectural upgrade preparing Echo OCR for self-hosted SaaS deployment: - Auth: Built-in Auth.js v5 with credentials + Authentik OIDC SSO, JWT sessions, middleware route protection, login/signup/setup pages, registration API - UI: Dashboard layout with collapsible sidebar nav, AppShell wrapper, route groups for (dashboard) and (auth), new pages for events/people/reports - Schema: Auth.js tables (Account, Session, VerificationToken), Organization, OrgMember, Location, CollectionDay, Invitation, SystemConfig, ApiKey models; proper User relations to ResponseCard/ActivityLog/Notification - Permissions: Role hierarchy (owner/admin/editor/reviewer/viewer) with action-based permission map and requirePermission/requireAuth helpers - Onboarding: Multi-step setup wizard for first-user bootstrap (account, org, location) with SystemConfig tracking - Events: CollectionDay model with rrule support for recurring church services - Auto-assign: Event-aware card assignment engine replacing getPreviousSunday() - Migration: seed-migration.ts script for upgrading existing deployments Made-with: Cursor
180 lines
5 KiB
TypeScript
180 lines
5 KiB
TypeScript
/**
|
|
* Data migration script for existing deployments upgrading to v2.
|
|
*
|
|
* Run with: npx tsx prisma/seed-migration.ts
|
|
*
|
|
* This script:
|
|
* 1. Creates a SystemConfig singleton (setup complete)
|
|
* 2. Creates a default Organization from existing data
|
|
* 3. Creates a default Location
|
|
* 4. Creates a default "Sunday Service" CollectionDay
|
|
* 5. Migrates existing Users into OrgMembers
|
|
* 6. Backfills organizationId and locationId on ResponseCards
|
|
* 7. Backfills collectionDayId using existing dates
|
|
*/
|
|
|
|
import "dotenv/config";
|
|
import { PrismaClient } from "../src/generated/prisma/client";
|
|
import { PrismaPg } from "@prisma/adapter-pg";
|
|
import pg from "pg";
|
|
|
|
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
|
|
const adapter = new PrismaPg(pool);
|
|
const prisma = new PrismaClient({ adapter });
|
|
|
|
async function main() {
|
|
console.log("Starting v2 data migration...\n");
|
|
|
|
// 1. SystemConfig
|
|
const existingConfig = await prisma.systemConfig.findUnique({
|
|
where: { id: "singleton" },
|
|
});
|
|
|
|
if (existingConfig?.isSetupComplete) {
|
|
console.log("Setup already marked complete. Skipping migration.");
|
|
return;
|
|
}
|
|
|
|
await prisma.systemConfig.upsert({
|
|
where: { id: "singleton" },
|
|
update: { isSetupComplete: true, setupStep: 3 },
|
|
create: { id: "singleton", isSetupComplete: true, setupStep: 3 },
|
|
});
|
|
console.log("[1/7] SystemConfig created (setup complete)");
|
|
|
|
// 2. Default Organization
|
|
let org = await prisma.organization.findFirst();
|
|
if (!org) {
|
|
org = await prisma.organization.create({
|
|
data: {
|
|
name: "My Church",
|
|
slug: "my-church",
|
|
type: "church",
|
|
timezone: "America/Chicago",
|
|
},
|
|
});
|
|
console.log(`[2/7] Created default organization: ${org.name} (${org.id})`);
|
|
} else {
|
|
console.log(`[2/7] Organization already exists: ${org.name}`);
|
|
}
|
|
|
|
// 3. Default Location
|
|
let location = await prisma.location.findFirst({
|
|
where: { organizationId: org.id },
|
|
});
|
|
if (!location) {
|
|
location = await prisma.location.create({
|
|
data: {
|
|
name: "Main Campus",
|
|
organizationId: org.id,
|
|
},
|
|
});
|
|
console.log(`[3/7] Created default location: ${location.name} (${location.id})`);
|
|
} else {
|
|
console.log(`[3/7] Location already exists: ${location.name}`);
|
|
}
|
|
|
|
// 4. Default CollectionDay
|
|
let collectionDay = await prisma.collectionDay.findFirst({
|
|
where: { locationId: location.id },
|
|
});
|
|
if (!collectionDay) {
|
|
collectionDay = await prisma.collectionDay.create({
|
|
data: {
|
|
locationId: location.id,
|
|
name: "Sunday Service",
|
|
dayOfWeek: 0,
|
|
timeStart: "09:00",
|
|
timeEnd: "12:00",
|
|
isRecurring: true,
|
|
},
|
|
});
|
|
console.log(`[4/7] Created default collection day: ${collectionDay.name}`);
|
|
} else {
|
|
console.log(`[4/7] Collection day already exists: ${collectionDay.name}`);
|
|
}
|
|
|
|
// 5. Migrate Users to OrgMembers
|
|
const users = await prisma.user.findMany();
|
|
let memberCount = 0;
|
|
for (const user of users) {
|
|
const existing = await prisma.orgMember.findUnique({
|
|
where: {
|
|
userId_organizationId: {
|
|
userId: user.id,
|
|
organizationId: org.id,
|
|
},
|
|
},
|
|
});
|
|
if (!existing) {
|
|
await prisma.orgMember.create({
|
|
data: {
|
|
userId: user.id,
|
|
organizationId: org.id,
|
|
role: user.role || "viewer",
|
|
},
|
|
});
|
|
memberCount++;
|
|
}
|
|
}
|
|
console.log(`[5/7] Migrated ${memberCount} users to org members (${users.length} total users)`);
|
|
|
|
// 6. Backfill ResponseCard.organizationId and locationId
|
|
const cardResult = await prisma.responseCard.updateMany({
|
|
where: { organizationId: null },
|
|
data: {
|
|
organizationId: org.id,
|
|
locationId: location.id,
|
|
},
|
|
});
|
|
console.log(`[6/7] Backfilled ${cardResult.count} cards with org/location`);
|
|
|
|
// 7. Backfill collectionDayId
|
|
const cardsWithDates = await prisma.responseCard.findMany({
|
|
where: {
|
|
collectionDayId: null,
|
|
OR: [
|
|
{ firstTimeGuestDate: { not: null } },
|
|
{ createdAt: { not: undefined } },
|
|
],
|
|
},
|
|
select: { id: true, firstTimeGuestDate: true, createdAt: true },
|
|
});
|
|
|
|
let assignedCount = 0;
|
|
for (const card of cardsWithDates) {
|
|
const refDate = card.firstTimeGuestDate || card.createdAt;
|
|
const sunday = new Date(refDate);
|
|
const day = sunday.getDay();
|
|
if (day !== 0) sunday.setDate(sunday.getDate() - day);
|
|
sunday.setHours(0, 0, 0, 0);
|
|
|
|
await prisma.responseCard.update({
|
|
where: { id: card.id },
|
|
data: {
|
|
collectionDayId: collectionDay.id,
|
|
collectionDate: sunday,
|
|
},
|
|
});
|
|
assignedCount++;
|
|
}
|
|
console.log(`[7/7] Assigned ${assignedCount} cards to default collection day`);
|
|
|
|
// 8. Backfill ProcessingJob.organizationId
|
|
await prisma.processingJob.updateMany({
|
|
where: { organizationId: null },
|
|
data: { organizationId: org.id },
|
|
});
|
|
|
|
console.log("\nMigration complete!");
|
|
}
|
|
|
|
main()
|
|
.catch((e) => {
|
|
console.error("Migration failed:", e);
|
|
process.exit(1);
|
|
})
|
|
.finally(async () => {
|
|
await prisma.$disconnect();
|
|
await pool.end();
|
|
});
|