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'; @Injectable() 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(500).nullish(), cursor: z.string().nullish(), }), ) .query(async ({ input }) => { const limit = input.limit ?? 500; 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, }); 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, }); return account; }), delete: this.trpcService.protectedProcedure .input(z.string()) .mutation(async ({ input: id }) => { await this.prismaService.account.delete({ where: { id } }); return id; }), }); feedRouter = this.trpcService.router({ list: this.trpcService.protectedProcedure .input( z.object({ limit: z.number().min(1).max(500).nullish(), cursor: z.string().nullish(), }), ) .query(async ({ input }) => { const limit = input.limit ?? 500; 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.string()) .mutation(async ({ input: mpId }) => { await this.trpcService.refreshMpArticlesAndUpdateFeed(mpId); }), }); articleRouter = this.trpcService.router({ list: this.trpcService.protectedProcedure .input( z.object({ limit: z.number().min(1).max(500).nullish(), cursor: z.string().nullish(), mpId: z.string().nullish(), }), ) .query(async ({ input }) => { const limit = input.limit ?? 500; 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('auth')!.code; if (authCode && req.headers.authorization !== authCode) { return { errorMsg: 'authCode不正确!', }; } return { errorMsg: null, }; }, middleware: (req, res, next) => { next(); }, }), ); } } export type AppRouter = TrpcRouter[`appRouter`];