|
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) { |
|
|
|
|
|
|
|
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) { |
|
|
|
|
|
|
|
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) { |
|
|
|
|
|
|
|
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`]; |
|
|