Remove multi-tenant company_id scoping from entire codebase

Drop company_id column from all 22 domain tables via migration.
Remove companyId from JWT payload, auth plugins, all service method
signatures (~215 occurrences), all route handlers (~105 occurrences),
test runner, test suites, and frontend auth store/types.

The company table stays as store settings (name, timezone). Tenant
isolation in a SaaS deployment would be at the database level (one
DB per customer) not the application level.

All 107 API tests pass. Zero TSC errors across all packages.
This commit is contained in:
Ryan Moon
2026-03-29 14:58:33 -05:00
parent 55f8591cf1
commit d36c6f7135
35 changed files with 353 additions and 511 deletions

View File

@@ -30,15 +30,13 @@ async function generateUniqueNumber(
db: PostgresJsDatabase<any>,
table: typeof repairTickets | typeof repairBatches,
column: typeof repairTickets.ticketNumber | typeof repairBatches.batchNumber,
companyId: string,
companyIdColumn: Column,
): Promise<string> {
for (let attempt = 0; attempt < 10; attempt++) {
const num = String(Math.floor(100000 + Math.random() * 900000))
const [existing] = await db
.select({ id: table.id })
.from(table)
.where(and(eq(companyIdColumn, companyId), eq(column, num)))
.where(eq(column, num))
.limit(1)
if (!existing) return num
}
@@ -46,15 +44,14 @@ async function generateUniqueNumber(
}
export const RepairTicketService = {
async create(db: PostgresJsDatabase<any>, companyId: string, input: RepairTicketCreateInput) {
async create(db: PostgresJsDatabase<any>, input: RepairTicketCreateInput) {
const ticketNumber = await generateUniqueNumber(
db, repairTickets, repairTickets.ticketNumber, companyId, repairTickets.companyId,
db, repairTickets, repairTickets.ticketNumber,
)
const [ticket] = await db
.insert(repairTickets)
.values({
companyId,
ticketNumber,
customerName: input.customerName,
customerPhone: input.customerPhone,
@@ -76,16 +73,16 @@ export const RepairTicketService = {
return ticket
},
async getById(db: PostgresJsDatabase<any>, companyId: string, id: string) {
async getById(db: PostgresJsDatabase<any>, id: string) {
const [ticket] = await db
.select()
.from(repairTickets)
.where(and(eq(repairTickets.id, id), eq(repairTickets.companyId, companyId)))
.where(eq(repairTickets.id, id))
.limit(1)
return ticket ?? null
},
async list(db: PostgresJsDatabase<any>, companyId: string, params: PaginationInput, filters?: {
async list(db: PostgresJsDatabase<any>, params: PaginationInput, filters?: {
status?: string[]
conditionIn?: string[]
isBatch?: boolean
@@ -97,7 +94,7 @@ export const RepairTicketService = {
completedDateFrom?: string
completedDateTo?: string
}) {
const conditions: SQL[] = [eq(repairTickets.companyId, companyId)]
const conditions: SQL[] = []
if (params.q) {
const search = buildSearchCondition(params.q, [
@@ -128,7 +125,7 @@ export const RepairTicketService = {
if (filters?.completedDateFrom) conditions.push(gte(repairTickets.completedDate, new Date(filters.completedDateFrom)))
if (filters?.completedDateTo) conditions.push(lte(repairTickets.completedDate, new Date(filters.completedDateTo)))
const where = and(...conditions)
const where = conditions.length > 0 ? and(...conditions) : undefined
const sortableColumns: Record<string, Column> = {
ticket_number: repairTickets.ticketNumber,
@@ -151,8 +148,8 @@ export const RepairTicketService = {
return paginatedResponse(data, total, params.page, params.limit)
},
async listByBatch(db: PostgresJsDatabase<any>, companyId: string, batchId: string, params: PaginationInput) {
const baseWhere = and(eq(repairTickets.companyId, companyId), eq(repairTickets.repairBatchId, batchId))
async listByBatch(db: PostgresJsDatabase<any>, batchId: string, params: PaginationInput) {
const baseWhere = eq(repairTickets.repairBatchId, batchId)
const searchCondition = params.q
? buildSearchCondition(params.q, [repairTickets.ticketNumber, repairTickets.customerName, repairTickets.instrumentDescription])
: undefined
@@ -177,7 +174,7 @@ export const RepairTicketService = {
return paginatedResponse(data, total, params.page, params.limit)
},
async update(db: PostgresJsDatabase<any>, companyId: string, id: string, input: RepairTicketUpdateInput) {
async update(db: PostgresJsDatabase<any>, id: string, input: RepairTicketUpdateInput) {
const values: Record<string, unknown> = { ...input, updatedAt: new Date() }
if (input.estimatedCost !== undefined) values.estimatedCost = input.estimatedCost.toString()
if (input.promisedDate !== undefined) values.promisedDate = input.promisedDate ? new Date(input.promisedDate) : null
@@ -185,12 +182,12 @@ export const RepairTicketService = {
const [ticket] = await db
.update(repairTickets)
.set(values)
.where(and(eq(repairTickets.id, id), eq(repairTickets.companyId, companyId)))
.where(eq(repairTickets.id, id))
.returning()
return ticket ?? null
},
async updateStatus(db: PostgresJsDatabase<any>, companyId: string, id: string, status: string) {
async updateStatus(db: PostgresJsDatabase<any>, id: string, status: string) {
const updates: Record<string, unknown> = { status, updatedAt: new Date() }
if (status === 'ready' || status === 'picked_up' || status === 'delivered') {
updates.completedDate = new Date()
@@ -199,17 +196,17 @@ export const RepairTicketService = {
const [ticket] = await db
.update(repairTickets)
.set(updates)
.where(and(eq(repairTickets.id, id), eq(repairTickets.companyId, companyId)))
.where(eq(repairTickets.id, id))
.returning()
return ticket ?? null
},
async delete(db: PostgresJsDatabase<any>, companyId: string, id: string) {
async delete(db: PostgresJsDatabase<any>, id: string) {
// Soft-cancel: set status to cancelled rather than hard delete
const [ticket] = await db
.update(repairTickets)
.set({ status: 'cancelled', updatedAt: new Date() })
.where(and(eq(repairTickets.id, id), eq(repairTickets.companyId, companyId)))
.where(eq(repairTickets.id, id))
.returning()
return ticket ?? null
},
@@ -254,19 +251,7 @@ export const RepairLineItemService = {
return paginatedResponse(data, total, params.page, params.limit)
},
async verifyOwnership(db: PostgresJsDatabase<any>, companyId: string, lineItemId: string): Promise<boolean> {
const [item] = await db
.select({ ticketCompanyId: repairTickets.companyId })
.from(repairLineItems)
.innerJoin(repairTickets, eq(repairLineItems.repairTicketId, repairTickets.id))
.where(and(eq(repairLineItems.id, lineItemId), eq(repairTickets.companyId, companyId)))
.limit(1)
return !!item
},
async update(db: PostgresJsDatabase<any>, companyId: string, id: string, input: RepairLineItemUpdateInput) {
if (!(await this.verifyOwnership(db, companyId, id))) return null
async update(db: PostgresJsDatabase<any>, id: string, input: RepairLineItemUpdateInput) {
const values: Record<string, unknown> = { ...input }
if (input.qty !== undefined) values.qty = input.qty.toString()
if (input.unitPrice !== undefined) values.unitPrice = input.unitPrice.toString()
@@ -281,9 +266,7 @@ export const RepairLineItemService = {
return item ?? null
},
async delete(db: PostgresJsDatabase<any>, companyId: string, id: string) {
if (!(await this.verifyOwnership(db, companyId, id))) return null
async delete(db: PostgresJsDatabase<any>, id: string) {
const [item] = await db
.delete(repairLineItems)
.where(eq(repairLineItems.id, id))
@@ -293,15 +276,14 @@ export const RepairLineItemService = {
}
export const RepairBatchService = {
async create(db: PostgresJsDatabase<any>, companyId: string, input: RepairBatchCreateInput) {
async create(db: PostgresJsDatabase<any>, input: RepairBatchCreateInput) {
const batchNumber = await generateUniqueNumber(
db, repairBatches, repairBatches.batchNumber, companyId, repairBatches.companyId,
db, repairBatches, repairBatches.batchNumber,
)
const [batch] = await db
.insert(repairBatches)
.values({
companyId,
batchNumber,
accountId: input.accountId,
locationId: input.locationId,
@@ -317,21 +299,19 @@ export const RepairBatchService = {
return batch
},
async getById(db: PostgresJsDatabase<any>, companyId: string, id: string) {
async getById(db: PostgresJsDatabase<any>, id: string) {
const [batch] = await db
.select()
.from(repairBatches)
.where(and(eq(repairBatches.id, id), eq(repairBatches.companyId, companyId)))
.where(eq(repairBatches.id, id))
.limit(1)
return batch ?? null
},
async list(db: PostgresJsDatabase<any>, companyId: string, params: PaginationInput) {
const baseWhere = eq(repairBatches.companyId, companyId)
async list(db: PostgresJsDatabase<any>, params: PaginationInput) {
const searchCondition = params.q
? buildSearchCondition(params.q, [repairBatches.batchNumber, repairBatches.contactName, repairBatches.contactEmail])
: undefined
const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere
const sortableColumns: Record<string, Column> = {
batch_number: repairBatches.batchNumber,
@@ -340,19 +320,19 @@ export const RepairBatchService = {
created_at: repairBatches.createdAt,
}
let query = db.select().from(repairBatches).where(where).$dynamic()
let query = db.select().from(repairBatches).where(searchCondition).$dynamic()
query = withSort(query, params.sort, params.order, sortableColumns, repairBatches.createdAt)
query = withPagination(query, params.page, params.limit)
const [data, [{ total }]] = await Promise.all([
query,
db.select({ total: count() }).from(repairBatches).where(where),
db.select({ total: count() }).from(repairBatches).where(searchCondition),
])
return paginatedResponse(data, total, params.page, params.limit)
},
async update(db: PostgresJsDatabase<any>, companyId: string, id: string, input: RepairBatchUpdateInput) {
async update(db: PostgresJsDatabase<any>, id: string, input: RepairBatchUpdateInput) {
const values: Record<string, unknown> = { ...input, updatedAt: new Date() }
if (input.pickupDate !== undefined) values.pickupDate = input.pickupDate ? new Date(input.pickupDate) : null
if (input.dueDate !== undefined) values.dueDate = input.dueDate ? new Date(input.dueDate) : null
@@ -360,12 +340,12 @@ export const RepairBatchService = {
const [batch] = await db
.update(repairBatches)
.set(values)
.where(and(eq(repairBatches.id, id), eq(repairBatches.companyId, companyId)))
.where(eq(repairBatches.id, id))
.returning()
return batch ?? null
},
async updateStatus(db: PostgresJsDatabase<any>, companyId: string, id: string, status: string) {
async updateStatus(db: PostgresJsDatabase<any>, id: string, status: string) {
const updates: Record<string, unknown> = { status, updatedAt: new Date() }
if (status === 'completed') updates.completedDate = new Date()
if (status === 'delivered') updates.deliveredDate = new Date()
@@ -373,12 +353,12 @@ export const RepairBatchService = {
const [batch] = await db
.update(repairBatches)
.set(updates)
.where(and(eq(repairBatches.id, id), eq(repairBatches.companyId, companyId)))
.where(eq(repairBatches.id, id))
.returning()
return batch ?? null
},
async approve(db: PostgresJsDatabase<any>, companyId: string, id: string, approvedBy: string) {
async approve(db: PostgresJsDatabase<any>, id: string, approvedBy: string) {
const [batch] = await db
.update(repairBatches)
.set({
@@ -387,30 +367,29 @@ export const RepairBatchService = {
approvedAt: new Date(),
updatedAt: new Date(),
})
.where(and(eq(repairBatches.id, id), eq(repairBatches.companyId, companyId)))
.where(eq(repairBatches.id, id))
.returning()
return batch ?? null
},
async reject(db: PostgresJsDatabase<any>, companyId: string, id: string) {
async reject(db: PostgresJsDatabase<any>, id: string) {
const [batch] = await db
.update(repairBatches)
.set({
approvalStatus: 'rejected',
updatedAt: new Date(),
})
.where(and(eq(repairBatches.id, id), eq(repairBatches.companyId, companyId)))
.where(eq(repairBatches.id, id))
.returning()
return batch ?? null
},
}
export const RepairServiceTemplateService = {
async create(db: PostgresJsDatabase<any>, companyId: string, input: RepairServiceTemplateCreateInput) {
async create(db: PostgresJsDatabase<any>, input: RepairServiceTemplateCreateInput) {
const [template] = await db
.insert(repairServiceTemplates)
.values({
companyId,
name: input.name,
instrumentType: input.instrumentType,
size: input.size,
@@ -424,8 +403,8 @@ export const RepairServiceTemplateService = {
return template
},
async list(db: PostgresJsDatabase<any>, companyId: string, params: PaginationInput) {
const baseWhere = and(eq(repairServiceTemplates.companyId, companyId), eq(repairServiceTemplates.isActive, true))
async list(db: PostgresJsDatabase<any>, params: PaginationInput) {
const baseWhere = eq(repairServiceTemplates.isActive, true)
const searchCondition = params.q
? buildSearchCondition(params.q, [repairServiceTemplates.name, repairServiceTemplates.instrumentType, repairServiceTemplates.size, repairServiceTemplates.description])
: undefined
@@ -451,7 +430,7 @@ export const RepairServiceTemplateService = {
return paginatedResponse(data, total, params.page, params.limit)
},
async update(db: PostgresJsDatabase<any>, companyId: string, id: string, input: RepairServiceTemplateUpdateInput) {
async update(db: PostgresJsDatabase<any>, id: string, input: RepairServiceTemplateUpdateInput) {
const values: Record<string, unknown> = { ...input, updatedAt: new Date() }
if (input.defaultPrice !== undefined) values.defaultPrice = input.defaultPrice.toString()
if (input.defaultCost !== undefined) values.defaultCost = input.defaultCost.toString()
@@ -459,16 +438,16 @@ export const RepairServiceTemplateService = {
const [template] = await db
.update(repairServiceTemplates)
.set(values)
.where(and(eq(repairServiceTemplates.id, id), eq(repairServiceTemplates.companyId, companyId)))
.where(eq(repairServiceTemplates.id, id))
.returning()
return template ?? null
},
async delete(db: PostgresJsDatabase<any>, companyId: string, id: string) {
async delete(db: PostgresJsDatabase<any>, id: string) {
const [template] = await db
.update(repairServiceTemplates)
.set({ isActive: false, updatedAt: new Date() })
.where(and(eq(repairServiceTemplates.id, id), eq(repairServiceTemplates.companyId, companyId)))
.where(eq(repairServiceTemplates.id, id))
.returning()
return template ?? null
},
@@ -514,16 +493,7 @@ export const RepairNoteService = {
return paginatedResponse(data, total, params.page, params.limit)
},
async delete(db: PostgresJsDatabase<any>, companyId: string, id: string) {
// Verify note belongs to a ticket owned by this company
const [owned] = await db
.select({ id: repairNotes.id })
.from(repairNotes)
.innerJoin(repairTickets, eq(repairNotes.repairTicketId, repairTickets.id))
.where(and(eq(repairNotes.id, id), eq(repairTickets.companyId, companyId)))
.limit(1)
if (!owned) return null
async delete(db: PostgresJsDatabase<any>, id: string) {
const [note] = await db
.delete(repairNotes)
.where(eq(repairNotes.id, id))