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:
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user