Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,8 @@ npx tsx src/db/seeds/bibles.ts || { echo "ERROR: bibles seed failed"; exit 1; }
echo "Seeding bible texts..."
npx tsx src/db/seeds/bible-texts.ts || { echo "ERROR: bible texts seed failed"; exit 1; }

echo "Seeding local dev workflow..."
npx tsx src/db/seeds/dev-workflow.ts || { echo "ERROR: dev workflow seed failed"; exit 1; }

echo "Starting dev server..."
exec npx tsx watch src/index.ts
4 changes: 3 additions & 1 deletion fapi.sh
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,8 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
exec_api npm run db:seed:bibles
echo_running "Seeding bible texts..."
exec_api npm run db:seed:bible-texts
echo_running "Seeding local dev workflow..."
exec_api npm run db:seed:dev-workflow
echo_success "All seeds complete."
;;

Expand Down Expand Up @@ -691,7 +693,7 @@ Development (runs in API container):
Database:
db:init Run migrations + all seeds (interactive confirmation)
db:migrate Run Drizzle migrations
db:seed Seed all data (org, roles, RBAC, dev users, languages, books, bibles, bible texts)
db:seed Seed all data (org, roles, RBAC, dev users, languages, books, bibles, bible texts, dev workflow)
db:generate <name> Generate a new Drizzle migration
db:studio Launch Drizzle Studio on the host
db:psql Open psql session
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"db:seed:books": "npx tsx src/db/seeds/books.ts",
"db:seed:bibles": "npx tsx src/db/seeds/bibles.ts",
"db:seed:bible-texts": "npx tsx src/db/seeds/bible-texts.ts",
"db:seed:dev-workflow": "npx tsx src/db/seeds/dev-workflow.ts",
"db:migrate-users": "npx tsx src/db/scripts/migrate-users-to-betterauth.ts",
"db:set-password": "npx tsx src/db/scripts/set-password.ts",
"db:create-user": "npx tsx src/db/scripts/create-user.ts",
Expand Down
23 changes: 14 additions & 9 deletions src/db/scripts/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { seedBibleTexts } from '@/db/seeds/bible-texts';
import { seedBibles } from '@/db/seeds/bibles';
import { seedBooks } from '@/db/seeds/books';
import { seedDevUsers } from '@/db/seeds/dev-users';
import { seedDevWorkflow } from '@/db/seeds/dev-workflow';
import { seedLanguages } from '@/db/seeds/languages';
import { seedOrganizations } from '@/db/seeds/organizations';
import { seedRbac } from '@/db/seeds/rbac';
Expand All @@ -12,42 +13,46 @@ import { seedRoles } from '@/db/seeds/roles';
async function setup() {
console.log('=== Fluent DB Setup ===\n');

console.log('[1/9] Running migrations...');
console.log('[1/10] Running migrations...');
execSync('npx drizzle-kit migrate', { stdio: 'inherit' });
console.log('Migrations complete.\n');

console.log('[2/9] Seeding organizations...');
console.log('[2/10] Seeding organizations...');
await seedOrganizations();
console.log('');

console.log('[3/9] Seeding roles...');
console.log('[3/10] Seeding roles...');
await seedRoles();
console.log('');

console.log('[4/9] Seeding RBAC...');
console.log('[4/10] Seeding RBAC...');
await seedRbac();
console.log('');

console.log('[5/9] Seeding dev users...');
console.log('[5/10] Seeding dev users...');
await seedDevUsers();
console.log('');

console.log('[6/9] Seeding languages...');
console.log('[6/10] Seeding languages...');
await seedLanguages();
console.log('');

console.log('[7/9] Seeding books...');
console.log('[7/10] Seeding books...');
await seedBooks();
console.log('');

console.log('[8/9] Seeding bibles...');
console.log('[8/10] Seeding bibles...');
await seedBibles();
console.log('');

console.log('[9/9] Seeding bible texts...');
console.log('[9/10] Seeding bible texts...');
await seedBibleTexts();
console.log('');

console.log('[10/10] Seeding local dev workflow...');
await seedDevWorkflow();
console.log('');

console.log('=== Setup complete ===');
const managerEmail = process.env.SEED_MANAGER_EMAIL ?? 'pm@fluent.local';
const managerPassword = process.env.SEED_MANAGER_PASSWORD ?? 'pm@123456';
Expand Down
27 changes: 27 additions & 0 deletions src/db/seeds/data/dev-workflow.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"projects": [
{
"seedKey": "local-dev-sample-project",
"name": "Local Dev Sample Project",
"organizationName": "Fluent Dev",
"bibleAbbreviation": "IRV",
"bookCodes": ["GEN", "EXO"],
"sourceLanguageCode": "guj",
"targetLanguageCode": "guj",
"createdByEmail": "$SEED_MANAGER",
"memberEmails": ["$SEED_MANAGER", "$SEED_TRANSLATOR"],
"chapterAssignments": [
{
"bookCode": "GEN",
"chapterNumber": 1,
"assignedUserEmail": "$SEED_TRANSLATOR"
},
{
"bookCode": "EXO",
"chapterNumber": 1,
"assignedUserEmail": "$SEED_MANAGER"
}
]
}
]
}
75 changes: 52 additions & 23 deletions src/db/seeds/dev-users.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { hashPassword } from 'better-auth/crypto';
import { eq } from 'drizzle-orm';
import { and, eq } from 'drizzle-orm';
import crypto from 'node:crypto';
import { fileURLToPath } from 'node:url';

import type { RoleName } from '@/lib/roles';
import type { DbTransaction } from '@/lib/types';

import { db } from '@/db';
import { authAccount, authUser, organizations, roles, users } from '@/db/schema';
Expand All @@ -16,6 +17,39 @@ interface DevUserConfig {
roleName: RoleName;
}

async function ensureCredentialPassword(
tx: DbTransaction,
authUserId: string,
email: string,
password: string
): Promise<void> {
const hashedPassword = await hashPassword(password);

const [credentialAccount] = await tx
.select({ id: authAccount.id })
.from(authAccount)
.where(and(eq(authAccount.userId, authUserId), eq(authAccount.providerId, 'credential')))
.limit(1);

if (credentialAccount) {
await tx
.update(authAccount)
.set({ password: hashedPassword, updatedAt: new Date() })
.where(eq(authAccount.id, credentialAccount.id));
return;
}

await tx.insert(authAccount).values({
id: crypto.randomUUID(),
userId: authUserId,
accountId: email,
providerId: 'credential',
password: hashedPassword,
createdAt: new Date(),
updatedAt: new Date(),
});
}

export async function seedDevUsers() {
const DEV_USERS: DevUserConfig[] = [
{
Expand Down Expand Up @@ -51,43 +85,46 @@ export async function seedDevUsers() {
throw new Error(`Role "${config.roleName}" not found. Run seedRoles first.`);
}

const authUserId = crypto.randomUUID();
const hashedPassword = await hashPassword(config.password);

await db.transaction(async (tx) => {
const [existingAuthUser] = await tx
.select({ id: authUser.id })
.from(authUser)
.where(eq(authUser.email, config.email))
.limit(1);

if (existingAuthUser) {
console.log(`Skipping ${config.email} β€” already exists in auth_user.`);
return;
}

const [existingUserByEmail] = await tx
const [existingAppUser] = await tx
.select({ id: users.id })
.from(users)
.where(eq(users.email, config.email))
.limit(1);

if (existingUserByEmail) {
console.log(`Skipping ${config.email} β€” already exists in users.`);
if (existingAuthUser && existingAppUser) {
await ensureCredentialPassword(tx, existingAuthUser.id, config.email, config.password);
console.log(`Ensured dev user credentials: ${config.email} (${config.roleName})`);
return;
}

if (existingAuthUser || existingAppUser) {
throw new Error(
`Dev user "${config.email}" is partially seeded (auth=${Boolean(existingAuthUser)}, users=${Boolean(existingAppUser)}). ` +
'Run on a fresh database or repair manually.'
);
}

const [existingUserByUsername] = await tx
.select({ id: users.id })
.from(users)
.where(eq(users.username, config.username))
.limit(1);

if (existingUserByUsername) {
console.log(`Skipping ${config.username} β€” username already exists in users.`);
return;
throw new Error(
`Username "${config.username}" already exists for a different dev user. Resolve manually before re-seeding.`
);
}

const authUserId = crypto.randomUUID();

await tx.insert(authUser).values({
id: authUserId,
email: config.email,
Expand All @@ -97,15 +134,7 @@ export async function seedDevUsers() {
updatedAt: new Date(),
});

await tx.insert(authAccount).values({
id: crypto.randomUUID(),
userId: authUserId,
accountId: config.email,
providerId: 'credential',
password: hashedPassword,
createdAt: new Date(),
updatedAt: new Date(),
});
await ensureCredentialPassword(tx, authUserId, config.email, config.password);

await tx.insert(users).values({
username: config.username,
Expand Down
Loading