feat: Add apps crud (#174)

* wip: add apps crud

* wip: add edit for apps

* feat: add apps crud

* fix: color of icon for no app results wrong

* ci: fix lint issues

* test: add unit tests for app crud

* ci: fix format issue

* fix: missing rename in edit form

* fix: missing callback deepsource issues
This commit is contained in:
Meier Lukas
2024-03-04 22:13:40 +01:00
committed by GitHub
parent 70f34efd53
commit 8d5984c58a
17 changed files with 1501 additions and 0 deletions

View File

@@ -1,3 +1,4 @@
import { appRouter as innerAppRouter } from "./router/app";
import { boardRouter } from "./router/board";
import { integrationRouter } from "./router/integration";
import { userRouter } from "./router/user";
@@ -7,6 +8,7 @@ export const appRouter = createTRPCRouter({
user: userRouter,
integration: integrationRouter,
board: boardRouter,
app: innerAppRouter,
});
// export type definition of API

View File

@@ -0,0 +1,71 @@
import { TRPCError } from "@trpc/server";
import { asc, createId, eq } from "@homarr/db";
import { apps } from "@homarr/db/schema/sqlite";
import { validation } from "@homarr/validation";
import { createTRPCRouter, publicProcedure } from "../trpc";
export const appRouter = createTRPCRouter({
all: publicProcedure.query(async ({ ctx }) => {
return await ctx.db.query.apps.findMany({
orderBy: asc(apps.name),
});
}),
byId: publicProcedure
.input(validation.app.byId)
.query(async ({ ctx, input }) => {
const app = await ctx.db.query.apps.findFirst({
where: eq(apps.id, input.id),
});
if (!app) {
throw new TRPCError({
code: "NOT_FOUND",
message: "App not found",
});
}
return app;
}),
create: publicProcedure
.input(validation.app.manage)
.mutation(async ({ ctx, input }) => {
await ctx.db.insert(apps).values({
id: createId(),
name: input.name,
description: input.description,
iconUrl: input.iconUrl,
href: input.href,
});
}),
update: publicProcedure
.input(validation.app.edit)
.mutation(async ({ ctx, input }) => {
const app = await ctx.db.query.apps.findFirst({
where: eq(apps.id, input.id),
});
if (!app) {
throw new TRPCError({
code: "NOT_FOUND",
message: "App not found",
});
}
await ctx.db
.update(apps)
.set({
name: input.name,
description: input.description,
iconUrl: input.iconUrl,
href: input.href,
})
.where(eq(apps.id, input.id));
}),
delete: publicProcedure
.input(validation.app.byId)
.mutation(async ({ ctx, input }) => {
await ctx.db.delete(apps).where(eq(apps.id, input.id));
}),
});

View File

@@ -0,0 +1,209 @@
import { describe, expect, test, vi } from "vitest";
import type { Session } from "@homarr/auth";
import { createId } from "@homarr/db";
import { apps } from "@homarr/db/schema/sqlite";
import { createDb } from "@homarr/db/test";
import { appRouter } from "../app";
// Mock the auth module to return an empty session
vi.mock("@homarr/auth", () => ({ auth: () => ({}) as Session }));
describe("all should return all apps", () => {
test("should return all apps", async () => {
const db = createDb();
const caller = appRouter.createCaller({
db,
session: null,
});
await db.insert(apps).values([
{
id: "2",
name: "Mantine",
description: "React components and hooks library",
iconUrl: "https://mantine.dev/favicon.svg",
href: "https://mantine.dev",
},
{
id: "1",
name: "Tabler Icons",
iconUrl: "https://tabler.io/favicon.ico",
},
]);
const result = await caller.all();
expect(result.length).toBe(2);
expect(result[0]!.id).toBe("2");
expect(result[1]!.id).toBe("1");
expect(result[0]!.href).toBeDefined();
expect(result[0]!.description).toBeDefined();
expect(result[1]!.href).toBeNull();
expect(result[1]!.description).toBeNull();
});
});
describe("byId should return an app by id", () => {
test("should return an app by id", async () => {
const db = createDb();
const caller = appRouter.createCaller({
db,
session: null,
});
await db.insert(apps).values([
{
id: "2",
name: "Mantine",
description: "React components and hooks library",
iconUrl: "https://mantine.dev/favicon.svg",
href: "https://mantine.dev",
},
{
id: "1",
name: "Tabler Icons",
iconUrl: "https://tabler.io/favicon.ico",
},
]);
const result = await caller.byId({ id: "2" });
expect(result.name).toBe("Mantine");
});
test("should throw an error if the app does not exist", async () => {
const db = createDb();
const caller = appRouter.createCaller({
db,
session: null,
});
const act = async () => await caller.byId({ id: "2" });
await expect(act()).rejects.toThrow("App not found");
});
});
describe("create should create a new app with all arguments", () => {
test("should create a new app", async () => {
const db = createDb();
const caller = appRouter.createCaller({
db,
session: null,
});
const input = {
name: "Mantine",
description: "React components and hooks library",
iconUrl: "https://mantine.dev/favicon.svg",
href: "https://mantine.dev",
};
await caller.create(input);
const dbApp = await db.query.apps.findFirst();
expect(dbApp).toBeDefined();
expect(dbApp!.name).toBe(input.name);
expect(dbApp!.description).toBe(input.description);
expect(dbApp!.iconUrl).toBe(input.iconUrl);
expect(dbApp!.href).toBe(input.href);
});
test("should create a new app only with required arguments", async () => {
const db = createDb();
const caller = appRouter.createCaller({
db,
session: null,
});
const input = {
name: "Mantine",
description: null,
iconUrl: "https://mantine.dev/favicon.svg",
href: null,
};
await caller.create(input);
const dbApp = await db.query.apps.findFirst();
expect(dbApp).toBeDefined();
expect(dbApp!.name).toBe(input.name);
expect(dbApp!.description).toBe(input.description);
expect(dbApp!.iconUrl).toBe(input.iconUrl);
expect(dbApp!.href).toBe(input.href);
});
});
describe("update should update an app", () => {
test("should update an app", async () => {
const db = createDb();
const caller = appRouter.createCaller({
db,
session: null,
});
const appId = createId();
const toInsert = {
id: appId,
name: "Mantine",
iconUrl: "https://mantine.dev/favicon.svg",
};
await db.insert(apps).values(toInsert);
const input = {
id: appId,
name: "Mantine2",
description: "React components and hooks library",
iconUrl: "https://mantine.dev/favicon.svg2",
href: "https://mantine.dev",
};
await caller.update(input);
const dbApp = await db.query.apps.findFirst();
expect(dbApp).toBeDefined();
expect(dbApp!.name).toBe(input.name);
expect(dbApp!.description).toBe(input.description);
expect(dbApp!.iconUrl).toBe(input.iconUrl);
expect(dbApp!.href).toBe(input.href);
});
test("should throw an error if the app does not exist", async () => {
const db = createDb();
const caller = appRouter.createCaller({
db,
session: null,
});
const act = async () =>
await caller.update({
id: createId(),
name: "Mantine",
iconUrl: "https://mantine.dev/favicon.svg",
description: null,
href: null,
});
await expect(act()).rejects.toThrow("App not found");
});
});
describe("delete should delete an app", () => {
test("should delete an app", async () => {
const db = createDb();
const caller = appRouter.createCaller({
db,
session: null,
});
const appId = createId();
await db.insert(apps).values({
id: appId,
name: "Mantine",
iconUrl: "https://mantine.dev/favicon.svg",
});
await caller.delete({ id: appId });
const dbApp = await db.query.apps.findFirst();
expect(dbApp).toBeUndefined();
});
});