Fix dev seed for single-company schema, sync RBAC on startup

- Remove all company_id references from dev-seed.ts (removed in 0021)
- seedPermissions now syncs role-permission assignments for system roles
  when new permissions are added (e.g., vault.view assigned to admin)
- Fix enum migration: use text cast workaround for PostgreSQL's
  "unsafe use of new enum value" error on fresh DB creation
This commit is contained in:
Ryan Moon
2026-03-30 07:10:20 -05:00
parent e346e072b8
commit 328b4a1f7b
4 changed files with 50 additions and 27 deletions

View File

@@ -1 +1,2 @@
ALTER TYPE "repair_ticket_status" ADD VALUE 'new' BEFORE 'in_transit';
-- Must commit before using the new enum value
ALTER TYPE "repair_ticket_status" ADD VALUE IF NOT EXISTS 'new' BEFORE 'in_transit';

View File

@@ -1 +1,3 @@
ALTER TABLE "repair_ticket" ALTER COLUMN "status" SET DEFAULT 'new';
-- Use text cast to avoid the "unsafe use of new enum value" error
-- when 0019 (ADD VALUE) and this run in the same session
ALTER TABLE "repair_ticket" ALTER COLUMN "status" SET DEFAULT 'new'::text::repair_ticket_status;

View File

@@ -18,7 +18,7 @@ async function seed() {
const [company] = await sql`SELECT id FROM company WHERE id = ${COMPANY_ID}`
if (!company) {
await sql`INSERT INTO company (id, name, timezone) VALUES (${COMPANY_ID}, 'Forte Music Store', 'America/Chicago')`
await sql`INSERT INTO location (id, company_id, name) VALUES ('a0000000-0000-0000-0000-000000000002', ${COMPANY_ID}, 'Main Store')`
await sql`INSERT INTO location (id, name) VALUES ('a0000000-0000-0000-0000-000000000002', 'Main Store')`
console.log(' Created company and location')
// Seed RBAC
@@ -29,7 +29,7 @@ async function seed() {
const permRows = await sql`SELECT id, slug FROM permission`
const permMap = new Map(permRows.map((r: any) => [r.slug, r.id]))
for (const roleDef of DEFAULT_ROLES) {
const [role] = await sql`INSERT INTO role (company_id, name, slug, description, is_system) VALUES (${COMPANY_ID}, ${roleDef.name}, ${roleDef.slug}, ${roleDef.description}, true) RETURNING id`
const [role] = await sql`INSERT INTO role (name, slug, description, is_system) VALUES (${roleDef.name}, ${roleDef.slug}, ${roleDef.description}, true) RETURNING id`
for (const permSlug of roleDef.permissions) {
const permId = permMap.get(permSlug)
if (permId) await sql`INSERT INTO role_permission (role_id, permission_id) VALUES (${role.id}, ${permId}) ON CONFLICT DO NOTHING`
@@ -39,23 +39,18 @@ async function seed() {
}
// --- Admin user (if not exists) ---
const [adminUser] = await sql`SELECT id, company_id FROM "user" WHERE email = 'admin@forte.dev'`
const [adminUser] = await sql`SELECT id FROM "user" WHERE email = 'admin@forte.dev'`
if (!adminUser) {
const bcrypt = await import('bcrypt')
const hashedPw = await (bcrypt.default || bcrypt).hash('admin1234', 10)
const [user] = await sql`INSERT INTO "user" (company_id, email, password_hash, first_name, last_name, role) VALUES (${COMPANY_ID}, 'admin@forte.dev', ${hashedPw}, 'Admin', 'User', 'admin') RETURNING id`
const [adminRole] = await sql`SELECT id FROM role WHERE company_id = ${COMPANY_ID} AND slug = 'admin' LIMIT 1`
const [user] = await sql`INSERT INTO "user" (email, password_hash, first_name, last_name, role) VALUES ('admin@forte.dev', ${hashedPw}, 'Admin', 'User', 'admin') RETURNING id`
const [adminRole] = await sql`SELECT id FROM role WHERE slug = 'admin' LIMIT 1`
if (adminRole) {
await sql`INSERT INTO user_role_assignment (user_id, role_id) VALUES (${user.id}, ${adminRole.id}) ON CONFLICT DO NOTHING`
}
console.log(' Created admin user: admin@forte.dev / admin1234')
} else {
// Ensure admin is in the right company and has the admin role
if (adminUser.company_id !== COMPANY_ID) {
await sql`UPDATE "user" SET company_id = ${COMPANY_ID} WHERE id = ${adminUser.id}`
console.log(' Moved admin user to correct company')
}
const [adminRole] = await sql`SELECT id FROM role WHERE company_id = ${COMPANY_ID} AND slug = 'admin' LIMIT 1`
const [adminRole] = await sql`SELECT id FROM role WHERE slug = 'admin' LIMIT 1`
if (adminRole) {
await sql`DELETE FROM user_role_assignment WHERE user_id = ${adminUser.id}`
await sql`INSERT INTO user_role_assignment (user_id, role_id) VALUES (${adminUser.id}, ${adminRole.id}) ON CONFLICT DO NOTHING`
@@ -76,13 +71,13 @@ async function seed() {
const acctIds: Record<string, string> = {}
for (const a of accounts) {
const existing = await sql`SELECT id FROM account WHERE company_id = ${COMPANY_ID} AND name = ${a.name}`
const existing = await sql`SELECT id FROM account WHERE name = ${a.name}`
if (existing.length > 0) {
acctIds[a.name] = existing[0].id
continue
}
const num = String(Math.floor(100000 + Math.random() * 900000))
const [row] = await sql`INSERT INTO account (company_id, name, email, phone, account_number, billing_mode) VALUES (${COMPANY_ID}, ${a.name}, ${a.email}, ${a.phone}, ${num}, 'consolidated') RETURNING id`
const [row] = await sql`INSERT INTO account (name, email, phone, account_number, billing_mode) VALUES (${a.name}, ${a.email}, ${a.phone}, ${num}, 'consolidated') RETURNING id`
acctIds[a.name] = row.id
console.log(` Account: ${a.name}`)
}
@@ -101,10 +96,10 @@ async function seed() {
for (const m of members) {
const acctId = acctIds[m.accountName]
const existing = await sql`SELECT id FROM member WHERE company_id = ${COMPANY_ID} AND first_name = ${m.firstName} AND last_name = ${m.lastName}`
const existing = await sql`SELECT id FROM member WHERE first_name = ${m.firstName} AND last_name = ${m.lastName}`
if (existing.length > 0) continue
const num = String(Math.floor(100000 + Math.random() * 900000))
await sql`INSERT INTO member (company_id, account_id, first_name, last_name, email, member_number, is_minor) VALUES (${COMPANY_ID}, ${acctId}, ${m.firstName}, ${m.lastName}, ${m.email ?? null}, ${num}, ${m.isMinor ?? false})`
await sql`INSERT INTO member (account_id, first_name, last_name, email, member_number, is_minor) VALUES (${acctId}, ${m.firstName}, ${m.lastName}, ${m.email ?? null}, ${num}, ${m.isMinor ?? false})`
console.log(` Member: ${m.firstName} ${m.lastName}`)
}
@@ -129,9 +124,9 @@ async function seed() {
]
for (const t of templates) {
const existing = await sql`SELECT id FROM repair_service_template WHERE company_id = ${COMPANY_ID} AND name = ${t.name} AND COALESCE(instrument_type, '') = ${t.instrumentType ?? ''} AND COALESCE(size, '') = ${t.size ?? ''}`
const existing = await sql`SELECT id FROM repair_service_template WHERE name = ${t.name} AND COALESCE(instrument_type, '') = ${t.instrumentType ?? ''} AND COALESCE(size, '') = ${t.size ?? ''}`
if (existing.length > 0) continue
await sql`INSERT INTO repair_service_template (company_id, name, instrument_type, size, item_type, default_price, default_cost, is_active) VALUES (${COMPANY_ID}, ${t.name}, ${t.instrumentType}, ${t.size}, ${t.itemType}, ${t.price}, ${t.cost}, true)`
await sql`INSERT INTO repair_service_template (name, instrument_type, size, item_type, default_price, default_cost, is_active) VALUES (${t.name}, ${t.instrumentType}, ${t.size}, ${t.itemType}, ${t.price}, ${t.cost}, true)`
console.log(` Template: ${t.name} ${t.instrumentType ?? ''} ${t.size ?? ''}`)
}
@@ -146,20 +141,20 @@ async function seed() {
]
for (const t of tickets) {
const existing = await sql`SELECT id FROM repair_ticket WHERE company_id = ${COMPANY_ID} AND customer_name = ${t.customer} AND problem_description = ${t.problem}`
const existing = await sql`SELECT id FROM repair_ticket WHERE customer_name = ${t.customer} AND problem_description = ${t.problem}`
if (existing.length > 0) continue
const num = String(Math.floor(100000 + Math.random() * 900000))
const acctId = acctIds[t.customer] ?? null
await sql`INSERT INTO repair_ticket (company_id, ticket_number, customer_name, account_id, instrument_description, serial_number, problem_description, condition_in, status, estimated_cost) VALUES (${COMPANY_ID}, ${num}, ${t.customer}, ${acctId}, ${t.instrument}, ${t.serial}, ${t.problem}, ${t.condition}, ${t.status}, ${t.estimate})`
await sql`INSERT INTO repair_ticket (ticket_number, customer_name, account_id, instrument_description, serial_number, problem_description, condition_in, status, estimated_cost) VALUES (${num}, ${t.customer}, ${acctId}, ${t.instrument}, ${t.serial}, ${t.problem}, ${t.condition}, ${t.status}, ${t.estimate})`
console.log(` Ticket: ${t.customer}${t.instrument} [${t.status}]`)
}
// --- Repair Batch ---
const batchExists = await sql`SELECT id FROM repair_batch WHERE company_id = ${COMPANY_ID} AND contact_name = 'Mr. Williams'`
const batchExists = await sql`SELECT id FROM repair_batch WHERE contact_name = 'Mr. Williams'`
if (batchExists.length === 0) {
const batchNum = String(Math.floor(100000 + Math.random() * 900000))
const schoolId = acctIds['Lincoln High School']
const [batch] = await sql`INSERT INTO repair_batch (company_id, batch_number, account_id, contact_name, contact_phone, contact_email, instrument_count, notes, status) VALUES (${COMPANY_ID}, ${batchNum}, ${schoolId}, 'Mr. Williams', '555-0210', 'williams@lincoln.edu', 5, 'Annual band instrument checkup — 5 instruments', 'intake') RETURNING id`
const [batch] = await sql`INSERT INTO repair_batch (batch_number, account_id, contact_name, contact_phone, contact_email, instrument_count, notes, status) VALUES (${batchNum}, ${schoolId}, 'Mr. Williams', '555-0210', 'williams@lincoln.edu', 5, 'Annual band instrument checkup — 5 instruments', 'intake') RETURNING id`
const batchTickets = [
{ instrument: 'Student Flute', problem: 'Pads worn, needs replacement check', condition: 'fair' },
@@ -171,7 +166,7 @@ async function seed() {
for (const bt of batchTickets) {
const num = String(Math.floor(100000 + Math.random() * 900000))
await sql`INSERT INTO repair_ticket (company_id, ticket_number, customer_name, account_id, repair_batch_id, instrument_description, problem_description, condition_in, status) VALUES (${COMPANY_ID}, ${num}, 'Lincoln High School', ${schoolId}, ${batch.id}, ${bt.instrument}, ${bt.problem}, ${bt.condition}, 'new')`
await sql`INSERT INTO repair_ticket (ticket_number, customer_name, account_id, repair_batch_id, instrument_description, problem_description, condition_in, status) VALUES (${num}, 'Lincoln High School', ${schoolId}, ${batch.id}, ${bt.instrument}, ${bt.problem}, ${bt.condition}, 'new')`
console.log(` Batch ticket: ${bt.instrument}`)
}
console.log(` Batch: Lincoln High School — 5 instruments`)

View File

@@ -7,15 +7,40 @@ import { ForbiddenError } from '../lib/errors.js'
import { withPagination, withSort, buildSearchCondition, paginatedResponse } from '../utils/pagination.js'
export const RbacService = {
/** Seed system permissions (global, run once) */
/** Seed system permissions and sync role assignments for any new permissions */
async seedPermissions(db: PostgresJsDatabase<any>) {
const existing = await db.select({ slug: permissions.slug }).from(permissions)
const existingSlugs = new Set(existing.map((p) => p.slug))
const toInsert = SYSTEM_PERMISSIONS.filter((p) => !existingSlugs.has(p.slug))
if (toInsert.length === 0) return
if (toInsert.length > 0) {
await db.insert(permissions).values(toInsert)
}
await db.insert(permissions).values(toInsert)
// Sync system role permission assignments — ensures new permissions
// (e.g., vault.view) get assigned to existing roles on restart
const allPerms = await db.select().from(permissions)
const permMap = new Map(allPerms.map((p) => [p.slug, p.id]))
const systemRoles = await db.select().from(roles).where(eq(roles.isSystem, true))
for (const role of systemRoles) {
const roleDef = DEFAULT_ROLES.find((r) => r.slug === role.slug)
if (!roleDef) continue
const existingAssignments = await db.select({ permissionId: rolePermissions.permissionId })
.from(rolePermissions).where(eq(rolePermissions.roleId, role.id))
const assignedPermIds = new Set(existingAssignments.map((a) => a.permissionId))
const missingPermIds = roleDef.permissions
.map((slug) => permMap.get(slug))
.filter((id): id is string => id !== undefined && !assignedPermIds.has(id))
if (missingPermIds.length > 0) {
await db.insert(rolePermissions).values(
missingPermIds.map((permissionId) => ({ roleId: role.id, permissionId })),
)
}
}
},
/** Seed default roles */