dorfteich/apps/api/prisma/seed.ts
Claude Fable 5 64928f0ac0
All checks were successful
CD / Build and push images (push) Successful in 1m46s
CI / Lint, typecheck, test (push) Successful in 1m17s
CI / Auth e2e pack (push) Successful in 1m39s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 8s
CD / Smoke tests against Test (push) Successful in 1m6s
CD / Promote to Int (push) Successful in 10s
Quota foundation: overrides, resolution, race-safe consumption (#22)
- quota_overrides + pond_usage models (BigInt values, unique per
  subject+key); migration 20260705185146_quotas
- instance-default quota keys in the settings registry (editors 5,
  readers 50, additional ponds 0, storage 1 GiB, max file 25 MiB)
- QuotaService: getEffective with pond → user → instance resolution
  (zero counts as a value, not a gap); assertCanCreateSharedPond and
  checkAndConsume serialize via pg_advisory_xact_lock inside the guarded
  write's transaction; release never drops below zero
- pond creation enforces additional_ponds (personal ponds don't count);
  quota errors carry code quota_exceeded + {quotaKey, limit}, localized
- seed grants fixtures an additional_ponds override (default is 0)
- table-driven resolution tests, parallel-consumption test, e2e for the
  pond-creation limit

Closes #22

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UpQz6ypHJsLfMf4S6fyQEB
2026-07-05 20:55:59 +02:00

136 lines
4.2 KiB
TypeScript

/**
* Development/Test fixture seeding. Idempotent: running it twice must not
* duplicate anything. Never run against production data.
*
* Fixture matrix (documented in apps/web/e2e/README.md):
* fixture-admin active, Site Admin
* fixture-user active, regular account
* fixture-pending registered but e-mail not verified
*
* All fixture accounts share the password below — they exist only on
* dev machines and disposable CI/Test databases. On shared stages
* (test/int), set FIXTURE_ADMIN_PASSWORD / FIXTURE_USER_PASSWORD to give
* those two accounts non-public passwords.
*/
import { slugify } from '@dorfteich/shared';
import { PrismaClient, UserStatus } from '@prisma/client';
import { hashPassword } from '../src/users/password';
export const FIXTURE_PASSWORD = 'fixture passwort 123';
const PASSWORD_OVERRIDES: Record<string, string | undefined> = {
'fixture-admin': process.env.FIXTURE_ADMIN_PASSWORD,
'fixture-user': process.env.FIXTURE_USER_PASSWORD,
};
const prisma = new PrismaClient();
interface FixtureUser {
username: string;
displayName: string;
status: UserStatus;
isSiteAdmin: boolean;
}
const FIXTURES: FixtureUser[] = [
{ username: 'fixture-admin', displayName: 'Fixture Admin', status: 'ACTIVE', isSiteAdmin: true },
{ username: 'fixture-user', displayName: 'Fixture User', status: 'ACTIVE', isSiteAdmin: false },
{
username: 'fixture-pending',
displayName: 'Fixture Pending',
status: 'PENDING_VERIFICATION',
isSiteAdmin: false,
},
];
async function upsertFixtureUser(fixture: FixtureUser): Promise<void> {
const email = `${fixture.username}@dorfteich.test`;
const user = await prisma.user.upsert({
where: { username: fixture.username },
create: {
username: fixture.username,
email,
displayName: fixture.displayName,
locale: 'de',
status: fixture.status,
isSiteAdmin: fixture.isSiteAdmin,
emailVerifiedAt: fixture.status === 'ACTIVE' ? new Date() : null,
},
update: {
status: fixture.status,
isSiteAdmin: fixture.isSiteAdmin,
},
});
// Re-hash on every run so a changed override takes effect on re-seed.
const credential = await hashPassword(PASSWORD_OVERRIDES[fixture.username] ?? FIXTURE_PASSWORD);
await prisma.userIdentity.upsert({
where: { provider_subject: { provider: 'password', subject: user.id } },
create: {
userId: user.id,
provider: 'password',
subject: user.id,
credential,
},
update: { credential },
});
// Active accounts get their personal pond, mirroring what e-mail
// verification does for real signups (issue #21).
if (fixture.status === 'ACTIVE') {
const existing = await prisma.pond.findFirst({
where: { ownerId: user.id, type: 'PERSONAL' },
select: { id: true },
});
if (!existing) {
await prisma.pond.create({
data: {
slug: slugify(fixture.displayName) || fixture.username,
name: fixture.displayName,
type: 'PERSONAL',
ownerId: user.id,
},
});
}
// The instance default for additional_ponds is 0 (ADR 0011) — give
// the fixtures headroom so pond flows are exercisable in dev/e2e.
await prisma.quotaOverride.upsert({
where: {
subjectType_subjectId_quotaKey: {
subjectType: 'USER',
subjectId: user.id,
quotaKey: 'additional_ponds',
},
},
create: {
subjectType: 'USER',
subjectId: user.id,
quotaKey: 'additional_ponds',
value: 100,
},
update: { value: 100 },
});
}
}
async function main(): Promise<void> {
// Fresh rate-limit budget for e2e runs — seed targets are always
// disposable dev/CI databases, never production.
await prisma.rateLimit.deleteMany({});
for (const fixture of FIXTURES) {
await upsertFixtureUser(fixture);
}
await prisma.instanceSetting.upsert({
where: { key: 'seed.marker' },
create: { key: 'seed.marker', value: { seededAt: new Date().toISOString() } },
update: { value: { seededAt: new Date().toISOString() } },
});
console.log(`seed: done (${FIXTURES.length} fixture users)`);
}
main()
.catch((error) => {
console.error(error);
process.exitCode = 1;
})
.finally(() => prisma.$disconnect());