Spaces:
Paused
Paused
| import { INestApplication, Injectable, Logger } from '@nestjs/common'; | |
| import { z } from 'zod'; | |
| import { TrpcService } from '@server/trpc/trpc.service'; | |
| import * as trpcExpress from '@trpc/server/adapters/express'; | |
| import { TRPCError } from '@trpc/server'; | |
| import { PrismaService } from '@server/prisma/prisma.service'; | |
| import { statusMap } from '@server/constants'; | |
| import { ConfigService } from '@nestjs/config'; | |
| import { ConfigurationType } from '@server/configuration'; | |
| () | |
| export class TrpcRouter { | |
| constructor( | |
| private readonly trpcService: TrpcService, | |
| private readonly prismaService: PrismaService, | |
| private readonly configService: ConfigService, | |
| ) {} | |
| private readonly logger = new Logger(this.constructor.name); | |
| accountRouter = this.trpcService.router({ | |
| list: this.trpcService.protectedProcedure | |
| .input( | |
| z.object({ | |
| limit: z.number().min(1).max(1000).nullish(), | |
| cursor: z.string().nullish(), | |
| }), | |
| ) | |
| .query(async ({ input }) => { | |
| const limit = input.limit ?? 1000; | |
| const { cursor } = input; | |
| const items = await this.prismaService.account.findMany({ | |
| take: limit + 1, | |
| where: {}, | |
| select: { | |
| id: true, | |
| name: true, | |
| status: true, | |
| createdAt: true, | |
| updatedAt: true, | |
| token: false, | |
| }, | |
| cursor: cursor | |
| ? { | |
| id: cursor, | |
| } | |
| : undefined, | |
| orderBy: { | |
| createdAt: 'asc', | |
| }, | |
| }); | |
| let nextCursor: typeof cursor | undefined = undefined; | |
| if (items.length > limit) { | |
| // Remove the last item and use it as next cursor | |
| // eslint-disable-next-line @typescript-eslint/no-non-null-assertion | |
| const nextItem = items.pop()!; | |
| nextCursor = nextItem.id; | |
| } | |
| const disabledAccounts = this.trpcService.getBlockedAccountIds(); | |
| return { | |
| blocks: disabledAccounts, | |
| items, | |
| nextCursor, | |
| }; | |
| }), | |
| byId: this.trpcService.protectedProcedure | |
| .input(z.string()) | |
| .query(async ({ input: id }) => { | |
| const account = await this.prismaService.account.findUnique({ | |
| where: { id }, | |
| }); | |
| if (!account) { | |
| throw new TRPCError({ | |
| code: 'BAD_REQUEST', | |
| message: `No account with id '${id}'`, | |
| }); | |
| } | |
| return account; | |
| }), | |
| add: this.trpcService.protectedProcedure | |
| .input( | |
| z.object({ | |
| id: z.string().min(1).max(32), | |
| token: z.string().min(1), | |
| name: z.string().min(1), | |
| status: z.number().default(statusMap.ENABLE), | |
| }), | |
| ) | |
| .mutation(async ({ input }) => { | |
| const { id, ...data } = input; | |
| const account = await this.prismaService.account.upsert({ | |
| where: { | |
| id, | |
| }, | |
| update: data, | |
| create: input, | |
| }); | |
| this.trpcService.removeBlockedAccount(id); | |
| return account; | |
| }), | |
| edit: this.trpcService.protectedProcedure | |
| .input( | |
| z.object({ | |
| id: z.string(), | |
| data: z.object({ | |
| token: z.string().min(1).optional(), | |
| name: z.string().min(1).optional(), | |
| status: z.number().optional(), | |
| }), | |
| }), | |
| ) | |
| .mutation(async ({ input }) => { | |
| const { id, data } = input; | |
| const account = await this.prismaService.account.update({ | |
| where: { id }, | |
| data, | |
| }); | |
| this.trpcService.removeBlockedAccount(id); | |
| return account; | |
| }), | |
| delete: this.trpcService.protectedProcedure | |
| .input(z.string()) | |
| .mutation(async ({ input: id }) => { | |
| await this.prismaService.account.delete({ where: { id } }); | |
| this.trpcService.removeBlockedAccount(id); | |
| return id; | |
| }), | |
| }); | |
| feedRouter = this.trpcService.router({ | |
| list: this.trpcService.protectedProcedure | |
| .input( | |
| z.object({ | |
| limit: z.number().min(1).max(1000).nullish(), | |
| cursor: z.string().nullish(), | |
| }), | |
| ) | |
| .query(async ({ input }) => { | |
| const limit = input.limit ?? 1000; | |
| const { cursor } = input; | |
| const items = await this.prismaService.feed.findMany({ | |
| take: limit + 1, | |
| where: {}, | |
| cursor: cursor | |
| ? { | |
| id: cursor, | |
| } | |
| : undefined, | |
| orderBy: { | |
| createdAt: 'asc', | |
| }, | |
| }); | |
| let nextCursor: typeof cursor | undefined = undefined; | |
| if (items.length > limit) { | |
| // Remove the last item and use it as next cursor | |
| // eslint-disable-next-line @typescript-eslint/no-non-null-assertion | |
| const nextItem = items.pop()!; | |
| nextCursor = nextItem.id; | |
| } | |
| return { | |
| items: items, | |
| nextCursor, | |
| }; | |
| }), | |
| byId: this.trpcService.protectedProcedure | |
| .input(z.string()) | |
| .query(async ({ input: id }) => { | |
| const feed = await this.prismaService.feed.findUnique({ | |
| where: { id }, | |
| }); | |
| if (!feed) { | |
| throw new TRPCError({ | |
| code: 'BAD_REQUEST', | |
| message: `No feed with id '${id}'`, | |
| }); | |
| } | |
| return feed; | |
| }), | |
| add: this.trpcService.protectedProcedure | |
| .input( | |
| z.object({ | |
| id: z.string(), | |
| mpName: z.string(), | |
| mpCover: z.string(), | |
| mpIntro: z.string(), | |
| syncTime: z | |
| .number() | |
| .optional() | |
| .default(Math.floor(Date.now() / 1e3)), | |
| updateTime: z.number(), | |
| status: z.number().default(statusMap.ENABLE), | |
| }), | |
| ) | |
| .mutation(async ({ input }) => { | |
| const { id, ...data } = input; | |
| const feed = await this.prismaService.feed.upsert({ | |
| where: { | |
| id, | |
| }, | |
| update: data, | |
| create: input, | |
| }); | |
| return feed; | |
| }), | |
| edit: this.trpcService.protectedProcedure | |
| .input( | |
| z.object({ | |
| id: z.string(), | |
| data: z.object({ | |
| mpName: z.string().optional(), | |
| mpCover: z.string().optional(), | |
| mpIntro: z.string().optional(), | |
| syncTime: z.number().optional(), | |
| updateTime: z.number().optional(), | |
| status: z.number().optional(), | |
| }), | |
| }), | |
| ) | |
| .mutation(async ({ input }) => { | |
| const { id, data } = input; | |
| const feed = await this.prismaService.feed.update({ | |
| where: { id }, | |
| data, | |
| }); | |
| return feed; | |
| }), | |
| delete: this.trpcService.protectedProcedure | |
| .input(z.string()) | |
| .mutation(async ({ input: id }) => { | |
| await this.prismaService.feed.delete({ where: { id } }); | |
| return id; | |
| }), | |
| refreshArticles: this.trpcService.protectedProcedure | |
| .input( | |
| z.object({ | |
| mpId: z.string().optional(), | |
| }), | |
| ) | |
| .mutation(async ({ input: { mpId } }) => { | |
| if (mpId) { | |
| await this.trpcService.refreshMpArticlesAndUpdateFeed(mpId); | |
| } else { | |
| await this.trpcService.refreshAllMpArticlesAndUpdateFeed(); | |
| } | |
| }), | |
| isRefreshAllMpArticlesRunning: this.trpcService.protectedProcedure.query( | |
| async () => { | |
| return this.trpcService.isRefreshAllMpArticlesRunning; | |
| }, | |
| ), | |
| getHistoryArticles: this.trpcService.protectedProcedure | |
| .input( | |
| z.object({ | |
| mpId: z.string().optional(), | |
| }), | |
| ) | |
| .mutation(async ({ input: { mpId = '' } }) => { | |
| this.trpcService.getHistoryMpArticles(mpId); | |
| }), | |
| getInProgressHistoryMp: this.trpcService.protectedProcedure.query( | |
| async () => { | |
| return this.trpcService.inProgressHistoryMp; | |
| }, | |
| ), | |
| }); | |
| articleRouter = this.trpcService.router({ | |
| list: this.trpcService.protectedProcedure | |
| .input( | |
| z.object({ | |
| limit: z.number().min(1).max(1000).nullish(), | |
| cursor: z.string().nullish(), | |
| mpId: z.string().nullish(), | |
| }), | |
| ) | |
| .query(async ({ input }) => { | |
| const limit = input.limit ?? 1000; | |
| const { cursor, mpId } = input; | |
| const items = await this.prismaService.article.findMany({ | |
| orderBy: [ | |
| { | |
| publishTime: 'desc', | |
| }, | |
| ], | |
| take: limit + 1, | |
| where: mpId ? { mpId } : undefined, | |
| cursor: cursor | |
| ? { | |
| id: cursor, | |
| } | |
| : undefined, | |
| }); | |
| let nextCursor: typeof cursor | undefined = undefined; | |
| if (items.length > limit) { | |
| // Remove the last item and use it as next cursor | |
| // eslint-disable-next-line @typescript-eslint/no-non-null-assertion | |
| const nextItem = items.pop()!; | |
| nextCursor = nextItem.id; | |
| } | |
| return { | |
| items, | |
| nextCursor, | |
| }; | |
| }), | |
| byId: this.trpcService.protectedProcedure | |
| .input(z.string()) | |
| .query(async ({ input: id }) => { | |
| const article = await this.prismaService.article.findUnique({ | |
| where: { id }, | |
| }); | |
| if (!article) { | |
| throw new TRPCError({ | |
| code: 'BAD_REQUEST', | |
| message: `No article with id '${id}'`, | |
| }); | |
| } | |
| return article; | |
| }), | |
| add: this.trpcService.protectedProcedure | |
| .input( | |
| z.object({ | |
| id: z.string(), | |
| mpId: z.string(), | |
| title: z.string(), | |
| picUrl: z.string().optional().default(''), | |
| publishTime: z.number(), | |
| }), | |
| ) | |
| .mutation(async ({ input }) => { | |
| const { id, ...data } = input; | |
| const article = await this.prismaService.article.upsert({ | |
| where: { | |
| id, | |
| }, | |
| update: data, | |
| create: input, | |
| }); | |
| return article; | |
| }), | |
| delete: this.trpcService.protectedProcedure | |
| .input(z.string()) | |
| .mutation(async ({ input: id }) => { | |
| await this.prismaService.article.delete({ where: { id } }); | |
| return id; | |
| }), | |
| }); | |
| platformRouter = this.trpcService.router({ | |
| getMpArticles: this.trpcService.protectedProcedure | |
| .input( | |
| z.object({ | |
| mpId: z.string(), | |
| }), | |
| ) | |
| .mutation(async ({ input: { mpId } }) => { | |
| try { | |
| const results = await this.trpcService.getMpArticles(mpId); | |
| return results; | |
| } catch (err: any) { | |
| this.logger.log('getMpArticles err: ', err); | |
| throw new TRPCError({ | |
| code: 'INTERNAL_SERVER_ERROR', | |
| message: err.response?.data?.message || err.message, | |
| cause: err.stack, | |
| }); | |
| } | |
| }), | |
| getMpInfo: this.trpcService.protectedProcedure | |
| .input( | |
| z.object({ | |
| wxsLink: z | |
| .string() | |
| .refine((v) => v.startsWith('https://mp.weixin.qq.com/s/')), | |
| }), | |
| ) | |
| .mutation(async ({ input: { wxsLink: url } }) => { | |
| try { | |
| const results = await this.trpcService.getMpInfo(url); | |
| return results; | |
| } catch (err: any) { | |
| this.logger.log('getMpInfo err: ', err); | |
| throw new TRPCError({ | |
| code: 'INTERNAL_SERVER_ERROR', | |
| message: err.response?.data?.message || err.message, | |
| cause: err.stack, | |
| }); | |
| } | |
| }), | |
| createLoginUrl: this.trpcService.protectedProcedure.mutation(async () => { | |
| return this.trpcService.createLoginUrl(); | |
| }), | |
| getLoginResult: this.trpcService.protectedProcedure | |
| .input( | |
| z.object({ | |
| id: z.string(), | |
| }), | |
| ) | |
| .query(async ({ input }) => { | |
| return this.trpcService.getLoginResult(input.id); | |
| }), | |
| }); | |
| appRouter = this.trpcService.router({ | |
| feed: this.feedRouter, | |
| account: this.accountRouter, | |
| article: this.articleRouter, | |
| platform: this.platformRouter, | |
| }); | |
| async applyMiddleware(app: INestApplication) { | |
| app.use( | |
| `/trpc`, | |
| trpcExpress.createExpressMiddleware({ | |
| router: this.appRouter, | |
| createContext: ({ req }) => { | |
| const authCode = | |
| this.configService.get<ConfigurationType['auth']>('auth')!.code; | |
| if (authCode && req.headers.authorization !== authCode) { | |
| return { | |
| errorMsg: 'authCode不正确!', | |
| }; | |
| } | |
| return { | |
| errorMsg: null, | |
| }; | |
| }, | |
| middleware: (req, res, next) => { | |
| next(); | |
| }, | |
| }), | |
| ); | |
| } | |
| } | |
| export type AppRouter = TrpcRouter[`appRouter`]; | |