Add paginated users/roles, user status, frontend permissions, profile pictures, identifier file storage

- Users page: paginated, searchable, sortable with inline roles (no N+1)
- Roles page: paginated, searchable, sortable + /roles/all for dropdowns
- User is_active field with migration, PATCH toggle, auth check (disabled=401)
- Frontend permission checks: auth store loads permissions, sidebar/buttons conditional
- Profile pictures via file storage for users and members, avatar component
- Identifier images use file storage API instead of base64
- Fix TypeScript errors across admin UI
- 64 API tests passing (10 new)
This commit is contained in:
Ryan Moon
2026-03-29 08:16:34 -05:00
parent 92371ff228
commit b9f78639e2
48 changed files with 1689 additions and 643 deletions

View File

@@ -1,4 +1,4 @@
import { eq, and, sql, count, exists } from 'drizzle-orm'
import { eq, and, sql, count, exists, type Column } from 'drizzle-orm'
import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'
import {
accounts,
@@ -30,11 +30,11 @@ import {
} from '../utils/pagination.js'
async function generateUniqueNumber(
db: PostgresJsDatabase,
db: PostgresJsDatabase<any>,
table: typeof accounts | typeof members,
column: typeof accounts.accountNumber | typeof members.memberNumber,
companyId: string,
companyIdColumn: typeof accounts.companyId,
companyIdColumn: Column,
): Promise<string> {
for (let attempt = 0; attempt < 10; attempt++) {
const num = String(Math.floor(100000 + Math.random() * 900000))
@@ -58,7 +58,7 @@ function normalizeAddress(address?: { street?: string; city?: string; state?: st
}
export const AccountService = {
async create(db: PostgresJsDatabase, companyId: string, input: AccountCreateInput) {
async create(db: PostgresJsDatabase<any>, companyId: string, input: AccountCreateInput) {
const accountNumber = await generateUniqueNumber(db, accounts, accounts.accountNumber, companyId, accounts.companyId)
const [account] = await db
@@ -78,7 +78,7 @@ export const AccountService = {
return account
},
async getById(db: PostgresJsDatabase, companyId: string, id: string) {
async getById(db: PostgresJsDatabase<any>, companyId: string, id: string) {
const [account] = await db
.select()
.from(accounts)
@@ -88,7 +88,7 @@ export const AccountService = {
return account ?? null
},
async update(db: PostgresJsDatabase, companyId: string, id: string, input: AccountUpdateInput) {
async update(db: PostgresJsDatabase<any>, companyId: string, id: string, input: AccountUpdateInput) {
const [account] = await db
.update(accounts)
.set({ ...input, updatedAt: new Date() })
@@ -98,7 +98,7 @@ export const AccountService = {
return account ?? null
},
async softDelete(db: PostgresJsDatabase, companyId: string, id: string) {
async softDelete(db: PostgresJsDatabase<any>, companyId: string, id: string) {
const [account] = await db
.update(accounts)
.set({ isActive: false, updatedAt: new Date() })
@@ -108,7 +108,7 @@ export const AccountService = {
return account ?? null
},
async list(db: PostgresJsDatabase, companyId: string, params: PaginationInput) {
async list(db: PostgresJsDatabase<any>, companyId: string, params: PaginationInput) {
const baseWhere = and(eq(accounts.companyId, companyId), eq(accounts.isActive, true))
const accountSearch = params.q
@@ -133,7 +133,7 @@ export const AccountService = {
const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere
const sortableColumns: Record<string, typeof accounts.name> = {
const sortableColumns: Record<string, Column> = {
name: accounts.name,
email: accounts.email,
created_at: accounts.createdAt,
@@ -155,7 +155,7 @@ export const AccountService = {
export const MemberService = {
async create(
db: PostgresJsDatabase,
db: PostgresJsDatabase<any>,
companyId: string,
input: {
accountId: string
@@ -210,7 +210,7 @@ export const MemberService = {
return member
},
async getById(db: PostgresJsDatabase, companyId: string, id: string) {
async getById(db: PostgresJsDatabase<any>, companyId: string, id: string) {
const [member] = await db
.select()
.from(members)
@@ -220,7 +220,7 @@ export const MemberService = {
return member ?? null
},
async list(db: PostgresJsDatabase, companyId: string, params: PaginationInput) {
async list(db: PostgresJsDatabase<any>, companyId: string, params: PaginationInput) {
const baseWhere = eq(members.companyId, companyId)
const searchCondition = params.q
@@ -229,7 +229,7 @@ export const MemberService = {
const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere
const sortableColumns: Record<string, typeof members.firstName> = {
const sortableColumns: Record<string, Column> = {
first_name: members.firstName,
last_name: members.lastName,
email: members.email,
@@ -268,14 +268,14 @@ export const MemberService = {
},
async listByAccount(
db: PostgresJsDatabase,
db: PostgresJsDatabase<any>,
companyId: string,
accountId: string,
params: PaginationInput,
) {
const where = and(eq(members.companyId, companyId), eq(members.accountId, accountId))
const sortableColumns: Record<string, typeof members.firstName> = {
const sortableColumns: Record<string, Column> = {
first_name: members.firstName,
last_name: members.lastName,
created_at: members.createdAt,
@@ -294,7 +294,7 @@ export const MemberService = {
},
async update(
db: PostgresJsDatabase,
db: PostgresJsDatabase<any>,
companyId: string,
id: string,
input: {
@@ -325,7 +325,7 @@ export const MemberService = {
return member ?? null
},
async move(db: PostgresJsDatabase, companyId: string, memberId: string, targetAccountId: string) {
async move(db: PostgresJsDatabase<any>, companyId: string, memberId: string, targetAccountId: string) {
const member = await this.getById(db, companyId, memberId)
if (!member) return null
@@ -351,7 +351,7 @@ export const MemberService = {
return updated
},
async delete(db: PostgresJsDatabase, companyId: string, id: string) {
async delete(db: PostgresJsDatabase<any>, companyId: string, id: string) {
const [member] = await db
.delete(members)
.where(and(eq(members.id, id), eq(members.companyId, companyId)))
@@ -362,7 +362,7 @@ export const MemberService = {
}
export const ProcessorLinkService = {
async create(db: PostgresJsDatabase, companyId: string, input: ProcessorLinkCreateInput) {
async create(db: PostgresJsDatabase<any>, companyId: string, input: ProcessorLinkCreateInput) {
const [link] = await db
.insert(accountProcessorLinks)
.values({
@@ -375,7 +375,7 @@ export const ProcessorLinkService = {
return link
},
async getById(db: PostgresJsDatabase, companyId: string, id: string) {
async getById(db: PostgresJsDatabase<any>, companyId: string, id: string) {
const [link] = await db
.select()
.from(accountProcessorLinks)
@@ -384,19 +384,31 @@ export const ProcessorLinkService = {
return link ?? null
},
async listByAccount(db: PostgresJsDatabase, companyId: string, accountId: string) {
return db
.select()
.from(accountProcessorLinks)
.where(
and(
eq(accountProcessorLinks.companyId, companyId),
eq(accountProcessorLinks.accountId, accountId),
),
)
async listByAccount(db: PostgresJsDatabase<any>, companyId: string, accountId: string, params: PaginationInput) {
const baseWhere = and(eq(accountProcessorLinks.companyId, companyId), eq(accountProcessorLinks.accountId, accountId))
const searchCondition = params.q
? buildSearchCondition(params.q, [accountProcessorLinks.processorCustomerId, accountProcessorLinks.processor])
: undefined
const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere
const sortableColumns: Record<string, Column> = {
processor: accountProcessorLinks.processor,
created_at: accountProcessorLinks.createdAt,
}
let query = db.select().from(accountProcessorLinks).where(where).$dynamic()
query = withSort(query, params.sort, params.order, sortableColumns, accountProcessorLinks.createdAt)
query = withPagination(query, params.page, params.limit)
const [data, [{ total }]] = await Promise.all([
query,
db.select({ total: count() }).from(accountProcessorLinks).where(where),
])
return paginatedResponse(data, total, params.page, params.limit)
},
async update(db: PostgresJsDatabase, companyId: string, id: string, input: ProcessorLinkUpdateInput) {
async update(db: PostgresJsDatabase<any>, companyId: string, id: string, input: ProcessorLinkUpdateInput) {
const [link] = await db
.update(accountProcessorLinks)
.set(input)
@@ -405,7 +417,7 @@ export const ProcessorLinkService = {
return link ?? null
},
async delete(db: PostgresJsDatabase, companyId: string, id: string) {
async delete(db: PostgresJsDatabase<any>, companyId: string, id: string) {
const [link] = await db
.delete(accountProcessorLinks)
.where(and(eq(accountProcessorLinks.id, id), eq(accountProcessorLinks.companyId, companyId)))
@@ -415,7 +427,7 @@ export const ProcessorLinkService = {
}
export const PaymentMethodService = {
async create(db: PostgresJsDatabase, companyId: string, input: PaymentMethodCreateInput) {
async create(db: PostgresJsDatabase<any>, companyId: string, input: PaymentMethodCreateInput) {
// If this is the default, unset any existing default for this account
if (input.isDefault) {
await db
@@ -447,7 +459,7 @@ export const PaymentMethodService = {
return method
},
async getById(db: PostgresJsDatabase, companyId: string, id: string) {
async getById(db: PostgresJsDatabase<any>, companyId: string, id: string) {
const [method] = await db
.select()
.from(accountPaymentMethods)
@@ -456,19 +468,32 @@ export const PaymentMethodService = {
return method ?? null
},
async listByAccount(db: PostgresJsDatabase, companyId: string, accountId: string) {
return db
.select()
.from(accountPaymentMethods)
.where(
and(
eq(accountPaymentMethods.companyId, companyId),
eq(accountPaymentMethods.accountId, accountId),
),
)
async listByAccount(db: PostgresJsDatabase<any>, companyId: string, accountId: string, params: PaginationInput) {
const baseWhere = and(eq(accountPaymentMethods.companyId, companyId), eq(accountPaymentMethods.accountId, accountId))
const searchCondition = params.q
? buildSearchCondition(params.q, [accountPaymentMethods.cardBrand, accountPaymentMethods.lastFour])
: undefined
const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere
const sortableColumns: Record<string, Column> = {
card_brand: accountPaymentMethods.cardBrand,
processor: accountPaymentMethods.processor,
created_at: accountPaymentMethods.createdAt,
}
let query = db.select().from(accountPaymentMethods).where(where).$dynamic()
query = withSort(query, params.sort, params.order, sortableColumns, accountPaymentMethods.createdAt)
query = withPagination(query, params.page, params.limit)
const [data, [{ total }]] = await Promise.all([
query,
db.select({ total: count() }).from(accountPaymentMethods).where(where),
])
return paginatedResponse(data, total, params.page, params.limit)
},
async update(db: PostgresJsDatabase, companyId: string, id: string, input: PaymentMethodUpdateInput) {
async update(db: PostgresJsDatabase<any>, companyId: string, id: string, input: PaymentMethodUpdateInput) {
// If setting as default, unset existing default
if (input.isDefault) {
const existing = await this.getById(db, companyId, id)
@@ -494,7 +519,7 @@ export const PaymentMethodService = {
return method ?? null
},
async delete(db: PostgresJsDatabase, companyId: string, id: string) {
async delete(db: PostgresJsDatabase<any>, companyId: string, id: string) {
const [method] = await db
.delete(accountPaymentMethods)
.where(and(eq(accountPaymentMethods.id, id), eq(accountPaymentMethods.companyId, companyId)))
@@ -504,7 +529,7 @@ export const PaymentMethodService = {
}
export const TaxExemptionService = {
async create(db: PostgresJsDatabase, companyId: string, input: TaxExemptionCreateInput) {
async create(db: PostgresJsDatabase<any>, companyId: string, input: TaxExemptionCreateInput) {
const [exemption] = await db
.insert(taxExemptions)
.values({
@@ -521,7 +546,7 @@ export const TaxExemptionService = {
return exemption
},
async getById(db: PostgresJsDatabase, companyId: string, id: string) {
async getById(db: PostgresJsDatabase<any>, companyId: string, id: string) {
const [exemption] = await db
.select()
.from(taxExemptions)
@@ -530,19 +555,33 @@ export const TaxExemptionService = {
return exemption ?? null
},
async listByAccount(db: PostgresJsDatabase, companyId: string, accountId: string) {
return db
.select()
.from(taxExemptions)
.where(
and(
eq(taxExemptions.companyId, companyId),
eq(taxExemptions.accountId, accountId),
),
)
async listByAccount(db: PostgresJsDatabase<any>, companyId: string, accountId: string, params: PaginationInput) {
const baseWhere = and(eq(taxExemptions.companyId, companyId), eq(taxExemptions.accountId, accountId))
const searchCondition = params.q
? buildSearchCondition(params.q, [taxExemptions.certificateNumber, taxExemptions.certificateType, taxExemptions.issuingState])
: undefined
const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere
const sortableColumns: Record<string, Column> = {
certificate_number: taxExemptions.certificateNumber,
status: taxExemptions.status,
expires_at: taxExemptions.expiresAt,
created_at: taxExemptions.createdAt,
}
let query = db.select().from(taxExemptions).where(where).$dynamic()
query = withSort(query, params.sort, params.order, sortableColumns, taxExemptions.createdAt)
query = withPagination(query, params.page, params.limit)
const [data, [{ total }]] = await Promise.all([
query,
db.select({ total: count() }).from(taxExemptions).where(where),
])
return paginatedResponse(data, total, params.page, params.limit)
},
async update(db: PostgresJsDatabase, companyId: string, id: string, input: TaxExemptionUpdateInput) {
async update(db: PostgresJsDatabase<any>, companyId: string, id: string, input: TaxExemptionUpdateInput) {
const [exemption] = await db
.update(taxExemptions)
.set({ ...input, updatedAt: new Date() })
@@ -551,7 +590,7 @@ export const TaxExemptionService = {
return exemption ?? null
},
async approve(db: PostgresJsDatabase, companyId: string, id: string, approvedBy: string) {
async approve(db: PostgresJsDatabase<any>, companyId: string, id: string, approvedBy: string) {
const [exemption] = await db
.update(taxExemptions)
.set({
@@ -565,7 +604,7 @@ export const TaxExemptionService = {
return exemption ?? null
},
async revoke(db: PostgresJsDatabase, companyId: string, id: string, revokedBy: string, reason: string) {
async revoke(db: PostgresJsDatabase<any>, companyId: string, id: string, revokedBy: string, reason: string) {
const [exemption] = await db
.update(taxExemptions)
.set({
@@ -582,7 +621,7 @@ export const TaxExemptionService = {
}
export const MemberIdentifierService = {
async create(db: PostgresJsDatabase, companyId: string, input: MemberIdentifierCreateInput) {
async create(db: PostgresJsDatabase<any>, companyId: string, input: MemberIdentifierCreateInput) {
// If setting as primary, unset existing primary for this member
if (input.isPrimary) {
await db
@@ -616,19 +655,31 @@ export const MemberIdentifierService = {
return identifier
},
async listByMember(db: PostgresJsDatabase, companyId: string, memberId: string) {
return db
.select()
.from(memberIdentifiers)
.where(
and(
eq(memberIdentifiers.companyId, companyId),
eq(memberIdentifiers.memberId, memberId),
),
)
async listByMember(db: PostgresJsDatabase<any>, companyId: string, memberId: string, params: PaginationInput) {
const baseWhere = and(eq(memberIdentifiers.companyId, companyId), eq(memberIdentifiers.memberId, memberId))
const searchCondition = params.q
? buildSearchCondition(params.q, [memberIdentifiers.value, memberIdentifiers.label, memberIdentifiers.issuingAuthority])
: undefined
const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere
const sortableColumns: Record<string, Column> = {
type: memberIdentifiers.type,
created_at: memberIdentifiers.createdAt,
}
let query = db.select().from(memberIdentifiers).where(where).$dynamic()
query = withSort(query, params.sort, params.order, sortableColumns, memberIdentifiers.createdAt)
query = withPagination(query, params.page, params.limit)
const [data, [{ total }]] = await Promise.all([
query,
db.select({ total: count() }).from(memberIdentifiers).where(where),
])
return paginatedResponse(data, total, params.page, params.limit)
},
async getById(db: PostgresJsDatabase, companyId: string, id: string) {
async getById(db: PostgresJsDatabase<any>, companyId: string, id: string) {
const [identifier] = await db
.select()
.from(memberIdentifiers)
@@ -637,7 +688,7 @@ export const MemberIdentifierService = {
return identifier ?? null
},
async update(db: PostgresJsDatabase, companyId: string, id: string, input: MemberIdentifierUpdateInput) {
async update(db: PostgresJsDatabase<any>, companyId: string, id: string, input: MemberIdentifierUpdateInput) {
if (input.isPrimary) {
const existing = await this.getById(db, companyId, id)
if (existing) {
@@ -661,7 +712,7 @@ export const MemberIdentifierService = {
return identifier ?? null
},
async delete(db: PostgresJsDatabase, companyId: string, id: string) {
async delete(db: PostgresJsDatabase<any>, companyId: string, id: string) {
const [identifier] = await db
.delete(memberIdentifiers)
.where(and(eq(memberIdentifiers.id, id), eq(memberIdentifiers.companyId, companyId)))

View File

@@ -24,7 +24,7 @@ function getExtension(contentType: string): string {
export const FileService = {
async upload(
db: PostgresJsDatabase,
db: PostgresJsDatabase<any>,
storage: StorageProvider,
companyId: string,
input: {
@@ -91,7 +91,7 @@ export const FileService = {
return file
},
async getById(db: PostgresJsDatabase, companyId: string, id: string) {
async getById(db: PostgresJsDatabase<any>, companyId: string, id: string) {
const [file] = await db
.select()
.from(files)
@@ -101,7 +101,7 @@ export const FileService = {
},
async listByEntity(
db: PostgresJsDatabase,
db: PostgresJsDatabase<any>,
companyId: string,
entityType: string,
entityId: string,
@@ -120,7 +120,7 @@ export const FileService = {
},
async delete(
db: PostgresJsDatabase,
db: PostgresJsDatabase<any>,
storage: StorageProvider,
companyId: string,
id: string,

View File

@@ -1,4 +1,4 @@
import { eq, and, count } from 'drizzle-orm'
import { eq, and, count, type Column } from 'drizzle-orm'
import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'
import { categories, suppliers } from '../db/schema/inventory.js'
import type {
@@ -16,7 +16,7 @@ import {
} from '../utils/pagination.js'
export const CategoryService = {
async create(db: PostgresJsDatabase, companyId: string, input: CategoryCreateInput) {
async create(db: PostgresJsDatabase<any>, companyId: string, input: CategoryCreateInput) {
const [category] = await db
.insert(categories)
.values({ companyId, ...input })
@@ -24,7 +24,7 @@ export const CategoryService = {
return category
},
async getById(db: PostgresJsDatabase, companyId: string, id: string) {
async getById(db: PostgresJsDatabase<any>, companyId: string, id: string) {
const [category] = await db
.select()
.from(categories)
@@ -33,7 +33,7 @@ export const CategoryService = {
return category ?? null
},
async list(db: PostgresJsDatabase, companyId: string, params: PaginationInput) {
async list(db: PostgresJsDatabase<any>, companyId: string, params: PaginationInput) {
const baseWhere = and(eq(categories.companyId, companyId), eq(categories.isActive, true))
const searchCondition = params.q
@@ -42,7 +42,7 @@ export const CategoryService = {
const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere
const sortableColumns: Record<string, typeof categories.name> = {
const sortableColumns: Record<string, Column> = {
name: categories.name,
sort_order: categories.sortOrder,
created_at: categories.createdAt,
@@ -60,7 +60,7 @@ export const CategoryService = {
return paginatedResponse(data, total, params.page, params.limit)
},
async update(db: PostgresJsDatabase, companyId: string, id: string, input: CategoryUpdateInput) {
async update(db: PostgresJsDatabase<any>, companyId: string, id: string, input: CategoryUpdateInput) {
const [category] = await db
.update(categories)
.set({ ...input, updatedAt: new Date() })
@@ -69,7 +69,7 @@ export const CategoryService = {
return category ?? null
},
async softDelete(db: PostgresJsDatabase, companyId: string, id: string) {
async softDelete(db: PostgresJsDatabase<any>, companyId: string, id: string) {
const [category] = await db
.update(categories)
.set({ isActive: false, updatedAt: new Date() })
@@ -80,7 +80,7 @@ export const CategoryService = {
}
export const SupplierService = {
async create(db: PostgresJsDatabase, companyId: string, input: SupplierCreateInput) {
async create(db: PostgresJsDatabase<any>, companyId: string, input: SupplierCreateInput) {
const [supplier] = await db
.insert(suppliers)
.values({ companyId, ...input })
@@ -88,7 +88,7 @@ export const SupplierService = {
return supplier
},
async getById(db: PostgresJsDatabase, companyId: string, id: string) {
async getById(db: PostgresJsDatabase<any>, companyId: string, id: string) {
const [supplier] = await db
.select()
.from(suppliers)
@@ -97,7 +97,7 @@ export const SupplierService = {
return supplier ?? null
},
async list(db: PostgresJsDatabase, companyId: string, params: PaginationInput) {
async list(db: PostgresJsDatabase<any>, companyId: string, params: PaginationInput) {
const baseWhere = and(eq(suppliers.companyId, companyId), eq(suppliers.isActive, true))
const searchCondition = params.q
@@ -106,7 +106,7 @@ export const SupplierService = {
const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere
const sortableColumns: Record<string, typeof suppliers.name> = {
const sortableColumns: Record<string, Column> = {
name: suppliers.name,
created_at: suppliers.createdAt,
}
@@ -123,7 +123,7 @@ export const SupplierService = {
return paginatedResponse(data, total, params.page, params.limit)
},
async update(db: PostgresJsDatabase, companyId: string, id: string, input: SupplierUpdateInput) {
async update(db: PostgresJsDatabase<any>, companyId: string, id: string, input: SupplierUpdateInput) {
const [supplier] = await db
.update(suppliers)
.set({ ...input, updatedAt: new Date() })
@@ -132,7 +132,7 @@ export const SupplierService = {
return supplier ?? null
},
async softDelete(db: PostgresJsDatabase, companyId: string, id: string) {
async softDelete(db: PostgresJsDatabase<any>, companyId: string, id: string) {
const [supplier] = await db
.update(suppliers)
.set({ isActive: false, updatedAt: new Date() })

View File

@@ -14,7 +14,7 @@ function createLookupService(
systemSeeds: ReadonlyArray<{ slug: string; name: string; description: string; sortOrder: number }>,
) {
return {
async seedForCompany(db: PostgresJsDatabase, companyId: string) {
async seedForCompany(db: PostgresJsDatabase<any>, companyId: string) {
const existing = await db
.select()
.from(table)
@@ -32,7 +32,7 @@ function createLookupService(
)
},
async list(db: PostgresJsDatabase, companyId: string) {
async list(db: PostgresJsDatabase<any>, companyId: string) {
return db
.select()
.from(table)
@@ -40,7 +40,7 @@ function createLookupService(
.orderBy(table.sortOrder)
},
async getBySlug(db: PostgresJsDatabase, companyId: string, slug: string) {
async getBySlug(db: PostgresJsDatabase<any>, companyId: string, slug: string) {
const [row] = await db
.select()
.from(table)
@@ -49,7 +49,7 @@ function createLookupService(
return row ?? null
},
async create(db: PostgresJsDatabase, companyId: string, input: LookupCreateInput) {
async create(db: PostgresJsDatabase<any>, companyId: string, input: LookupCreateInput) {
const [row] = await db
.insert(table)
.values({
@@ -64,7 +64,7 @@ function createLookupService(
return row
},
async update(db: PostgresJsDatabase, companyId: string, id: string, input: LookupUpdateInput) {
async update(db: PostgresJsDatabase<any>, companyId: string, id: string, input: LookupUpdateInput) {
// Prevent modifying system rows' slug or system flag
const existing = await db
.select()
@@ -85,7 +85,7 @@ function createLookupService(
return row ?? null
},
async delete(db: PostgresJsDatabase, companyId: string, id: string) {
async delete(db: PostgresJsDatabase<any>, companyId: string, id: string) {
const existing = await db
.select()
.from(table)
@@ -104,7 +104,7 @@ function createLookupService(
return row ?? null
},
async validateSlug(db: PostgresJsDatabase, companyId: string, slug: string): Promise<boolean> {
async validateSlug(db: PostgresJsDatabase<any>, companyId: string, slug: string): Promise<boolean> {
const row = await this.getBySlug(db, companyId, slug)
return row !== null && row.isActive
},

View File

@@ -1,4 +1,4 @@
import { eq, and, count } from 'drizzle-orm'
import { eq, and, count, type Column } from 'drizzle-orm'
import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'
import { products, inventoryUnits, priceHistory } from '../db/schema/inventory.js'
import { ValidationError } from '../lib/errors.js'
@@ -18,7 +18,7 @@ import {
import { UnitStatusService, ItemConditionService } from './lookup.service.js'
export const ProductService = {
async create(db: PostgresJsDatabase, companyId: string, input: ProductCreateInput) {
async create(db: PostgresJsDatabase<any>, companyId: string, input: ProductCreateInput) {
const [product] = await db
.insert(products)
.values({
@@ -32,7 +32,7 @@ export const ProductService = {
return product
},
async getById(db: PostgresJsDatabase, companyId: string, id: string) {
async getById(db: PostgresJsDatabase<any>, companyId: string, id: string) {
const [product] = await db
.select()
.from(products)
@@ -41,7 +41,7 @@ export const ProductService = {
return product ?? null
},
async list(db: PostgresJsDatabase, companyId: string, params: PaginationInput) {
async list(db: PostgresJsDatabase<any>, companyId: string, params: PaginationInput) {
const baseWhere = and(eq(products.companyId, companyId), eq(products.isActive, true))
const searchCondition = params.q
@@ -50,7 +50,7 @@ export const ProductService = {
const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere
const sortableColumns: Record<string, typeof products.name> = {
const sortableColumns: Record<string, Column> = {
name: products.name,
sku: products.sku,
brand: products.brand,
@@ -71,7 +71,7 @@ export const ProductService = {
},
async update(
db: PostgresJsDatabase,
db: PostgresJsDatabase<any>,
companyId: string,
id: string,
input: ProductUpdateInput,
@@ -106,7 +106,7 @@ export const ProductService = {
return product ?? null
},
async softDelete(db: PostgresJsDatabase, companyId: string, id: string) {
async softDelete(db: PostgresJsDatabase<any>, companyId: string, id: string) {
const [product] = await db
.update(products)
.set({ isActive: false, updatedAt: new Date() })
@@ -117,7 +117,7 @@ export const ProductService = {
}
export const InventoryUnitService = {
async create(db: PostgresJsDatabase, companyId: string, input: InventoryUnitCreateInput) {
async create(db: PostgresJsDatabase<any>, companyId: string, input: InventoryUnitCreateInput) {
if (input.condition) {
const valid = await ItemConditionService.validateSlug(db, companyId, input.condition)
if (!valid) throw new ValidationError(`Invalid condition: "${input.condition}"`)
@@ -144,7 +144,7 @@ export const InventoryUnitService = {
return unit
},
async getById(db: PostgresJsDatabase, companyId: string, id: string) {
async getById(db: PostgresJsDatabase<any>, companyId: string, id: string) {
const [unit] = await db
.select()
.from(inventoryUnits)
@@ -154,7 +154,7 @@ export const InventoryUnitService = {
},
async listByProduct(
db: PostgresJsDatabase,
db: PostgresJsDatabase<any>,
companyId: string,
productId: string,
params: PaginationInput,
@@ -164,7 +164,7 @@ export const InventoryUnitService = {
eq(inventoryUnits.productId, productId),
)
const sortableColumns: Record<string, typeof inventoryUnits.serialNumber> = {
const sortableColumns: Record<string, Column> = {
serial_number: inventoryUnits.serialNumber,
status: inventoryUnits.status,
condition: inventoryUnits.condition,
@@ -184,7 +184,7 @@ export const InventoryUnitService = {
},
async update(
db: PostgresJsDatabase,
db: PostgresJsDatabase<any>,
companyId: string,
id: string,
input: InventoryUnitUpdateInput,

View File

@@ -1,12 +1,14 @@
import { eq, and, inArray } from 'drizzle-orm'
import { eq, and, inArray, count, type Column } from 'drizzle-orm'
import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'
import type { PaginationInput } from '@forte/shared/schemas'
import { permissions, roles, rolePermissions, userRoles } from '../db/schema/rbac.js'
import { SYSTEM_PERMISSIONS, DEFAULT_ROLES } from '../db/seeds/rbac.js'
import { ForbiddenError } from '../lib/errors.js'
import { withPagination, withSort, buildSearchCondition, paginatedResponse } from '../utils/pagination.js'
export const RbacService = {
/** Seed system permissions (global, run once) */
async seedPermissions(db: PostgresJsDatabase) {
async seedPermissions(db: PostgresJsDatabase<any>) {
const existing = await db.select({ slug: permissions.slug }).from(permissions)
const existingSlugs = new Set(existing.map((p) => p.slug))
@@ -17,7 +19,7 @@ export const RbacService = {
},
/** Seed default roles for a company */
async seedRolesForCompany(db: PostgresJsDatabase, companyId: string) {
async seedRolesForCompany(db: PostgresJsDatabase<any>, companyId: string) {
const existingRoles = await db
.select({ slug: roles.slug })
.from(roles)
@@ -57,7 +59,7 @@ export const RbacService = {
},
/** Get all permissions for a user (union of all role permissions) */
async getUserPermissions(db: PostgresJsDatabase, userId: string): Promise<string[]> {
async getUserPermissions(db: PostgresJsDatabase<any>, userId: string): Promise<string[]> {
const userRoleRecords = await db
.select({ roleId: userRoles.roleId })
.from(userRoles)
@@ -85,21 +87,40 @@ export const RbacService = {
},
/** List all permissions */
async listPermissions(db: PostgresJsDatabase) {
async listPermissions(db: PostgresJsDatabase<any>) {
return db.select().from(permissions).orderBy(permissions.domain, permissions.action)
},
/** List roles for a company */
async listRoles(db: PostgresJsDatabase, companyId: string) {
return db
.select()
.from(roles)
.where(and(eq(roles.companyId, companyId), eq(roles.isActive, true)))
.orderBy(roles.name)
async listRoles(db: PostgresJsDatabase<any>, companyId: string, params: PaginationInput) {
const baseWhere = and(eq(roles.companyId, companyId), eq(roles.isActive, true))
const searchCondition = params.q
? buildSearchCondition(params.q, [roles.name, roles.slug])
: undefined
const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere
const sortableColumns: Record<string, Column> = {
name: roles.name,
slug: roles.slug,
created_at: roles.createdAt,
}
let query = db.select().from(roles).where(where).$dynamic()
query = withSort(query, params.sort, params.order, sortableColumns, roles.name)
query = withPagination(query, params.page, params.limit)
const [data, [{ total }]] = await Promise.all([
query,
db.select({ total: count() }).from(roles).where(where),
])
return paginatedResponse(data, total, params.page, params.limit)
},
/** Get role with its permissions */
async getRoleWithPermissions(db: PostgresJsDatabase, companyId: string, roleId: string) {
async getRoleWithPermissions(db: PostgresJsDatabase<any>, companyId: string, roleId: string) {
const [role] = await db
.select()
.from(roles)
@@ -119,7 +140,7 @@ export const RbacService = {
/** Create a custom role */
async createRole(
db: PostgresJsDatabase,
db: PostgresJsDatabase<any>,
companyId: string,
input: { name: string; slug: string; description?: string; permissionSlugs: string[] },
) {
@@ -139,7 +160,7 @@ export const RbacService = {
},
/** Update role permissions (replace all) */
async setRolePermissions(db: PostgresJsDatabase, roleId: string, permissionSlugs: string[]) {
async setRolePermissions(db: PostgresJsDatabase<any>, roleId: string, permissionSlugs: string[]) {
// Delete existing
await db.delete(rolePermissions).where(eq(rolePermissions.roleId, roleId))
@@ -160,7 +181,7 @@ export const RbacService = {
/** Update a role */
async updateRole(
db: PostgresJsDatabase,
db: PostgresJsDatabase<any>,
companyId: string,
roleId: string,
input: { name?: string; description?: string; permissionSlugs?: string[] },
@@ -184,7 +205,7 @@ export const RbacService = {
},
/** Delete a custom role */
async deleteRole(db: PostgresJsDatabase, companyId: string, roleId: string) {
async deleteRole(db: PostgresJsDatabase<any>, companyId: string, roleId: string) {
const [role] = await db
.select()
.from(roles)
@@ -207,7 +228,7 @@ export const RbacService = {
},
/** Assign a role to a user */
async assignRole(db: PostgresJsDatabase, userId: string, roleId: string, assignedBy?: string) {
async assignRole(db: PostgresJsDatabase<any>, userId: string, roleId: string, assignedBy?: string) {
const [existing] = await db
.select()
.from(userRoles)
@@ -225,7 +246,7 @@ export const RbacService = {
},
/** Remove a role from a user */
async removeRole(db: PostgresJsDatabase, userId: string, roleId: string) {
async removeRole(db: PostgresJsDatabase<any>, userId: string, roleId: string) {
const [removed] = await db
.delete(userRoles)
.where(and(eq(userRoles.userId, userId), eq(userRoles.roleId, roleId)))
@@ -235,7 +256,7 @@ export const RbacService = {
},
/** Get roles assigned to a user */
async getUserRoles(db: PostgresJsDatabase, userId: string) {
async getUserRoles(db: PostgresJsDatabase<any>, userId: string) {
return db
.select({
id: roles.id,