mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-25 00:59:50 +00:00
feat(trpc): implement tRPC integration for accounts module
- Add tRPC server setup with NestJS (nestjs-trpc) - Create AccountsTrpcRouter with CRUD operations - Add tRPC client configuration in webapp - Create tRPC React hooks for accounts module - Replace existing REST hooks with tRPC hooks across 35+ files - Maintain backward compatibility with existing REST API - Add proper cache invalidation for mutations New files: - packages/server/src/modules/Trpc/* - packages/webapp/src/trpc.ts - packages/webapp/src/hooks/trpc/* - shared/bigcapital-utils/src/trpc.ts Dependencies added: - @trpc/server, @trpc/client, @trpc/react-query - nestjs-trpc, superjson - @tanstack/react-query Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -127,7 +127,10 @@
|
||||
"uuid": "^10.0.0",
|
||||
"xlsx": "^0.18.5",
|
||||
"yup": "^0.28.1",
|
||||
"zod": "^3.23.8"
|
||||
"zod": "^3.23.8",
|
||||
"@trpc/server": "^11.0.0-rc.648",
|
||||
"nestjs-trpc": "^1.6.1",
|
||||
"superjson": "^2.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.0.0",
|
||||
|
||||
@@ -104,6 +104,7 @@ import { BillLandedCostsModule } from '../BillLandedCosts/BillLandedCosts.module
|
||||
import { SocketModule } from '../Socket/Socket.module';
|
||||
import { ThrottlerGuard } from '@nestjs/throttler';
|
||||
import { AppThrottleModule } from './AppThrottle.module';
|
||||
import { AppTrpcModule } from '../Trpc/Trpc.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -256,6 +257,7 @@ import { AppThrottleModule } from './AppThrottle.module';
|
||||
UsersModule,
|
||||
ContactsModule,
|
||||
SocketModule,
|
||||
AppTrpcModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [
|
||||
|
||||
19
packages/server/src/modules/Trpc/Trpc.context.ts
Normal file
19
packages/server/src/modules/Trpc/Trpc.context.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { TRPCContext, ContextOptions } from 'nestjs-trpc';
|
||||
|
||||
@Injectable()
|
||||
export class TrpcContext implements TRPCContext {
|
||||
async create(opts: ContextOptions): Promise<Record<string, unknown>> {
|
||||
const { req } = opts;
|
||||
|
||||
// Extract auth token and organization from headers
|
||||
const token = req.headers['x-access-token'];
|
||||
const organizationId = req.headers['organization-id'];
|
||||
|
||||
return {
|
||||
token,
|
||||
organizationId: organizationId ? parseInt(organizationId as string, 10) : null,
|
||||
req,
|
||||
};
|
||||
}
|
||||
}
|
||||
19
packages/server/src/modules/Trpc/Trpc.module.ts
Normal file
19
packages/server/src/modules/Trpc/Trpc.module.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TRPCModule } from 'nestjs-trpc';
|
||||
import { TrpcService } from './Trpc.service';
|
||||
import { TrpcContext } from './Trpc.context';
|
||||
import { AccountsTrpcRouter } from './routers/Accounts.router';
|
||||
import { AccountsModule } from '@/modules/Accounts/Accounts.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TRPCModule.forRoot({
|
||||
basePath: '/api/trpc',
|
||||
context: TrpcContext,
|
||||
}),
|
||||
AccountsModule,
|
||||
],
|
||||
providers: [TrpcService, TrpcContext, AccountsTrpcRouter],
|
||||
exports: [TrpcService],
|
||||
})
|
||||
export class AppTrpcModule {}
|
||||
13
packages/server/src/modules/Trpc/Trpc.service.ts
Normal file
13
packages/server/src/modules/Trpc/Trpc.service.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
export interface TrpcContext {
|
||||
req: Request;
|
||||
res: Response;
|
||||
user: any;
|
||||
organizationId: number | null;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class TrpcService {
|
||||
}
|
||||
192
packages/server/src/modules/Trpc/routers/Accounts.router.ts
Normal file
192
packages/server/src/modules/Trpc/routers/Accounts.router.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Router, Query, Mutation } from 'nestjs-trpc';
|
||||
import { z } from 'zod';
|
||||
import { AccountsApplication } from '@/modules/Accounts/AccountsApplication.service';
|
||||
import { CreateAccountDTO } from '@/modules/Accounts/CreateAccount.dto';
|
||||
import { EditAccountDTO } from '@/modules/Accounts/EditAccount.dto';
|
||||
import { IAccountsStructureType } from '@/modules/Accounts/Accounts.types';
|
||||
|
||||
const accountResponseSchema = z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
slug: z.string(),
|
||||
code: z.string(),
|
||||
index: z.number(),
|
||||
accountType: z.string(),
|
||||
accountTypeLabel: z.string(),
|
||||
parentAccountId: z.number().nullable(),
|
||||
predefined: z.boolean(),
|
||||
currencyCode: z.string(),
|
||||
active: z.boolean(),
|
||||
bankBalance: z.number(),
|
||||
bankBalanceFormatted: z.string(),
|
||||
lastFeedsUpdatedAt: z.union([z.string(), z.date(), z.null()]),
|
||||
lastFeedsUpdatedAtFormatted: z.string(),
|
||||
amount: z.number(),
|
||||
formattedAmount: z.string(),
|
||||
plaidItemId: z.string(),
|
||||
plaidAccountId: z.string().nullable(),
|
||||
isFeedsActive: z.boolean(),
|
||||
isSyncingOwner: z.boolean(),
|
||||
isFeedsPaused: z.boolean(),
|
||||
accountNormal: z.string(),
|
||||
accountNormalFormatted: z.string(),
|
||||
flattenName: z.string(),
|
||||
accountLevel: z.number().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
});
|
||||
|
||||
const accountTypeSchema = z.object({
|
||||
label: z.string(),
|
||||
key: z.string(),
|
||||
normal: z.string(),
|
||||
parentType: z.string(),
|
||||
rootType: z.string(),
|
||||
multiCurrency: z.boolean(),
|
||||
balanceSheet: z.boolean(),
|
||||
incomeSheet: z.boolean(),
|
||||
});
|
||||
|
||||
const getAccountsQuerySchema = z.object({
|
||||
onlyInactive: z.boolean().optional(),
|
||||
structure: z.nativeEnum(IAccountsStructureType).optional(),
|
||||
page: z.number().optional(),
|
||||
pageSize: z.number().optional(),
|
||||
searchKeyword: z.string().optional(),
|
||||
});
|
||||
|
||||
const getAccountsResponseSchema = z.object({
|
||||
accounts: z.array(z.any()),
|
||||
filterMeta: z.object({
|
||||
count: z.number(),
|
||||
total: z.number(),
|
||||
page: z.number(),
|
||||
pageSize: z.number(),
|
||||
}),
|
||||
});
|
||||
|
||||
const getAccountTransactionsQuerySchema = z.object({
|
||||
accountId: z.number(),
|
||||
});
|
||||
|
||||
const createAccountInputSchema = z.object({
|
||||
name: z.string().min(3).max(255),
|
||||
code: z.string().min(3).max(6).optional(),
|
||||
currencyCode: z.string().optional(),
|
||||
accountType: z.string().min(3).max(255),
|
||||
description: z.string().max(65535).optional(),
|
||||
parentAccountId: z.number().optional(),
|
||||
active: z.boolean().optional(),
|
||||
plaidAccountId: z.string().optional(),
|
||||
plaidItemId: z.string().optional(),
|
||||
});
|
||||
|
||||
const editAccountInputSchema = createAccountInputSchema.partial();
|
||||
|
||||
const bulkDeleteInputSchema = z.object({
|
||||
ids: z.array(z.number()),
|
||||
skipUndeletable: z.boolean().optional(),
|
||||
});
|
||||
|
||||
const validateBulkDeleteResponseSchema = z.object({
|
||||
deletableIds: z.array(z.number()),
|
||||
nonDeletableIds: z.array(z.number()),
|
||||
deletableCount: z.number(),
|
||||
nonDeletableCount: z.number(),
|
||||
});
|
||||
|
||||
@Injectable()
|
||||
@Router({ alias: 'accounts' })
|
||||
export class AccountsTrpcRouter {
|
||||
constructor(private readonly accountsApplication: AccountsApplication) {}
|
||||
|
||||
@Query({
|
||||
input: getAccountsQuerySchema,
|
||||
output: getAccountsResponseSchema,
|
||||
})
|
||||
async getAccounts(input: z.infer<typeof getAccountsQuerySchema>) {
|
||||
return this.accountsApplication.getAccounts(input);
|
||||
}
|
||||
|
||||
@Query({
|
||||
input: z.object({ id: z.number() }),
|
||||
output: accountResponseSchema,
|
||||
})
|
||||
async getAccount(input: { id: number }) {
|
||||
return this.accountsApplication.getAccount(input.id);
|
||||
}
|
||||
|
||||
@Query({
|
||||
output: z.array(accountTypeSchema),
|
||||
})
|
||||
async getAccountTypes() {
|
||||
return this.accountsApplication.getAccountTypes();
|
||||
}
|
||||
|
||||
@Query({
|
||||
input: getAccountTransactionsQuerySchema,
|
||||
output: z.array(z.any()),
|
||||
})
|
||||
async getAccountTransactions(input: z.infer<typeof getAccountTransactionsQuerySchema>) {
|
||||
return this.accountsApplication.getAccountsTransactions({
|
||||
accountId: input.accountId,
|
||||
limit: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation({
|
||||
input: createAccountInputSchema,
|
||||
})
|
||||
async createAccount(input: z.infer<typeof createAccountInputSchema>) {
|
||||
return this.accountsApplication.createAccount(input as CreateAccountDTO);
|
||||
}
|
||||
|
||||
@Mutation({
|
||||
input: z.object({
|
||||
id: z.number(),
|
||||
data: editAccountInputSchema,
|
||||
}),
|
||||
})
|
||||
async editAccount(input: { id: number; data: any }) {
|
||||
return this.accountsApplication.editAccount(input.id, input.data as EditAccountDTO);
|
||||
}
|
||||
|
||||
@Mutation({
|
||||
input: z.object({ id: z.number() }),
|
||||
})
|
||||
async deleteAccount(input: { id: number }) {
|
||||
return this.accountsApplication.deleteAccount(input.id);
|
||||
}
|
||||
|
||||
@Mutation({
|
||||
input: z.object({ id: z.number() }),
|
||||
})
|
||||
async activateAccount(input: { id: number }) {
|
||||
return this.accountsApplication.activateAccount(input.id);
|
||||
}
|
||||
|
||||
@Mutation({
|
||||
input: z.object({ id: z.number() }),
|
||||
})
|
||||
async inactivateAccount(input: { id: number }) {
|
||||
return this.accountsApplication.inactivateAccount(input.id);
|
||||
}
|
||||
|
||||
@Mutation({
|
||||
input: bulkDeleteInputSchema,
|
||||
})
|
||||
async bulkDeleteAccounts(input: z.infer<typeof bulkDeleteInputSchema>) {
|
||||
return this.accountsApplication.bulkDeleteAccounts(input.ids, {
|
||||
skipUndeletable: input.skipUndeletable ?? false,
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation({
|
||||
input: z.object({ ids: z.array(z.number()) }),
|
||||
output: validateBulkDeleteResponseSchema,
|
||||
})
|
||||
async validateBulkDeleteAccounts(input: { ids: number[] }) {
|
||||
return this.accountsApplication.validateBulkDeleteAccounts(input.ids);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user