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:
@@ -0,0 +1 @@
|
||||
ALTER TABLE "user" ADD COLUMN "is_active" boolean NOT NULL DEFAULT true;
|
||||
@@ -99,6 +99,13 @@
|
||||
"when": 1774730000000,
|
||||
"tag": "0013_rbac",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 14,
|
||||
"version": "7",
|
||||
"when": 1774740000000,
|
||||
"tag": "0014_user_is_active",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { pgTable, uuid, varchar, timestamp, pgEnum, uniqueIndex } from 'drizzle-orm/pg-core'
|
||||
import { pgTable, uuid, varchar, timestamp, pgEnum, uniqueIndex, boolean } from 'drizzle-orm/pg-core'
|
||||
import { companies } from './stores.js'
|
||||
|
||||
export const userRoleEnum = pgEnum('user_role', [
|
||||
@@ -19,6 +19,7 @@ export const users = pgTable('user', {
|
||||
firstName: varchar('first_name', { length: 100 }).notNull(),
|
||||
lastName: varchar('last_name', { length: 100 }).notNull(),
|
||||
role: userRoleEnum('role').notNull().default('staff'),
|
||||
isActive: boolean('is_active').notNull().default(true),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
})
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import fp from 'fastify-plugin'
|
||||
import fjwt from '@fastify/jwt'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { users } from '../db/schema/users.js'
|
||||
import { RbacService } from '../services/rbac.service.js'
|
||||
|
||||
declare module 'fastify' {
|
||||
@@ -72,6 +74,18 @@ export const authPlugin = fp(async (app) => {
|
||||
await request.jwtVerify()
|
||||
request.companyId = request.user.companyId
|
||||
|
||||
// Check if user account is active
|
||||
const [dbUser] = await app.db
|
||||
.select({ isActive: users.isActive })
|
||||
.from(users)
|
||||
.where(eq(users.id, request.user.id))
|
||||
.limit(1)
|
||||
|
||||
if (!dbUser || !dbUser.isActive) {
|
||||
reply.status(401).send({ error: { message: 'Account disabled', statusCode: 401 } })
|
||||
return
|
||||
}
|
||||
|
||||
// Load permissions from DB and expand with inheritance
|
||||
const permSlugs = await RbacService.getUserPermissions(app.db, request.user.id)
|
||||
request.permissions = expandPermissions(permSlugs)
|
||||
|
||||
@@ -2,7 +2,7 @@ import fp from 'fastify-plugin'
|
||||
import { AppError, ValidationError } from '../lib/errors.js'
|
||||
|
||||
export const errorHandlerPlugin = fp(async (app) => {
|
||||
app.setErrorHandler((error, request, reply) => {
|
||||
app.setErrorHandler((error: Error & { statusCode?: number; stack?: string }, request, reply) => {
|
||||
// Use AppError statusCode if available, else Fastify's, else 500
|
||||
const statusCode = error instanceof AppError
|
||||
? error.statusCode
|
||||
|
||||
@@ -124,6 +124,7 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
|
||||
if (!member) return reply.status(404).send({ error: { message: 'Member not found', statusCode: 404 } })
|
||||
const account = await AccountService.create(app.db, request.companyId, {
|
||||
name: `${member.firstName} ${member.lastName}`,
|
||||
billingMode: 'consolidated',
|
||||
})
|
||||
targetAccountId = account.id
|
||||
}
|
||||
@@ -148,8 +149,9 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
|
||||
|
||||
app.get('/members/:memberId/identifiers', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => {
|
||||
const { memberId } = request.params as { memberId: string }
|
||||
const identifiers = await MemberIdentifierService.listByMember(app.db, request.companyId, memberId)
|
||||
return reply.send({ data: identifiers })
|
||||
const params = PaginationSchema.parse(request.query)
|
||||
const result = await MemberIdentifierService.listByMember(app.db, request.companyId, memberId, params)
|
||||
return reply.send(result)
|
||||
})
|
||||
|
||||
app.patch('/identifiers/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.edit')] }, async (request, reply) => {
|
||||
@@ -191,8 +193,9 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
|
||||
|
||||
app.get('/accounts/:accountId/processor-links', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => {
|
||||
const { accountId } = request.params as { accountId: string }
|
||||
const links = await ProcessorLinkService.listByAccount(app.db, request.companyId, accountId)
|
||||
return reply.send({ data: links })
|
||||
const params = PaginationSchema.parse(request.query)
|
||||
const result = await ProcessorLinkService.listByAccount(app.db, request.companyId, accountId, params)
|
||||
return reply.send(result)
|
||||
})
|
||||
|
||||
app.patch('/processor-links/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.edit')] }, async (request, reply) => {
|
||||
@@ -227,8 +230,9 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
|
||||
|
||||
app.get('/accounts/:accountId/payment-methods', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => {
|
||||
const { accountId } = request.params as { accountId: string }
|
||||
const methods = await PaymentMethodService.listByAccount(app.db, request.companyId, accountId)
|
||||
return reply.send({ data: methods })
|
||||
const params = PaginationSchema.parse(request.query)
|
||||
const result = await PaymentMethodService.listByAccount(app.db, request.companyId, accountId, params)
|
||||
return reply.send(result)
|
||||
})
|
||||
|
||||
app.get('/payment-methods/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => {
|
||||
@@ -270,8 +274,9 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
|
||||
|
||||
app.get('/accounts/:accountId/tax-exemptions', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => {
|
||||
const { accountId } = request.params as { accountId: string }
|
||||
const exemptions = await TaxExemptionService.listByAccount(app.db, request.companyId, accountId)
|
||||
return reply.send({ data: exemptions })
|
||||
const params = PaginationSchema.parse(request.query)
|
||||
const result = await TaxExemptionService.listByAccount(app.db, request.companyId, accountId, params)
|
||||
return reply.send(result)
|
||||
})
|
||||
|
||||
app.get('/tax-exemptions/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => {
|
||||
|
||||
@@ -176,7 +176,7 @@ export const authRoutes: FastifyPluginAsync = async (app) => {
|
||||
if (!user) return reply.status(404).send({ error: { message: 'User not found', statusCode: 404 } })
|
||||
|
||||
// Generate a signed reset token that expires in 1 hour
|
||||
const resetToken = app.jwt.sign({ userId: user.id, purpose: 'password-reset' }, { expiresIn: '1h' })
|
||||
const resetToken = app.jwt.sign({ userId: user.id, purpose: 'password-reset' } as any, { expiresIn: '1h' })
|
||||
const resetLink = `${process.env.APP_URL ?? 'http://localhost:5173'}/reset-password?token=${resetToken}`
|
||||
|
||||
request.log.info({ userId, generatedBy: request.user.id }, 'Password reset link generated')
|
||||
|
||||
@@ -42,7 +42,7 @@ export const fileRoutes: FastifyPluginAsync = async (app) => {
|
||||
}
|
||||
|
||||
// Validate entityType is a known type
|
||||
const allowedEntityTypes = ['member', 'member_identifier', 'product', 'rental_agreement', 'repair_ticket']
|
||||
const allowedEntityTypes = ['user', 'member', 'member_identifier', 'product', 'rental_agreement', 'repair_ticket']
|
||||
if (!allowedEntityTypes.includes(entityType)) {
|
||||
throw new ValidationError(`Invalid entityType: ${entityType}`)
|
||||
}
|
||||
|
||||
@@ -1,28 +1,110 @@
|
||||
import type { FastifyPluginAsync } from 'fastify'
|
||||
import { eq, and } from 'drizzle-orm'
|
||||
import { eq, and, count, sql, type Column } from 'drizzle-orm'
|
||||
import { PaginationSchema } from '@forte/shared/schemas'
|
||||
import { RbacService } from '../../services/rbac.service.js'
|
||||
import { ValidationError } from '../../lib/errors.js'
|
||||
import { users } from '../../db/schema/users.js'
|
||||
import { roles, userRoles } from '../../db/schema/rbac.js'
|
||||
import { withPagination, withSort, buildSearchCondition, paginatedResponse } from '../../utils/pagination.js'
|
||||
|
||||
export const rbacRoutes: FastifyPluginAsync = async (app) => {
|
||||
// --- Users list ---
|
||||
|
||||
app.get('/users', { preHandler: [app.authenticate, app.requirePermission('users.view')] }, async (request, reply) => {
|
||||
const allUsers = await app.db
|
||||
const params = PaginationSchema.parse(request.query)
|
||||
const baseWhere = eq(users.companyId, request.companyId)
|
||||
|
||||
const searchCondition = params.q
|
||||
? buildSearchCondition(params.q, [users.firstName, users.lastName, users.email])
|
||||
: undefined
|
||||
|
||||
const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere
|
||||
|
||||
const sortableColumns: Record<string, Column> = {
|
||||
name: users.lastName,
|
||||
email: users.email,
|
||||
created_at: users.createdAt,
|
||||
}
|
||||
|
||||
let query = app.db
|
||||
.select({
|
||||
id: users.id,
|
||||
email: users.email,
|
||||
firstName: users.firstName,
|
||||
lastName: users.lastName,
|
||||
role: users.role,
|
||||
isActive: users.isActive,
|
||||
createdAt: users.createdAt,
|
||||
})
|
||||
.from(users)
|
||||
.where(eq(users.companyId, request.companyId))
|
||||
.orderBy(users.lastName)
|
||||
.where(where)
|
||||
.$dynamic()
|
||||
|
||||
return reply.send({ data: allUsers })
|
||||
query = withSort(query, params.sort, params.order, sortableColumns, users.lastName)
|
||||
query = withPagination(query, params.page, params.limit)
|
||||
|
||||
const [data, [{ total }]] = await Promise.all([
|
||||
query,
|
||||
app.db.select({ total: count() }).from(users).where(where),
|
||||
])
|
||||
|
||||
// Attach roles to each user
|
||||
const userIds = data.map((u) => u.id)
|
||||
const roleAssignments = userIds.length > 0
|
||||
? await app.db
|
||||
.select({
|
||||
userId: userRoles.userId,
|
||||
roleId: roles.id,
|
||||
roleName: roles.name,
|
||||
roleSlug: roles.slug,
|
||||
isSystem: roles.isSystem,
|
||||
})
|
||||
.from(userRoles)
|
||||
.innerJoin(roles, eq(userRoles.roleId, roles.id))
|
||||
.where(sql`${userRoles.userId} IN ${userIds}`)
|
||||
: []
|
||||
|
||||
const rolesByUser = new Map<string, { id: string; name: string; slug: string; isSystem: boolean }[]>()
|
||||
for (const ra of roleAssignments) {
|
||||
const list = rolesByUser.get(ra.userId) ?? []
|
||||
list.push({ id: ra.roleId, name: ra.roleName, slug: ra.roleSlug, isSystem: ra.isSystem })
|
||||
rolesByUser.set(ra.userId, list)
|
||||
}
|
||||
|
||||
const usersWithRoles = data.map((u) => ({
|
||||
...u,
|
||||
roles: rolesByUser.get(u.id) ?? [],
|
||||
}))
|
||||
|
||||
return reply.send(paginatedResponse(usersWithRoles, total, params.page, params.limit))
|
||||
})
|
||||
// --- User status ---
|
||||
|
||||
app.patch('/users/:userId/status', { preHandler: [app.authenticate, app.requirePermission('users.admin')] }, async (request, reply) => {
|
||||
const { userId } = request.params as { userId: string }
|
||||
const { isActive } = request.body as { isActive?: boolean }
|
||||
|
||||
if (typeof isActive !== 'boolean') {
|
||||
throw new ValidationError('isActive (boolean) is required')
|
||||
}
|
||||
|
||||
// Prevent disabling yourself
|
||||
if (userId === request.user.id) {
|
||||
throw new ValidationError('Cannot change your own account status')
|
||||
}
|
||||
|
||||
const [updated] = await app.db
|
||||
.update(users)
|
||||
.set({ isActive, updatedAt: new Date() })
|
||||
.where(and(eq(users.id, userId), eq(users.companyId, request.companyId)))
|
||||
.returning({ id: users.id, isActive: users.isActive })
|
||||
|
||||
if (!updated) return reply.status(404).send({ error: { message: 'User not found', statusCode: 404 } })
|
||||
|
||||
request.log.info({ userId, isActive, changedBy: request.user.id }, 'User status changed')
|
||||
return reply.send(updated)
|
||||
})
|
||||
|
||||
// --- Permissions ---
|
||||
|
||||
app.get('/permissions', { preHandler: [app.authenticate, app.requirePermission('users.view')] }, async (request, reply) => {
|
||||
@@ -33,7 +115,18 @@ export const rbacRoutes: FastifyPluginAsync = async (app) => {
|
||||
// --- Roles ---
|
||||
|
||||
app.get('/roles', { preHandler: [app.authenticate, app.requirePermission('users.view')] }, async (request, reply) => {
|
||||
const data = await RbacService.listRoles(app.db, request.companyId)
|
||||
const params = PaginationSchema.parse(request.query)
|
||||
const result = await RbacService.listRoles(app.db, request.companyId, params)
|
||||
return reply.send(result)
|
||||
})
|
||||
|
||||
// Unpaginated list for dropdowns/selectors
|
||||
app.get('/roles/all', { preHandler: [app.authenticate, app.requirePermission('users.view')] }, async (request, reply) => {
|
||||
const data = await app.db
|
||||
.select()
|
||||
.from(roles)
|
||||
.where(and(eq(roles.companyId, request.companyId), eq(roles.isActive, true)))
|
||||
.orderBy(roles.name)
|
||||
return reply.send({ data })
|
||||
})
|
||||
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() })
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user