feature: add trpc openapi (#1818)

This commit is contained in:
Manuel
2024-01-14 22:20:51 +01:00
committed by GitHub
parent 33da630db5
commit c701f723cf
18 changed files with 2177 additions and 134 deletions

View File

@@ -11,12 +11,18 @@ import * as https from 'https';
export const appRouter = createTRPCRouter({
ping: publicProcedure
.meta({ openapi: { method: 'GET', path: '/app/ping', tags: ['app'] } })
.input(
z.object({
id: z.string(),
configName: z.string(),
})
}),
)
.output(z.object({
status: z.number(),
statusText: z.string(),
state: z.string()
}))
.query(async ({ input }) => {
const config = getConfig(input.configName);
const app = config.apps.find((app) => app.id === input.id);
@@ -62,7 +68,7 @@ export const appRouter = createTRPCRouter({
if (error.code === 'ECONNABORTED') {
Consola.error(
`Ping timed out for app with id '${input.id}' in config '${input.configName}' -> url: ${app.url})`
`Ping timed out for app with id '${input.id}' in config '${input.configName}' -> url: ${app.url})`,
);
throw new TRPCError({
code: 'TIMEOUT',

View File

@@ -13,30 +13,43 @@ import { writeConfig } from '~/tools/config/writeConfig';
import { configNameSchema } from '~/validations/boards';
export const boardRouter = createTRPCRouter({
all: protectedProcedure.query(async ({ ctx }) => {
const files = fs.readdirSync('./data/configs').filter((file) => file.endsWith('.json'));
all: protectedProcedure
.meta({ openapi: { method: 'GET', path: '/boards/all', tags: ['board'] } })
.input(z.void())
.output(z.array(z.object({
name: z.string(),
allowGuests: z.boolean(),
countApps: z.number().min(0),
countWidgets: z.number().min(0),
countCategories: z.number().min(0),
isDefaultForUser: z.boolean(),
})))
.query(async ({ ctx }) => {
const files = fs.readdirSync('./data/configs').filter((file) => file.endsWith('.json'));
const defaultBoard = await getDefaultBoardAsync(ctx.session.user.id, 'default');
const defaultBoard = await getDefaultBoardAsync(ctx.session.user.id, 'default');
return await Promise.all(
files.map(async (file) => {
const name = file.replace('.json', '');
const config = await getFrontendConfig(name);
return await Promise.all(
files.map(async (file) => {
const name = file.replace('.json', '');
const config = await getFrontendConfig(name);
const countApps = config.apps.length;
const countApps = config.apps.length;
return {
name: name,
allowGuests: config.settings.access.allowGuests,
countApps: countApps,
countWidgets: config.widgets.length,
countCategories: config.categories.length,
isDefaultForUser: name === defaultBoard,
};
}),
);
}),
return {
name: name,
allowGuests: config.settings.access.allowGuests,
countApps: countApps,
countWidgets: config.widgets.length,
countCategories: config.categories.length,
isDefaultForUser: name === defaultBoard,
};
}),
);
}),
addAppsForContainers: adminProcedure
.meta({ openapi: { method: 'POST', path: '/boards/add-apps', tags: ['board'] } })
.output(z.void())
.input(
z.object({
boardName: configNameSchema,
@@ -89,10 +102,12 @@ export const boardRouter = createTRPCRouter({
fs.writeFileSync(targetPath, JSON.stringify(newConfig, null, 2), 'utf8');
}),
renameBoard: protectedProcedure
.meta({ openapi: { method: 'PUT', path: '/boards/rename', tags: ['board'] } })
.input(z.object({
oldName: z.string(),
newName: z.string().min(1),
}))
.output(z.void())
.mutation(async ({ input }) => {
if (input.oldName === 'default') {
Consola.error(`Attempted to rename default configuration. Aborted deletion.`);
@@ -127,9 +142,11 @@ export const boardRouter = createTRPCRouter({
Consola.info(`Deleted ${input.oldName} from file system`);
}),
duplicateBoard: protectedProcedure
.meta({ openapi: { method: 'POST', path: '/boards/duplicate', tags: ['board'] } })
.input(z.object({
boardName: z.string(),
}))
.output(z.void())
.mutation(async ({ input }) => {
if (!configExists(input.boardName)) {
Consola.error(`Tried to duplicate ${input.boardName} but this configuration does not exist.`);

View File

@@ -15,11 +15,13 @@ import { adminProcedure, createTRPCRouter, publicProcedure } from '../trpc';
export const configRouter = createTRPCRouter({
delete: adminProcedure
.meta({ openapi: { method: 'DELETE', path: '/configs', tags: ['config'] } })
.input(
z.object({
name: configNameSchema,
}),
)
.output(z.object({ message: z.string() }))
.mutation(async ({ input }) => {
if (input.name.toLowerCase() === 'default') {
Consola.error('Rejected config deletion because default configuration can\'t be deleted');
@@ -160,11 +162,21 @@ export const configRouter = createTRPCRouter({
};
}),
byName: publicProcedure
.meta({
openapi: {
method: 'GET',
path: '/configs/byName',
tags: ['config'],
deprecated: true,
summary: 'Retrieve content of the JSON configuration. Deprecated because JSON will be removed in a future version and be replaced with a relational database.'
}
})
.input(
z.object({
name: configNameSchema,
}),
)
.output(z.custom<ConfigType>())
.query(async ({ ctx, input }) => {
if (!configExists(input.name)) {
throw new TRPCError({
@@ -177,6 +189,7 @@ export const configRouter = createTRPCRouter({
}),
saveCustomization: adminProcedure
.input(boardCustomizationSchema.and(z.object({ name: configNameSchema })))
.output(z.void())
.mutation(async ({ input }) => {
const previousConfig = getConfig(input.name);
const newConfig = {

View File

@@ -4,35 +4,40 @@ import { LocalIconsRepository } from '~/tools/server/images/local-icons-reposito
import { UnpkgIconsRepository } from '~/tools/server/images/unpkg-icons-repository';
import { createTRPCRouter, publicProcedure } from '../trpc';
import { z } from 'zod';
import { NormalizedIconRepositoryResult } from '~/tools/server/images/abstract-icons-repository';
export const IconRespositories = [
new LocalIconsRepository(),
new GitHubIconsRepository(
GitHubIconsRepository.walkxcode,
'Walkxcode Dashboard Icons',
'Walkxcode on Github'
'Walkxcode on Github',
),
new UnpkgIconsRepository(
UnpkgIconsRepository.tablerRepository,
'Tabler Icons',
'Tabler Icons - GitHub (MIT)'
'Tabler Icons - GitHub (MIT)',
),
new JsdelivrIconsRepository(
JsdelivrIconsRepository.papirusRepository,
'Papirus Icons',
'Papirus Development Team on GitHub (Apache 2.0)'
'Papirus Development Team on GitHub (Apache 2.0)',
),
new JsdelivrIconsRepository(
JsdelivrIconsRepository.homelabSvgAssetsRepository,
'Homelab Svg Assets',
'loganmarchione on GitHub (MIT)'
'loganmarchione on GitHub (MIT)',
),
];
export const iconRouter = createTRPCRouter({
all: publicProcedure.query(async () => {
const fetches = IconRespositories.map((rep) => rep.fetch());
const data = await Promise.all(fetches);
return data;
}),
all: publicProcedure
.meta({ openapi: { method: 'GET', path: '/icons', tags: ['icon'] } })
.input(z.void())
.output(z.array(z.custom<NormalizedIconRepositoryResult>()))
.query(async () => {
const fetches = IconRespositories.map((rep) => rep.fetch());
return await Promise.all(fetches);
}),
});

View File

@@ -5,17 +5,26 @@ import { z } from 'zod';
import { db } from '~/server/db';
import { invites } from '~/server/db/schema';
import { adminProcedure, createTRPCRouter, publicProcedure } from '../../trpc';
import { adminProcedure, createTRPCRouter } from '../../trpc';
export const inviteRouter = createTRPCRouter({
all: adminProcedure
.meta({ openapi: { method: 'GET', path: '/invites', tags: ['invite'] } })
.input(
z.object({
limit: z.number().min(1).max(100).default(10),
page: z.number().min(0),
})
}),
)
.query(async ({ ctx, input }) => {
.output(z.object({
invites: z.array(z.object({
id: z.string(),
expires: z.date(),
creator: z.string().or(z.null()),
})),
countPages: z.number().min(0),
}))
.query(async ({ input }) => {
const limit = input.limit;
const dbInvites = await db.query.invites.findMany({
limit: limit,
@@ -44,14 +53,20 @@ export const inviteRouter = createTRPCRouter({
};
}),
create: adminProcedure
.meta({ openapi: { method: 'POST', path: '/invites', tags: ['invite'] } })
.input(
z.object({
expiration: z
.date()
.min(dayjs().add(5, 'minutes').toDate())
.max(dayjs().add(6, 'months').toDate()),
})
}),
)
.output(z.object({
id: z.string(),
token: z.string(),
expires: z.date(),
}))
.mutation(async ({ ctx, input }) => {
const inviteToInsert = {
id: randomUUID(),
@@ -67,7 +82,11 @@ export const inviteRouter = createTRPCRouter({
expires: inviteToInsert.expires,
};
}),
delete: adminProcedure.input(z.object({ id: z.string() })).mutation(async ({ input }) => {
await db.delete(invites).where(eq(invites.id, input.id));
}),
delete: adminProcedure
.meta({ openapi: { method: 'DELETE', path: '/invites', tags: ['invite'] } })
.input(z.object({ id: z.string() }))
.output(z.void())
.mutation(async ({ input }) => {
await db.delete(invites).where(eq(invites.id, input.id));
}),
});

View File

@@ -22,6 +22,7 @@ import {
updateSettingsValidationSchema,
} from '~/validations/user';
import { PossibleRoleFilter } from '~/pages/manage/users';
import { createSelectSchema } from 'drizzle-zod';
export const userRouter = createTRPCRouter({
createOwnerAccount: publicProcedure.input(signUpFormSchema).mutation(async ({ ctx, input }) => {
@@ -41,6 +42,7 @@ export const userRouter = createTRPCRouter({
});
}),
updatePassword: adminProcedure
.meta({ openapi: { method: 'PUT', path: '/users/password', tags: ['user'] } })
.input(
z.object({
userId: z.string(),
@@ -48,6 +50,7 @@ export const userRouter = createTRPCRouter({
terminateExistingSessions: z.boolean(),
}),
)
.output(z.void())
.mutation(async ({ input, ctx }) => {
const user = await db.query.users.findFirst({
where: eq(users.id, input.userId),
@@ -81,9 +84,13 @@ export const userRouter = createTRPCRouter({
})
.where(eq(users.id, input.userId));
}),
count: publicProcedure.query(async () => {
return await getTotalUserCountAsync();
}),
count: publicProcedure
.meta({ openapi: { method: 'GET', path: '/users/count', tags: ['user'] } })
.input(z.void())
.output(z.number())
.query(async () => {
return await getTotalUserCountAsync();
}),
createFromInvite: publicProcedure
.input(
signUpFormSchema.and(
@@ -133,7 +140,9 @@ export const userRouter = createTRPCRouter({
.where(eq(userSettings.userId, ctx.session?.user?.id));
}),
changeRole: adminProcedure
.meta({ openapi: { method: 'PUT', path: '/users/roles', tags: ['user'] } })
.input(z.object({ id: z.string(), type: z.enum(['promote', 'demote']) }))
.output(z.void())
.mutation(async ({ ctx, input }) => {
if (ctx.session?.user?.id === input.id) {
throw new TRPCError({
@@ -166,11 +175,13 @@ export const userRouter = createTRPCRouter({
.where(eq(users.id, input.id));
}),
changeLanguage: protectedProcedure
.meta({ openapi: { method: 'PUT', path: '/users/language', tags: ['user'] } })
.input(
z.object({
language: z.string(),
}),
)
.output(z.void())
.mutation(async ({ ctx, input }) => {
await db
.update(userSettings)
@@ -218,6 +229,8 @@ export const userRouter = createTRPCRouter({
}),
makeDefaultDashboard: protectedProcedure
.meta({ openapi: { method: 'POST', path: '/users/make-default-dashboard', tags: ['user'] } })
.output(z.void())
.input(z.object({ board: z.string() }))
.mutation(async ({ ctx, input }) => {
await db
@@ -243,7 +256,20 @@ export const userRouter = createTRPCRouter({
}),
}),
)
.query(async ({ ctx, input }) => {
.output(z.object({
users: z.array(z.object({
id: z.string(),
name: z.string(),
email: z.string().or(z.null()).optional(),
isAdmin: z.boolean(),
isOwner: z.boolean(),
})),
countPages: z.number().min(0),
stats: z.object({
roles: z.record(z.number()),
}),
}))
.query(async ({ input }) => {
const roleFilter = () => {
if (input.search.role === PossibleRoleFilter[1].id) {
@@ -309,30 +335,54 @@ export const userRouter = createTRPCRouter({
},
};
}),
create: adminProcedure.input(createNewUserSchema).mutation(async ({ input }) => {
await createUserIfNotPresent(input);
}),
details: adminProcedure.input(z.object({ userId: z.string() })).query(async ({ input }) => {
return db.query.users.findFirst({
where: eq(users.id, input.userId),
});
}),
updateDetails: adminProcedure.input(z.object({
userId: z.string(),
username: z.string(),
eMail: z.string().optional().transform(value => value?.length === 0 ? null : value),
})).mutation(async ({ input }) => {
await db.update(users).set({
name: input.username,
email: input.eMail as string | null,
}).where(eq(users.id, input.userId));
}),
create: adminProcedure
.meta({ openapi: { method: 'POST', path: '/users', tags: ['user'] } })
.input(createNewUserSchema)
.output(z.void())
.mutation(async ({ input }) => {
await createUserIfNotPresent(input);
}),
details: adminProcedure
.meta({ openapi: { method: 'GET', path: '/users/getById', tags: ['user'] } })
.input(z.object({ userId: z.string() }))
.output(
createSelectSchema(users)
.omit({
password: true,
salt: true,
})
.optional())
.query(async ({ input }) => {
return db.query.users.findFirst({
where: eq(users.id, input.userId),
columns: {
password: false,
salt: false,
},
});
}),
updateDetails: adminProcedure
.meta({ openapi: { method: 'PUT', path: '/users/details', tags: ['user'] } })
.input(z.object({
userId: z.string(),
username: z.string(),
eMail: z.string().optional().transform(value => value?.length === 0 ? null : value),
}))
.output(z.void())
.mutation(async ({ input }) => {
await db.update(users).set({
name: input.username,
email: input.eMail as string | null,
}).where(eq(users.id, input.userId));
}),
deleteUser: adminProcedure
.meta({ openapi: { method: 'DELETE', path: '/users', tags: ['user'] } })
.input(
z.object({
id: z.string(),
}),
)
.output(z.void())
.mutation(async ({ ctx, input }) => {
const user = await db.query.users.findFirst({
where: eq(users.id, input.id),

View File

@@ -13,6 +13,7 @@ import superjson from 'superjson';
import { ZodError } from 'zod';
import { getServerAuthSession } from '../auth';
import { OpenApiMeta } from 'trpc-openapi';
/**
* 1. CONTEXT
@@ -70,7 +71,7 @@ export const createTRPCContext = async (opts: CreateNextContextOptions) => {
* errors on the backend.
*/
const t = initTRPC.context<typeof createTRPCContext>().create({
const t = initTRPC.context<typeof createTRPCContext>().meta<OpenApiMeta>().create({
transformer: superjson,
errorFormatter({ shape, error }) {
return {